
简介面向51单片机初学者与嵌入式开发者这是一套基于I²C总线读取SHT30温湿度传感器并通过串口打印数据的完整C工程。代码覆盖硬件接线、I²C初始化、测量命令发送、温湿度数据解析校验及UART串口输出等关键步骤支持单次/周期测量可直接在Keil中编译烧录用于环境监测或实验室数据采集。压缩包共36个文件包含5个c源码、5个h头文件、6个obj和6个lst编译中间文件、1个hex烧录文件以及uvproj工程配置整体仅83KB结构紧凑。已有3071人学习下载工程完整保留了SHT30的协议实现与驱动代码可帮助读者从底层理解I²C通信和传感器数据解析流程便于二次开发。1. 项目概述与整体设计思路1.1 为什么是SHT30从DHT11到数字传感器的一次进阶这周帮一个学生调51单片机的温湿度采集功能板子用的是经典的STC89C52RC传感器选了SHT30。折腾了半天发现很多人卡在同一个地方不是I2C时序写不对就是读回来的数据不会换算。我干脆把整套51单片机读取SHT30温湿度传感器程序的思路重新整理了一遍从硬件接线到代码实现再到踩坑记录一步不落写清楚。先说说为什么要选SHT30而不是烂大街的DHT11。DHT11虽然便宜、教程多但测量精度实在不敢恭维温度精度只有±2℃湿度精度±5%RH而且响应速度慢采样间隔要求1秒以上。SHT30是Sensirion推出的数字温湿度传感器温度精度±0.2℃湿度精度±2%RH走标准I2C接口通信速率最高1MHz还能通过CRC校验确保数据完整性。价格上SHT30模块也就几块钱比DHT11贵不了多少性能却是两个档次。如果你的项目对测量精度有要求或者要做长时间数据采集SHT30绝对是更靠谱的选择。这里插一句网上有人拿SHT30和AHT20、HDC1080做对比这几个都是数字I2C接口的传感器。SHT30的优势在于命令结构简单、数据手册清晰、例程多对51单片机这种资源紧张的MCU来说实现起来最省事。而且它的封装有DFN和贴片两种手工焊接也不难对DIY玩家很友好。1.2 系统架构与核心工作流程整个系统的逻辑链路并不复杂51单片机作为主控通过GPIO模拟I2C时序与SHT30通信发送测量命令后读取6字节数据经过CRC校验和公式换算得到温度和湿度值最后通过串口发送到上位机或显示在LCD1602上。如果你用的是自带硬件I2C的增强型51单片机比如STC15系列、STC8系列理论上可以走硬件I2C但大多数教程板还是以STC89C52RC为主这颗芯片没有硬件I2C外设所以模拟I2C依然是通用性最强的方案。模拟I2C的好处是任意两个IO口都能用不受硬件引脚限制坏处是需要自己控制时序对延时精度有一定要求。整个程序的调试难点集中在两个地方一是I2C时序的起始、停止、应答信号必须严格符合协议规范二是SHT30的测量命令和数据格式要和数据手册完全对应任何一位偏移都会导致读回乱码。后面我会把这两部分拆开细讲。2. 硬件连接与开发环境准备2.1 SHT30引脚功能与最小系统电路SHT30的封装引脚不多一共8个脚DFN-8但实际使用只需要关注4个VDD电源、GND地、SDA数据线、SCL时钟线。剩下还有一个ADDR引脚用于选择I2C从机地址VDD和GND之间需要接一个100nF的退耦电容模块板上通常已经集成如果你买的是裸芯片记得自己加上。从机地址这里是个重点。SHT30的I2C地址由ADDR引脚的电平决定ADDR电平从机地址接地GND0x44接高VDD0x45悬空0x44默认大多数模块板默认ADDR已接地所以地址是0x44。在I2C通信中发送地址时最低位要补一个读写标志位写操作为0读操作为1。因此向SHT30写数据时发送的地址字节是0x441即0x88读数据时发送0x89。这个移位操作是新手最容易犯的错误后面代码里会特别标注。硬件接线建议这样的SHT30引脚51单片机引脚说明VDD3.3V或5VSHT30支持2.4V~5.5V供电GNDGND共地SCLP2.1任意IO口均可代码里可改SDAP2.0任意IO口均可与SCL配合SDA和SCL需要接上拉电阻一般4.7kΩ到10kΩ都可以。如果你买的是现成模块板上已经有了上拉电阻直接接线即可。如果自己搭电路一定要加上拉否则I2C通信会不稳定表现为偶尔读回0xFF或者数据跳变。这块我在后面“常见问题”部分会详细讲。2.2 开发环境与工具链选择51单片机开发的环境选择比较固定Keil C51 STC-ISP。Keil用来编写和编译C代码STC-ISP用来下载固件到芯片。如果你用的是STC89C52RC下载方式是通过USB转TTL模块连接串口STC-ISP软件里选择对应芯片型号波特率一般选9600或115200点击下载后给板上电就能烧录。调试过程中我强烈建议在PC上接一个串口助手推荐SSCOM或XCOM把SHT30采集到的温湿度值通过串口发出来直接在电脑上看十进制数值比盯着LCD1602或者数码管猜数据高效得多。尤其是排查数据换算问题时串口打印定位问题非常快。如果你手头有逻辑分析仪哪怕是最便宜的8通道24MHz版本也能直接抓取I2C波形排查时序问题一目了然。没有的话就用示波器再不行用LED指示灯延时盲调但效率会低一些。我个人是推荐入门阶段就备一个逻辑分析仪几十块钱的投资对排查I2C、SPI这类协议问题帮助极大。3. 核心代码实现与解析3.1 I2C底层驱动从零模拟标准时序I2C总线的本质就两条线SCL时钟和SDA数据。所有通信都由主机51单片机发起时序的核心是四个基本操作起始条件、停止条件、发送字节、读取字节。先看起始和停止条件。在SCL为高电平时SDA从高变低表示起始在SCL为高电平时SDA从低变高表示停止。下面这段代码用C语言模拟这两组时序sbit SCL P2^1; sbit SDA P2^0; void I2C_Delay(void) // 约5us延时11.0592MHz晶振实测 { unsigned char i; _nop_(); _nop_(); _nop_(); i 10; while (i--); } void I2C_Start(void) { SDA 1; I2C_Delay(); SCL 1; I2C_Delay(); SDA 0; I2C_Delay(); SCL 0; // 拉低SCL准备传输数据 I2C_Delay(); } void I2C_Stop(void) { SDA 0; I2C_Delay(); SCL 1; I2C_Delay(); SDA 1; I2C_Delay(); }这里有个关键点SCL拉低之后SDA上的电平可以自由变化SCL拉高期间SDA必须保持稳定从设备在此时采样。这是I2C协议的铁律顺序写反就会导致数据错误。发送一个字节的逻辑是从最高位开始逐位把数据放到SDA上每放一位就翻转一次SCL完成一次数据采样。对于每一位的时序要先拉低SCL再设置SDA再拉高SCL再拉低SCL如此循环8次。代码实现如下void I2C_SendByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SDA (dat 0x80) 7; // 取最高位 dat 1; I2C_Delay(); SCL 1; I2C_Delay(); SCL 0; I2C_Delay(); } }发送完字节后要接收从设备的应答信号。从设备如果正常接收会在第9个时钟周期把SDA拉低表示ACK。所以主机的逻辑是释放SDA置1然后拉高SCL读SDA引脚状态0表示应答正常。这个判断很关键如果从设备没有应答说明地址错误或者通信失败。bit I2C_WaitAck(void) { SDA 1; // 释放SDA总线 I2C_Delay(); SCL 1; I2C_Delay(); if (SDA) // SDA为高无应答 { SCL 0; I2C_Delay(); return 1; // 返回1表示无应答 } SCL 0; I2C_Delay(); return 0; // 返回0表示有应答 }读取字节则是发送字节的逆过程。主机释放总线后在每个SCL高电平期间读取SDA状态从最高位开始拼装数据。SHT30的数据手册要求主机在读完最后一个字节后需要回一个NACK非应答信号告诉从设备数据已经收完不要再发了。所以读字节函数最好带一个参数用来决定最后是否发送ACK。unsigned char I2C_ReadByte(bit ack) { unsigned char i, dat 0; for (i 0; i 8; i) { SCL 1; I2C_Delay(); dat 1; if (SDA) dat | 0x01; SCL 0; I2C_Delay(); } if (ack) { SDA 0; // 发送ACK } else { SDA 1; // 发送NACK } SCL 1; I2C_Delay(); SCL 0; I2C_Delay(); SDA 1; // 释放总线 return dat; }这几个函数拼起来就是一个完整的I2C底层驱动SHT30以及后续你接其他任何I2C设备比如AT24C02存储芯片、OLED屏幕都可以复用。我建议把这段代码单独存成一个i2c.c文件后续项目直接调用。3.2 SHT30驱动测量命令与数据读取SHT30支持多种测量模式最常用的是单次测量模式clock stretching或无clock stretching两种方式。这里选了无clock stretching的单次测量模式命令是0x2C06表示高重复性测量。高重复性意味着内部ADC采样时间更长、精度更高响应时间大约4.5ms对绝大多数应用场景完全够用。发送测量命令的完整流程是I2C起始 → 发送0x880x44地址写标志→ 等待应答 → 发送0x2C命令高字节→ 等待应答 → 发送0x06命令低字节→ 等待应答 → I2C停止。代码实现void SHT30_SendMeasureCmd(void) { I2C_Start(); I2C_SendByte(0x88); // 0x44左移一位写操作 I2C_WaitAck(); I2C_SendByte(0x2C); // 命令高字节 I2C_WaitAck(); I2C_SendByte(0x06); // 命令低字节 I2C_WaitAck(); I2C_Stop(); }命令发送完成后SHT30开始内部测量。根据数据手册高重复性测量需要等待约4.5ms实际程序里我习惯延时10~20ms保险起见。然后发起I2C起始发送0x890x44地址读标志从设备应答后依次读取6个字节第1字节温度数据高8位第2字节温度数据低8位第3字节温度数据的CRC校验值第4字节湿度数据高8位第5字节湿度数据低8位第6字节湿度数据的CRC校验值注意读前5个字节时要回ACK读最后一个字节时回NACK。完整代码如下void SHT30_ReadData(unsigned char *temp_h, unsigned char *temp_l, unsigned char *humi_h, unsigned char *humi_l) { I2C_Start(); I2C_SendByte(0x89); // 0x44地址读操作 I2C_WaitAck(); *temp_h I2C_ReadByte(1); // ACK *temp_l I2C_ReadByte(1); // ACK I2C_ReadByte(1); // 温度CRC接收后丢弃 *humi_h I2C_ReadByte(1); // ACK *humi_l I2C_ReadByte(0); // NACK I2C_Stop(); }这里温度CRC那个字节可以接收但不参与换算不过后面调试时我建议把它打印出来做CRC校验能提前发现总线干扰问题。具体校验算法我放在后面的“常见问题”部分。读回原始数据后换算成实际的温度和湿度值就简单了。SHT30原始数据是16位无符号数范围0~65535对应关系如下温度-40℃~125℃换算公式T -45 175 * raw_t / 65535湿度0%~100%RH换算公式RH 100 * raw_h / 65535C语言里如果直接用整数运算要注意数据类型建议用浮点数或先乘后除再转浮点。51单片机处理浮点数有点慢但一次换算也就几十微秒完全不影响。换算代码float SHT30_CalcT(unsigned int raw_t) { return -45.0 175.0 * raw_t / 65535.0; } float SHT30_CalcRH(unsigned int raw_h) { return 100.0 * raw_h / 65535.0; }一个细节读回来的两个字节拼装成16位整数时记得是高字节在前raw temp_h 8 | temp_l;很多新手在这里把大小端搞反算出来的温度直接是负的几十度排查半天才发现是字节顺序写反了。3.3 主程序逻辑与串口输出主程序的结构非常简单初始化串口 → 延时等待传感器稳定 → 主循环里发送测量命令、延时、读取数据、换算、串口打印、再延时。SHT30不需要每次上电单独初始化直接发命令测量就行这点比DHT11那种需要时序握手的传感器省心很多。完整的main函数参考如下#include reg52.h #include stdio.h void Uart_Init(void) { SCON 0x50; // 串口模式1 TMOD 0x20; // 定时器1模式2 TH1 0xFD; // 11.0592MHz晶振波特率9600 TL1 0xFD; TR1 1; } void Delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 110; j); } void main(void) { unsigned char th, tl, hh, hl; unsigned int raw_t, raw_h; float temp, humi; Uart_Init(); Delay_ms(100); while (1) { SHT30_SendMeasureCmd(); Delay_ms(20); SHT30_ReadData(th, tl, hh, hl); raw_t (unsigned int)th 8 | tl; raw_h (unsigned int)hh 8 | hl; temp SHT30_CalcT(raw_t); humi SHT30_CalcRH(raw_h); printf(T: %.2f C, H: %.2f %%RH\r\n, temp, humi); Delay_ms(500); // 每500ms刷新一次 } }这里串口输出的格式是T: 25.36 C, H: 45.21 %RH通过串口助手就能看到连续采集的温湿度曲线。如果你用的是LCD1602显示思路完全一样只是把printf换成写LCD的函数。实际测试下来SHT30的响应速度比DHT11快得多在主频11.0592MHz的51单片机上一次完整的测量读取换算整个流程不超过30ms即便你每100ms刷新一次数据也不会出现跳动。我一般设置500ms间隔既稳定又省MCU资源如果需要更快的数据更新把最后的Delay_ms改小即可。4. 常见问题与排查技巧实录4.1 I2C总线无应答的排查思路这是我被问得最多的问题。现象是I2C_WaitAck函数一直返回1或者读回来的数据全是0xFF。按下面这几步排查八成能定位到问题。先查硬件接线。重点看SDA和SCL有没有接上拉电阻。很多DIY玩家用的是最小系统板传感器模块直连模块板上自带4.7kΩ上拉这种情况没问题但如果用的是裸芯片自己搭板漏加上拉电阻的话I2C总线高电平拉不上去从设备根本无法正常工作。用万用表量SDA、SCL对VDD的电压通信空闲时应该在3.3V或5V左右如果不是就是上拉的问题。再看地址对不对。很多新手直接把0x44填进发送函数忘了左移一位补读写标志位。0x44左移一位是0x88写操作读操作是0x89。如果你用的是ADDR接高电平的模块地址0x45那读写地址就变成0x8A和0x8B。这个从模块板上的丝印或者卖家电路图能看出来不确定的话用万用表量ADDR引脚的电平。最后看延时是否太短。51单片机的IO翻转速度有限如果你的I2C_Delay函数几乎不延时SCL频率可能超过SHT30的上限400kHz标准模式。在11.0592MHz晶振下我上面给的那个延时函数实测SCL频率在100kHz左右完全安全。如果你用的STC8系列等高速51记得适当增加延时代码中的循环次数。4.2 数据异常温度偏低、湿度超100%的原因读回来的数据能通信但数值不对这个问题同样常见。比如温度一直在-40℃附近或者湿度在0%和100%之间乱跳。温度接近-40℃即原始数据接近0的情况八成是字节拼接顺序反了。SHT30的数据是高字节在前、低字节在后拼成16位整型时一定要(unsigned int)tmp_h 8 | tmp_l。如果写反了一个正常的25℃温度原始数据约32768左右就会变成0x0080这样极小的值换算出来就接近-45℃。湿度偶尔显示100%或者超过100%则要检查是不是CPU在处理其他中断时错过了SCL高电平期间的采样。51单片机没有硬件I2C模拟时序时一旦被中断打断采样窗口就会错过。解决办法是读取数据期间关中断EA 0读完再打开EA 1。如果项目里必须保留定时器中断建议用任务调度的方法在I2C通信期间挂起中断。4.3 提高读取可靠性的几个习惯我实测过几块STC89C52RC板子发现只要把下面这几个习惯养成SHT30的读取成功率能到99.9%以上。第一启动后延时100ms再开始第一次测量。SHT30上电后内部需要一段时间稳定数据手册写的是上电后100ms即可正常工作。如果你一上电就发命令偶尔会得到不稳定的结果。第二做CRC校验。SHT30返回的温度和湿度各带一个CRC8校验字节多项式是0x31初始值是0xFF。在数据量要求高的场景比如长时间环境监测我会在解析数据前先做校验失败就丢弃本次数据下次重新读取。CRC校验代码如下unsigned char SHT30_CRC8(unsigned char *data, unsigned char len) { unsigned char crc 0xFF; unsigned char i; while (len--) { crc ^ *data; for (i 0; i 8; i) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc; }第三如果SHT30长时间不响应发一个软复位命令0x30A2让它缓过来。软复位和上电复位的效果一样寄存器恢复默认值之后重新发测量命令即可。这个操作虽然不常用但在电磁环境复杂或者热插拔传感器之后能省去重新上电的麻烦。做这个项目前前后后测了三天最大的体会是SHT30这枚传感器本身很稳定大部分问题都出在I2C时序和代码细节上。把底层驱动写好、把数据手册的时序图吃透整套程序实际上比DHT11那种单总线时序要更规整、更好调。现在这套代码我已经整理成了模板换STC15、STC8这些增强型51单片机时只需要改一下引脚定义和延时参数就能直接用。如果你也想做温湿度采集建议直接跳过DHT11从SHT30起步省下的调试时间足够你多写好几个功能模块了。本文还有配套的精品资源点击获取