4、I2C读写操作:主机写从机、主机读从机、组合格式(复合帧)
好,咱们今天聊聊I2C通信里最核心的操作——读写。说白了,就是主机怎么把数据塞给从机,又从从机那里把数据掏出来。
很多初学者觉得I2C协议复杂,其实拆开来看,无非就是三种基本操作:主机写、主机读、还有组合格式(也叫复合帧)。我个人习惯把第三种叫做「先写后读」,这样好记。
4.1 主机写从机:最基础的操作
先讲最简单的——主机往从机里写数据。这个场景很常见,比如你要配置一个传感器的寄存器,或者给一个OLED屏幕发显示指令。
流程是这样的:
- 主机发送起始信号(S)
- 主机发送从机地址 + 写位(0)
- 等待从机应答(ACK)
- 主机发送寄存器地址(或者指令码)
- 等待从机应答
- 主机发送数据字节
- 等待从机应答
- 重复步骤6-7,直到数据发完
- 主机发送停止信号(P)
嗯,这里要注意:每次发送一个字节后,主机都要释放SDA线,让从机拉低来应答。如果从机没拉低,那就是NACK,说明出问题了。
关键点:写操作时,数据方向始终是从主机到从机。从机只负责应答,不发送数据。
看个实际代码片段,我用的是STM32的HAL库:
// 往从机地址0x50的寄存器0x10写入0xAB
uint8_t data[2] = {0x10, 0xAB};
HAL_I2C_Master_Transmit(&hi2c1, 0x50 << 1, data, 2, 100);
这里有个坑——地址要左移一位。因为HAL库内部已经帮你处理了读写位,你传进去的地址是7位的,它自动左移后把最低位留给R/W。我刚开始用的时候忘了移位,结果通信死活不通,查了半天才发现是地址错了。
4.2 主机读从机:从设备里取数据
读操作稍微复杂一点。你想啊,你要从从机里读数据,总得先告诉它读哪个地址吧?所以读操作其实包含两个阶段:先写地址,再读数据。
流程是这样的:
- 主机发送起始信号(S)
- 主机发送从机地址 + 写位(0)
- 等待从机应答
- 主机发送要读取的寄存器地址
- 等待从机应答
- 主机发送重复起始信号(Sr)
- 主机发送从机地址 + 读位(1)
- 等待从机应答
- 从机发送数据,主机应答(ACK)
- 重复步骤9,直到数据收完
- 最后一个字节主机不应答(NACK),然后发送停止信号
注意:最后一个字节主机必须发送NACK!这是告诉从机「够了,别再发了」。如果发了ACK,从机会继续发数据,导致总线卡死。我曾经在这个问题上栽过跟头,调试了一下午才发现是最后一个应答位没处理好。
代码示例:
// 从从机地址0x50的寄存器0x10读取2个字节
uint8_t reg_addr = 0x10;
uint8_t rx_buffer[2];
HAL_I2C_Master_Transmit(&hi2c1, 0x50 << 1, ®_addr, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, 0x50 << 1, rx_buffer, 2, 100);
你看,这里分成了两步:先发地址,再收数据。但有些库提供了组合函数,一步到位。
4.3 组合格式(复合帧):最灵活的方式
组合格式,说白了就是把写和读合并到一个事务里。中间用重复起始信号连接,不产生停止信号。
为什么要用组合格式?
- 原子性:整个操作不会被其他主机打断。如果先发停止再发起始,中间可能有其他主机抢占总线。
- 效率高:省去了重新仲裁的时间。
- 某些从机强制要求:比如一些温度传感器,必须用组合格式才能正确读取数据。
流程示意图:
S + 从机地址(W) + ACK + 寄存器地址 + ACK + Sr + 从机地址(R) + ACK + 数据 + NACK + P
注意那个 Sr,它就是重复起始信号。和普通起始信号的区别在于:发送Sr之前,主机没有发送停止信号。
小技巧:我习惯用逻辑分析仪抓波形来区分普通起始和重复起始。普通起始之前一定有停止信号,而重复起始之前是数据位和应答位。波形上看,SDA从高到低跳变时,SCL都是高电平,但上下文不同。
代码示例(使用HAL库的组合函数):
// 组合格式:先写寄存器地址,再读数据
uint8_t reg_addr = 0x10;
uint8_t rx_buffer[2];
HAL_I2C_Mem_Read(&hi2c1, 0x50 << 1, reg_addr, I2C_MEMADD_SIZE_8BIT, rx_buffer, 2, 100);
这个 HAL_I2C_Mem_Read 函数内部就实现了组合格式。它先发寄存器地址,然后发重复起始,再读数据。你想想看,如果自己手动实现,得写好几行代码,还容易出错。
4.4 三种操作的对比
| 操作类型 | 起始信号次数 | 地址发送次数 | 数据方向 | 典型应用 |
|---|---|---|---|---|
| 主机写 | 1次 | 1次(写地址) | 主机→从机 | 配置寄存器、发送指令 |
| 主机读 | 2次(含重复起始) | 2次(先写后读) | 先主机→从机,后从机→主机 | 读取传感器数据 |
| 组合格式 | 2次(含重复起始) | 2次(先写后读) | 同上,但无停止信号 | 原子操作、多字节读取 |
4.5 避坑指南
我做了这么多年嵌入式,在I2C读写上踩过的坑真不少。给你总结几个最常见的:
- 地址移位问题:不同库对地址的处理方式不同。有的要求7位地址,有的要求8位(左移后)。一定要看文档!
- 应答超时:如果从机没应答,检查地址对不对、从机是否上电、上拉电阻是否正常。
- 最后一个字节的NACK:读操作时,主机必须在最后一个字节发NACK。很多初学者忘了这个,导致总线卡死。
- 重复起始信号:有些MCU的I2C外设需要特殊配置才能产生重复起始。用库函数时,确保调用了正确的组合函数。
我的经验:调试I2C通信时,逻辑分析仪是最好的工具。抓一次波形,什么都能看清楚。别光靠眼睛盯着代码看,波形不会骗人。
好了,关于I2C的读写操作就讲到这里。说白了就是三种套路:只写、先写后读(带停止)、先写后读(不带停止)。掌握了这些,大部分I2C设备你都能驾驭。