51单片机读取SHT30温湿度传感器并数码管显示详解

发布时间:2026/10/14 17:22:06
51单片机读取SHT30温湿度传感器并数码管显示详解
简介面向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引脚连接目标说明VCC3.3V模块板带电平转换时可接5V供电模块板注意跳线GND系统GND共地SCLP1.0I2C时钟线SDAP1.1I2C数据线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还是0x45SHT30的I2C从机地址由ADDR引脚决定这是新手最容易忽略的地方。ADDR接GND或悬空时7位地址是0x44ADDR接VCC时7位地址变成0x45。换算成8位读写地址写地址分别是0x88和0x89读地址分别是0x89和0x8B。代码里一般这样定义#define SHT30_ADDR_W 0x88 // ADDRGND时的写地址即0x441 #define SHT30_ADDR_R 0x89 // ADDRGND时的读地址这段代码需要注意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产生下降沿是StartSCL高电平时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类型最大值65535175乘以65535等于11468625远超16位整数的范围编译器会悄悄截断算出来的结果完全错误。必须把其中一个操作数强制转成unsigned longfloat 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一个测环境一个测设备内部温度能少踩很多硬件选型的坑。本文还有配套的精品资源点击获取