公司动态

STM32 IIC协议驱动SHT30温湿度传感器:从时序到数据解析全流程

📅 2026/8/1 4:18:45
STM32 IIC协议驱动SHT30温湿度传感器:从时序到数据解析全流程
1. 项目概述从IIC协议到SHT30温湿度数据做嵌入式开发尤其是和传感器打交道温湿度采集是个绕不开的经典项目。你可能用过DHT11简单是简单但精度和响应速度总感觉差那么点意思特别是在一些对数据质量有要求的场合比如环境监测、仓储管理或者精密仪器内部的环境补偿。这时候像SHT30这类基于IIC总线的高精度数字温湿度传感器就成了更优的选择。我这次要聊的就是如何通过IIC协议稳定、可靠地从SHT30传感器中读取温湿度数据。这不仅仅是调用一个库函数那么简单它涉及到对IIC通信时序的精准把控、对传感器命令集的正确理解以及对采样数据的校验与换算。整个过程就像和传感器进行一次严谨的对话你说发送命令它听接收并执行然后它回答返回数据你再解读校验并计算。很多新手容易在这里栽跟头要么时序不对通信失败要么数据校验出错不自知拿到的温湿度值跳得跟心电图似的。这个内容非常适合正在学习STM32、ESP32等单片机开发想要深入理解IIC总线实操或者需要在项目中集成高精度环境传感器的朋友。我会从最底层的时序波形讲起一步步带你完成从硬件连接到软件驱动再到数据解析的全过程并分享几个我调试时踩过的“坑”和总结的“稳如老狗”的经验。你会发现吃透了IIC和SHT30市面上绝大多数IIC接口的传感器你都能举一反三手到擒来。2. IIC协议核心精要与SHT30特性解析2.1 IIC协议两根线的艺术IICInter-Integrated Circuit总线凭借其简洁的物理连接仅需两根线SDA数据线和SCL时钟线和支持多主多从的架构在嵌入式领域占据了绝对重要的地位。理解它不能只停留在“开始信号-发送地址-读写数据-停止信号”这个流程上必须深入到时序和状态里去看。核心要点一开漏输出与上拉电阻。这是IIC硬件设计的第一课。SDA和SCL线都要求接上拉电阻通常4.7kΩ~10kΩ这是因为总线上的设备主设备和所有从设备其IO口都配置为开漏输出模式。开漏输出只能主动将线路拉低输出0而不能主动拉高输出1。当所有设备都不拉低线路时由上拉电阻将线路拉到高电平1。这种“线与”特性是实现多设备仲裁的基础任何一个设备输出低电平整条线就是低电平。如果你忘记接上拉电阻或者单片机IO模式配置成了推挽输出通信百分之百会失败。核心要点二地址与读写位。IIC设备都有一个7位或10位的地址。SHT30通常是7位地址常见的有0x44和0x45由ADDR引脚电平决定。主设备发起通信时发送的是8位数据高7位是从机地址最低位是读写控制位0表示写1表示读。所以向地址0x44的SHT30写命令时发送的从机地址字节实际是0x44 1 | 0 0x88从它读取数据时地址字节则是0x44 1 | 1 0x89。这个概念混淆是很多驱动写不对的根源。核心要点三ACK/NACK机制。这是IIC的握手信号。主设备每发送完8位数据或地址都会释放SDA线并在第9个时钟脉冲期间检测从设备是否将SDA拉低。拉低即为ACK应答表示从设备成功接收保持高电平则为NACK非应答通常表示出错或通信结束。同样主设备读取数据时在读完最后一个字节后需要给从设备发送一个NACK信号紧接着发送停止信号告知从设备“我读完了谢谢”。注意很多模拟IIC的代码bug就出在ACK检测的时序上。SCL拉高后必须有一个短暂的延时再读取SDA线状态给从设备足够的时间去驱动SDA。这个延时通常需要微秒级具体取决于总线上设备的响应速度。2.2 SHT30传感器命令集与采样模式SHT30是Sensirion公司出品的经典数字温湿度传感器精度高湿度±2%RH温度±0.2℃响应快并且内部集成了标定数据直接输出数字量省去了复杂的模拟信号调理电路。关键命令解析SHT30通过发送特定的命令字来控制其工作模式。命令字为16位2个字节发送时需先发高字节MSB再发低字节LSB。几个最常用的命令单次采样时钟拉伸禁止0x2400(MSB: 0x24, LSB: 0x00)。这是最常用的模式主机发起一次测量传感器完成后将数据存入寄存器主机再去读取。无时钟拉伸对主机时序控制要求严格。单次采样时钟拉伸使能0x2C06(MSB: 0x2C, LSB: 0x06)。主机发起测量后SCL线会被传感器拉低时钟拉伸直到测量完成。这简化了主机端的等待逻辑但会独占IIC总线。周期性采样例如0x2032(1次/秒)。启动传感器以固定频率自动测量数据就绪后主机可直接读取适合需要频繁获取数据的场景。采样数据格式一次完整的测量会返回6个字节数据顺序如下温度数据高8位温度数据低8位温度数据的CRC8校验和湿度数据高8位湿度数据低8位湿度数据的CRC8校验和温度和湿度数据都是16位无符号整数Big-Endian即高字节在前。必须对每组数据的CRC校验和进行验证这是确保数据在传输过程中没有出错的关键步骤。SHT30使用的CRC8多项式是x^8 x^5 x^4 1初始值为0xFF。忽略CRC校验相当于相信数据永远正确在实际工程中这是非常危险的做法。数据换算公式验证CRC通过后将16位原始数据转换为实际物理值。温度T (°C) -45 175 * (S_T / 65535)。其中S_T是16位的温度原始值。湿度RH (%RH) 100 * (S_RH / 65535)。其中S_RH是16位的湿度原始值。这两个公式是传感器数据手册给出的直接套用即可。计算时注意使用浮点数运算或者为了在无FPU的MCU上提高速度可以事先进行定点数优化。3. 硬件连接与模拟IIC驱动实现3.1 硬件电路设计与注意事项SHT30的硬件连接极其简单但“简单”不代表可以随意。电源VDD典型工作电压是3.3V。确保电源干净、稳定尤其在传感器模拟部分工作时电压纹波过大会影响ADC的精度。建议在VDD和GND之间并联一个100nF的陶瓷去耦电容位置尽量靠近传感器的电源引脚。IIC总线SCL和SDA分别连接到MCU的任意两个GPIO需支持开漏模式。切记这两条线上必须各接一个上拉电阻到3.3V。阻值根据总线速度和布线电容选择通常4.7kΩ在标准模式100kHz和快速模式400kHz下都能良好工作。如果总线较长或设备较多可以适当减小阻值以增强驱动能力但会增加功耗。地址选择引脚ADDR此引脚电平决定了传感器的7位IIC地址。接GND时地址为0x44接VDD时地址为0x45。如果总线上只有一个SHT30通常接地即可。如果需要挂载两个SHT30则可以通过配置不同的ADDR电平来实现。Alert引脚这是一个可选的输出引脚当测量值超出用户设定的阈值窗口时会触发警报。如果不需要此功能可以悬空。实操心得布线时尽量让IIC总线走线简短远离高频或大电流线路以减少干扰。如果通信不稳定第一个要检查的就是上拉电阻和电源。我曾遇到一个案例电源纹波过大导致传感器内部工作异常通信时好时坏换了LDO并加强滤波后才解决。3.2 基于STM32 HAL库的模拟IIC驱动虽然很多MCU有硬件IIC外设但其复杂的状态机和中断处理常常让初学者头疼且不同厂商的硬件IIC库可能存在兼容性问题。因此使用GPIO模拟IIC软件IIC在项目初期或对时序有绝对控制需求的场合反而更加直观和可靠。这里以STM32的HAL库为例展示模拟IIC的底层驱动。首先定义GPIO和时序相关的宏方便修改和移植// 定义IIC所使用的GPIO端口和引脚 #define IIC_SDA_PORT GPIOB #define IIC_SDA_PIN GPIO_PIN_7 #define IIC_SCL_PORT GPIOB #define IIC_SCL_PIN GPIO_PIN_6 // 定义IO操作宏提高代码可读性和执行效率 #define IIC_SDA_HIGH() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET) #define IIC_SDA_LOW() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET) #define IIC_SCL_HIGH() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET) #define IIC_SCL_LOW() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET) #define IIC_SDA_READ() HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN) // 定义时序延时根据MCU主频调整。此处假设主频72MHz延时约5us。 #define IIC_DELAY() delay_us(5) // 一个简单的微秒延时函数可以用SysTick或定时器实现 void delay_us(uint32_t us) { uint32_t ticks us * (SystemCoreClock / 1000000) / 5; // 粗略计算 while(ticks--); }接下来实现最基础的IIC时序函数起始、停止、发送一位、接收一位、发送字节、接收字节。// 产生IIC起始信号SCL高电平期间SDA产生一个下降沿。 void IIC_Start(void) { IIC_SDA_HIGH(); IIC_SCL_HIGH(); IIC_DELAY(); IIC_SDA_LOW(); IIC_DELAY(); IIC_SCL_LOW(); // 钳住总线准备发送数据 IIC_DELAY(); } // 产生IIC停止信号SCL高电平期间SDA产生一个上升沿。 void IIC_Stop(void) { IIC_SDA_LOW(); IIC_DELAY(); IIC_SCL_HIGH(); IIC_DELAY(); IIC_SDA_HIGH(); IIC_DELAY(); } // 发送一个字节8位数据。返回从机是否应答0-应答1-非应答。 uint8_t IIC_SendByte(uint8_t byte) { uint8_t i, ack; for (i 0; i 8; i) { if (byte 0x80) IIC_SDA_HIGH(); // 发送最高位 else IIC_SDA_LOW(); IIC_DELAY(); IIC_SCL_HIGH(); IIC_DELAY(); // 确保SCL高电平周期足够从机在此采样 IIC_SCL_LOW(); IIC_DELAY(); byte 1; // 左移准备发送下一位 } // 读取第9个时钟周期的ACK位 IIC_SDA_HIGH(); // 主机释放SDA线 IIC_DELAY(); IIC_SCL_HIGH(); IIC_DELAY(); ack IIC_SDA_READ(); // 读取从机应答信号 IIC_SCL_LOW(); IIC_DELAY(); return ack; // 0为ACK1为NACK } // 接收一个字节。参数ack_flag决定接收后主机是否发送应答0-发送ACK1-发送NACK。 uint8_t IIC_ReadByte(uint8_t ack_flag) { uint8_t i, byte 0; IIC_SDA_HIGH(); // 确保主机释放SDA由从机控制 for (i 0; i 8; i) { byte 1; IIC_SCL_HIGH(); IIC_DELAY(); if (IIC_SDA_READ()) byte | 0x01; // 读取SDA线状态 IIC_SCL_LOW(); IIC_DELAY(); } // 发送应答位 if (ack_flag 0) IIC_SDA_LOW(); // 发送ACK else IIC_SDA_HIGH(); // 发送NACK IIC_DELAY(); IIC_SCL_HIGH(); IIC_DELAY(); IIC_SCL_LOW(); IIC_DELAY(); IIC_SDA_HIGH(); // 释放SDA线 return byte; }有了这些底层函数我们就可以构建与SHT30通信的中间层函数了例如发送命令和读取数据块。// 向SHT30发送一个16位的命令 uint8_t SHT30_Write_Cmd(uint16_t cmd) { uint8_t ack; IIC_Start(); // 发送写地址 (0x44 1) | 0 0x88 ack IIC_SendByte(0x88); if (ack) goto error; // 如果无应答跳转到错误处理 // 发送命令高字节 ack IIC_SendByte((cmd 8) 0xFF); if (ack) goto error; // 发送命令低字节 ack IIC_SendByte(cmd 0xFF); if (ack) goto error; IIC_Stop(); return 0; // 成功 error: IIC_Stop(); return 1; // 失败 } // 从SHT30读取6个字节的测量数据 uint8_t SHT30_Read_Data(uint8_t* dat) { uint8_t ack, i; IIC_Start(); // 发送读地址 (0x44 1) | 1 0x89 ack IIC_SendByte(0x89); if (ack) goto error; // 连续读取6个字节数据前5个字节后需要发送ACK最后一个字节后发送NACK for (i 0; i 6; i) { if (i 5) { dat[i] IIC_ReadByte(1); // 最后一个字节发送NACK } else { dat[i] IIC_ReadByte(0); // 非最后一个字节发送ACK } } IIC_Stop(); return 0; // 成功 error: IIC_Stop(); return 1; // 失败 }4. 完整的温湿度采样流程与数据解析4.1 单次采样流程代码实现将底层驱动组合起来形成一个完整的单次温湿度采样函数。我们选择“单次采样时钟拉伸禁止”模式命令0x2400这意味着我们需要在发送启动命令后主动等待一段测量时间对于SHT30在高速模式下的典型测量时间是15ms然后再去读取数据。// SHT30单次测量并读取数据 // 参数temp - 用于返回温度值摄氏度 humi - 用于返回湿度值百分比 // 返回值0成功1通信失败2CRC校验失败 uint8_t SHT30_Get_Temp_Humi(float* temp, float* humi) { uint8_t data[6]; uint16_t raw_temp, raw_humi; // 1. 发送测量命令0x2400 高速模式时钟拉伸禁止 if (SHT30_Write_Cmd(0x2400)) { return 1; // 命令发送失败 } // 2. 等待测量完成。数据手册标明最大测量时间此处等待20ms确保完成。 HAL_Delay(20); // 使用HAL库的延时或自己实现的毫秒延时 // 3. 读取6个字节的测量数据 if (SHT30_Read_Data(data)) { return 1; // 数据读取失败 } // 4. 校验CRC if (Check_CRC8(data[0], 2, data[2]) ! 0) { // 校验温度数据CRC return 2; // 温度CRC错误 } if (Check_CRC8(data[3], 2, data[5]) ! 0) { // 校验湿度数据CRC return 2; // 湿度CRC错误 } // 5. 数据转换 raw_temp (data[0] 8) | data[1]; // 组合温度原始值 raw_humi (data[3] 8) | data[4]; // 组合湿度原始值 *temp (float)raw_temp * 175.0f / 65535.0f - 45.0f; // 转换为摄氏度 *humi (float)raw_humi * 100.0f / 65535.0f; // 转换为百分比湿度 return 0; // 成功 }这个函数封装了完整的操作流程调用一次即可获得温湿度值。其中Check_CRC8函数需要根据SHT30指定的多项式实现。4.2 CRC8校验算法的实现与重要性CRC校验是保证数据可靠性的铁闸。SHT30使用的CRC8多项式是0x31(多项式:x^8 x^5 x^4 1)初始值为0xFF结果异或值为0x00。算法流程如下初始化一个8位的CRC寄存器为0xFF。将数据字节与CRC寄存器进行异或。循环8次 a. 判断CRC寄存器最高位第7位是否为1。 b. 如果为1则CRC寄存器左移一位然后与多项式0x31进行异或。 c. 如果为0则CRC寄存器仅左移一位。重复步骤2和3处理下一个数据字节直到所有数据字节处理完毕。最终的CRC寄存器值即为校验和。代码实现// CRC8校验函数 // data: 待校验数据指针 // len: 数据长度字节数 // crc_init: 初始值SHT30为0xFF // 返回值计算得到的CRC8值 uint8_t Calc_CRC8(const uint8_t* data, uint16_t len, uint8_t crc_init) { uint8_t crc crc_init; uint16_t i, j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x80) { crc (crc 1) ^ 0x31; // 多项式 0x31 } else { crc 1; } } } return crc; } // 检查CRC的函数 // data: 数据指针 // len: 数据长度 // crc_received: 接收到的CRC值 // 返回值0表示CRC正确非0表示错误 uint8_t Check_CRC8(const uint8_t* data, uint16_t len, uint8_t crc_received) { uint8_t crc_calc Calc_CRC8(data, len, 0xFF); return (crc_calc crc_received) ? 0 : 1; }为什么必须做CRC校验在复杂的电磁环境中IIC总线上的数据可能因干扰而出错。虽然概率不高但一旦发生你将得到一个完全错误但“看起来合理”的温湿度值。例如温度字节的一位翻转可能导致计算结果偏差好几度。CRC校验能以极高的概率发现这种错误让你的程序可以选择丢弃这次数据或者重试而不是把错误数据上报给系统。这是工业级应用和玩具级应用的一个重要区别。4.3 数据滤波与软件抗干扰处理即使通信和校验都通过了传感器读回来的原始数据也可能因为环境噪声如电源噪声、空气微流动而有微小跳动。对于显示或控制来说这种跳动会影响用户体验。因此适当的软件滤波是必要的。移动平均滤波是最简单有效的方法之一。其原理是维护一个固定长度的数据队列每次新采样值到来时替换掉队列中最旧的值然后计算队列中所有值的平均值作为输出。#define FILTER_LEN 10 // 滤波窗口长度 typedef struct { float buffer[FILTER_LEN]; uint8_t index; float sum; } MovingAverageFilter; void Filter_Init(MovingAverageFilter* f) { memset(f-buffer, 0, sizeof(f-buffer)); f-index 0; f-sum 0.0f; } float Filter_Update(MovingAverageFilter* f, float new_val) { // 减去即将被替换的旧值 f-sum - f-buffer[f-index]; // 加入新值 f-buffer[f-index] new_val; f-sum new_val; // 更新索引 f-index (f-index 1) % FILTER_LEN; // 返回平均值 return f-sum / FILTER_LEN; }在主循环中你可以这样使用MovingAverageFilter temp_filter, humi_filter; float raw_temp, raw_humi, filtered_temp, filtered_humi; // 初始化滤波器 Filter_Init(temp_filter); Filter_Init(humi_filter); while(1) { if (SHT30_Get_Temp_Humi(raw_temp, raw_humi) 0) { filtered_temp Filter_Update(temp_filter, raw_temp); filtered_humi Filter_Update(humi_filter, raw_humi); // 使用 filtered_temp 和 filtered_humi 进行显示或控制 printf(Temp: %.2f C, Humi: %.2f %%\r\n, filtered_temp, filtered_humi); } else { printf(SHT30 read error!\r\n); } HAL_Delay(1000); // 每秒采样一次 }滤波窗口长度FILTER_LEN需要根据采样频率和应用场景权衡。长度越大曲线越平滑但响应速度越慢。对于温湿度这种变化相对缓慢的量5~10点的窗口通常效果就不错。5. 调试技巧、常见问题与性能优化5.1 使用逻辑分析仪抓取IIC波形当通信失败时最有效的调试手段就是使用逻辑分析仪或者示波器的数字通道抓取SDA和SCL线上的实际波形。将分析仪的采样率设置为足够高如10MHz以上然后连接SDA、SCL和地线触发一次完整的读写操作。如何分析波形看起始和停止信号起始信号SDA在SCL高时由高变低和停止信号SDA在SCL高时由低变高是否清晰、干净边沿是否有过冲或振铃看地址和ACK主机发送的7位地址加1位读写位是否正确从机在第9个时钟周期是否给出了ACK低电平如果地址错误或从机不存在你会看到NACK高电平。看数据位每个数据位在SCL高电平期间是否稳定SDA的变化是否发生在SCL低电平期间这是IIC协议要求的看时序测量SCL的高低电平时间、起始/停止条件建立时间等是否满足SHT30数据手册的要求通常标准模式4.7us快速模式0.6us你的IIC_DELAY()是否提供了足够的延时逻辑分析仪软件如Saleae Logic通常自带IIC协议分析器可以自动解析出地址、数据、ACK/NACK极大提高了调试效率。这是定位IIC问题最直接的工具没有之一。5.2 常见问题排查速查表问题现象可能原因排查步骤与解决方案通信完全无应答1. 硬件连接错误电源、地、上拉电阻2. IIC地址错误3. 传感器损坏4. MCU IO模式配置错误1. 用万用表检查电源电压、连接是否牢固确认上拉电阻已焊接。2. 核对SHT30的ADDR引脚电平计算正确的7位和8位地址。3. 尝试更换一个传感器。4. 确认MCU的SDA和SCL引脚已配置为开漏输出模式并且使能了内部/外部上拉。偶尔通信失败数据错误1. 时序过快不满足传感器建立/保持时间2. 电源噪声或纹波过大3. 总线干扰长线、靠近噪声源4. CRC校验未通过1. 用逻辑分析仪检查时序适当增加IIC_DELAY()的延时。2. 检查电源在传感器VDD引脚就近增加一个10uF电解电容并联一个100nF陶瓷电容。3. 缩短总线长度远离电机、继电器等干扰源或尝试使用屏蔽线。4.务必实现并启用CRC校验丢弃校验失败的数据包。读取的数据值固定不变或明显错误1. 数据字节顺序弄错大小端2. 数据换算公式错误3. 读取了错误的寄存器或命令响应1. 确认组合16位数据时是高字节在前 (data[0]8 | data[1])。2. 核对数据手册中的换算公式检查代码中的常数和运算顺序。3. 确认发送的命令是否正确读取的是测量数据寄存器而不是状态寄存器。时钟拉伸导致总线卡死1. 使用了时钟拉伸使能命令但主机未正确处理拉伸2. 从机故障一直拉低SCL1. 如果使用时钟拉伸模式主机在发送读命令后必须检测SCL是否为低并等待其变高后再继续发送时钟。建议初学者先用时钟拉伸禁止模式。2. 尝试硬件复位断电重启传感器或发送软复位命令0x30A2。5.3 软件优化与高级应用优化一非阻塞式等待与状态机上面的示例代码在发送测量命令后使用了HAL_Delay(20)进行阻塞式等待。这在简单的单任务系统中没问题但在需要同时处理多个任务的RTOS或复杂状态机中这会浪费宝贵的CPU时间。更好的做法是使用状态机和非阻塞延时。typedef enum { SHT30_IDLE, SHT30_CMD_SENT, SHT30_READY_TO_READ } SHT30_State_t; SHT30_State_t sht30_state SHT30_IDLE; uint32_t measurement_start_tick 0; void SHT30_Start_Measurement_NonBlocking(void) { if (SHT30_Write_Cmd(0x2400) 0) { sht30_state SHT30_CMD_SENT; measurement_start_tick HAL_GetTick(); // 记录开始时间 } } uint8_t SHT30_Process_NonBlocking(float* temp, float* humi) { switch (sht30_state) { case SHT30_CMD_SENT: // 检查是否等待了足够的时间如20ms if (HAL_GetTick() - measurement_start_tick 20) { sht30_state SHT30_READY_TO_READ; } break; case SHT30_READY_TO_READ: // 执行读取操作 if (SHT30_Get_Temp_Humi(temp, humi) 0) { sht30_state SHT30_IDLE; return 1; // 读取成功 } else { sht30_state SHT30_IDLE; return 0; // 读取失败 } break; default: break; } return 0; // 尚未完成 }在主循环中你可以周期性地调用SHT30_Process_NonBlocking而不会阻塞其他任务。优化二降低功耗对于电池供电设备功耗是关键。SHT30在单次测量模式下的平均功耗可以非常低。降低采样频率如果不是必须不要每秒采样多次。根据应用需要可以每分钟甚至每十分钟采样一次。使用MCU的低功耗模式在两次采样间隔让MCU进入睡眠模式Stop或Standby通过RTC或外部中断定时唤醒进行测量可以极大降低系统平均功耗。关闭传感器电源如果长时间不需要测量可以通过一个MOS管完全切断SHT30的电源实现零待机功耗。需要测量时再上电并注意上电后需要给传感器一定的稳定时间参考数据手册通常几毫秒即可。优化三多传感器管理与地址冲突如果需要连接多个SHT30如前所述可以通过配置不同的ADDR引脚电平一个接GND地址0x44一个接VDD地址0x45来实现。在代码中你需要为每个传感器维护独立的地址变量和滤波器状态。如果总线上的IIC设备很多还需要注意总线的电容负载过长的总线或过多的设备可能导致信号边沿变缓通信失败此时可能需要降低通信速率或使用IIC缓冲器。