I2C(Inter-Integrated Circuit)是嵌入式开发中最常用的串行通信协议之一,它只需要两根线(SCL时钟线和SDA数据线)就能实现主从设备之间的数据交换。在STM32开发中,我们通常使用STM32CubeIDE这个强大的集成开发环境来配置和编写I2C通信代码。
在实际项目中,I2C通信有三种主要的实现模式:轮询模式、中断模式和DMA模式。这三种模式各有特点,适用于不同的应用场景。轮询模式最简单直接,适合初学者入门;中断模式能提高CPU利用率,适合中等数据量的传输;DMA模式则能最大程度解放CPU,适合大数据量或高实时性要求的场景。
以AHT20温湿度传感器为例,这个常见的I2C设备工作地址通常是0x70,它需要先发送初始化命令(0xBE),然后发送触发测量命令(0xAC),最后读取6字节的测量数据。我们将通过这个具体案例,展示三种模式下的代码实现差异。
在STM32CubeIDE中配置I2C轮询模式非常简单。首先打开.ioc文件,在Pinout & Configuration选项卡中找到I2C外设,选择I2C1(根据实际硬件连接选择),将模式设置为"I2C",时钟速度通常设置为100kHz或400kHz。
这里有个实用技巧:即使硬件电路已经加了上拉电阻,我习惯在软件配置中也把SCL和SDA引脚设置为上拉输入模式,这样可以提高通信稳定性。具体操作是在GPIO设置中,将这两个引脚的模式设置为"GPIO_Input",上拉/下拉选择"Pull-up"。
配置完成后生成代码,STM32CubeIDE会自动生成I2C初始化代码。我们只需要在main.c中包含"i2c.h"头文件,就可以直接使用HAL库提供的I2C函数了。
轮询模式的核心是使用HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive这两个阻塞式函数。下面是我在实际项目中使用的AHT20驱动代码:
c复制// aht20.h
#ifndef INC_AHT20_H_
#define INC_AHT20_H_
#include "i2c.h"
void AHT20_Init();
void AHT20_Read(float* Temperature, float* Humidity);
#endif /* INC_AHT20_H_ */
// aht20.c
#include "aht20.h"
#define AHT20_ADDRESS 0x70
void AHT20_Init() {
uint8_t readBuffer;
HAL_Delay(40); // 等待传感器上电稳定
// 读取状态字检查是否已校准
HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, &readBuffer, 1, HAL_MAX_DELAY);
if((readBuffer & 0x80) == 0x00) { // 检查校准位
uint8_t sendBuffer[3] = {0xBE, 0x80, 0x00}; // 初始化命令
HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);
}
}
void AHT20_Read(float* Temperature, float* Humidity) {
uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00}; // 触发测量命令
uint8_t readBuffer[6]; // 存储读取的数据
HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);
HAL_Delay(75); // 等待测量完成
HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, readBuffer, 6, HAL_MAX_DELAY);
if((readBuffer[0] & 0x80) == 0x00) { // 检查状态位
uint32_t date = 0;
// 解析湿度数据
date = (((uint32_t)readBuffer[3]>>4) + ((uint32_t)readBuffer[2]<<4) +
((uint32_t)readBuffer[1]<<12));
*Humidity = date * 100.0f / (1<<20);
// 解析温度数据
date = (((uint32_t)readBuffer[3]&0x0F)<<16) + ((uint32_t)readBuffer[4]<<8) +
(uint32_t)readBuffer[5];
*Temperature = date * 200.0f / (1<<20) - 50;
}
}
这段代码有几个关键点需要注意:
轮询模式的优点是简单直接,代码容易理解。但它有个明显缺点:在I2C通信期间,CPU会一直被占用,无法执行其他任务。对于简单的应用这可能不是问题,但在复杂的系统中,这会严重影响整体性能。
中断模式的核心思想是将I2C通信过程交给硬件处理,CPU只需要在通信开始和结束时介入。在STM32CubeIDE中配置中断模式,首先需要在I2C配置界面勾选"I2C全局中断",然后在NVIC设置中启用对应的中断通道。
与轮询模式不同,中断模式使用的是HAL_I2C_Master_Transmit_IT和HAL_I2C_Master_Receive_IT这两个非阻塞函数。这些函数启动通信后会立即返回,通信完成后会触发中断回调函数。
在实际项目中,我通常会把AHT20的读取过程分成三个阶段:
每个阶段完成后都会触发中断,我们在中断回调函数中设置状态标志,主循环根据状态标志决定下一步操作。
下面是中断模式的核心代码:
c复制// 定义全局变量
uint8_t readBuffer[6];
uint8_t aht20State = 0; // 状态机变量
// 测量函数
void AHT20_Measure() {
static uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00};
HAL_I2C_Master_Transmit_IT(&hi2c1, AHT20_ADDRESS, sendBuffer, 3);
}
// 读取函数
void AHT20_Get() {
HAL_I2C_Master_Receive_IT(&hi2c1, AHT20_ADDRESS, readBuffer, 6);
}
// 数据解析函数
void AHT20_Analysis(float* Temperature, float* Humidity) {
if((readBuffer[0] & 0x80) == 0x00) {
uint32_t date = 0;
date = (((uint32_t)readBuffer[3]>>4) + ((uint32_t)readBuffer[2]<<4) +
((uint32_t)readBuffer[1]<<12));
*Humidity = date * 100.0f / (1<<20);
date = (((uint32_t)readBuffer[3]&0x0F)<<16) + ((uint32_t)readBuffer[4]<<8) +
(uint32_t)readBuffer[5];
*Temperature = date * 200.0f / (1<<20) - 50;
}
}
// 主循环中的处理
if(aht20State == 0) {
AHT20_Measure();
aht20State = 1;
} else if(aht20State == 2) {
HAL_Delay(75); // 等待测量完成
AHT20_Get();
aht20State = 3;
} else if(aht20State == 4) {
AHT20_Analysis(&temperature, &humidity);
// 这里可以添加数据显示或传输代码
aht20State = 0;
}
// 中断回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
if(hi2c == &hi2c1) {
aht20State = 2; // 发送完成,进入等待状态
}
}
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) {
if(hi2c == &hi2c1) {
aht20State = 4; // 接收完成,进入解析状态
}
}
中断模式相比轮询模式有几个明显优势:
不过中断模式也有缺点:每次通信都需要CPU介入处理中断,对于高频或大数据量通信,中断开销仍然较大。这时候就需要考虑使用DMA模式了。
DMA(Direct Memory Access)模式是三种模式中最高效的一种,它允许外设直接与内存交换数据,完全不需要CPU参与。在STM32CubeIDE中配置DMA模式,需要以下几个步骤:
DMA模式使用的是HAL_I2C_Master_Transmit_DMA和HAL_I2C_Master_Receive_DMA函数。这些函数启动后,整个数据传输过程都由DMA控制器自动完成,数据传输完成后会触发DMA中断。
下面是DMA模式的核心代码:
c复制// 主机(Master)代码
uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00};
while (1) {
if(HAL_I2C_Master_Transmit_DMA(&hi2c1, AHT20_ADDRESS, sendBuffer, sizeof(sendBuffer)) != HAL_OK)
printf("传输错误\r\n");
HAL_Delay(1000); // 每秒发送一次
}
// 传输完成回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
if(hi2c == &hi2c1) {
// 可以在这里处理传输完成后的操作
sendBuffer[0]++; // 示例:修改发送数据
}
}
// 从机(Slave)代码
uint8_t readBuffer[6];
int main(void) {
// 初始化代码...
HAL_I2C_Slave_Receive_DMA(&hi2c1, readBuffer, sizeof(readBuffer));
while (1) {
// 主循环可以处理其他任务
}
}
// 接收完成回调函数
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) {
if(hi2c == &hi2c1) {
// 处理接收到的数据
printf("收到数据: %02X %02X %02X\r\n", readBuffer[0], readBuffer[1], readBuffer[2]);
// 重新启动DMA接收
HAL_I2C_Slave_Receive_DMA(&hi2c1, readBuffer, sizeof(readBuffer));
}
}
DMA模式的最大优势是极低的CPU占用率,特别适合以下场景:
不过DMA模式也有其复杂性:
在实际项目中,我通常会先使用轮询模式验证硬件连接和基本功能,然后根据需要升级到中断或DMA模式。对于AHT20这样的低速传感器,中断模式通常已经足够;但如果系统中有多个I2C设备或需要处理大量数据,DMA模式会是更好的选择。