2. STM32 I2C外设框图解析:时钟树、数据寄存器、控制寄存器、状态寄存器

好,咱们直接进入正题。上一章我们聊了I2C协议的基本时序,这一章我们要把目光转向STM32内部——看看那个负责产生这些时序的硬件模块到底长什么样。

说实话,我刚接触STM32的I2C外设时,第一反应是:这寄存器也太多了吧?但后来我发现,只要把框图理清楚,寄存器就只是个“填空题”。你填对了,硬件就按你的意思跑。

2.1 I2C外设整体架构

先看一张整体框图,我把它简化成了几个核心模块:

STM32 I2C外设核心架构 时钟控制 PCLK1 / 分频器 控制逻辑 CR1 / CR2 / OAR1 / OAR2 数据路径 DR / 移位寄存器 状态与中断 SR1 / SR2 / ITR 引脚控制 SCL / SDA 输出控制 SCL SDA 核心模块:时钟→控制→数据→状态→引脚,数据流从DR经移位寄存器到SDA线

这个框图我建议你多看几眼。它把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
我的习惯:配置时钟分频时,我会先确认PCLK1的实际频率。有一次项目里PCLK1被改成了48MHz,我忘了更新CCR值,结果I2C时序全乱套。嗯,从那以后我每次初始化I2C前都会读一下RCC的时钟配置。

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 一个完整的初始化流程

说了这么多,咱们串起来看一个实际初始化代码。我习惯按这个顺序来:

  1. 使能时钟:RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
  2. 配置GPIO:把SCL和SDA引脚设为开漏输出,并配置为复用功能。
  3. 复位I2C外设:I2C1->CR1 |= I2C_CR1_SWRST; 然后清除。
  4. 配置CR2:设置PCLK1分频值和从机地址。
  5. 配置CR1:使能外设(PE=1),设置ACK位。
  6. 等待就绪:检查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);
}

你想想看,这个流程其实就是把前面讲的几个寄存器串起来了。时钟→控制→状态,每一步都有对应的寄存器操作。