简介:面向51单片机学习者和嵌入式入门工程师,这份资源围绕SHT30温湿度传感器,演示了I2C总线从初始化、写命令、读数据、校验到解析的完整流程,最终通过串口打印输出实测温湿度,适合环境监测、智能家居和课程设计等场景。压缩包共36个文件,体积约83KB,核心由5个C源文件与5个头文件构成,涵盖延时、主控逻辑、串口驱动、I2C时序、SHT30寄存器操作等模块;附带Keil工程配置、可烧录的HEX文件以及LST、OBJ等编译中间产物,便于对照学习从编写源码到生成固件的完整过程。该资源已有3075人浏览学习。通过研读源码,可以掌握SHT30单次与周期测量的命令格式、温湿度数据校验及补码换算方法,也能理解I2C读写时序和UART串口输出机制;对于经常遇到通信失败或数据跳变的学习者,其中状态判断与重试思路也是很好的排错参考。整体代码划分清晰,便于按模块提取复用或二次开发。
1. 51单片机与SHT30:一片几块钱的传感器为何值得折腾
做单片机开发的人迟早会和温湿度打交道。51单片机配SHT30是一套特别省心的组合:SHT30是I2C接口的数字传感器,输出已经是校准过的16位温湿度数据,不用像DHT11那样自己抠时序读电平,也不用像DS18B20那样拉一大截线做单总线协议。这份“51单片机读取SHT30温湿度传感器程序”要解决的事情很直接——把SHT30的温湿度读出来,换算成真实数值,再送到四位数码管显示。适合刚学完I2C协议想找实战练手的人,也适合实验室、小型温室这类不需要高精度但需要稳定读数的场景。我拆完之后的感觉是:协议不难,但数据格式、时序等待和显示刷新的坑比想象中多。
2. 硬件连接与上电时序:I2C总线上的两个地址陷阱
2.1 引脚分配与上拉电阻:为什么SCL和SDA不能裸奔
拿到SHT30模块,先别急着接线。SHT30本身是3.3V器件,市面上常见的模块板大多带了电平转换电路,可以直接用5V供电;如果是裸芯片或者只有四个引脚的简易板,就得注意电平匹配。我一般用STC89C52或者别的国产51内核单片机,I/O口是高电平弱上拉,和3.3V的SHT30通信时,如果直接怼5V,长期跑会发热甚至烧掉传感器。
推荐连接方式:
| SHT30引脚 | 连接目标 | 说明 |
|---|---|---|
| VCC | 3.3V(模块板带电平转换时可接5V) | 供电,模块板注意跳线 |
| GND | 系统GND | 共地 |
| SCL | P1.0 | I2C时钟线 |
| SDA | P1.1 | I2C数据线 |
| ADDR | 不接或接GND | 决定I2C地址,见2.2节 |
SCL和SDA必须接上拉电阻,典型值4.7kΩ到10kΩ。很多模块板上已经焊了上拉电阻,如果是自己画的板子,别忘了在两个引脚上各接一个4.7kΩ电阻到3.3V。没有上拉电阻的典型症状是:I2C通信偶尔成功、大概率失败,或者干脆读不到ACK。你可能会怀疑是代码问题,实际上用示波器看SCL波形,发现上升沿慢得像爬坡,这时候换上拉电阻立竿见影。
I2C是开漏结构,总线靠上拉电阻把电平拉高,器件只能把总线拉低。没有上拉电阻,主控发高电平信号时,总线电平就是浮空的,从机根本识别不了。这个坑在51单片机软件模拟I2C时尤其隐蔽,因为51的I/O口本身有弱上拉,有时候能跑通,但批量生产时就会偶发失败。
2.2 器件地址选择:ADDR引脚决定0x44还是0x45
SHT30的I2C从机地址由ADDR引脚决定,这是新手最容易忽略的地方。ADDR接GND或悬空时,7位地址是0x44;ADDR接VCC时,7位地址变成0x45。换算成8位读写地址,写地址分别是0x88和0x89,读地址分别是0x89和0x8B。
代码里一般这样定义:
#define SHT30_ADDR_W 0x88 // ADDR=GND时的写地址,即0x44<<1 #define SHT30_ADDR_R 0x89 // ADDR=GND时的读地址这段代码需要注意:I2C总线上传输的地址字节是7位地址左移一位,最低位是读写标志位。0x44左移一位变成0x88,最低位写0就是写操作;再加1变成0x89,就是读操作。很多新手直接发送0x44,结果总线一直在等ACK,永远等不到。
如果换了一个模块,读不到数据,第一件事查ADDR引脚。有的模块把ADDR引出来让你跳线选择,有的模块固定接GND,有的默认接VCC。我遇到过一块红色PCB的模块,丝印上写着ADDR可选,实际焊了个0Ω电阻接到了VCC,地址是0x45而不是0x44,排查了整整一个下午。
2.3 上电初始化流程:为什么不能上电立刻读数据
SHT30上电后需要一段时间才能稳定,数据手册给出的典型值是上电后100微秒内完成启动,但这个时间会受电源质量影响。更稳妥的做法是上电后延时10毫秒再开始通信。另外,SHT30还支持软复位命令0x30 0xA2,发送后延时1毫秒再操作。
完整初始化流程:
void SHT30_Init(void) { I2C_Start(); I2C_SendByte(SHT30_ADDR_W); // 发送写地址 if (I2C_WaitAck() != 0) // 等待从机ACK应答 { // 上电后这里失败率高,先延时再重试 } I2C_SendByte(0x30); // 软复位命令高字节 I2C_SendByte(0xA2); // 软复位命令低字节 I2C_Stop(); delay_ms(10); // 复位后至少等10ms }这里要解释两个参数:软复位命令0x30 0xA2是SHT30固定支持的复位指令,复位后内部寄存器回到默认状态,但不影响校准数据。10毫秒的延时不是数据手册里写的,而是我在实际调试中总结的经验值——延时不够时,紧接着发送测量命令会收到NACK,第一次读数据永远失败。
核心逻辑是:I2C的Start和Stop信号必须按协议时序发送,SCL高电平时SDA产生下降沿是Start,SCL高电平时SDA产生上升沿是Stop。51单片机没有硬件I2C外设(部分增强型51有,但为了通用性多数人用软件模拟),所以这些信号的时序都要自己用延时函数控制。软件模拟I2C的时序要求并不苛刻,几百纳秒的偏差都能容忍,但前提是SCL高电平时SDA不能乱跳。
3. 读温度与湿度:从寄存器手册到第一份有效数据
3.1 测量命令与数据格式:0x2C 0x06和0x24 0x00的区别
SHT30的测量命令分两大类:单次测量和周期测量。最常用的单次测量命令是0x2C 0x06,含义是“单次测量,高可重复性,使能时钟延展”;另一条0x24 0x00含义是“单次测量,高可重复性,禁用时钟延展”。
这两条命令的区别在于:使能时钟延展(Clock Stretching)时,传感器会在测量期间把SCL拉低,告诉主控“我还没准备好,别发数据”;禁用时,主控需要自己延时等待测量完成。51单片机软件模拟I2C时,我更推荐用0x24 0x00,自己延时20毫秒再去读,逻辑更可控。
测量完成后,SHT30返回6字节数据:
| 字节 | 内容 |
|---|---|
| Byte0 | 温度数据高8位 |
| Byte1 | 温度数据低8位 |
| Byte2 | 温度数据CRC8校验值 |
| Byte3 | 湿度数据高8位 |
| Byte4 | 湿度数据低8位 |
| Byte5 | 湿度数据CRC8校验值 |
温湿度都是16位无符号整数,范围0到65535。换算公式是:
温度 = -45 + 175 × rawTemp / 65535
湿度 = 100 × rawHumi / 65535
这个公式是SHT30内部校准后的标准换算公式,但注意:温度数据范围是-40°C到+125°C,湿度是0%到100%。如果算出来湿度超过100%,说明数据有问题,大概率是读取时序错了。
3.2 正确读取6字节数据:不能多读也不能少读
读取时序是:先发读地址,然后连续读6个字节,前5个字节接收后要回复ACK,最后一个字节回复NACK告诉传感器“够了,别发了”,然后发送Stop。
很多51例程里会把6个字节都回ACK,最后一个也回ACK,这样读出来的第6个字节可能是下一帧数据的开头,导致数据错位。原因在于:I2C的读操作由主控决定何时结束,最后一个字节如果不回NACK,传感器会认为你还要继续读。
读取函数:
void SHT30_ReadData(unsigned char *buf) { unsigned char i; I2C_Start(); I2C_SendByte(SHT30_ADDR_R); // 读地址0x89 I2C_WaitAck(); for (i = 0; i < 6; i++) { buf[i] = I2C_RecvByte(); // 读取一字节 if (i < 5) { I2C_SendAck(0); // 前5个字节回ACK } else { I2C_SendAck(1); // 最后一个字节回NACK } } I2C_Stop(); }关键点:I2C_SendAck(0)表示主机在SCL高电平期间拉低SDA,这是确认信号;I2C_SendAck(1)表示主机释放SDA,让SDA保持高电平,这是非确认信号。很多初学者把这两个参数写反,导致数据错位或总线挂死。
在发送读地址之前,要确认前面测量命令的延时已经结束。常见做法是延时20毫秒。如果你用0x2C 0x06(使能时钟延展),理论上SCL会被拉低等待测量完成,但51软件模拟I2C很难可靠检测SCL被拉低的时机,所以我一般直接延时,简单粗暴且稳定。
3.3 温湿度换算模块代码:避开整数溢出
拿到6字节原始数据后,第一步是拼装16位整数,第二步是换算。这里有个C语言的经典坑:表达式175 * rawTemp中,rawTemp是unsigned int类型,最大值65535,175乘以65535等于11468625,远超16位整数的范围,编译器会悄悄截断,算出来的结果完全错误。必须把其中一个操作数强制转成unsigned long:
float SHT30_GetTemperature(unsigned char *buf) { unsigned int rawTemp; float temp; // 拼装16位温度数据:高字节左移8位,或上低字节 rawTemp = ((unsigned int)buf[0] << 8) | buf[1]; // 强制转换成unsigned long再乘,避免整数溢出 temp = -45.0f + 175.0f * (float)rawTemp / 65535.0f; return temp; }参数说明:buf[0]是温度高字节,buf[1]是温度低字节。移位运算前必须转成unsigned int,否则buf[0]是unsigned char类型(8位),左移8位直接溢出变0,拼出来的数据永远不对。转换公式中的-45.0f和175.0f是SHT30数据手册给出的固定参数。
湿度换算同理:
float SHT30_GetHumidity(unsigned char *buf) { unsigned int rawHumi; float humi; rawHumi = ((unsigned int)buf[3] << 8) | buf[4]; humi = 100.0f * (float)rawHumi / 65535.0f; return humi; }两个函数返回的都是float类型。注意:51单片机默认不开启浮点运算,Keil C51编译器要勾选浮点库支持,否则链接报错。另外,浮点数在51上运算速度很慢,一个除法和乘法可能要几十微秒,但对这个场景来说完全够用。如果追求速度,可以转成定点数,比如用放大100倍的整数表示温湿度,运算全用整数完成。
4. 四位数码管显示:整数小数拆分与刷新策略
4.1 动态扫描与显示缓冲区:为什么显示会闪
SHT30读出来的是浮点数,数码管不能直接显示浮点,需要把小数部分和整数部分拆开。我用四位数码管,温度显示格式是“XX.X”,湿度显示格式是“XX.X”。如果温度是负的,就多显示一个负号,但四位数码管空间有限,我一般只显示温度舍入后的整数和一位小数,负号用第一位显示。
数码管驱动原理:动态扫描就是逐位点亮,利用人眼视觉暂留造成“同时亮”的错觉。每一位点亮时间通常5到10毫秒,四位一循环就是20到40毫秒一帧,刷新率大约25到50Hz,不会闪烁。
显示代码:
unsigned char code SEG_CODE[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, // 0,1,2,3,4 0x6D, 0x7D, 0x07, 0x7F, 0x6F // 5,6,7,8,9 }; unsigned char display_buf[4]; // 显示缓冲区,下标0~3对应千位到个位 void Display_Scan(void) { unsigned char i; for (i = 0; i < 4; i++) { P2 = 0x01 << i; // 位选:选中第i位数码管 P0 = SEG_CODE[display_buf[i]]; // 段选:输出字形码 delay_us(200); // 每一位点亮200us P0 = 0x00; // 消隐,防止拖影 } }参数说明:P2口做位选,P0口做段选,这是最常见的51接法。0x01 << i依次选中第0、1、2、3位。delay_us(200)控制每一位的点亮时间,太短亮度不够,太长会闪烁。消隐那句P0 = 0x00很重要——如果不在切换位选之前清掉段选,上一位的字形会串到下一位,出现拖影。
4.2 温湿度切换显示与小数点处理
要在同一组数码管上切换显示温度和湿度,最简单的方法是加一个按键,每次按下切换显示模式。温度模式点亮个位的小数点,湿度模式也点亮个位的小数点,让用户知道当前显示的是什么。
拆分数值:
void Float2Display(float value) { int integer_part; int decimal_part; unsigned char flag_negative = 0; if (value < 0) { flag_negative = 1; value = -value; } integer_part = (int)value; // 取整数部分 decimal_part = (int)((value - integer_part) * 10); // 取一位小数 display_buf[0] = flag_negative ? 0x40 : 0x00; // 第一位显示负号或无 display_buf[1] = integer_part / 10; // 十位 display_buf[2] = integer_part % 10; // 个位 display_buf[3] = decimal_part; // 小数位 // 个位加上小数点,0x80是共阴数码管小数点的段码 P0 = SEG_CODE[display_buf[2]] | 0x80; }这里有个细节:(int)value强制转换直接去掉小数部分,不是四舍五入。如果温度是25.76,整数部分是25,小数部分是(25.76 - 25) * 10 = 7.6,再转int得到7,显示25.7而非25.8。如果在意精度,可以用(int)(value * 10 + 0.5)做一位小数的四舍五入。
4.3 主循环的组织方式:读取和显示如何共存
很多人写这个项目会把SHT30读取和数码管显示都放在主循环里,结果发现数码管闪得厉害。原因很简单:SHT30单次测量要等20毫秒,这期间如果在主循环里死等,数码管就停止扫描了,用户看到的是“亮一下、灭一下”的闪烁效果。
我一般把显示扫描放进定时器中断,主循环只管采集数据:
void Timer0_ISR(void) interrupt 1 { TH0 = 0xFC; // 12MHz晶振,1ms定时 TL0 = 0x18; Display_Scan(); // 每1ms扫描一次数码管 } void main(void) { unsigned char sht30_buf[6]; float temp, humi; unsigned char mode = 0; // 0=温度 1=湿度 SHT30_Init(); Timer0_Init(); while (1) { // 发送单次测量命令 I2C_Start(); I2C_SendByte(SHT30_ADDR_W); I2C_SendByte(0x24); // 单次测量,高可重复性 I2C_SendByte(0x00); // 禁用时钟延展 I2C_Stop(); delay_ms(20); // 等测量完成 SHT30_ReadData(sht30_buf); temp = SHT30_GetTemperature(sht30_buf); humi = SHT30_GetHumidity(sht30_buf); if (mode == 0) { Float2Display(temp); } else { Float2Display(humi); } // 按键切换模式,防抖延时 if (KEY == 0) { delay_ms(10); if (KEY == 0) { mode = !mode; } } } }这段代码的关键改进:把显示扫描放进定时器中断后,即使主循环在等SHT30的20毫秒延时,数码管也不会停。这是实时系统和前后台系统的经典分工——中断里做时间敏感的任务,主循环做耗时任务。
5. 避坑实录:从0xFFFF到校验和误差的三次翻车
5.1 现象:读出来的温度永远是0xFFFF,湿度也是0xFFFF
第一次上电调试,数码管显示温度-45.0,湿度0.0,这其实是浮点数值的“无数据”表现——因为rawTemp和rawHumi都是0xFFFF,代入公式正好算出-45和0。这个现象背后的数据含义是:I2C总线上一片空白,传感器没有应答,读回来的字节全是高电平。
排查过程:先查地址。如果ADDR引脚判断错误,写地址和读地址都不对,传感器根本不会ACK。再查上拉电阻,用电表测SCL和SDA的电平,正常空闲状态应该是高电平,如果测到0V,说明上拉电阻没接或者接错了。最后查接线,SDA和SCL接反也会导致完全相同的问题。
解决:换一个已知好用的模块,排除硬件故障;用逻辑分析仪抓I2C波形,看Start信号之后有没有ACK。这个坑的教训是:SHT30不像DHT11那样有一个“固定协议”,它是标准I2C设备,所有调试手段都要围绕I2C协议来。
5.2 现象:湿度读数偶尔超过100%,跳到102%甚至120%
数据手册明确写了湿度范围是0到100%,超过100%一定是数据异常。最常见的原因是电噪声干扰——I2C线太长或者和电源线绑在一起,高电平被干扰成毛刺,读回来的数据错位。还有一个隐蔽原因:读取时序中ACK/NACK发错,导致数据字节错位,本来湿度高字节读到了温度低字节。
解决:把I2C线缩短到20厘米以内,远离继电器、电机等干扰源;在SHT30的VCC和GND之间加一个0.1μF的陶瓷电容,靠近传感器引脚放置;软件上增加数据合法性检查,超出范围就丢弃,保留上一次的有效值。
这个检查其实很重要:SHT30自带CRC8校验字节,但很多51例程根本不验证它。我建议至少做范围检查,因为CRC校验代码需要额外内存和计算时间。
5.3 现象:浮点运算结果错误,温度算出来是-45或者59.7这种固定值
用Keil C51编译时,默认的浮点运算库可能没有被正确链接,或者定点运算和浮点运算混用导致精度丢失。我遇到过一个问题:175.0f * (float)rawTemp这一步是对的,但如果把175.0f写成175,编译器会尝试用整数乘法,结果直接溢出。
解决:编译时在Options for Target里勾选“Use Floating Point”相关选项,并确保代码中所有字面量都带小数点和f后缀;调试时用串口打印rawTemp的原始值,如果原始值正常,问题就在换算公式;乘法顺序上,先除以65535再乘以175也可以,但浮点数除法更慢,不如先乘后除,前提是转成float。
5.4 现象:同一个传感器,昨天读数正常,今天全部偏大1.5°C
这个现象跟软件没关系,大概率是传感器劣化了。SHT30在长期高湿环境下,湿敏元件会老化,读数偏移是正常现象。另一个可能是传感器曾经暴露在有机溶剂蒸汽中,比如打胶固定时的三防漆气味、焊接助焊剂残留。
解决:检查SHT30的加热器功能,部分模块支持内置加热器除湿(命令0x20 0x04开启加热器),加热几分钟后读数会恢复正常;如果长期偏移,只能做单点校准——把传感器和标准温度计放在一起,测出差值,在软件里加一个偏移量补偿。这也是为什么工业场景会用SHT35、SHT85这种更高精度的版本。
6. 进阶玩法:CRC8校验、低功耗测量与数据滤波
6.1 CRC8校验:让每个字节都有据可查
SHT30返回的6字节数据中,Byte2是温度数据的CRC8校验值,Byte5是湿度数据的CRC8校验值。CRC8的多项式是0x31,初始值是0xFF。在51上实现CRC8校验并不难:
unsigned char SHT30_CRC8(unsigned char *data, unsigned char len) { unsigned char crc = 0xFF; unsigned char 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; }用法很简单:读回6字节后,对buf[0]和buf[1]计算CRC8,看结果是否等于buf[2];对buf[3]和buf[4]计算CRC8,看是否等于buf[5]。如果CRC校验失败,说明传输过程中数据被干扰,读取的值不可信。从那以后我每次联调,都强制走一遍“先软复位、再发送测量命令、延时足够、检查CRC、再显示”的完整流程,这个习惯帮我省掉了大把排查时间,希望帮到你。
6.2 低功耗测量:用周期模式降低功耗
如果设备用电池供电,单次测量每秒钟读一次就够了,测完让SHT30进入空闲状态。SHT30支持周期测量模式,命令0x20 0x30表示每秒测一次,0x20 0x32表示每2秒测一次,0x20 0x34表示每4秒测一次。周期模式下传感器自动按设定频率测量,主控随时可以读取最新数据,同时读取时不需要再发测量命令。
周期模式的功耗在几百微安级别,比单次模式连续读取低一个数量级。我自己做低功耗设计时,主控采用定时唤醒,每隔10秒给SHT30发一次单次测量命令,读取完成后进入休眠,比周期模式更省电。
6.3 数据滤波:应对读数抖动的简单办法
SHT30的理论精度是±0.3°C(典型值),但实际读数会出现±0.1°C的小幅抖动,这是正常现象。如果显示值跳来跳去让人不舒服,可以做一次简单的一阶低通滤波:
float filtered_temp; float filter_factor = 0.2f; // 滤波系数,越小越平滑 filtered_temp = filter_factor * new_temp + (1.0f - filter_factor) * filtered_temp;filter_factor取值0.1到0.3之间,值越小,输出越平滑,但响应越慢。我实测下来,0.2是个不错的折中:传感器读数每秒更新一次,滤波后肉眼几乎看不出抖动,同时温度变化1°C时,显示在5秒内跟上。这个方法同样适合湿度数据。
点开资源包时,先确认里面有没有带CRC校验函数,如果没有,建议按上面的代码补上。串口打印版本的程序比数码管版本更适合调试,建议先跑串口版确认数据正常,再接数码管显示。资金有富余就多买几个不同ADDR地址的模块,同时在I2C总线上挂两个SHT30,一个测环境,一个测设备内部温度,能少踩很多硬件选型的坑。
本文还有配套的精品资源,点击获取