首页 / 资讯中心 / 文章详情

SHT21源代码拆解:I2C时序、CRC校验与移植实战

SHT21源代码拆解:I2C时序、CRC校验与移植实战 ★ FEATURED ARTICLE
简介SHT21.zip是一份面向嵌入式开发者与传感器学习者的SHT21温湿度传感器驱动源码包适用于物联网、智能家居、环境监测等场景的快速集成。SHT21由盛思瑞出品具备高精度、低功耗与快速响应特性采用I2C接口通信仅需SDA与SCL两条线即可完成数据与时钟传输便于接入各类微控制器。包内共80个文件约222KB以C源码与头文件为核心包含sht21.c、i2c.c、bcm2835.c等驱动实现并配有Makefile构建脚本、示例程序、README说明及工程配置文件覆盖初始化、数据读取、单位换算与错误处理等环节。已有8136人学习下载读者可据此理解I2C时序与命令交互流程快速完成湿度、温度采集并在此基础上调整测量频率、优化功耗或移植到Arduino、嵌入式Linux等平台是学习传感器驱动与产品开发的实用参考。1. SHT21 源代码拆解从一份压缩包到能跑的温湿度采集手里拿到一个SHT21.zip里面躺着 SHT21 的源代码作者信息还带着shtfabgmail.com这种典型个人邮箱痕迹——这几乎是嵌入式圈子里最常见的一类“遗产代码”开局。SHT21 是 Sensirion 出的数字温湿度传感器I2C 接口DFN 封装精度和长期稳定性在同类里口碑不错大量出现在冷链记录仪、机房监控、农业大棚节点里。但真正让人头疼的从来不是传感器本身而是那份源代码它可能跑在 51 上也可能跑在 STM32 上可能用软件模拟 I2C也可能用硬件外设甚至可能只是半成品。这篇笔记就围绕这份 SHT21 源代码把 I2C 时序、命令字、CRC 校验、温湿度换算、以及怎么把它移植到自己的板子上跑通一层层拆开讲清楚。适合手里有 SHT21 硬件、拿到一份来源不明的驱动代码、想快速验证并投入实际采集的嵌入式工程师。2. SHT21 的 I2C 时序与命令字源代码里最该先看懂的部分拿到一份 SHT21 源代码第一件事不是编译而是把 I2C 读写时序和命令字对照数据手册过一遍。很多“跑不通”的根因就藏在这几十行里。SHT21 的 I2C 地址是固定的0x407 位写操作地址字节0x80读操作0x81。它不支持地址引脚配置所以一条总线上只能挂一颗想挂多颗必须用 I2C 多路复用器这是选型时就要想清楚的边界。2.1 触发测量与读取的完整命令序列SHT21 的测量不是“读寄存器”那种模式而是“发命令 → 等待 → 读结果”。源代码里如果只写了一个读函数大概率是错的。常见做法是封装三个动作发触发命令、延时等待、读 3 字节2 字节数据 1 字节 CRC。/* SHT21 基本命令字来自数据手册 */ #define SHT21_ADDR_W 0x80 /* 7位地址0x40左移一位写 */ #define SHT21_ADDR_R 0x81 /* 读 */ #define SHT21_CMD_T_MEAS 0xE3 /* 触发温度测量保持主机 */ #define SHT21_CMD_RH_MEAS 0xE5 /* 触发湿度测量保持主机 */ #define SHT21_CMD_T_NOHOLD 0xF3 /* 温度非保持 */ #define SHT21_CMD_RH_NOHOLD 0xF5 /* 湿度非保持 */ #define SHT21_CMD_SOFT_RST 0xFE /* 软复位 */ #define SHT21_CMD_USER_REG_W 0xE6 /* 写用户寄存器 */ #define SHT21_CMD_USER_REG_R 0xE7 /* 读用户寄存器 */这段宏定义是整份源代码的骨架。0xE3和0xE5是“保持模式”意思是测量期间 I2C 总线被占用主机必须等传感器把结果吐出来0xF3和0xF5是“非保持模式”发完命令就可以松手过一会儿再来读。保持模式实现简单但会阻塞总线非保持模式对多任务系统友好但时序要自己掐。源代码里用哪种决定了你后面移植时要不要改调度逻辑。2.2 测量等待时间不能拍脑袋温度测量典型 66ms、最大 85ms湿度典型 22ms、最大 29ms如果是 14 位温度 12 位湿度。很多源代码里写delay_ms(100)一把梭能跑但湿度采集频率上不去。更稳的做法是按测量类型给不同延时并且在非保持模式下用轮询或定时器回调。/* 按测量类型给等待时间留 20% 余量 */ static void sht21_wait_meas(uint8_t cmd) { if (cmd SHT21_CMD_T_MEAS || cmd SHT21_CMD_T_NOHOLD) { delay_ms(100); /* 温度最大85ms留余量 */ } else { delay_ms(40); /* 湿度最大29ms留余量 */ } }参数说明这里的delay_ms是阻塞延时裸机够用如果跑 RTOS应该换成vTaskDelay或状态机。注意湿度测量对时间更敏感延时过长不会出错但会拉低采样率延时过短读到的是 NACK 或全 1表现为“数据一直是 0xFFFF”。2.3 CRC 校验源代码里最容易被删掉的一行SHT21 每个测量结果后面跟一个 CRC-8 校验字节多项式x^8 x^5 x^4 1初值0x00。不少“精简版”源代码直接把第三个字节丢掉短距离、低干扰环境下确实能跑但一旦走长线或电机旁边数据跳变就来了。建议保留校验失败重读一次。/* SHT21 CRC-8 校验多项式 0x31初值 0x00 */ static uint8_t sht21_crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0x00; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t b 0; b 8; b) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc; }逻辑说明逐字节异或后逐位左移最高位为 1 时异或0x31即多项式去掉最高位。参数data是前两字节原始数据len传 2返回值和第三字节比较。如果源代码里没有这个函数建议补上这是判断“代码能不能上现场”的分水岭。3. 从原始值到温湿度换算公式与源代码里的定点数处理读完原始 2 字节只是拿到了一个 16 位整数真正要的是摄氏度和相对湿度。这一步源代码里通常有两种写法浮点运算和定点整数运算。选哪种取决于你的 MCU 有没有 FPU、以及你对代码体积和速度的要求。3.1 温度与湿度的标准换算公式按数据手册先屏蔽状态位低 2 位再代入公式温度T -46.85 175.72 * (S_T / 65536)湿度RH -6.0 125.0 * (S_RH / 65536)其中S_T、S_RH是去掉低 2 位后的 16 位值。注意湿度结果要限幅到 0~100%因为公式在极端情况下会算出负值或超过 100。/* 原始值转温湿度浮点版适合带 FPU 的 M3/M4 */ static void sht21_convert_float(uint16_t raw_t, uint16_t raw_rh, float *temp, float *humi) { raw_t 0xFFFC; /* 清掉状态位 */ raw_rh 0xFFFC; *temp -46.85f 175.72f * ((float)raw_t / 65536.0f); *humi -6.0f 125.0f * ((float)raw_rh / 65536.0f); if (*humi 100.0f) *humi 100.0f; if (*humi 0.0f) *humi 0.0f; }参数说明raw_t、raw_rh是 I2C 读回的 16 位原始值必须先 0xFFFC。temp、humi是输出指针。浮点版代码直观但在 M0 或无 FPU 的 51 上会拖慢速度、增大体积。3.2 定点数版本没有 FPU 时的稳妥选择如果目标平台是 51 或 M0建议用定点运算把结果放大 100 倍存成整数显示时再插小数点。这样避免链接浮点库代码能小几 KB。/* 定点版温度、湿度均放大100倍返回 int16_t */ static void sht21_convert_fixed(uint16_t raw_t, uint16_t raw_rh, int16_t *temp_x100, uint16_t *humi_x100) { int32_t t, rh; raw_t 0xFFFC; raw_rh 0xFFFC; /* T -46.85 175.72 * raw / 65536放大100倍 */ t -4685 (int32_t)((17572LL * raw_t) 16); *temp_x100 (int16_t)t; /* RH -6.0 125.0 * raw / 65536放大100倍 */ rh -600 (int32_t)((12500LL * raw_rh) 16); if (rh 10000) rh 10000; if (rh 0) rh 0; *humi_x100 (uint16_t)rh; }逻辑说明把系数也放大 100 倍用 64 位中间量做乘加再右移 16 位等价于除以 65536。17572是175.72 × 10012500是125.0 × 100。这样全程整数结果精度到 0.01足够一般监控场景。注意17572LL的LL不能省否则 16 位平台会溢出。3.3 用户寄存器分辨率与加热功能怎么配SHT21 的用户寄存器控制测量分辨率、片上加热器、以及电池电压检测。默认值0x3A温度 14 位、湿度 12 位。如果源代码里从没写过这个寄存器那它跑的就是默认配置。想改分辨率或开加热器除凝露就得会写0xE6。位名称默认说明7保留0必须写 06电池检测0置 1 后测 VDD5~3保留0必须写 02加热器0置 1 开启功耗上升1OTP0出厂校准勿动0分辨率0012/14位18/12位写寄存器时注意读-改-写别把保留位写乱。加热器开启后测量值会偏高只适合短时除凝露不能长期开。4. 移植到自己的板子SHT21 源代码适配的四个动作一份 SHT21 源代码能不能用关键看它和你的硬件层耦合有多深。常见情况是作者用的是软件模拟 I2C引脚定义写死在代码里延时靠空循环。移植的本质就是把这四件事替换掉。4.1 替换底层 I2C 读写函数先找到源代码里的I2C_Start、I2C_Stop、I2C_WriteByte、I2C_ReadByte、I2C_Ack这几个函数它们就是移植接口。如果你用 STM32 硬件 I2C可以整体替换成 HAL 库调用如果用 GPIO 模拟就改引脚宏。/* 移植接口示例把源代码里的模拟 I2C 换成 HAL */ static uint8_t sht21_i2c_write(uint8_t addr, const uint8_t *buf, uint16_t len) { return HAL_I2C_Master_Transmit(hi2c1, addr, (uint8_t *)buf, len, 100); } static uint8_t sht21_i2c_read(uint8_t addr, uint8_t *buf, uint16_t len) { return HAL_I2C_Master_Receive(hi2c1, addr, buf, len, 100); }参数说明addr是 8 位地址0x80/0x81HAL 库要求左移后的地址正好对上。100是超时毫秒。替换后原来的SHT21_ReadBytes之类函数内部调用这两个即可上层换算逻辑不用动。4.2 引脚与延时的重新定义模拟 I2C 的源代码里通常有SHT21_SDA_H、SHT21_SCL_L这类宏。移植时把它们指向你的 GPIO 端口和引脚。延时函数如果是delay_us确认你的系统里有没有微秒级延时没有就用 DWT 或定时器补一个。/* 模拟 I2C 引脚宏按自己的板子改 */ #define SHT21_SCL_PORT GPIOB #define SHT21_SCL_PIN GPIO_PIN_6 #define SHT21_SDA_PORT GPIOB #define SHT21_SDA_PIN GPIO_PIN_7 #define SHT21_SCL_H() HAL_GPIO_WritePin(SHT21_SCL_PORT, SHT21_SCL_PIN, GPIO_PIN_SET) #define SHT21_SCL_L() HAL_GPIO_WritePin(SHT21_SCL_PORT, SHT21_SCL_PIN, GPIO_PIN_RESET) #define SHT21_SDA_H() HAL_GPIO_WritePin(SHT21_SDA_PORT, SHT21_SDA_PIN, GPIO_PIN_SET) #define SHT21_SDA_L() HAL_GPIO_WritePin(SHT21_SDA_PORT, SHT21_SDA_PIN, GPIO_PIN_RESET) #define SHT21_SDA_READ() HAL_GPIO_ReadPin(SHT21_SDA_PORT, SHT21_SDA_PIN)注意 SDA 在读 ACK 时要切换输入输出方向很多源代码漏了这一步表现为“第一个字节能写第二个就卡死”。模拟 I2C 的 SCL 频率别超过 100kHzSHT21 虽然支持 400kHz但长线时降速更稳。4.3 上电后的软复位与首次测量SHT21 上电后需要约 15ms 稳定之后建议发一次软复位0xFE再等 15ms。有些源代码省略了这步冷启动时第一次读数偶尔异常。软复位后用户寄存器回到默认值如果你之前配过分辨率要重新写。/* 上电初始化序列 */ void sht21_init(void) { delay_ms(20); /* 上电稳定 */ sht21_i2c_write(SHT21_ADDR_W, (uint8_t[]){SHT21_CMD_SOFT_RST}, 1); delay_ms(20); /* 复位等待 */ /* 如需改分辨率在此写用户寄存器 */ }逻辑说明软复位命令只有一个字节直接写。复位后不要立刻测量等 15ms 以上。如果总线上还有其他器件确认复位命令不会误触发它们——SHT21 地址固定一般不会冲突。4.4 用示波器确认时序而不是盲调移植后如果读不到数据先别改代码拿示波器或逻辑分析仪抓 SCL/SDA。看三件事起始条件是否干净、第 9 个时钟是否有 ACK、停止条件是否完整。血泪经验是80% 的“SHT21 不响应”其实是 SDA 没释放或上拉电阻太大。SHT21 模块一般自带 4.7k 上拉如果自己画板上拉别超过 10k否则上升沿太缓高速下误码。5. SHT21 源代码避坑五个让采集翻车的现场问题这一章按“现象 → 原因 → 解决”写都是实际调试里反复出现的。5.1 现象读回数据恒为 0xFFFF 或 0x0000原因I2C 根本没通信成功主机读到的只是总线空闲电平。常见于地址写错用了 7 位地址0x40而不是 8 位0x80、SDA/SCL 接反、或者上拉缺失。解决先用 I2C 扫描程序确认0x40是否应答再检查地址宏最后量上拉电压。注意有些 HAL 库要求传 7 位地址会自己左移别重复移位。5.2 现象温度正常湿度一直偏高或跳变原因湿度测量对延时和加热器状态敏感。如果之前开过加热器没关或者测量等待时间不够湿度原始值会异常。另外 PCB 布局上传感器靠近发热元件也会让湿度偏低、温度偏高。解决确认用户寄存器 bit2 为 0湿度延时给足 40ms传感器开槽隔离别铺铜。5.3 现象CRC 校验偶尔失败重读就好原因总线干扰或时序临界。模拟 I2C 在中断频繁的系统里SCL 高电平被拉长SHT21 可能提前采样。解决测量期间关中断或改用硬件 I2C降低 SCL 频率到 50kHz缩短排线。如果现场无法改硬件就在软件里做“失败重读一次”但别无限重试否则会卡死主循环。5.4 现象非保持模式下读到的总是上一次的值原因非保持模式发完命令后传感器需要时间转换如果立刻读拿到的是旧数据或 NACK。解决发完0xF3/0xF5后至少等对应测量时间再发起读或者用“读命令 轮询 ACK”的方式直到传感器应答再读 3 字节。很多源代码把保持和非保持的读时序混用这是典型翻车点。5.5 现象低功耗场景下电流比手册高原因SHT21 测量期间电流约 300~500uA但如果你让它一直处于保持模式且频繁测量平均电流下不来。另外用户寄存器分辨率设成 14/12 位时每次测量时间最长。解决改用非保持模式 定时唤醒不需要高精度时把分辨率降到 12/8 位两次测量间隔拉长。注意软复位后寄存器回默认低功耗配置要在每次唤醒后重写或者干脆不软复位。6. 把 SHT21 源代码用稳批量校准与长期漂移的应对单板跑通只是开始真正投入项目后批量一致性和长期漂移才是分水岭。SHT21 出厂校准不错但不同批次、不同 PCB 布局下湿度读数仍会有几个百分点的分散。我一般会做两件事一是留一个“参考通道”用同一批里挑一颗做基准其余节点和它比对二是在固件里加一个可写的偏移量产线标定后写入 Flash。具体做法在换算函数后面加一层补偿偏移量存在结构体里从 Flash 读。typedef struct { int16_t temp_offset_x100; /* 温度偏移放大100倍 */ int16_t humi_offset_x100; /* 湿度偏移放大100倍 */ } sht21_cal_t; static sht21_cal_t g_cal {0, 0}; void sht21_apply_cal(int16_t *temp_x100, uint16_t *humi_x100) { int32_t t *temp_x100 g_cal.temp_offset_x100; int32_t h (int32_t)*humi_x100 g_cal.humi_offset_x100; if (h 10000) h 10000; if (h 0) h 0; *temp_x100 (int16_t)t; *humi_x100 (uint16_t)h; }参数说明偏移量由产线用标准温湿度箱标定或者和参考节点比对得出。g_cal上电时从 Flash 加载标定命令通过串口或无线下发。注意偏移量别设太大超过 ±5% RH 说明硬件有问题应该先查布局和凝露。长期漂移方面SHT21 的湿度敏感层会随灰尘、化学蒸汽缓慢变化。现场经验是普通室内环境一年漂移 1~2% RH厨房、车间更明显。应对办法不是频繁换传感器而是定期比如半年用饱和盐溶液做单点核查只校偏移不校斜率。如果漂移超过 5%再考虑更换。验证方法上我习惯在固件里加一个“原始值 换算值 CRC 状态”一起上报的调试模式上位机画曲线。这样一旦现场数据异常能立刻区分是传感器问题、通信问题还是换算问题。这个习惯帮我省了很多来回跑现场的时间。做嵌入式采集代码跑通只是及格线能把数据长期稳住、出了问题能定位才算真正交付。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站