之前的文章已经详细地介绍了 I2C 总线。本节介绍 STM32 的 I2C 外设以及硬件 I2C 的读写方式。虽然 I2C 协议具有控制简单、对时序要求不严格等优点,使得软件 I2C 的实现并不困难。不过硬件 I2C 在通信速率和时序规整性上有很大优势,能有效节约 CPU 资源。特别是 STM32 的硬件 I2C 实现了完整的协议模型,不但可以方便地作为从机方,还支持多主机的通信。
I2C外设结构
STM32 内置了 I2C 外设,专门负责实现 I2C 通信协议,只要配置好该外设,它就会自动通过硬件收发器产生或接受 I2C 协议所需要的通信时序,收发数据并缓存,内核只要检测该外设的状态和访问数据寄存器,就能完成数据收发。
STM32F1 有两个 I2C 外设,它们都位于 APB1 总线上。以下展示了 STM32 I2C 外设的结构:

STM32 的 I2C 结构并不复杂,主要可以分为时钟部分、数据部分和控制部分。
值得注意的是数据部分。I2C 的 SDA 主要由数据移位寄存器将数据寄存器 DR 的值按位发送出去;同时数据移位寄存器在接收数据时也负责将 SDA 得到的数据逐位搬移到数据寄存器中。
STM32 的硬件 I2C 支持从机模式,自身地址寄存器用于存储作为从机的地址。在接收地址时,数据移位寄存器会把接收到的地址与自身地址寄存器通过比较器判断,以便响应主机的寻址;并且 STM32 可以使用双地址寄存器同时拥有两个 I2C 设备地址。
STM32 支持帧错误校验(Packet Error Checking, PEC)功能,这是 I2C 协议规定的可选部分。如果收发两方约定使用帧错误校验功能,那么在协议层上需要额外发送校验码,就像 UART 的奇偶校验码一样。此时,接收到的数据会由 PCE 计算结构计算,将结果存放在 PEC 寄存器中,并通过比较器与校验码对比。
控制部分负责协调整个 I2C 外设。控制部分在工作时,可以产生 I2C 中断信号、DMA 请求及设置 I2C 的各种工作标志,可以通过状态寄存器读取 I2C 的工作状态(如数据发送完毕、总线忙碌、应答失败等)。
在使用 I2C 外设时,SCL 与 SDA 对应的引脚都是确定的,分别为:
引脚 | I2C1 | I2C2 |
---|---|---|
SCL | PB6 / PB8(通过复用功能重映射) | PB10 |
SDA | PB7 / PB9(通过复用功能重映射) | PB11 |
I2C外设读写示例
I2C外设的初始化
和其它外设一样,STM32 标准外设库提供了标准外设文件 stm32f10x_i2c.c 及其头文件,其中包含了 I2C 所需要用到的功能函数和各种定义。
同样地,I2C 的初始化也是由结构体和对应的函数 I2C_Init()
完成的。该结构体的定义如下:
其中包含的结构成员含义如下:
成员 .I2C_Mode
用于设置 I2C 外设的工作模式,这里只需选择 I2C 模式 I2C_Mode_I2C
即可。
I2C 协议虽然很好用,但是完整的规范太过复杂,因此还有一些协议在 I2C 的基础上做了一些改进。例如,系统管理总线(System Management Bus) SMBus 就由 I2C 改进而来,主要用于笔记本电源管理系统中。SMBus 协议和 I2C 很像,某些情况下 SMBus 设备甚至可以和 I2C 设备在同一条总线上通信。因此,STM32 的 I2C 外设还兼容了对 SMBus 模式的支持,如果想要通过 I2C 外设完成 SMBus 的通信,就需要更换 SMBus 的模式。
成员 .I2C_ClockSpeed
用于设置 I2C 的传输速率。I2C 协议对时序的要求并不严格,因此这个值可以根据需求任意设置,只要不大于 I2C 外设支持的最大传输速率 400_000
(单位:bit/s)即可。
对于 I2C 外设来说,如果通信速率大于 100_000
,那么 I2C 就处于快速通信模式。在快速通信模式下,可以通过 .I2C_DutyCycle
成员设置时钟线 SCL 的占空比,可选的值有 低电平时间:高电平时间 = 2:1 以及 低电平时间:高电平时间 = 16:9 。在要求并不会太严格时,可以任意选取。
这个占空比的意义和 GPIO 初始化结构体中速度的意义是类似的。在通信速率较快的情况下,高低电平的上升/下降时间的占用就会比较严重,尤其是低电平持续时间不足的情况下,设备可能会来不及准备数据,就会导致错误,这时在通信速率保存不变的情况下,就要尽可能多分配给低电平的时间比例。
其实,如果不使用标准外设库,而是直接操作寄存器的话,这两个参数需要根据系统的时钟频率算出合适的值才能写入寄存器。而标准外设库自动处理了这一过程的计算,非常方便使用。
下一个成员 .I2C_OwnAddress1
配置 STM32 的 I2C 外设作为从机时,在总线上的地址。这个值可以任意选取,只要是 I2C 总线上唯一的即可。
成员 .I2C_Ack
设置 I2C 外设是否需要对外发送应答信号。大部分情况下,I2C 的通信都需要应答信号才能正常工作,因此一般都设置为 I2C_Ack_Enable
启用应答。
最后一个成员 .I2C_AcknowledgeAddress
选择 I2C 作为从机时,它的地址是 7 位还是 10 位的寻址方式,具体需要根据主机的要求设定。
所以,如果需要初始化 I2C 的通信,需要执行以下步骤:
- 初始化对应的 GPIO 引脚为复用开漏输出模式
- 初始化 I2C 外设
- 使能 I2C 外设
I2C的硬件通信流程
在初始化之后,I2C 外设就可以对外通信了。接下来介绍使用 I2C 发送数据的主要代码。
首先,可以调用以下两个函数产生起始和终止信号:
- void I2C_GenerateSTART(I2C_TypeDef *I2Cx, FunctionalState NewState);
- void I2C_GenerateSTOP(I2C_TypeDef *I2Cx, FunctionalState NewState);
然后是地址和数据的发送。在软件 I2C 的 7 位地址模式下,地址(包括读写标志位)和数据的发送都是通过 SendByte()
函数完成的。标准库则将其拆分为了两个函数:
- void I2C_Send7bitAddress(I2C_TypeDef *I2Cx, uint8_t Address, uint8_t I2C_Direction);
- void I2C_SendData(I2C_TypeDef *I2Cx, uint8_t Data);
不过要注意的是,在使用 I2C_Send7bitAddress()
时,它的底层函数也是通过字节的形式发送,因此在表示地址时,需要采用左对齐的形式。例如 AT24C02 的设备地址虽然是 7 位的 0x50
,但是在调用该函数时,需要填入的参数为左对齐得到的 8 位 0xA0
。
至于 ACK 位的读取,这一过程会由硬件自动处理,并可以通过标志位的形式来读取这个应答。
这些工具已经允许 I2C 外设的主模式发送数据了。不过在使用硬件执行 I2C 通信时会有一个问题:在使用软件通信时,函数执行完毕就意味着对应的波形也也发送完毕;但硬件的通信是一种非阻塞式的通信,调用完函数只是将数据写入了寄存器中,还需要等待硬件产生完毕波形才可以继续流程的下一步。
为了确保发送完毕,很容易地就想到通过读取状态寄存器的相应标志位来确定硬件的工作状态。不过,有些时候硬件的工作状态需要通过多个标志位组合才能反映。为了简化这一流程的处理,STM32 在描述收发流程中引入了事件的概念。在这里,事件指的是多个标志位的状态组合。
例如,在 STM32 作为主设备的发送方的发送过程中,可能会产生如下事件:

可以通过以下函数检查 I2C 外设最后产生的事件是否是用户期望的事件:
- ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
例如,I2C 外设在使能后,默认作为从机接收端;但如果调用 I2C_GenerateSTART()
主动发送起始条件后,I2C 外设会自动变为主机方。如果需要确保已经处于主机模式,可以检查 EV5 事件。在标准外设库中,无需记住对应流程的事件编号,标准外设库已经将它们统一命名了,EV5 事件称为主机模式选择,等待该事件发生的代码为:
当事件发生后,调用函数不仅返回成功的状态,也会清除相应的标志位。
如果使能了 I2C 中断,以上所有事件发生时都会产生 I2C 中断信号,进入同一个中断服务函数。同样可以通过该函数检查当前处于的流程。
硬件I2C读写AT24C02
根据之前的文章对 AT24C02 的介绍,向 AT24C02 写入一个字节需要执行以下动作:
- 发送起始信号
- 发送 AT24C02 地址和读写标志
- 发送要写入的地址
- 发送要写入的数据
在介绍了 STM32 与 I2C 通信相关的函数后,实现以上功能对应的代码如下:
硬件I2C编程的稳健性设计
STM32 的硬件 I2C 虽然在易用性和执行效率上都高于软件模拟的 I2C ,但是硬件的问题也更加复杂且难以调试。这其中固然有 STM32 硬件设计上的缺陷,但是良好设计的程序也可以一定程度上避免潜在的问题。
在编写 I2C 程序时,在产生起始信号前,应尽量先检查总线是否处于忙碌状态。在检查各种标志位和事件前,可以设置一个超时时间,如果超时则说明 I2C 总线可能出现了问题。I2C 的控制寄存器中有一个软件复位的控制位,标准库中提供了以下函数,可以在出现问题时用于复位 I2C 控制器:
- void I2C_SoftwareResetCmd(I2C_TypeDef *I2Cx, FunctionalState NewState);
接下来是数据的读取。在 STM32 作为主设备的接收方的发送过程中,可能会产生如下事件:

这里以 AT24C02 顺序读的方式介绍 I2C 接收方的编程方式。在读取时,注意等待总线不处于忙碌状态时再读取。相应的程序实现如下: