
1. STM32硬件IIC事件驱动编程的核心思路第一次接触STM32硬件IIC时我被它独特的事件驱动机制惊艳到了。和常见的轮询方式不同STM32把IIC总线上的每个关键动作都转化成了特定事件这种设计让代码结构变得异常清晰。以STM32F103为例当它作为主设备时主要需要处理EV5、EV6、EV7、EV8和EV8_2这五个核心事件。实际项目中我发现这种事件驱动模型特别适合处理IIC这种有时序要求的协议。比如EV5事件对应起始信号发送成功EV6事件表示从机地址已得到响应而EV8_2事件则标志着一个字节数据完整发送。每个事件都像是一个里程碑告诉开发者上一步已完成可以执行下一步了。2. 硬件IIC初始化实战2.1 GPIO和时钟配置先来看最基础的硬件初始化。我习惯把PB6和PB7用作IIC1的SCL和SDA线这两个引脚需要配置为复用开漏输出模式。这里有个坑要注意必须开启GPIO时钟和IIC外设时钟漏掉任何一个都会导致通信失败。// 开启GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启I2C1时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_OD; // 复用开漏 GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct);2.2 IIC参数设置接下来是IIC外设的核心配置。STM32F103的硬件IIC最高支持400kHz实际项目中我一般用100kHz就足够稳定。这里有个关键点I2C_DutyCycle_2表示SCL高低电平时间比为2:1这是标准模式下的典型配置。I2C_InitTypeDef I2C_InitStruct; I2C_StructInit(I2C_InitStruct); // 先用默认值初始化 I2C_InitStruct.I2C_ClockSpeed 100000; // 100kHz I2C_InitStruct.I2C_Mode I2C_Mode_I2C; I2C_InitStruct.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStruct.I2C_OwnAddress1 0x30; // 主模式可随意设置 I2C_InitStruct.I2C_Ack I2C_Ack_Enable; I2C_InitStruct.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, I2C_InitStruct); I2C_Cmd(I2C1, ENABLE);3. 事件等待函数的实现3.1 基础事件等待事件驱动编程的核心就是等待和检测特定事件。我封装了一个通用的事件等待函数这个函数会阻塞直到检测到目标事件或超时。实测发现加入超时机制能有效防止总线锁死。#define I2C_TIMEOUT 10000 // 超时计数器值 uint8_t I2C_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) { uint32_t timeout I2C_TIMEOUT; while(!I2C_CheckEvent(I2Cx, I2C_EVENT)) { if((timeout--) 0) { // 这里可以加入错误处理 return 0; // 超时返回错误 } } return 1; // 成功检测到事件 }3.2 关键事件解析STM32硬件IIC有9个标准事件但作为主设备时主要关注以下5个EV5 (I2C_EVENT_MASTER_MODE_SELECT)起始信号成功发送EV6 (I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)从机地址写方向已应答EV6 (I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)从机地址读方向已应答EV8 (I2C_EVENT_MASTER_BYTE_TRANSMITTING)数据寄存器空(正在发送)EV8_2 (I2C_EVENT_MASTER_BYTE_TRANSMITTED)一个字节完整发送完成4. 完整数据传输实现4.1 单字节写入流程通过事件驱动方式实现单字节写入代码逻辑会非常清晰。每个关键步骤后等待对应事件就像在跟硬件对话一样。void I2C_WriteByte(uint8_t devAddr, uint8_t regAddr, uint8_t data) { // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); // EV5 // 发送设备地址写方向 I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Transmitter); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6 // 发送寄存器地址 I2C_SendData(I2C1, regAddr); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING); // EV8 // 发送数据 I2C_SendData(I2C1, data); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // EV8_2 // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE); }4.2 多字节连续写入对于需要连续写入多个字节的场景只需要在EV8事件后循环发送数据即可。注意最后一个字节发送完成后要等待EV8_2事件。void I2C_WriteBuffer(uint8_t devAddr, uint8_t regAddr, uint8_t *pData, uint16_t len) { // 起始信号和设备地址部分与单字节写入相同 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Transmitter); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // 发送起始寄存器地址 I2C_SendData(I2C1, regAddr); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING); // 循环发送数据 while(len--) { I2C_SendData(I2C1, *pData); // 最后一个字节等待EV8_2其他等待EV8 if(len 0) { I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); } else { I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING); } } I2C_GenerateSTOP(I2C1, ENABLE); }5. 数据读取的实现5.1 单字节读取流程读取操作比写入稍复杂需要先发送设备地址写方向来指定寄存器地址然后再发送重复起始信号和设备地址读方向。uint8_t I2C_ReadByte(uint8_t devAddr, uint8_t regAddr) { uint8_t data; // 第一阶段发送寄存器地址 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Transmitter); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); I2C_SendData(I2C1, regAddr); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 第二阶段重新启动并读取数据 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Receiver); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // 准备接收最后一个字节 I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一个字节不应答 I2C_GenerateSTOP(I2C1, ENABLE); // 提前发送停止信号 // 等待数据接收完成 I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED); data I2C_ReceiveData(I2C1); // 恢复应答使能 I2C_AcknowledgeConfig(I2C1, ENABLE); return data; }5.2 多字节连续读取多字节读取时除了最后一个字节外其他字节都需要发送应答信号。这个细节很容易出错我在实际项目中就踩过这个坑。void I2C_ReadBuffer(uint8_t devAddr, uint8_t regAddr, uint8_t *pData, uint16_t len) { // 第一阶段发送寄存器地址与单字节读取相同 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Transmitter); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); I2C_SendData(I2C1, regAddr); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 第二阶段重新启动并准备接收数据 I2C_GenerateSTART(I2C1, ENABLE); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Receiver); I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // 接收多个字节 while(len) { if(len 1) { // 最后一个字节 I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateSTOP(I2C1, ENABLE); } I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED); *pData I2C_ReceiveData(I2C1); len--; } // 恢复应答使能 I2C_AcknowledgeConfig(I2C1, ENABLE); }6. 常见问题与调试技巧6.1 总线锁死处理硬件IIC最让人头疼的就是总线锁死问题。我的经验是在初始化前先执行一次I2C_DeInit()并在每次通信失败后复位IIC外设。void I2C_RecoverBus(void) { GPIO_InitTypeDef GPIO_InitStruct; // 1. 暂时禁用I2C I2C_Cmd(I2C1, DISABLE); // 2. 将SCL和SDA配置为普通开漏输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); // 3. 手动模拟I2C总线恢复序列 GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); // SCL和SDA拉高 for(int i0; i9; i) { GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL拉低 Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL拉高 Delay_us(5); } // 4. 发送停止条件 GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL拉低 Delay_us(5); GPIO_ResetBits(GPIOB, GPIO_Pin_7); // SDA拉低 Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL拉高 Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); // SDA拉高 // 5. 重新配置为I2C功能 GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_OD; GPIO_Init(GPIOB, GPIO_InitStruct); // 6. 重新初始化I2C I2C_DeInit(I2C1); I2C_Init(I2C1, I2C_InitStruct); I2C_Cmd(I2C1, ENABLE); }6.2 时序优化建议通过示波器观察IIC波形是调试的重要手段。我发现以下几个优化点很实用适当调整GPIO速度GPIO_Speed可以改善信号质量在连续读写时加入微小延时1-2us能提高稳定性使用10kΩ上拉电阻比4.7kΩ更适合长距离通信7. 中断方式实现虽然本文主要讲事件驱动编程但中断方式在某些场景下也很实用。这里简单给出中断配置示例// 配置I2C事件中断 NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel I2C1_EV_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); // 配置错误中断 NVIC_InitStruct.NVIC_IRQChannel I2C1_ER_IRQn; NVIC_Init(NVIC_InitStruct); // 使能I2C中断 I2C_ITConfig(I2C1, I2C_IT_EVT | I2C_IT_ERR, ENABLE);中断服务函数中可以通过I2C_GetLastEvent()获取当前事件然后根据状态机模型处理各个阶段。这种方式更适合复杂应用场景。