4、I2C主机模式驱动开发:主机模式初始化、主机发送/接收流程、超时处理机制、多主机通信仲裁
好,我们进入第四章。I2C 主机模式,说白了就是由你掌控时钟线 SCL,主动发起通信。很多初学者觉得 I2C 简单,不就是两根线嘛。但实际做产品时,主机模式的稳定性直接决定了整个系统的可靠性。我在一个工业传感器项目中,就曾因为主机初始化漏配了一个滤波参数,导致批量产品在强干扰环境下频繁丢数据。嗯,从那以后我对主机模式的每个细节都不敢马虎。
4.1 主机模式初始化:别漏了这几步
初始化主机模式,不只是把 GPIO 掰成开漏模式那么简单。你需要配置的,是 I2C 外设的核心时序参数。我个人习惯把初始化拆成四个步骤,缺一不可:
- 时钟使能与 GPIO 复用:先打开 I2C 外设时钟,再把对应引脚设为开漏输出、复用功能。
- 软件复位:很多芯片的 I2C 外设上电后处于未知状态。我建议你在配置寄存器前,先往 I2C_CR1 的 SWRST 位写 1,保持至少 4 个时钟周期再清 0。这一步能清掉所有挂起的错误标志。
- 配置通信速率:标准模式 100kHz 或快速模式 400kHz。你需要根据 APB 总线时钟计算 CCR 寄存器里的分频系数。举个例子,如果 APB 时钟是 36MHz,要得到 400kHz,CCR 的值就是 36MHz / (2 * 400kHz) = 45。
- 使能外设:最后把 I2C_CR1 的 PE 位置 1。注意,使能之前不要操作任何数据寄存器,否则可能触发意外中断。
核心要点:初始化顺序不能乱。先复位再配速率,最后使能。我曾经见过有人把使能放在复位之前,结果外设一直报总线错误,查了两天才找到原因。
下面是一个典型的初始化代码片段,以 STM32 标准库风格为例:
void I2C_Master_Init(void)
{
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 2. GPIO 配置:开漏输出,复用功能
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // SCL, SDA
gpio.GPIO_Mode = GPIO_Mode_AF_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
// 3. 软件复位
I2C_SoftwareResetCmd(I2C1, ENABLE);
delay_us(10);
I2C_SoftwareResetCmd(I2C1, DISABLE);
// 4. 配置速率:400kHz
I2C_InitTypeDef i2c;
i2c.I2C_ClockSpeed = 400000;
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_DutyCycle = I2C_DutyCycle_2;
i2c.I2C_Ack = I2C_Ack_Enable;
i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &i2c);
// 5. 使能外设
I2C_Cmd(I2C1, ENABLE);
}
小技巧:如果你用的是快速模式(400kHz),记得把 CCR 寄存器里的 DUTY 位置 1,让高低电平保持 1:2 的比例。标准模式不需要这个设置。
4.2 主机发送与接收流程:状态机思维
I2C 主机发送和接收,本质上是一个有限状态机。你写代码时,脑子里要时刻清楚当前处于哪个状态。我习惯把流程画成一张图,对着图写代码,不容易漏掉某个事件。
先看发送流程。主机发送一字节数据,标准步骤是:
- 产生起始条件(START)。
- 发送从机地址 + 写位(0)。
- 等待 ADDR 标志置位,然后清除它。
- 发送要写的数据字节到 DR 寄存器。
- 等待 TXE 标志置位,表示数据已移出。
- 如果需要继续发送,重复步骤 4-5。
- 发送完成后,产生停止条件(STOP)。
接收流程类似,只是把写位换成读位(1),并且主机需要在接收完每个字节后主动发送 ACK 或 NACK:
- 产生起始条件。
- 发送从机地址 + 读位(1)。
- 等待 ADDR 标志置位,清除它。
- 从 DR 寄存器读取接收到的数据。
- 如果还要继续接收,在读取后自动发送 ACK。
- 如果这是最后一个字节,在读取前设置 NACK 位,然后产生停止条件。
注意:清除 ADDR 标志的时机非常关键。在 STM32 上,读 SR1 再读 SR2 才能清掉 ADDR。如果你只读了 SR1,ADDR 不会清掉,后续的发送或接收流程会卡死。我踩过这个坑,排查时用逻辑分析仪看到 SCL 一直拉低,就是 ADDR 没清导致的。
下面是一个主机发送多字节的代码示例:
uint8_t I2C_Master_Transmit(uint8_t slaveAddr, uint8_t *data, uint16_t len)
{
// 产生起始条件
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送从机地址 + 写位
I2C_Send7bitAddress(I2C1, slaveAddr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 逐字节发送
for(uint16_t i = 0; i < len; i++)
{
I2C_SendData(I2C1, data[i]);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
// 产生停止条件
I2C_GenerateSTOP(I2C1, ENABLE);
return 0;
}
4.3 超时处理机制:别让系统死等
你想想看,如果从机突然掉电或者 SCL 被拉死,你的主机就会卡在 while 循环里,整个系统都瘫痪了。这就是为什么超时处理是 I2C 驱动里必不可少的一环。
我个人习惯在每个等待标志的循环里都加一个超时计数器。超时值怎么定?一般取 10ms 就够了。I2C 通信一帧数据最多几十微秒,10ms 已经足够宽裕。如果超过 10ms 还没等到标志,就认为总线异常,执行错误恢复流程。
错误恢复流程包括:
- 先产生停止条件,释放总线。
- 如果停止条件也发不出去(SCL 被拉死),就手动切换 GPIO 模式,用软件模拟的方式产生 9 个时钟脉冲,让从机释放 SDA。
- 最后重新初始化 I2C 外设。
避坑指南:我曾经在一个电池供电的项目里,把超时时间设成了 100ms。结果从机偶尔响应慢,主机就频繁触发超时复位,导致功耗飙升。后来我把超时时间调到 50ms,同时增加了重试次数,问题就解决了。超时值不是越大越好,要根据实际场景调。
带超时的发送函数骨架:
uint8_t I2C_Master_Transmit_Timeout(uint8_t slaveAddr, uint8_t *data, uint16_t len, uint32_t timeout)
{
uint32_t tick = 0;
// 起始条件
I2C_GenerateSTART(I2C1, ENABLE);
tick = GetTick();
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
if((GetTick() - tick) > timeout)
{
I2C_ErrorRecovery();
return 1; // 超时
}
}
// ... 后续步骤类似,每个 while 都加超时判断
// ... 省略中间代码
I2C_GenerateSTOP(I2C1, ENABLE);
return 0;
}
4.4 多主机通信仲裁:谁先说话谁赢
多主机通信,就是多个主机挂在同一条 I2C 总线上。这时候谁控制时钟?答案是:谁先拉低 SDA 谁就赢了。I2C 的仲裁机制是基于线与逻辑的——多个主机可以同时发送数据,但一旦某个主机试图拉高 SDA 而检测到 SDA 被其他主机拉低,它就输了,自动退出竞争。
仲裁发生在两个地方:
- 地址阶段:多个主机同时发送起始条件,谁的地址更小(二进制值更低),谁就赢得仲裁。
- 数据阶段:赢得仲裁的主机继续发送数据,输掉的主机转为从机模式,等待下一次机会。
硬件会自动处理仲裁过程。你作为驱动开发者,只需要做两件事:
- 使能多主机模式(有些芯片需要配置 CR2 的 MULT 位)。
- 在发送完起始条件后,检查是否发生了仲裁丢失(AF 标志)。如果 AF 置位,说明你输了,需要重新发起通信。
经验之谈:多主机场景下,我建议每个主机在发送前先检测总线是否空闲。怎么检测?看 SDA 和 SCL 是否同时为高电平超过 4.7μs(标准模式下的总线空闲时间)。有些芯片有 BUSY 标志位,可以直接读。如果没有,就用 GPIO 轮询。
仲裁丢失的处理代码:
void I2C_Event_Handler(void)
{
if(I2C_GetITStatus(I2C1, I2C_IT_AF))
{
// 仲裁丢失,清除标志
I2C_ClearITPendingBit(I2C1, I2C_IT_AF);
// 产生停止条件,释放总线
I2C_GenerateSTOP(I2C1, ENABLE);
// 等待一段时间后重试
delay_ms(1);
I2C_Master_Transmit_Timeout(slaveAddr, data, len, 10);
}
}
下面这张图总结了主机模式的核心流程和仲裁机制:
最后总结一下主机模式开发的关键点:初始化要按顺序来,发送接收要按状态机走,超时处理不能省,多主机场景下要处理好仲裁丢失。把这些都做到位了,你的 I2C 驱动才算真正可靠。