ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

51单片机SHT30驱动详解:软件模拟I2C与CRC校验

51单片机SHT30驱动详解:软件模拟I2C与CRC校验 简介面向51单片机开发者这份SHT30温湿度传感器驱动源码解决在51平台上快速接入SHT30的需求。SHT30是瑞士盛思锐公司生产的高性能数字温湿度传感器具有精度高、功耗低、长期稳定性好等特点适用于智能家居、农业监测、气象站等环境监测场景。传感器通过两线制串行接口与主控通信驱动代码使用普通输入输出引脚模拟通信时序省去专用硬件。压缩包内共两个文件一个C源文件和一个头文件整体大小仅4KB结构精简清晰可直接加入工程使用。目前已有1545人学习下载。资源涵盖完整的SHT30驱动实现包括通信起始与停止时序、设备地址与命令发送、温湿度数据读取与拼接、应答异常和超时处理等内容重点展示了边沿时序控制和数据位搬运的具体写法。对于刚接触单片机驱动编写的初学者这份源码有助于理解通信协议在底层是如何一步步实现的对于有经验的工程师也可以直接参考其命令序列和寄存器配置快速集成到自己的项目中缩短开发调试周期。1. 51单片机SHT30驱动难的不是传感器而是I2C时序51单片机SHT30驱动这个东西焊上传感器、烧进程序之后读回0xFF是不少人第一次碰SHT30时的共同记忆。SHT30是Sensirion出品的数字温湿度传感器I2C接口地址固定为0x44理论上比DHT11干净利落命令、时序、计算公式都写在数据手册里。但麻烦在于51单片机里大量型号没有硬件I2C时序全靠GPIO模拟于是驱动写得对不对往往不是看公式而是看软件I2C的起始位、应答位和CRC三个细节。这篇文章把SHT30在51上的驱动拆开讲命令怎么发、数据怎么读、0xFF怎么查、滤波怎么做。课程设计里温控风扇、电子时钟这些常见项目照着这套驱动改改引脚就能复现。2. SHT30的I2C协议、常用命令与51单片机软件模拟I2C时序SHT30挂在I2C总线上主控发命令、传感器回数据。I2C是一条两线总线SCL和SDA都是开漏输出外部必须有上拉电阻。51单片机里很多型号没有硬件I2C模块所以驱动SHT30的实质就是用两个普通IO口把I2C时序“比出来”。这一章先把SHT30的地址和命令表讲清楚再给出51上最常用的软件I2C底层函数。2.1 SHT30的I2C地址与常用命令表0x44、0x2C06、0xE000SHT30的7位I2C地址由ADDR引脚决定。ADDR接地时地址是0x44ADDR接VCC时是0x45。换算成8位写地址分别是0x88和0x8A读地址分别是0x89和0x8B。如果只接一颗SHT30ADDR直接接地就行但地址位要会算排查问题时经常要拿读地址0x89去逻辑分析仪上对波形。命令字节数作用是否带CRC0xE0002软复位否0x2C063单次测量高重复性是0x24002单次测量高重复性否0x21303周期测量每秒0.5次是0x2C06这条命令比较特殊很多例程直接写成三字节0x2C、0x06、0x4F。其中0x4F是前两个字节的CRC8校验值发错或者漏发传感器会直接忽略命令表现为读回的数据一直是0xFF。0x2400不需要CRC发送链路更短但对电源噪声更敏感重复性略差。2.2 51单片机用普通IO口模拟I2C起始、停止、应答软件模拟I2C的关键是“SCL高电平期间SDA不能变化变化只允许发生在SCL低电平期间”。在51上写软件I2C核心就是控制SDA翻转和SCL翻转的先后顺序。下面是一套可以直接复制的最小驱动。2.2.1 引脚宏定义与准双向IO的置位问题#include reg52.h #include intrins.h sbit SHT30_SCL P1^0; sbit SHT30_SDA P1^1; #define I2C_DELAY() _nop_();_nop_();_nop_();_nop_();这里把SCL和SDA放在P1.0和P1.1接线时SCL接传感器SCLSDA接传感器SDAVCC接3.3VGND共地。I2C_DELAY里的四个_nop_在12MHz晶振、12T模式的标准51里大约是4个机器周期也就是4微秒左右跑100kbps的I2C足够。2.2.2 I2C的Start、Stop、WriteByte、ReadByte实现void I2C_Start(void) { SHT30_SDA 1; SHT30_SCL 1; I2C_DELAY(); SHT30_SDA 0; // SCL高电平时SDA拉低产生起始位 I2C_DELAY(); SHT30_SCL 0; } void I2C_Stop(void) { SHT30_SDA 0; SHT30_SCL 1; I2C_DELAY(); SHT30_SDA 1; // SCL高电平时SDA拉高产生停止位 } bit I2C_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SHT30_SCL 0; SHT30_SDA (dat 0x80) ? 1 : 0; dat 1; SHT30_SCL 1; I2C_DELAY(); } SHT30_SCL 0; SHT30_SDA 1; // 释放SDA让从设备拉低应答 SHT30_SCL 1; I2C_DELAY(); return SHT30_SDA; // 返回0表示收到ACK1表示NACK } unsigned char I2C_ReadByte(bit ack) { unsigned char i, dat 0; for (i 0; i 8; i) { SHT30_SCL 0; SHT30_SDA 1; // 准双向IO在读取外部电平前必须置1 SHT30_SCL 1; I2C_DELAY(); dat 1; if (SHT30_SDA) dat | 0x01; } SHT30_SCL 0; SHT30_SDA ack ? 1 : 0; // 读最后一字节时主控发NACK SHT30_SCL 1; I2C_DELAY(); SHT30_SCL 0; return dat; }这段代码里最容易忽略的是I2C_ReadByte里的SHT30_SDA 1。51的IO口是准双向结构读引脚前必须先把引脚拉高否则读进来永远是0。很多SHT30驱动读数据读出全0问题就出在这一行。I2C_WriteByte的返回值是ACK信号SHT30地址写错、传感器没上电、上拉电阻缺失时这个返回值会变成1可用于快速判断。2.3 0x2C06与0x2400时钟拉伸的取舍与延时补偿SHT30在单次测量模式下0x2C系列命令会启用时钟拉伸机制传感器测量期间把SCL拉低表示“我还没准备好”。51软件模拟I2C一般不检测SCL被外部拉低的情况因为标准51的SCL引脚没有开漏配置读回状态也不方便。所以实际工程里更稳妥的做法是发了0x2C06之后先延时10ms再读数据。测量时间大约是7.4ms10ms的延时已经覆盖了最坏情况即使SCL被传感器拉低过10ms后它也已经释放。0x2400命令不带时钟拉伸也不带CRC发送更简单。缺点是传感器在高重复性模式下对电源纹波更敏感供电不稳时读数会偶尔跳动。我一般会优先用0x2C06因为它有CRC保证命令不被误触发代价只是多写一个0x4F字节。两者在51上跑代码差异只有一行选型看项目环境电池供电、允许更低采样率就选0x2400数据要进控制回路就选0x2C06。3. 51单片机SHT30驱动完整程序初始化、测量、读回与CRC校验底层I2C函数就位后SHT30驱动本身只剩三件事初始化、触发测量、读数据。SHT30的测量结果以6字节形式返回分别是温度高字节、温度低字节、温度CRC、湿度高字节、湿度低字节、湿度CRC。这一章给出完整程序并说明CRC校验和温湿度换算的整数运算写法。3.1 SHT30初始化0xE000软复位与上电等待SHT30上电后不需要复杂的初始化流程但强烈建议做一次软复位。软复位命令是0xE000作用是清空状态寄存器、恢复默认配置。发送软复位后要等待至少10ms让传感器内部完成重启。void SHT30_Init(void) { I2C_Start(); I2C_WriteByte(0x88); // SHT30地址0x44写方向 I2C_WriteByte(0xE0); // 软复位命令高字节 I2C_WriteByte(0x00); // 软复位命令低字节 I2C_Stop(); delay_ms(20); }软复位命令不需要CRC所以只发三字节。0xE000是SHT30数据手册定义的固定命令写入后传感器内部执行复位流程所有寄存器回到默认值。这里delay_ms(20)比手册要求的10ms多留了一倍余量避免在增强型51上因为时钟不同导致延时不足。3.2 触发单次测量并读取6字节数据的核心函数SHT30单次测量的标准流程是发送测量命令等待测量完成发送读命令依次读6字节。下面的SHT30_ReadRaw函数把整个过程封装起来返回0表示成功返回1表示通信失败或CRC校验失败。bit SHT30_ReadRaw(unsigned int *temp, unsigned int *humi) { unsigned char buf[6]; unsigned char i; I2C_Start(); if (I2C_WriteByte(0x88)) { I2C_Stop(); return 1; } // 写地址 if (I2C_WriteByte(0x2C)) { I2C_Stop(); return 1; } // 测量命令高字节 if (I2C_WriteByte(0x06)) { I2C_Stop(); return 1; } // 测量命令低字节 if (I2C_WriteByte(0x4F)) { I2C_Stop(); return 1; } // 0x2C06的CRC8 I2C_Stop(); delay_ms(10); // 等待测量完成约7.4ms I2C_Start(); if (I2C_WriteByte(0x88)) { I2C_Stop(); return 1; } I2C_WriteByte(0x00); // 读数据命令高字节 I2C_WriteByte(0x00); // 读数据命令低字节 I2C_Start(); // 重新产生起始位进入读模式 if (I2C_WriteByte(0x89)) { I2C_Stop(); return 1; } // 读地址 for (i 0; i 6; i) { buf[i] I2C_ReadByte(i 5 ? 1 : 0); } I2C_Stop(); if (buf[2] ! SHT30_CRC8(buf, 2)) return 1; // 温度CRC if (buf[5] ! SHT30_CRC8(buf 3, 2)) return 1; // 湿度CRC *temp ((unsigned int)buf[0] 8) | buf[1]; *humi ((unsigned int)buf[3] 8) | buf[4]; return 0; }这段代码把0x2C06和0x4F写死在了发送序列里。0x4F是0x2C和0x06两个字节的CRC8校验结果如果项目里换了其他测量命令比如0x2C10低重复性CRC值也要跟着换不能直接套用。读数据前重新发送一次I2C起始位是为了把总线方向从写切换成读这是I2C读操作的常规做法。后面连续读6字节前5字节回ACK最后一字节回NACK告诉传感器“读完了可以停”。6字节数据布局如下字节索引内容说明0温度MSB14位温度原始值高字节1温度LSB14位温度原始值低字节2温度CRC对字节0和1的CRC83湿度MSB14位湿度原始值高字节4湿度LSB14位湿度原始值低字节5湿度CRC对字节3和4的CRC83.3 SHT30的CRC8查表法实现与校验时机SHT30的CRC8多项式是0x31初始值是0xFF输入数据按MSB-first逐位计算。逐位算法会占CPU时间在51上更推荐查表法。表可以提前算好放在数组里也可以运行时生成。unsigned char crc8_tab[256]; void sht30_crc8_gen_table(void) { unsigned char i, j, crc; for (i 0; i 256; i) { crc i; for (j 0; j 8; j) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } crc8_tab[i] crc; } } unsigned char SHT30_CRC8(unsigned char *ptr, unsigned char len) { unsigned char crc 0xFF; while (len--) { crc crc8_tab[crc ^ *ptr]; } return crc; }注意crc8_tab放在data区还是code区有讲究。51的code区在烧录后是只读的如果写成unsigned char code crc8_tab[256]就必须在编译时用初始化列表把256个值填进去运行时用unsigned char crc8_tab[256]则要先调用一次生成函数。这个表只有256字节data区够用就直接静态度调用一次生成函数就行。3.4 温湿度物理量换算的整数运算实现SHT30返回的原始值是14位无符号整数。温度计算公式是T -45 175 * rawT / 65535湿度计算公式是RH 100 * rawH / 65535。在51上直接用float算没问题但频繁读取时浮点运算会拖慢主循环所以工程上通常用长整型放大处理。// 输入rawT和rawH输出放大10倍的温度值和放大10倍的湿度值 void SHT30_Calc(unsigned int rawT, unsigned int rawH, long *t10, unsigned long *rh10) { *t10 ((long)rawT * 1750L) / 65535L - 450L; *rh10 ((unsigned long)rawH * 1000L) / 65535L; }温度先乘1750再除65535实际是把175放大10倍计算最后减450对应-45度的偏移这样*t10的单位是0.1摄氏度。湿度乘1000除65535*rh10的单位是0.1%RH。打印时用*t10 / 10和*t10 % 10分别取整数和小数部分。零下温度时*t10是负数C语言里负数取模结果是负数打印前要先判断符号这是常见坑。4. SHT30驱动读0xFF或温湿度乱跳时的排查顺序与滤波对策SHT30驱动写完了读数如果不对问题大多不在传感器本身而在时序和电路。这一章按排查顺序来先看波形再查硬件最后软件滤波。温度湿度读数跳来跳去很多时候不是驱动代码问题而是测量间隔太短或者总线被其他中断干扰。4.1 用逻辑分析仪抓SHT30波形时先看三个位置逻辑分析仪两个通道分别接SCL和SDA采样率设1MHz以上触发方式选SDA下降沿。抓到一次完整读操作后先看三个位置地址字节0x88之后的第9个时钟SDA有没有被拉低。这个低电平就是ACK没有ACK说明传感器没应答。测量命令0x2C 0x06 0x4F三个字节之后传感器有没有在第9个时钟给出第二次ACK。没有应答但前面地址有应答说明CRC发错了。读数据阶段读完第5字节后主控有没有发出NACK。最后一个字节如果不是NACK传感器会一直占着SDA下次通信就乱套。这三个位置在逻辑分析仪上非常直观比用万用表量电压快得多。我一般会保存一份正常波形调试异常时直接对照几十秒就能定位是主机时序问题还是传感器没进测量状态。4.2 SHT30读回0xFF时先查上拉、IO方向、地址与延时读回的数据全是0xFF说明总线上根本没有设备响应或者设备没有把SDA拉低。按下面顺序排查绝大多数问题出在前两步上拉电阻。SHT30的SCL和SDA是开漏输出外部必须接上拉电阻到VCC阻值4.7k或10k都行。没接上拉时总线空闲电平是浮空的读回的数据就是0xFF。读SDA之前有没有把引脚置1。51准双向IO口读外部电平前必须先写1I2C_ReadByte里漏掉SHT30_SDA 1这行读出来就是全0而不是全0xFF。这个现象经常被误判成驱动代码问题。ADDR引脚。ADDR悬空时内部下拉地址是0x44但有些模块板上已经把ADDR拉到了VCC地址变成0x45写地址就是0x8A而不是0x88。驱动里地址写错传感器不会应答。延时。0x2C06命令发出后延时不足7.4ms就去读数据传感器还在测量数据总线不会就绪。把延时加到15ms以上再试。还有一个容易被忽略的问题SHT30是3.3V器件如果51开发板是5V供电SCL和SDA的高电平是5V虽然有上拉电阻也不推荐长期这样用。常见做法是传感器单独用3.3V LDO供电I2C上拉电阻接到3.3V一侧SCL和SDA加一个2.2k串联电阻做电平缓冲。4.3 单次测量与周期测量模式对读数波动的影响单次测量模式下每次读数据前必须重新发一次测量命令。如果程序里只在初始化时发了一次0x2C06然后循环里反复读0x00 0x00那读到的永远是同一组数据。周期测量模式不一样传感器自己按照设定频率测量主控随时可以读最新数据适合温控这类需要定时采样的场景。模式命令数据更新方式典型场景单次测量0x2C06每次读前手动触发电池供电、低功耗采集周期测量0x2130传感器后台自动更新温控风扇、实时监控周期测量命令0x2130同样带CRCCRC值在数据手册里有专门表格。如果嫌查表麻烦又不想在代码里多算一个字节就用单次测量模式配合延时读取功能上完全够用。4.4 一阶低通与限幅滤波的工程选择温度湿度变化本身是慢变量读数偶尔跳一个值往往是电源纹波或IO口干扰导致的。软件滤波比换硬件快得多。滑动平均效果最好但占内存51上data区很紧张更多人用一阶低通或限幅滤波。滤波方法内存占用效果缺点滑动平均随窗口增大平滑度好51上窗口超过8就有点吃力一阶低通2字节平滑且实时有滞后限幅滤波2字节只剔除跳变无法处理连续抖动一阶低通的实现很直接unsigned int sht30_t_filter; // 每次读到新值后调用一次 unsigned int SHT30_IIR(unsigned int new_val, unsigned char kind) { // kind0处理温度kind1处理湿度 if (kind 0) { sht30_t_filter (sht30_t_filter * 7 new_val) / 8; return sht30_t_filter; } // 湿度滤波变量同理 return 0; }系数7/8表示新值占八分之一旧值占八分之七滤波结果滞后大约一个采样周期。如果采样间隔是500ms这种滞后在温控系统里完全可接受。限幅滤波更简单新值和上次值差超过阈值才更新否则丢弃用来干掉偶发的毛刺。5. SHT30驱动串口输出、总线复用与Proteus仿真验证驱动稳定后下一步通常是接串口打印观测或者和EEPROM挂一条总线。这一章给串口输出最小代码、总线地址划分和仿真验证的实际策略。5.1 用串口把SHT30温湿度打印到上位机51的串口配上printf重定向是最快的观测方式。重定向putchar之后printf直接可用。#include stdio.h void UART_Init(void) { SCON 0x50; // 串口模式18位UART TMOD 0x20; // 定时器1工作在模式2 TH1 0xFD; // 9600波特率11.0592MHz TL1 0xFD; TR1 1; } char putchar(char c) { SBUF c; while (!TI); TI 0; return c; }主循环里把t10和rh10打印出来printf(T%d.%d C RH%d.%d %%\r\n, (int)(*t10 / 10), (int)(*t10 % 10), (int)(*rh10 / 10), (int)(*rh10 % 10));零下温度时*t10是负数打印前先判断符号位否则-12.5会打印成-12.-5。串口乱码先查波特率设置CH340驱动的版本也要检查很多用CH340的开发板在Win10下乱码都是驱动过期引起的。5.2 SHT30与AT24C02同挂一条I2C总线的地址划分I2C总线上的设备靠7位地址区分。SHT30地址0x44写地址0x88AT24C02的地址是0xA08位写地址7位地址0x50。两个设备地址不冲突SCL和SDA可以并在一起上拉电阻共用一组。AT24C02的A0、A1、A2引脚决定最低三位地址全部接地就是0x50接不同的高低电平可以扩展出最多8个相同型号的EEPROM。实际接线时总线上每个设备都是开漏输出所以不需要给每个设备单独加上拉。一条总线上挂两三个I2C设备一个4.7k上拉电阻就够了。挂多了要考虑总线电容超过5个设备时把上拉换成2.2k。5.3 Proteus里验证SHT30驱动的实际策略Proteus标准库里没有SHT30模型常见做法是放SHT11代替但SHT11的时序和SHT30并不完全一样SHT11使用的是类I2C的单总线时序和SHT30的标准I2C命令集存在差异。所以Proteus仿真能验证的是51单片机端软件I2C的时序逻辑比如SCL频率、起始位和停止位的位置不能直接验证SHT30的CRC和温湿度换算。我的习惯是这样Proteus里放一个虚拟I2C从机模型或者SHT11先把软件I2C的底层层层跑通然后回到真机用逻辑分析仪抓SHT30的0x88地址ACK。只要地址那个第9个时钟的ACK是干净的后面读出的温湿度基本不会跑偏。真机读到的温湿度数值可以和手上另一颗SHT30模块对比偏差在0.3摄氏度和2%RH以内就算驱动正常。本文还有配套的精品资源点击获取
返回列表