1. IIC通信基础与STM32实战价值
第一次接触IIC通信时,我被它的简洁性惊艳到了——两根线就能搞定主从设备之间的数据交互。在实际项目中,这种特性让PCB布线变得异常简单,特别是当你的STM32F103C8T6需要连接多个传感器时。记得去年做智能农业监测系统,一块核心板通过IIC同时接入了温湿度、光照强度和土壤PH值三个传感器,省下的IO口正好用来驱动液晶屏。
IIC协议最精妙的设计在于它的地址寻址机制。每个从设备都有唯一的7位或10位地址,就像每户人家的门牌号。主设备发起通信时先"喊一嗓子"目标地址,只有地址匹配的从设备才会响应。这里有个实用技巧:很多传感器(比如SHT30)的地址引脚是可配置的,通过拉高或拉低某个引脚电平,可以让同一个传感器使用不同地址,这样就能在一条总线上挂载多个相同型号的设备。
STM32F103C8T6虽然自带硬件IIC外设,但在实际项目中我更喜欢用GPIO模拟。原因有三:首先硬件IIC的时钟配置比较麻烦,遇到不同速度的设备要反复调整;其次当通信异常时,软件模拟更容易加入调试信息;最重要的是,模拟IIC的代码移植性极强,换个单片机平台改改GPIO定义就能用。不过要注意,C8T6这款芯片的GPIO翻转速度最快也就18MHz,模拟IIC时时钟频率建议控制在100-400KHz之间。
2. 硬件搭建与GPIO配置要点
动手前先准备好这些材料:STM32F103C8T6最小系统板、SHT30温湿度传感器模块、4.7KΩ上拉电阻两个、杜邦线若干。这里特别提醒,虽然很多传感器模块已经内置上拉电阻,但实际测试发现并联使用更稳定。我曾遇到过IIC通信时好时坏的情况,后来在SDA和SCL线上各加了个4.7KΩ电阻到3.3V,问题立刻解决。
GPIO配置是软件IIC的关键第一步。推荐将PB6和PB7这两个引脚用作模拟IIC,因为它们正好对应着硬件IIC1的SCL和SDA,方便后期切换。配置时要特别注意三点:
- 模式选择开漏输出(GPIO_Mode_Out_OD),这样配合外部上拉才能实现真正的线与逻辑
- 速度设为50MHz(GPIO_Speed_50MHz),确保信号边沿足够陡峭
- 初始化时先将两个引脚置高,避免总线锁死
具体配置代码可以这样写:
void IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 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); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); // 初始化为高电平 }3. 时序模拟与基础通信函数
写IIC驱动就像在玩音乐节拍器,每个动作都要踩准时间点。起始信号的动作顺序是:SCL高电平时拉低SDA,就像指挥家举起指挥棒的瞬间。实测发现,起始信号前后的延时很关键,太快会导致从设备识别失败,太慢又影响通信效率。经过多次示波器抓取波形,我总结出最稳定的延时组合是:起始信号前延时5μs,起始信号后延时4μs。
字节传输函数要特别注意数据有效性规则——SCL高电平期间SDA必须保持稳定。常见错误是边改变数据边拉高时钟,这会导致数据错乱。正确的做法是:先设置SDA电平,延时确保信号稳定,再拉高SCL,保持足够时间后拉低SCL。这里分享一个经过实战检验的字节发送函数:
void IIC_Send_Byte(uint8_t byte) { for(uint8_t i=0; i<8; i++) { if(byte & 0x80) GPIO_SetBits(GPIOB, GPIO_Pin_7); else GPIO_ResetBits(GPIOB, GPIO_Pin_7); IIC_Delay(2); // 2μs数据建立时间 GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL拉高 IIC_Delay(4); // 4μs时钟高电平保持 byte <<= 1; GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL拉低 IIC_Delay(2); // 2μs时钟低电平保持 } // 接收ACK GPIO_SetBits(GPIOB, GPIO_Pin_7); // 释放SDA IIC_Delay(2); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL拉高 IIC_Delay(4); uint8_t ack = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7); GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL拉低 IIC_Delay(2); if(ack) { // 处理无应答情况 } }4. SHT30传感器驱动实战
以SHT30为例,这个传感器的典型地址是0x44(7位地址左移一位后为0x88),支持单次测量和周期测量两种模式。在智能家居项目中,我推荐使用单次测量模式,虽然每次都要发指令,但更省电。传感器上电后需要至少15ms的启动时间,这个细节很多教程都没提,直接导致第一次读取失败。
完整的温湿度采集流程分四步:
- 发送启动命令:0x2C06(高精度模式)
- 等待测量完成(约15ms)
- 读取6字节数据(温度高/低/CRC,湿度高/低/CRC)
- 进行数据校验和转换
这里有个容易踩坑的点:SHT30返回的数据是Big-endian格式,而STM32是Little-endian架构,直接强制类型转换会得到错误值。正确的处理方式如下:
float SHT30_Read_Temperature(void) { uint8_t data[6]; IIC_Start(); IIC_Send_Byte(0x88); // 写地址 IIC_Send_Byte(0x2C); // 命令高位 IIC_Send_Byte(0x06); // 命令低位 IIC_Stop(); Delay_ms(20); // 等待测量完成 IIC_Start(); IIC_Send_Byte(0x89); // 读地址 data[0] = IIC_Read_Byte(1); // 温度高字节 data[1] = IIC_Read_Byte(1); // 温度低字节 data[2] = IIC_Read_Byte(1); // CRC校验(实际项目要校验) // 湿度数据读取省略... IIC_Stop(); uint16_t temp_raw = (data[0] << 8) | data[1]; return -45 + 175 * (float)temp_raw / 65535.0f; }5. 错误处理与性能优化
稳定的IIC驱动必须包含超时检测机制。我曾遇到传感器接触不良导致程序卡死的情况,后来给每个关键操作都加了超时判断。比如起始信号发送函数可以这样改进:
bool IIC_Start_With_Timeout(uint32_t timeout) { uint32_t tick = Get_System_Tick(); GPIO_SetBits(GPIOB, GPIO_Pin_7 | GPIO_Pin_6); while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == RESET) { if(Get_System_Tick() - tick > timeout) return false; } // 剩余起始信号代码... return true; }通信速率优化方面,有三个实用技巧:
- 将延时函数改为基于SysTick的精确延时,避免因编译器优化导致时序错乱
- 在非关键路径上适当减少延时,比如字节间隔可以从5μs缩短到3μs
- 使用DMA+GPIO翻转来实现硬件加速(适合高速模式)
最后分享一个调试秘诀:用逻辑分析仪抓取波形时,如果发现ACK信号异常,八成是上拉电阻值不合适。标准模式下4.7KΩ最稳妥,快速模式建议2.2KΩ,超过400KHz的高速模式要用1KΩ以下。