I²C总线原理与STM32手写驱动详解
I²C(Inter-Integrated Circuit)是一种同步、半双工、两线制串行通信协议,广泛应用于嵌入式系统中MCU与EEPROM、传感器等外围器件的互联。其核心依赖开漏输出结构与上拉电阻实现线与逻辑,支撑多主多从拓扑与硬件级仲裁。协议通过严格定义的起始/停止条件、字节时序、ACK/NACK应答机制保障通信可靠性。在资源受限场景(如蓝桥杯嵌入式竞赛)中,基于STM32 GPIO的手写I²
1. I²C总线基础原理与硬件架构
I²C(Inter-Integrated Circuit)是一种由Philips(现NXP)公司于1982年推出的同步、半双工、多主多从串行通信总线。其设计初衷是为同一块PCB上多个集成电路提供一种简洁、可靠、低引脚数的互连方案。在嵌入式系统,尤其是资源受限的MCU平台(如STM32F103系列)中,I²C因其硬件开销极小、协议清晰、支持多设备挂载等优势,成为连接EEPROM、温度传感器、DAC、ADC等外围器件的首选接口。
1.1 物理层:两线制与开漏输出
I²C总线仅需两条信号线即可完成双向通信:
- SCL(Serial Clock Line) :时钟线,由当前总线主控器(Master)驱动,为所有从设备(Slave)提供同步基准。
- SDA(Serial Data Line) :数据线,为双向共享线,主从设备均可在特定时序下驱动该线。
关键物理特性在于, SCL与SDA均采用开漏(Open-Drain)或开集电极(Open-Collector)输出结构 。这意味着任何连接到总线上的器件,其IO引脚只能将对应线路 拉低至地(GND) ,而 无法主动将其驱动为高电平 。这种设计强制要求在总线两端(通常靠近主控和最远从设备处)外接 上拉电阻(Pull-up Resistor) 至供电电压(VDD,常见为3.3V或5V)。
上拉电阻值的选择至关重要,它直接决定了总线的上升时间(tr)与最大通信速率。过大的阻值会导致上升沿过缓,易受噪声干扰,限制最高时钟频率;过小的阻值则会增大静态功耗,并可能超出器件的灌电流(Sink Current)能力。对于标准模式(100 kbps)和快速模式(400 kbps),典型值范围为1.8 kΩ至10 kΩ。以蓝桥杯嵌入式竞赛板为例,其PB6(SCL)与PB7(SDA)引脚上均配置了4.7 kΩ的上拉电阻至VDD,这是经过实际电路验证的稳定值。
开漏结构的设计哲学在于实现“线与(Wired-AND)”逻辑。当任意一个设备将SDA或SCL拉低时,整条总线即被拉低;只有当所有设备均释放(即呈高阻态)时,上拉电阻才将总线拉至高电平。这一特性天然支持多主竞争仲裁与从设备应答机制,是I²C协议健壮性的物理基石。
1.2 通信模型:半双工与主从角色
I²C是一种 半双工(Half-Duplex) 通信协议。这意味着在任意给定时刻,数据只能沿一个方向流动:要么由主控器向从设备发送(写操作),要么由从设备向主控器发送(读操作)。这与全双工的SPI不同,后者拥有独立的MOSI与MISO线,可同时收发。I²C的半双工特性简化了硬件设计,但也要求通信双方严格遵循时序规范,避免总线冲突。
通信过程始终由 主控器(Master) 发起并主导。主控器负责产生SCL时钟、发起起始条件(START)、寻址从设备、发送/接收数据字节、生成应答(ACK/NACK)以及最终发出停止条件(STOP)。 从设备(Slave) 则完全被动响应,仅在被正确寻址且总线空闲时才参与通信。每个从设备都拥有一个唯一的7位或10位地址,主控器通过广播该地址来选择目标。在蓝桥杯板卡上,常见的从设备包括AT24C02(7位地址0x50)和MCP4017(7位地址0x2F),它们共享同一对SCL/SDA总线,由主控STM32F103通过地址区分。
1.3 总线状态:空闲、起始与停止
I²C总线的状态由SCL与SDA两条线的电平组合定义,其中最关键的是 空闲(Idle) 、 起始(START) 和 停止(STOP) 三种状态。
-
空闲状态(Bus Idle) :当SCL与SDA两条线均被上拉电阻拉至高电平时,总线处于空闲状态。这是所有I²C通信的起点与终点。在空闲状态下,任何主控器均可发起新的通信。
-
起始条件(START Condition) :起始条件是一个 同步事件 ,它标志着一次I²C事务(Transaction)的开始。其严格的电气定义为: 在SCL线保持高电平期间,SDA线发生由高到低的跳变(Falling Edge) 。这个跳变必须发生在SCL高电平的稳定时段内,否则将被视为无效。起始条件的生成,是主控器将SDA线从输出高电平切换为输出低电平的结果。
-
停止条件(STOP Condition) :停止条件标志着一次I²C事务的结束,并使总线恢复至空闲状态。其定义为: 在SCL线保持高电平期间,SDA线发生由低到高的跳变(Rising Edge) 。与起始条件类似,这个跳变也必须发生在SCL高电平的稳定时段内。停止条件的生成,是主控器将SDA线从输出低电平切换为输出高电平(即释放,交由上拉电阻拉高)的结果。
起始与停止条件的时序约束是I²C协议的核心。它们确保了通信的原子性——一旦起始条件发出,直到对应的停止条件出现之前,总线上不允许其他主控器介入,从而避免了数据冲突。在软件模拟I²C(Bit-banging)时,精确控制这两个跳变的时机,是实现可靠通信的第一道门槛。
2. I²C核心时序与协议要素
I²C协议的精髓在于其精巧的时序设计。所有数据传输、地址解析、应答确认等操作,都严格嵌套在SCL时钟周期构成的框架内。理解这些基本时序单元,是编写健壮I²C驱动程序的前提。
2.1 数据有效性与时钟同步
I²C的数据传输以 字节(Byte) 为单位,每个字节包含8位数据(MSB先行)。数据位在SCL时钟的 低电平期间 被主控器(写操作)或从设备(读操作)更新或采样,在 高电平期间 必须保持稳定。这是I²C协议的关键规则,违反此规则将导致数据采样错误。
具体而言:
- 数据建立时间(tSU:DAT) :在SCL上升沿到来之前,SDA线上的数据必须已稳定至少一段时间(典型值为250 ns)。这保证了从设备有足够时间在时钟边沿前锁定数据。
- 数据保持时间(tHD:DAT) :在SCL下降沿之后,SDA线上的数据必须继续保持稳定至少一段时间(典型值为0 ns,但实际设计需留有余量)。这保证了主控器在下一个周期开始前,数据仍有效。
因此,一个完整的SCL时钟周期(T)被划分为两个主要阶段:低电平阶段用于数据准备与变化,高电平阶段用于数据采样与保持。主控器在SCL为低时改变SDA,在SCL为高时读取SDA。这种“低变高采”的模式,是I²C区别于其他同步总线(如SPI的CPOL/CPHA)的显著特征。
2.2 应答(ACK)与非应答(NACK)机制
I²C的可靠性很大程度上依赖于其 应答(Acknowledgement, ACK)与非应答(Not Acknowledgement, NACK) 机制。该机制为每传输一个字节后提供一次握手,使主控器能实时确认从设备是否成功接收了数据,或是否准备好发送下一个字节。
应答发生在每个字节传输完成后的第9个SCL时钟周期。此时,主控器会:
1. 将SDA线设置为 输入(Input)模式 ,释放对总线的驱动权。
2. 将SCL线拉高(SCL = 1)。
3. 在SCL保持高电平期间, 采样SDA线的电平 。
- ACK(应答) :如果从设备在第9个SCL高电平期间,成功将SDA线拉低(SDA = 0),则表示它已正确接收了前一个字节(写操作)或已准备好发送下一个字节(读操作)。主控器检测到SDA=0,即判定为ACK。
- NACK(非应答) :如果在第9个SCL高电平期间,SDA线仍被上拉电阻拉至高电平(SDA = 1),则表示从设备未响应。这可能源于多种原因:从设备忙(如EEPROM正在写入)、地址不匹配、硬件故障、或主控器在读操作末尾主动发送NACK以终止读取。
在蓝桥杯竞赛提供的底层驱动代码中, I2C_WaitAck() 函数正是实现了上述逻辑。它首先将SDA设为输入,然后拉高SCL,再延时等待稳定,最后读取SDA。若读取到0,则返回成功;若读取到1,则进入一个有限循环(如5次),每次循环都尝试重新拉高SCL并读取,以应对可能的微小延迟。若循环结束后仍未收到ACK,则返回错误码,提示通信失败。这是一种典型的、兼顾鲁棒性与效率的软件实现。
2.3 字节发送(SendByte)与字节接收(ReadByte)流程
在掌握了起始、停止、ACK/NACK之后,数据的传输便水到渠成。 SendByte() 与 ReadByte() 是I²C驱动中最核心的两个原子操作。
-
SendByte() 流程(主控器发送) :
- 主控器将SDA设为 输出模式 。
- 对于待发送的8位数据(
data),主控器执行8次循环。 - 在每次循环中:
- SCL置低(SCL = 0)。
- 延时(确保SCL稳定)。
- 根据
data的最高位(bit 7)决定SDA电平:若为1,则SDA=1;若为0,则SDA=0。 - SCL置高(SCL = 1)。
- 延时(等待从设备采样)。
data左移一位(data <<= 1),使次高位变为新的最高位,为下一次循环做准备。
- 8位发送完毕后,主控器调用
I2C_WaitAck()等待从设备的ACK。
-
ReadByte() 流程(主控器接收) :
- 主控器将SDA设为 输入模式 。
- 主控器初始化一个8位变量(如
byte)为0。 - 执行8次循环:
- SCL置低(SCL = 0)。
- 延时。
- SCL置高(SCL = 1)。
- 延时(等待从设备将数据放到SDA上)。
- 主控器读取SDA电平(
SDA_Read()),得到1位数据。 - 将读取到的1位数据存入
byte的最低位(LSB)。 byte左移一位(byte <<= 1),为下一位腾出空间。
- 8位接收完毕后,主控器根据需要决定是发送ACK(继续读)还是NACK(结束读)。
值得注意的是, ReadByte() 中 byte 的构建方式是“先读低位,再左移”,这与 SendByte() 中“先处理高位,再左移”在逻辑上是等价的,都确保了MSB先行的传输顺序。这种位操作的细节,是手写I²C驱动时最容易出错的地方,必须结合真值表反复验证。
3. 蓝桥杯嵌入式板卡I²C硬件与驱动分析
蓝桥杯嵌入式竞赛所使用的开发板,其I²C硬件设计与配套驱动代码,是理解理论与实践如何结合的最佳范例。深入剖析其电路与代码,能让我们避开大量“玄学”调试,直击问题本质。
3.1 硬件电路解析:PB6/PB7与上拉网络
该板卡将I²C1总线映射至STM32F103的GPIOB端口:
- PB6 :复用为I²C1_SCL(串行时钟线)。
- PB7 :复用为I²C1_SDA(串行数据线)。
在原理图中,可以清晰地看到,PB6与PB7引脚并非直接悬空,而是通过两个独立的4.7 kΩ电阻(Rxx)分别上拉至VDD(3.3V)。更关键的是,这两条总线上还挂载了两个典型的I²C从设备:
- AT24C02 :一款2K-bit的串行EEPROM,其地址引脚A0、A1、A2均接地,故其7位设备地址固定为 0x50 (二进制 1010000 )。
- MCP4017 :一款7位数字电位器(Digital Potentiometer),其地址引脚A0接VDD,A1、A2接地,故其7位设备地址为 0x2F (二进制 0101111 )。
这种“一主多从”的拓扑结构,完美体现了I²C总线的多设备扩展能力。所有从设备的SCL与SDA引脚,都并联在主控的PB6/PB7线上。由于所有器件都采用开漏输出,它们可以安全地共享同一根总线,而不会因输出电平冲突而损坏。上拉电阻的存在,确保了当所有设备都释放总线时,SCL与SDA能被可靠地拉高至逻辑“1”。
3.2 官方驱动代码解构:从“招式”到工程实践
竞赛资料盘中提供的I²C底层驱动,是一套高度模块化、功能完备的手写(Bit-banging)代码。它将复杂的I²C协议,分解为一系列可复用的、原子化的“招式”,这正是工程实践中“分而治之”思想的体现。
这套驱动的核心函数族如下:
- I2C_Start() :生成起始条件。
- I2C_Stop() :生成停止条件。
- I2C_SendByte(uint8_t byte) :发送一个字节。
- I2C_ReadByte() :接收一个字节。
- I2C_WaitAck() :等待从设备应答(ACK)。
- I2C_SendAck() :主控器发送应答(ACK)。
- I2C_SendNack() :主控器发送非应答(NACK)。
以 I2C_Start() 为例,其代码逻辑与前述原理完全一致:
void I2C_Start(void)
{
SDA_H; // SDA = 1 (先确保SDA为高)
SCL_H; // SCL = 1 (先确保SCL为高)
Delay_us(5); // 短暂延时,确保稳定
SDA_L; // SDA = 0 (在SCL为高时,SDA拉低 -> START)
Delay_us(5);
SCL_L; // SCL = 0 (为后续数据传输做准备)
}
这段代码没有使用任何HAL库的抽象,而是直接操作GPIO寄存器( SDA_H / SDA_L 宏定义为对PB7的置1/清0操作),其目的非常明确: 精准地控制每一个电平跳变的时机 。在资源受限、时序要求严苛的竞赛环境中,这种裸机编程方式提供了最高的确定性与最小的代码体积。
同样, I2C_WaitAck() 函数中的5次重试循环,也绝非随意为之。它是在无数次实测后得出的经验值,用以覆盖AT24C02在内部写周期(Write Cycle)中长达5ms的“忙”状态。当主控器在写入一个字节后立即发送读请求时,AT24C02尚未完成EEPROM的写入,会拒绝ACK。此时,等待并重试,是唯一正确的处理方式。这背后是芯片数据手册(Datasheet)的深刻理解与工程经验的结晶。
4. 实际应用:I²C通信的完整事务流程
理论与硬件的最终落脚点,是完成一次有意义的、端到端的通信事务。以向AT24C02写入一个字节数据为例,整个流程将前述所有“招式”有机串联,形成一个闭环。
4.1 写操作(Write Transaction)详解
向AT24C02写入一个字节,需要经历以下步骤:
- 总线仲裁与起始 :主控器检测到总线空闲(SCL=1, SDA=1)后,调用
I2C_Start(),生成起始条件。 - 发送从机地址(写) :主控器调用
I2C_SendByte(0xA0)。此处0xA0是AT24C02的7位地址0x50左移一位,并在最低位置0(表示写操作)所得。发送完毕后,主控器调用I2C_WaitAck()。若AT24C02在线且地址匹配,它将拉低SDA,主控器收到ACK。 - 发送内存地址(Word Address) :AT24C02内部是一个256字节的线性存储空间。主控器需指定要写入的地址(0x00 - 0xFF)。调用
I2C_SendByte(addr)发送该地址,随后再次调用I2C_WaitAck()等待ACK。 - 发送数据字节 :主控器调用
I2C_SendByte(data)发送要写入的实际数据,然后再次等待ACK。 - 结束通信 :主控器调用
I2C_Stop(),生成停止条件,释放总线。
整个流程中, I2C_WaitAck() 的调用是成败的关键。若在任何一步未收到ACK,主控器应立即停止后续操作,返回错误。这比盲目地继续发送更能保护系统稳定性。
4.2 读操作(Read Transaction)详解
从AT24C02读取一个字节,则是一个稍显复杂的“两次起始”过程:
- 第一次起始与地址定位 :与写操作的前两步完全相同:
I2C_Start()->I2C_SendByte(0xA0)->I2C_WaitAck()->I2C_SendByte(addr)->I2C_WaitAck()。这一步的目的是将AT24C02的内部地址指针(Address Pointer)定位到所需读取的位置。 - 第二次起始(Repeated START) :在不发出STOP的情况下,主控器再次调用
I2C_Start()。这是一个关键区别,它告诉AT24C02:“我刚刚设置了地址,现在我要从那个地址开始读。” - 发送从机地址(读) :主控器调用
I2C_SendByte(0xA1)。0xA1是0x50左移一位后,最低位置1(表示读操作)所得。发送后等待ACK。 - 接收数据字节 :主控器调用
I2C_ReadByte()。AT24C02会将指定地址处的数据放到SDA线上,主控器在8个SCL周期内将其读取。 - 发送NACK并停止 :在读取完最后一个字节后,主控器必须发送NACK(
I2C_SendNack()),以通知AT24C02“我不再需要更多数据了”,然后调用I2C_Stop()结束通信。
这个“重复起始”的设计,是I²C协议为提高效率而做的精妙安排。它允许主控器在一次总线占用期内,连续完成地址设置与数据读取,避免了不必要的总线释放与重争用。
5. 工程实践中的常见陷阱与规避策略
即便掌握了所有原理与代码,实际开发中仍会遇到各种“坑”。这些陷阱往往源于对协议细节的疏忽或对硬件特性的误判。以下是我在多个项目中踩过的、最具代表性的几个问题及其解决方案。
5.1 “忙”状态与超时重试
如前所述,AT24C02在执行内部写操作(Write Cycle)时,会进入长达数毫秒的“忙”状态。在此期间,它对任何I²C请求(包括地址帧)都会忽略,表现为不拉低SDA,即不发送ACK。如果主控器在写入后立刻发起读操作,几乎必然失败。
规避策略 :永远不要假设从设备是“即时响应”的。在关键操作(尤其是写入EEPROM后)后,应加入一个“轮询-等待”循环。即,在发送地址帧后,调用 I2C_WaitAck() ,若失败,则延时1ms后重试,最多重试10次。这比简单的固定延时更高效、更可靠。
5.2 GPIO模式切换的时序漏洞
在 I2C_WaitAck() 和 I2C_ReadByte() 中,需要频繁地在SDA的“输出模式”与“输入模式”间切换。一个常见的错误是,在将SDA设为输入后,立即拉高SCL,而忽略了GPIO模式切换本身需要一个微小的稳定时间(Setup Time)。
规避策略 :在 SDA_InputMode() 之后,必须加入一个极短的延时(如 Delay_us(1) ),然后再执行 SCL_H 。这个看似微不足道的1微秒,往往是解决“偶发性ACK丢失”的灵丹妙药。它确保了GPIO外设寄存器的配置已完全生效,SDA引脚已真正进入高阻态,不会对总线造成意外的下拉。
5.3 上拉电阻不匹配导致的通信失真
在调试一个新设计的PCB时,曾遇到I²C通信在低温下完全失效的问题。最终发现,设计中为节省成本,将SCL与SDA共用了一个10 kΩ上拉电阻。这在室温下勉强可用,但在低温下,MOS管的导通电阻增大,导致总线的上升时间(tr)严重超标,SCL的高电平无法在规定时间内建立,从设备无法正确采样。
规避策略 : SCL与SDA必须各自拥有独立的上拉电阻 。这是I²C硬件设计的铁律。此外,在高速模式(>400 kbps)下,应优先选用较小的阻值(如2.2 kΩ),并考虑使用带有施密特触发器输入的IO引脚,以增强抗噪能力。
这些经验,无一不是从烧坏的芯片、漫长的示波器调试和无数个深夜的代码审查中得来的。它们无法在教科书中找到,却是嵌入式工程师最宝贵的财富。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)