2. STM32 I2C外设架构:模块框图、时钟树、中断与DMA
好,咱们进入正题。上一章聊了I2C协议的基本概念,这一章我们得把目光聚焦到STM32内部——看看这个I2C外设到底长什么样,怎么工作的。
说实话,我刚开始用STM32的I2C时,总觉得它就是个黑盒子。配置好寄存器,调用库函数,能通就行。直到有一次调试一个多从机系统,死活收不到应答信号,翻了两天手册才发现是时钟配置出了问题。从那以后,我养成了一个习惯:用I2C之前,先把它的架构图看明白。
2.1 I2C模块框图:外设内部长什么样
先看整体结构。STM32的I2C外设,说白了就是一条数据通道加上一堆控制逻辑。我习惯把它分成三个部分来看:
- 总线接口层:负责跟CPU、DMA、中断控制器打交道
- 协议控制层:负责SCL时钟生成、SDA数据采样、起始/停止条件检测
- 数据缓冲层:发送/接收移位寄存器 + 数据寄存器(DR)
你想想看,数据从CPU写进DR,然后移位寄存器一位一位往外送,同时SCL线按你设定的频率跳变。反过来,接收数据时移位寄存器把SDA上的比特收进来,拼成一个字节,再送到DR里等你来读。
核心要点:I2C外设内部有两个关键寄存器——DR(数据寄存器)和SR(状态寄存器)。DR负责数据中转,SR告诉你当前总线状态。这两个寄存器,调试时盯住了,问题就解决了一半。
下面这张图是我自己画的结构框图,把I2C外设的主要模块和它们之间的数据流向标清楚了:
2.2 时钟树:I2C的脉搏从哪来
时钟是I2C的心脏。SCL线上的每个高低电平变化,都来自时钟树的分配。我见过不少新手,配置I2C时钟时直接拿库函数默认参数一填,结果通信时快时慢,甚至完全不通。
STM32的I2C时钟源,通常来自APB1总线。为什么是APB1?因为I2C外设挂在APB1上,而APB1的最高频率有限制(一般是36MHz或50MHz,具体看型号)。
注意:I2C的SCL频率不是直接等于APB1时钟频率。它需要经过一个分频器。标准模式下SCL最高100kHz,快速模式400kHz,高速模式能到1MHz。分频系数 = (APB1时钟 / SCL目标频率) - 1。算错了,通信就乱套了。
举个例子,我常用的STM32F103,APB1时钟设为36MHz。要配成100kHz的标准模式:
// 分频系数计算
// CCR = (APB1时钟 / (2 * SCL频率)) - 1
// CCR = (36,000,000 / (2 * 100,000)) - 1 = 179
I2C1->CCR = 179; // 标准模式,100kHz
嗯,这里要注意:CCR寄存器在标准模式下是直接分频,在快速模式下还要考虑DUTY比。我踩过这个坑——快速模式配错了DUTY,SCL波形占空比不对,从机直接不认。
2.3 中断机制:什么时候该响应
I2C的中断,说白了就是外设在告诉你:「我有事了,快来处理」。STM32的I2C中断事件挺多的,我列个表,你一看就明白:
| 中断事件 | 触发条件 | 我常用的处理方式 |
|---|---|---|
| SB(起始位) | 发送了起始条件 | 写入从机地址 |
| ADDR(地址匹配) | 从机地址匹配成功 | 清除ADDR标志,准备数据 |
| TXE(发送缓冲区空) | DR为空,可以写数据 | 写入下一个要发送的字节 |
| RXNE(接收缓冲区非空) | DR有数据待读取 | 读取DR,存到缓冲区 |
| BTF(字节传输完成) | 移位寄存器和DR都为空 | 发送停止条件或继续下一字节 |
| STOPF(停止条件) | 检测到停止条件 | 清除标志,结束传输 |
| AF(应答失败) | 未收到从机应答 | 发送停止条件,记录错误 |
我个人习惯,在中断服务函数里只做最轻量的事——读DR、写DR、清标志。复杂的逻辑放到主循环或任务里处理。为什么?因为I2C中断频率不低,快速模式下400kHz,一个字节8位,加上应答,每秒要处理近4万次中断。你在中断里做太多事,CPU就别干别的了。
小技巧:调试时,我经常在中断服务函数里加一个GPIO翻转,用示波器看这个GPIO的波形,就能知道中断触发的频率和响应延迟。比翻寄存器快多了。
2.4 DMA:让数据自己跑起来
中断虽然好用,但数据量大时CPU负担重。这时候DMA就派上用场了。DMA说白了就是一个硬件搬运工——你告诉它「从哪搬,搬到哪,搬多少」,它就自己干,搬完了通知你一声。
I2C的DMA请求有两个方向:
- DMA发送请求:当DR为空时,DMA自动从内存搬一个字节到DR
- DMA接收请求:当DR有数据时,DMA自动从DR搬一个字节到内存
配置DMA时,有几点我得提醒你:
- 数据宽度必须一致:I2C的DR是8位,DMA的外设数据宽度也要配成8位
- 地址增量要配对:内存地址增量打开,外设地址增量关闭(因为DR地址固定)
- 传输完成中断:DMA传输完成后,记得在DMA的中断里发送停止条件
我曾经在一个项目里用DMA传输一批传感器数据,结果发现最后一个字节老是丢。查了半天,原来是DMA传输完成后,I2C还在等数据,没发停止条件。解决方案是在DMA传输完成中断里,手动触发停止条件。
// DMA传输完成中断回调
void DMA1_Channel6_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC6))
{
DMA_ClearITPendingBit(DMA1_IT_TC6);
// 等待BTF标志,确保最后一个字节已移出
while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_BTF));
// 发送停止条件
I2C_GenerateSTOP(I2C1, ENABLE);
}
}
经验之谈:DMA + I2C 的组合,适合传输数据量较大(比如几十个字节以上)的场景。如果只是读写几个寄存器,用中断就够了,DMA的配置开销反而划不来。
2.5 中断 vs DMA:怎么选
很多初学者会纠结这个问题。我直接给个判断标准:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 单字节读写 | 轮询或中断 | DMA配置开销大于收益 |
| 多字节连续传输(<16字节) | 中断 | 中断延迟可接受,代码简单 |
| 多字节连续传输(≥16字节) | DMA | CPU可以干别的,提高系统吞吐 |
| 实时性要求高 | DMA | 中断响应时间不确定,DMA是硬件行为 |
| 低功耗场景 | DMA | CPU可以进入睡眠模式,DMA自己干活 |
说白了,没有绝对的好坏,只有合不合适的场景。我自己的习惯是:原型阶段先用中断,跑通了再根据实际负载决定要不要切到DMA。
好了,这一章我们把STM32 I2C外设的架构、时钟、中断和DMA都过了一遍。这些是后面调试多从机通信的基础——你只有知道外设内部怎么工作的,出了问题才知道从哪下手查。