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外设的主要模块和它们之间的数据流向标清楚了:

STM32 I2C外设架构框图 CPU / 总线 中断控制器 DMA 控制器 I2C 外设 控制寄存器 状态寄存器 数据寄存器 (DR) 移位寄存器 时钟控制 噪声滤波器 SCL / SDA GPIO 引脚 说明: • 数据流向:CPU/DMA → DR → 移位寄存器 → SDA 引脚 • 控制流向:控制寄存器配置时钟、地址、模式;状态寄存器反馈总线状态 • 中断/DMA 是数据搬运的两种方式,互斥使用

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时,有几点我得提醒你:

  1. 数据宽度必须一致:I2C的DR是8位,DMA的外设数据宽度也要配成8位
  2. 地址增量要配对:内存地址增量打开,外设地址增量关闭(因为DR地址固定)
  3. 传输完成中断: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都过了一遍。这些是后面调试多从机通信的基础——你只有知道外设内部怎么工作的,出了问题才知道从哪下手查。