2. STM32 I2C外设框图解析:时钟树、数据寄存器、控制寄存器、状态寄存器
好,咱们直接进入正题。上一章我们聊了I2C协议的基本时序,这一章我们要把目光转向STM32内部——看看那个负责产生这些时序的硬件模块到底长什么样。
说实话,我刚接触STM32的I2C外设时,第一反应是:这寄存器也太多了吧?但后来我发现,只要把框图理清楚,寄存器就只是个“填空题”。你填对了,硬件就按你的意思跑。
2.1 I2C外设整体架构
先看一张整体框图,我把它简化成了几个核心模块:
这个框图我建议你多看几眼。它把I2C外设分成了五个功能区:时钟、控制、数据、状态、引脚。每个区对应一组寄存器,咱们一个一个拆。
2.2 时钟树:I2C的“心跳”从哪来?
I2C要工作,首先得有时钟。STM32的I2C外设挂在APB1总线上,时钟源是PCLK1。但PCLK1通常跑得很快——比如36MHz或72MHz——而I2C标准模式才100kHz,快速模式也才400kHz。所以必须分频。
分频靠的是时钟控制寄存器(CCR),它藏在I2C_CR2里。具体来说:
| 模式 | 分频公式 | 举例(PCLK1=36MHz) |
|---|---|---|
| 标准模式(100kHz) | CCR = PCLK1 / (2 × SCL频率) | CCR = 36M / 200k = 180 |
| 快速模式(400kHz) | CCR = PCLK1 / (3 × SCL频率) 或 / (25 × ...) | CCR = 36M / 1.2M ≈ 30 |
2.3 控制寄存器:告诉硬件“你要干什么”
控制寄存器是I2C外设的“大脑”。主要涉及三个:
- CR1(控制寄存器1):使能外设(PE位)、产生起始/停止条件(START/STOP位)、应答控制(ACK位)等。说白了,你发什么命令,全靠这个寄存器。
- CR2(控制寄存器2):设置从设备地址、传输方向(读写位)、以及上面说的时钟分频值。我一般先配CR2,再配CR1。
- OAR1 / OAR2(自身地址寄存器):当STM32作为从设备时,这两个寄存器告诉硬件“我的地址是什么”。主模式用不到。
举个例子,你想让STM32作为主机发送一个起始条件:
// 设置起始位
I2C1->CR1 |= I2C_CR1_START;
// 等待硬件确认起始条件已发送
while(!(I2C1->SR1 & I2C_SR1_SB));
你看,写一个位,硬件就去干活了。这就是控制寄存器的魅力。
2.4 数据寄存器:数据进出的“窗口”
数据寄存器(DR)是I2C外设和CPU之间的数据通道。你往DR里写一个字节,硬件就会把它移位到SDA线上发出去;反过来,硬件从SDA线上收完一个字节,你就能从DR里读出来。
这里有个关键点:DR是8位的,但移位寄存器是并行的。硬件内部有一个8位移位寄存器,它负责逐位收发。DR和移位寄存器之间是并行传输,所以一个字节的读写只需要一次寄存器操作。
避坑指南:我曾经在连续发送多个字节时,忘了检查TXE(发送数据寄存器空)标志位就写下一个字节,结果数据覆盖了。正确的做法是:每次写DR前,先确认TXE=1。
2.5 状态寄存器:硬件在“汇报工作”
状态寄存器是I2C外设的“反馈通道”。主要有两个:
- SR1(状态寄存器1):包含SB(起始条件已发送)、ADDR(地址已匹配)、TXE(发送数据寄存器空)、RXNE(接收数据寄存器非空)、BTF(字节传输完成)等标志位。这些位告诉你当前I2C总线处于什么状态。
- SR2(状态寄存器2):包含主从模式标志(MSL)、总线忙标志(BUSY)、发送/接收模式标志(TRA)等。我常用BUSY位来判断总线是否空闲。
读状态寄存器有个小技巧:很多标志位读完后会自动清零。比如ADDR位,你读一次SR1再读一次SR2,它就清了。如果你忘了这个顺序,程序可能会卡在等待循环里。
注意:SR1和SR2的读取顺序很重要。有些标志位(如ADDR)需要先读SR1再读SR2才能清除。我见过有人调了两天I2C,最后发现是读顺序反了。
2.6 寄存器操作速查表
为了方便你写代码时快速查阅,我整理了一个表格:
| 寄存器 | 主要位/功能 | 读/写 | 常用场景 |
|---|---|---|---|
| CR1 | PE, START, STOP, ACK, NACK | 读写 | 使能外设、发起通信 |
| CR2 | FREQ[5:0], ADD[9:0], READ | 读写 | 配置时钟、从机地址 |
| DR | DR[7:0] | 读写 | 收发数据 |
| SR1 | SB, ADDR, TXE, RXNE, BTF | 只读 | 查询状态、等待事件 |
| SR2 | MSL, BUSY, TRA | 只读 | 判断总线状态 |
| OAR1 | ADD[9:0], ADDMODE | 读写 | 从模式地址配置 |
2.7 一个完整的初始化流程
说了这么多,咱们串起来看一个实际初始化代码。我习惯按这个顺序来:
- 使能时钟:RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
- 配置GPIO:把SCL和SDA引脚设为开漏输出,并配置为复用功能。
- 复位I2C外设:I2C1->CR1 |= I2C_CR1_SWRST; 然后清除。
- 配置CR2:设置PCLK1分频值和从机地址。
- 配置CR1:使能外设(PE=1),设置ACK位。
- 等待就绪:检查SR2的BUSY位,确保总线空闲。
void I2C1_Init(void) {
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// 2. GPIO配置(略,假设PB6=SCL, PB7=SDA)
// 3. 复位
I2C1->CR1 |= I2C_CR1_SWRST;
I2C1->CR1 &= ~I2C_CR1_SWRST;
// 4. 配置CR2:PCLK1=36MHz,分频得100kHz
I2C1->CR2 = (36 << 0); // FREQ=36
// 5. 配置CR1:使能,应答使能
I2C1->CR1 |= I2C_CR1_PE | I2C_CR1_ACK;
// 6. 等待总线空闲
while(I2C1->SR2 & I2C_SR2_BUSY);
}
你想想看,这个流程其实就是把前面讲的几个寄存器串起来了。时钟→控制→状态,每一步都有对应的寄存器操作。