4. I2C主机驱动开发:主机控制器初始化、消息传输流程、DMA传输优化、时钟延展处理
好,咱们进入I2C主机驱动的核心环节。说实话,I2C从机驱动相对简单,你基本是“等别人来找你”。但主机驱动不一样——你得主动发起一切,控制总线节奏,处理各种异常。这一章,我把自己这些年踩过的坑和总结的经验都摊开来讲。
4.1 主机控制器初始化——别小看这一步
很多人觉得初始化就是配个时钟、使能外设就完事了。嗯,我刚开始也这么想,直到有一次板子死活不通,折腾了两天才发现是引脚复用没配对。
一个靠谱的I2C主机初始化,至少要做这几件事:
- 时钟使能:先开外设时钟,再开GPIO时钟。顺序反了可能会出奇怪的问题。
- 引脚配置:SCL和SDA必须配成开漏输出(Open-Drain),内部上拉别开,外部上拉电阻才是正道。
- 复位控制器:有些MCU上电后I2C外设处于未知状态,我习惯先软复位一次。
- 配置主频:设置I2C时钟频率,标准模式100kHz,快速模式400kHz,快速+模式1MHz。
- 使能外设:最后才把I2C控制器使能。
核心要点:初始化顺序错了,后面全白搭。我见过有人把使放前面,结果配置时钟时外设还在跑,直接挂死。
下面是一个典型的初始化代码,以STM32为例:
void i2c_master_init(uint32_t freq_hz)
{
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
// 2. 配置PB6(SCL)和PB7(SDA)为开漏输出
GPIOB->MODER &= ~(GPIO_MODER_MODER6 | GPIO_MODER_MODER7);
GPIOB->MODER |= (GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1); // 复用功能
GPIOB->OTYPER |= (GPIO_OTYPER_OT6 | GPIO_OTYPER_OT7); // 开漏
GPIOB->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR6 | GPIO_OSPEEDER_OSPEEDR7); // 高速
// 3. 复位I2C外设
RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST;
// 4. 配置时钟频率
uint32_t pclk = 42000000; // APB1时钟42MHz
uint32_t ccr = pclk / (freq_hz * 2);
I2C1->CCR = ccr;
// 5. 使能外设
I2C1->CR1 |= I2C_CR1_PE;
}
小技巧:我习惯在初始化最后加一段自检——写一个字节到自己设备的从机地址(如果支持),或者读一下状态寄存器。能提前发现总线有没有被拉死。
4.2 消息传输流程——主机是怎么“说话”的
I2C主机传输,说白了就是一套固定的“握手-说话-再见”流程。你想想看,主机要发数据,得先拉低SDA发起始位,然后发从机地址+写位,等从机应答,再发数据字节,最后发停止位。
整个过程,状态机是核心。我早期做项目时,喜欢用阻塞方式轮询状态寄存器,简单粗暴。但后来发现,这种方式在高速传输时CPU占用率太高,于是改成了中断驱动。
一个标准的主机发送流程如下:
| 步骤 | 操作 | 状态标志 | 说明 |
|---|---|---|---|
| 1 | 发送起始位 | SB=1 | 硬件自动完成,等待SB置位 |
| 2 | 发送从机地址+W | ADDR=1 | 地址匹配后产生ADDR标志 |
| 3 | 等待应答 | AF=0 | 检查应答失败标志,为0表示有应答 |
| 4 | 发送数据字节 | TXE=1 | 数据寄存器空时写入下一个字节 |
| 5 | 发送停止位 | STOP=1 | 所有数据发完后产生停止条件 |
代码实现上,我习惯用状态机+中断的方式:
typedef enum {
I2C_STATE_IDLE,
I2C_STATE_START,
I2C_STATE_ADDR,
I2C_STATE_DATA,
I2C_STATE_STOP,
I2C_STATE_ERROR
} i2c_state_t;
void I2C1_EV_IRQHandler(void)
{
uint32_t sr1 = I2C1->SR1;
switch (i2c_state) {
case I2C_STATE_START:
if (sr1 & I2C_SR1_SB) {
I2C1->DR = (slave_addr << 1) | 0; // 发送地址+写位
i2c_state = I2C_STATE_ADDR;
}
break;
case I2C_STATE_ADDR:
if (sr1 & I2C_SR1_ADDR) {
(void)I2C1->SR2; // 清除ADDR标志
i2c_state = I2C_STATE_DATA;
}
break;
case I2C_STATE_DATA:
if (sr1 & I2C_SR1_TXE) {
if (data_idx < data_len) {
I2C1->DR = data_buf[data_idx++];
} else {
I2C1->CR1 |= I2C_CR1_STOP; // 发停止位
i2c_state = I2C_STATE_IDLE;
}
}
break;
default:
break;
}
}
注意:清除ADDR标志的方式因MCU而异。有些芯片读SR1再读SR2就能清,有些需要写CR1。我曾在一次移植中忽略了这点,结果ADDR一直挂在那,后续传输全乱套。
4.3 DMA传输优化——把CPU解放出来
轮询方式写起来简单,但效率低。中断方式好一些,但每个字节都进中断,频繁的上下文切换也很浪费。DMA传输才是正经的优化手段——让DMA控制器帮你搬数据,CPU可以干别的。
我个人习惯在以下场景使用DMA:
- 大数据块传输:比如从传感器读几百字节,或者写LCD的帧缓冲区。
- 实时性要求高:CPU不能被I2C传输阻塞,比如同时要处理ADC采样。
- 低功耗场景:DMA传输完可以唤醒CPU,中间CPU可以睡大觉。
DMA传输的配置要点:
- 选择DMA通道:查手册,I2C的TX和RX分别对应哪个DMA通道。
- 配置传输方向:内存到外设(发送)或外设到内存(接收)。
- 设置数据宽度:I2C数据寄存器是8位的,所以字节宽度。
- 使能循环模式:一般不用,除非你要持续发相同数据。
- 配置中断:DMA传输完成中断,用来发停止位。
这里有个坑——DMA传输完成后,I2C不会自动发停止位。你得在DMA完成中断里手动设置STOP位。我曾经忘了这一步,结果总线一直处于占用状态,其他设备全挂。
void DMA1_Stream0_IRQHandler(void)
{
if (DMA1->HISR & DMA_HISR_TCIF0) {
DMA1->HIFCR |= DMA_HIFCR_CTCIF0; // 清除中断标志
I2C1->CR1 |= I2C_CR1_STOP; // 手动发停止位
i2c_dma_done = 1; // 通知主循环
}
}
性能对比:我实测过,传输256字节数据,轮询方式CPU占用率约85%,中断方式约40%,DMA方式不到5%。差距非常明显。
4.4 时钟延展处理——从机说“慢点”怎么办
时钟延展(Clock Stretching)是I2C协议里一个容易被忽略的机制。说白了,就是从机来不及处理数据时,会把SCL拉低,告诉主机“你等等我”。主机检测到SCL被拉低,就得停下来等。
为什么会这样?你想想看,有些从机处理速度慢,比如EEPROM写操作需要几毫秒,它会在写期间拉低SCL。主机如果不管不顾继续发数据,数据就丢了。
处理时钟延展,有两种策略:
| 策略 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 硬件自动处理 | MCU的I2C外设自动检测SCL电平 | 无需软件干预 | 可能造成总线死锁 |
| 软件超时检测 | 在等待SCL释放时加超时计数器 | 可避免死锁 | 增加代码复杂度 |
我建议采用第二种。因为硬件自动处理虽然省事,但如果从机出问题一直拉低SCL,主机就永远等下去了。加上超时机制,至少能恢复。
超时处理的实现思路:
uint8_t i2c_wait_scl_release(uint32_t timeout_us)
{
uint32_t tick = 0;
while (I2C1->SR1 & I2C_SR1_SB) { // 等待SCL释放
if (tick++ > timeout_us) {
return 1; // 超时,返回错误
}
delay_us(1);
}
return 0; // 正常释放
}
避坑指南:我曾经在一个项目里遇到某款温湿度传感器,它的时钟延展时间长达10ms,远超我设定的1ms超时。结果每次读数据都超时复位,数据根本读不到。后来查手册才发现,这款传感器在转换数据时延展时间就是10ms。所以,超时时间一定要根据从机手册来设,别拍脑袋。
另外,时钟延展在快速模式下更容易出问题。因为快速模式时钟周期短,从机可能来不及在半个周期内完成处理。我建议在快速模式下,适当降低时钟频率,或者增加SCL高电平的保持时间。
经验之谈:如果你发现I2C总线偶尔卡死,大概率是时钟延展没处理好。用逻辑分析仪抓一下SCL波形,看看有没有被拉低超过预期时间。这是排查I2C问题最快的方法。
4.5 本章知识体系
下面这张图把I2C主机驱动的核心脉络梳理了一下,方便你对照理解:
好了,I2C主机驱动的核心内容就这些。从初始化到消息传输,再到DMA优化和时钟延展处理,每一步都有讲究。我个人觉得,最难的不是写代码,而是理解总线上的时序关系。多拿逻辑分析仪看看波形,比看十遍手册都管用。