今天的学习聚焦于嵌入式领域最常用的两线制总线------I2C。从波形图入手,逐步拆解了起始信号、数据传输、应答机制和结束信号,并重点梳理了I2C的读写时序与代码实现框架。
一、 什么是I2C?
I2C (Inter-Integrated Circuit) 是一种同步、半双工、多主多从的串行通信总线。
-
两根线 :
SCL(Serial Clock,时钟线) 和SDA(Serial Data,数据线)。 -
数据传输 :采用高位先行 (MSB First)的原则。例如
0xAC的二进制是1010 1100,传输时先发1,最后发0。 -
通信速率 :标准模式为
100KHz,快速模式为400KHz,高速模式下有1.x MHz甚至3.x MHz。
二、 I2C核心时序剖析
结合波形图,I2C的时序由以下几个核心部分组成:
1. 起始信号 (Start)
-
规则 :当
SCL时钟线为高电平 时,SDA数据线由高变低出现下降沿。 -
作用:标志着一次通信的开始。
2. 数据传输
-
SCL高电平期间 :
SDA数据线上的电平必须保持稳定,此时接收方进行数据采样。 -
SCL低电平期间 :
SDA数据线上的电平可以改变,此时发送方发送数据。
3. 应答信号 (ACK / NACK)
-
规则 :在每次传输完8个位(1个字节)后的第9个时钟周期,接收方需要向发送方回复应答信号。
-
ACK (低电平):表示接收方成功接收到数据。
-
NACK (高电平):表示接收方未接收/拒绝接收。
4. 结束信号 (Stop)
-
规则 :当
SCL时钟线为高电平 时,SDA数据线由低变高出现上升沿。 -
作用:标志着一次通信的结束。
三、 设备地址与数据流向
起始信号后的第一个字节非常关键,它包含了设备地址和读写方向:
-
高7位 :从机设备地址。例如图中
0x10,其二进制为0001 000,这就意味着总线上最多可以有2^7 = 127个从机设备。 -
低1位 (LSB):数据流向位。
-
0:表示主机发送数据给从机(写)。 -
1:表示从机发送数据给主机(读)。
-
四、 I2C 读写操作流程
结合图片中的流程图,I2C的读写时序如下:
1. I2C 写时序(主机向从机指定寄存器写数据)
-
主机发送 起始信号。
-
主机发送 设备地址 + 写位(0) ,等待从机回复
ACK。 -
主机发送 寄存器地址 ,等待从机回复
ACK。 -
主机向该寄存器发送数据 ,等待从机回复
ACK。 -
主机发送 停止信号。
2. I2C 读时序(主机从从机指定寄存器读数据)
-
主机发送 起始信号。
-
主机发送 设备地址 + 写位(0) ,等待从机回复
ACK。(注:必须先写,告诉从机我们要操作哪个寄存器) -
主机发送 寄存器地址 ,等待从机回复
ACK。 -
主机重启起始信号 (Restart)。
-
主机发送 设备地址 + 读位(1) ,等待从机回复
ACK。 -
主机接收从机发来的数据(如果是读取多个字节,除了最后一个字节回
NACK外,其余都回ACK)。 -
主机发送 停止信号。
五、 代码实战:实现 i2c_read 函数
结合上述读时序,这里基于裸机硬件操作(或软件模拟I2C)写出 i2c_read 的核心逻辑框架。通常我们需要封装底层的起始、停止、发送字节、接收字节函数。
c
/*
* 假设已有底层基础函数:
* I2C_Start(); I2C_Stop(); I2C_SendByte(); I2C_WaitAck(); I2C_SendAck(); I2C_SendNack(); I2C_ReadByte();
*/
unsigned char i2c_read(unsigned char dev_addr, unsigned char reg_addr)
{
unsigned char data = 0;
// 1. 发送起始信号
I2C_Start();
// 2. 发送设备地址 + 写位 (0)
I2C_SendByte((dev_addr << 1) | 0x00);
I2C_WaitAck(); // 等待从机ACK应答
// 3. 发送寄存器地址
I2C_SendByte(reg_addr);
I2C_WaitAck(); // 等待从机ACK应答
// 4. 重新发送起始信号 (Restart)
I2C_Start();
// 5. 发送设备地址 + 读位 (1)
I2C_SendByte((dev_addr << 1) | 0x01);
I2C_WaitAck(); // 等待从机ACK应答
// 6. 接收数据
data = I2C_ReadByte();
// 只读一个字节,主机需要发送 NACK (非应答) 告诉从机不用再发数据了
I2C_SendNack();
// 7. 发送停止信号
I2C_Stop();
return data;
}
(注:如果是多字节连续读,需要在循环里 I2C_SendAck() 读取前面的数据,最后一次再 I2C_SendNack()。)
六、 硬件连接与寄存器寻址
从图示底部的内存映射/寄存器图可以看出,I2C从机设备(如EEPROM、传感器等)内部通常有 0x00 到 0xFF 的地址空间(即256个8位寄存器)。我们通过I2C总线发送的"寄存器地址",实际上就是去定位这些内部的格子,进而读写其中的数据。
今日总结与反思
-
时序是总线协议的灵魂:I2C的起始/停止信号非常巧妙,利用了电平的高低状态与跳变沿。必须在SCL高电平期间保持SDA稳定,这是最核心的采样规则。
-
"读"的本质是"先写后读":I2C读数据并不是直接读的,必须先发送写指令锁定寄存器地址,再重启发读指令,这个步骤在写代码时极其容易漏掉重启信号(Restart)。
-
ACK与NACK:主机在读完最后一个字节时,必须发送NACK(高电平)释放SDA线,这样主机才能成功发出停止信号。