尧图精选

手把手教你用软件IIC驱动STH20温湿度传感器

🕒 发布时间:2026/9/8 11:08:17 📁 来源:尧图网络
简介面向单片机开发者的SHT20温湿度传感器驱动压缩包聚焦IIC总线下的驱动控制与数据采集问题适用于在嵌入式项目中接入SHT20、完成温度湿度实时读取的场景。资源共2个文件包含1个h头文件和1个c源文件压缩包整体仅1KB结构精简h头文件用于声明接口与寄存器定义c源文件则实现具体IIC时序、命令发送与数据读取。驱动实现覆盖设备初始化、传感器参数配置如分辨率与测量模式、启动温湿度测量、原始数据读取、数据解码以及基础错误处理等关键环节并关注通信过程中的时序配合。由于SHT20返回的是编码数据驱动内还包含温湿度换算算法可直接得到工程可用的温度和湿度值。这套文件已有79人学习适合刚接触IIC协议或传感器驱动开发的读者对照学习。借助这套驱动能够快速理解SHT20与单片机之间的主从通信过程也为后续自行移植驱动或扩展其他IIC传感器提供参考。1. 项目概述为什么我选了 STH20 而不是 DHT11day06 是个分水岭。前五天我还在跟 GPIO、定时器、外部中断这些“一个引脚就能玩转”的外设死磕到了第六天拿到 IIC 总线和 STH20 温湿度传感器才算真正摸到了单片机通信的脉门。先解释一下标题里的东西IIC 是集成电路间通信总线飞利浦现在的 NXP在八十年代搞出来的两条线——SCL 时钟线和 SDA 数据线——就能挂一大堆设备从 EEPROM、温湿度传感器、陀螺仪到 OLED 屏幕全是它的地盘。STH20 是 Sensirion 出的一款数字温湿度传感器IIC 接口精度高、功耗低、出厂校准过最常用的场景就是环境监测、智能家居、农业大棚这些需要实时采集温湿度的设备。我这次拿到手的是一颗 STH20 的模块焊接在转接板上引脚已经引出来用杜邦线就能接到我的 STC 单片机开发板上。因为要写驱动文件所以整个 day06 的核心任务很明确用单片机的 IO 口模拟 IIC 时序跑通 STH20 的温湿度读取最后整理成一份可复用的驱动代码。这里先给新手吃一颗定心丸如果你用的是 STC15、STC8 或者 STM32F103 这种单片机IIC 外设硬件上都有但很多人包括我还是用 GPIO 软件模拟。原因后面细说但一句话概括就是软件模拟的 IIC 虽然代码略繁琐却让你真正“看见”时序出了问题也好排查而且随便哪个引脚都能用板子布线敢约束不到。这篇博文适合正在学单片机通信协议的人尤其是卡在 IIC 和传感器驱动这块不知道怎么把数据手册里的时序图变成真实可读的数据的读者。我会把从数据手册到驱动代码的完整链路掰开揉碎包括我踩过的坑和用逻辑分析仪抓出来的波形。2. 整体设计与思路拆解软件 IIC 和硬件 IIC我为什么选前者2.1 硬件 IIC 和软件 IIC 的本质区别IIC 通信总线的物理层极其简单一根 SCL一根 SDA加上两个上拉电阻没了。但就是这两根线承载了全部的数据交互时序要求非常严格。这里有个关键选择摆在面前——用单片机自带的硬件 IIC 外设还是用普通 GPIO 软件模拟硬件 IIC由单片机内部的外设模块自动产生时序你只需要配置寄存器往数据寄存器里写数据、读数据时钟信号由硬件自动翻转。优点是 CPU 占用低、速率稳定代码量小缺点是引脚固定、配置寄存器繁琐一旦时序出现问题很难排查——因为你看不见中间过程。软件 IIC用两个 GPIO 口在代码里按照时序要求手动拉高拉低 SCL、控制 SDA 的输入输出方向和电平状态。好处是引脚随意、时序完全可控每一 bit 的翻转你都知道发生了什么代价是占用 CPU、代码量稍大、波特率受主频和指令周期限制。我最终选择了软件模拟理由有三第一我手头的 STC8G1K08A 芯片硬件 IIC 的引脚分配固定在特定 IO 上而我的电路板上几个引脚已经被 LED 和按键占了软件模拟想用哪两个脚就用哪两个脚完全不用改板子。第二STH20 的数据手册里给出了非常详细的时序图包括起始条件、停止条件、字节传输顺序、响应位位置我刚好可以用逻辑分析仪一一对照从底层验证我写的代码对不对——这种学习过程是硬件 IIC 给不了的。第三软件 IIC 的代码几乎可以无缝移植到任何单片机平台从 STC 换到 STM32、换到 GD32只需要改一下引脚定义和延时实现驱动逻辑可以原封不动地带过去性价比很高。2.2 STH20 的选型考量为什么不用 DHT11你可能要问现在市面上的温湿度传感器上百种DHT11 便宜又热门为什么偏偏选 STH20我对比过两者的差异这直接决定了我的代码架构特性DHT11STH20通信协议单总线自定义时序IIC标准化协议湿度精度±5% RH±2% RH温度精度±2°C±0.3°C分辨率8bit / 8bit14bit / 14bit最大功耗测量时约 3mA测量时约 200uA数据输出直接输出温湿度值原始码需要公式换算地址无0x407bit地址DHT11 的单总线协议是 Sensirion 早期的 DHT 系列规范时序要求更苛刻而且精度在需要精确控温的场景下根本不够看。STH20 走的是标准 IIC有明确的从机地址、寄存器命令和 CRC 校验虽然我们一般不用数据完整性和可靠性上了一个档次。还有一个很重要但容易被新手忽略的点STH20 出厂前已经做了标定校准温湿度的原始 ADC 值可以直接通过数据手册给出的公式换算成物理量不需要自己做多点标定。这意味着你的驱动代码只要读回来的数字对得上公式结果就是准的不用来回调参。打个比方DHT11 像是一把带刻度的塑料尺能用但精度有限STH20 则是一把经过计量校准的不锈钢卡尺同样的价格段你当然选后者。2.3 驱动文件的分层思想底层协议和上层应用分离写驱动之前我脑子里先过了一遍架构规划。驱动文件如果一股脑全塞一个 .c 文件里短期看着省事后期维护就是灾难。我采用的方案是两层分离底层IIC 协议层iic.c / iic.h只负责 IIC 总线的时序产生包括起始信号、停止信号、发送一个字节、接收一个字节、发送 ACK、接收 ACK。这一层不关心你挂的是什么设备纯属“通信基础设施”可以给任何 IIC 设备复用。上层传感器应用层sth20.c / sth20.h基于 IIC 协议层封装 STH20 的初始化、触发测量、读取数据、数据处理等接口。这一层只调用协议层提供的函数不直接操作 GPIO。这样做的好处是以后如果我要换成 SHT30、BMP280 或者 OLED只需要新写一套应用层IIC 底层完全不用动。反过来如果我要换单片机平台只需要改底层应用层直接复用。实际项目里这个思路非常实用尤其是当你手里的传感器越来越多的时候分层设计省下的调试时间远超写代码多花的功夫。3. 核心细节解析与实操要点IIC 时序和 STH20 命令集3.1 IIC 时序的五个关键动作说起 IIC新手最容易懵的就是那一堆时序图。但本质上IIC 通信只有五个基本动作起始信号、停止信号、发送数据、接收数据、应答信号。把这五个动作练熟了什么 IIC 设备都能驱动起来。起始信号STARTSCL 保持高电平SDA 从高电平跳变到低电平。这个下降沿告诉总线上的所有设备注意主机要开始通信了。代码实现非常直接void IIC_START(void) { IIC_SCL 1; IIC_SDA 1; delay_us(5); IIC_SDA 0; // SCL高电平期间SDA产生下降沿 delay_us(5); IIC_SCL 0; // 拉低SCL准备传输数据 }注意最后一步一定要把 SCL 拉低。因为 IIC 规定只有在 SCL 为低电平的时候SDA 的数据才能变化。如果在高电平期间 SDA 变了会被误判成起始或停止信号。停止信号STOP和起始相反SCL 高电平期间SDA 从低电平跳变到高电平。void IIC_STOP(void) { IIC_SCL 0; IIC_SDA 0; delay_us(5); IIC_SCL 1; delay_us(5); IIC_SDA 1; // SCL高电平期间SDA产生上升沿 delay_us(5); }发送一个字节一个字节 8 位按照从高位到低位的顺序依次放到 SDA 上。每一位的发送流程是先把要发的 bit 放到 SDA 上然后把 SCL 拉高延时再把 SCL 拉低。SCL 拉高期间从机采样 SDA 的引脚电平这样就完成了一 bit 的传输。void IIC_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { IIC_SDA (dat 0x80) 7; // 先发高位 delay_us(2); IIC_SCL 1; delay_us(2); IIC_SCL 0; dat 1; } }接收一个字节反过来主机释放 SDA 线设置输入模式或者直接置 1然后每个时钟周期读一次 SDA 引脚电平依然是从高位到低位。uint8_t IIC_RecvByte(void) { uint8_t i, dat 0; for (i 0; i 8; i) { dat 1; IIC_SCL 1; delay_us(2); if (IIC_SDA) dat | 0x01; IIC_SCL 0; delay_us(2); } return dat; }应答信号ACK这是 IIC 协议最有趣的部分。主机每发完一个字节第 9 个时钟周期里从机会把 SDA 拉低表示“我收到了继续吧”。如果从机没拉低说明它没准备好或者地址错了数据通信就应该终止。应答信号的检查是 IIC 调试的重灾区。新手常常遇到一种情况代码一发完地址SDA 就一直没反应示波器一看SDA 上全是高电平——八成是地址写错了或者从机压根没上电响应。3.2 STH20 的命令集和寄存器操作STH20 的手册看下来其实操作极简因为它本质上就是一颗“按下按钮就出一组温湿度数据”的传感器。它的 IIC 从机地址是 0x40也就是 7 位地址写操作时发送 (0x40 1) | 0 0x80读操作时发送 (0x40 1) | 1 0x81。几个核心命令命令代码意义触发温度测量保持主机0xE3主机模式下测量温度测量期间 SCL 被拉低触发湿度测量保持主机0xE5主机模式下测量湿度触发温度测量无需主机0xF3测量结束后自动释放总线主机轮询等待数据触发湿度测量无需主机0xF5同上软复位0xFE重启传感器恢复默认配置读取状态寄存器0xE7返回 8bit 状态字这里我把“保持主机”和“无需主机”解释一下因为这个概念新手容易绕进去保持主机Hold Master模式你发完触发命令后传感器开始测量测量期间它会把 SCL 一直拉低相当于“握着你的手不让你走”。等测量完成了它才释放 SCL让你继续读数据。好处是数据一出来你就知道不用傻等坏处是如果传感器出故障SCL 一直被占着总线就死了。无需主机No Hold模式你发完命令就不用管了传感器迟早会把数据准备好放在寄存器里你过一段时间再主动去读就行。这种模式更安全适合多设备共享总线的情况。我这次用的是 0xF3无需主机模式测量温度和 0xF5无需主机模式测量湿度因为我的板子上还挂了 OLED如果传感器把 SCL 占住OLED 的命令根本传不出去。3.3 STH20 原始数据的物理量换算STH20 输出的不是咱们平时说的“多少摄氏度”或“百分之多少湿度”而是一个 16 位原始 ADC 码。拿到这个码必须经过公式换算才能变成人看的懂的数。温度换算公式T -45 175 * (raw / 65535)湿度换算公式RH -6 125 * (raw / 65535)其中 raw 是读回的两个字节拼成的 16 位整数——先读到的是高字节左移 8 位再或上低字节。换成 C 代码就是float STH20_CalcTemp(uint16_t raw) { return -45.0f 175.0f * (float)raw / 65535.0f; } float STH20_CalcHum(uint16_t raw) { return -6.0f 125.0f * (float)raw / 65535.0f; }注意湿度算出来是 0~100% 之间的数但如果你在极干燥或极端潮湿的环境下读数可能会轻微超过这个范围这是传感器物理特性决定的不用担心。另外STH20 的湿度测量在大约 10%~90% RH 范围内精度最佳超出这个区间误差会增大手册里都有说明。4. 实操过程与核心环节实现从驱动代码到实物数据4.1 引脚分配和初始化我的开发板是 STC8G1K08A内核是增强型 8051工作主频我设在了 12MHz。IIC 的引脚我选了 P1.5 作为 SCL、P1.6 作为 SDA旁边正好是电源和地杜邦线走线方便。初始化有两件事要做第一把引脚配置成推挽输出模式STC8G 的配置寄存器是 P1M0 和 P1M1保证 IO 口有足够的驱动能力第二初始状态下 SCL 和 SDA 都拉高让总线处于空闲状态。补充一个关键点IIC 总线上一般要接上拉电阻阻值 2.2kΩ~10kΩ 都可以。如果你的模块板上已经焊了上拉电阻那就不用操心如果是裸芯片记得外接 4.7kΩ 左右的上拉电阻到 VCC不然 SDA 拉高的时候供不出电通信时通时断。原因是 IIC 的 SDA 和 SCL 都是开漏结构高电平完全靠上拉电阻提供根本没有内部上拉的能力。4.2 STH20 驱动代码的完整实现下面是驱动文件的核心代码我会把每段干什么、注意什么讲清楚。先是头文件 sth20.h#ifndef __STH20_H__ #define __STH20_H__ #include iic.h #define STH20_ADDR_W 0x80 // IIC写地址 #define STH20_ADDR_R 0x81 // IIC读地址 #define STH20_CMD_TEMP 0xF3 // 无需主机模式测温 #define STH20_CMD_HUMI 0xF5 // 无需主机模式测湿 #define STH20_CMD_SOFTRESET 0xFE // 软复位 typedef struct { float temperature; float humidity; } STH20_Data; uint8_t STH20_Init(void); uint8_t STH20_ReadData(STH20_Data *dat); #endif然后是源文件 sth20.c#include sth20.h #include delay.h // 触发一次测量命令字为 cmd static void STH20_StartMeasure(uint8_t cmd) { IIC_START(); IIC_SendByte(STH20_ADDR_W); IIC_WaitAck(); IIC_SendByte(cmd); IIC_WaitAck(); IIC_STOP(); } // 读取16bit原始数据 static uint16_t STH20_ReadRaw(void) { uint8_t msb, lsb; uint16_t raw; IIC_START(); IIC_SendByte(STH20_ADDR_R); IIC_WaitAck(); msb IIC_RecvByte(); // 高字节 IIC_SendAck(0); // 告诉从机继续 lsb IIC_RecvByte(); // 低字节 IIC_SendAck(1); // 最后一字节回非应答 IIC_STOP(); raw ((uint16_t)msb 8) | lsb; return raw; } uint8_t STH20_Init(void) { IIC_Init(); // 初始化IIC引脚拉高SCL/SDA // 软复位一次确保传感器处于已知状态 IIC_START(); IIC_SendByte(STH20_ADDR_W); IIC_WaitAck(); IIC_SendByte(STH20_CMD_SOFTRESET); IIC_WaitAck(); IIC_STOP(); delay_ms(50); // 软复位后等待传感器稳定 return 0; } uint8_t STH20_ReadData(STH20_Data *dat) { uint16_t rawTemp, rawHumi; // 测量温度 STH20_StartMeasure(STH20_CMD_TEMP); delay_ms(20); // 等待测量完成最长约16ms rawTemp STH20_ReadRaw(); // 测量湿度 STH20_StartMeasure(STH20_CMD_HUMI); delay_ms(20); rawHumi STH20_ReadRaw(); // 物理量换算 dat-temperature -45.0f 175.0f * (float)rawTemp / 65535.0f; dat-humidity -6.0f 125.0f * (float)rawHumi / 65535.0f; return 0; }注意看下面几个细节都是我实际调试中总结的读取字节之后的应答处理读第一个字节高字节 msb时主机要回一个 ACK拉低 SDA告诉从机“我再要一个字节”读第二个字节低字节 lsb时主机要回一个 NACK释放 SDA保持高电平告诉从机“我不要了结束吧”。这个 NACK 很关键如果少发从机就会一直认为你还想要数据总线的状态就乱了。两次测量之间要不要重新发起始信号不需要。STH20 在无需主机模式下每次触发测量和读取数据都是独立的 IIC 过程——发起始、写命令、发停止然后再发起始、读数据、发停止。两次操作之间不必保持总线占用。延时时间宁可多不能少STH20 测量温度的最长时间约 16ms湿度约 16ms我统一延时 20ms。有些复刻代码里延时写成 10ms偶尔会出现读回来的数据全是 0xFF就是测量没完成就去读了。4.3 主函数调用范例驱动文件写好了主函数调用就非常清爽#include sth20.h #include delay.h #include uart.h void main(void) { STH20_Data sensor; UART_Init(); // 串口初始化用于打印数据 STH20_Init(); // 初始化传感器 while (1) { if (STH20_ReadData(sensor) 0) { printf(温度: %.2f °C 湿度: %.2f %%RH\r\n, sensor.temperature, sensor.humidity); } delay_ms(1000); // 1秒刷新一次 } }把这段代码烧进单片机打开串口助手正常情况下你就能看到滚动的温湿度数据了。我第一次跑通的时候序列终端里跳出“温度: 26.31 °C 湿度: 54.87 %RH”那种感觉还是很爽的——虽然这行字背后是无数次的时序调整和逻辑分析仪波形核对。5. 常见问题与排查技巧实录5.1 数据全是 0xFF 或者 0x00这是 IIC 调试最常见的问题。如果读回来的原始码是 0xFFFF换算出来的温度是 130°C、湿度是 119%RH那就不是环境真的那么热而是数据根本没读到。排查顺序第一检查 SDA 和 SCL 是不是接反了。这种错误最隐蔽因为电路上看着都通逻辑分析仪上也有波形但波形就是不对。第二检查从机地址对不对。STH20 的 7 位地址是 0x40但写入 IIC 总线的时候要左移一位再拼上读写位所以写地址是 0x80、读地址是 0x81。很多新手直接在代码里写 STH20_ADDR 0x40结果通信失败因为 0x40 发到总线上后从机发现地址不符压根不应答。第三检查上拉电阻。如果模块上没有上拉SDA 在高电平阶段是弱上拉或者浮空状态读数据大概率出错。5.2 ACK 一直收不到IIC_WaitAck() 函数如果一直返回失败大概率是以下原因从机电源没给够。STH20 工作电压范围是 1.8V~3.6V如果你给的是 5V传感器可能不工作。我吃过这个亏——模块板上标着 3.3V我却用开发板的 5V 输出供电结果 SDA 上完全没反应。换到 3.3V 供电后一切正常。地址写错或者读写位弄反。这个上面提过了说白了发送的字节必须是 0x80 或 0x81。总线上挂着多个设备地址冲突。如果 OLED 或者别的传感器恰好也是 0x40 地址就会冲突。检查你的 IIC 总线上还挂了什么设备。5.3 温湿度读数跳变严重跳变说明通信本身是通的但数据不稳定。最常见的原因是测量命令发出后延时时间不够读到了半截数据。比如温度还没有完全转换完你就去读了读回的高低字节拼出来就是乱七八糟的数。解决办法是把延时加到 25ms~30ms用时间换稳定。另外还有一个波形质量的问题。如果杜邦线接得很长比如超过 20cmIIC 时钟频率又快400kHz信号反射会带来波形畸变导致错读。这时候要么降速把 IIC_RecvByte 里的延时加大要么缩短杜邦线要么用一个干净的 PCB 布线。5.4 逻辑分析仪怎么看时序对不对强烈建议买一个几十块的 8 通道逻辑分析仪配合 Sigrok PulseView 或者商家送的软件调试 IIC 效率翻倍。操作要点把逻辑分析仪的 CH0 接 SCL、CH1 接 SDA、GND 和开发板共地解码协议选 IIC从机地址填 0x40。然后你就能在电脑屏幕上直观看到总线上每一帧通信的波形和解析结果——地址、命令、数据、应答全被标出来。我调试的时候发现过一个特别有意思的现象我明明在代码里写了 20ms 延时但在逻辑分析仪上看两次 IIC 操作之间的间隔明显不足 20ms。查了半天发现是 STC8G 的库函数 delay_ms() 在主频没配置对的情况下实际延时变成了标称值的一半。这就是“看起来在等其实没等够”的典型案例。5.5 驱动文件移植到其他单片机要注意什么如果你以后想把这份驱动搬到 STM32 上有两点必须改第一IIC 底层的 GPIO 操作要适配新的 HAL 库或标准库尤其是 SDA 的输入输出方向切换在 STM32 上需要配置 MODER 寄存器跟 51 的准双向 IO 概念不一样。第二延时函数要重新实现。51 上我用了简单的空循环STM32 上一般用 HAL_Delay() 或者 SysTick。特别提醒如果主频变了延时也要相应调整IIC 时序不是越慢越好但也不能快到超过 STH20 的极限速率 400kHz。6. 硬度和舒适度多传感器总线的扩展思路写完 STH20 的驱动IIC 这块算是入了门。接下来你可以用同一套底层 IIC去驱动其他设备——我后来几天就是这么干的OLED 屏幕用 IIC 显示温湿度EEPROM 存校准参数还有一个气压传感器 BMP280 也挂在同一根总线上。当你手里的 IIC 设备多起来之后有几个设计习惯需要养成第一建议每个设备单独做一层应用封装对外只提供初始化函数和读数据/写数据函数这样主循环的代码会非常干净。第二IIC 总线上挂多个设备时每个设备的地址必须不同选设备之前先翻数据手册确认地址。第三总线上的上拉电阻只需一组不要在每块模块上都加否则上拉过强低电平可能拉不下去——这个也是经验我之前挂了三块模块每块都自带 4.7k 上拉等效电阻变成了 1.5k 左右结果 SDA 拉不低通信失败后来去掉两块的跳线帽就好了。还有一个真实项目里的细节如果在同一个 IIC 总线上既有时序敏感的数据采集设备又有 OLED 这种刷新频率无所谓慢一点也没关系的显示设备建议把 IIC 速率调低一点比如 100kHz。这样显示会稍微慢一点但整条总线的稳定性会明显提升数据出错的概率大幅下降。说到底低速通信的可靠性远高于高速尤其是杜邦线飞线调试的阶段。STH20 这颗传感器我后来在几个项目里复用得很舒服一颗芯片就能搞定温湿度采集还不用校准数据直接出物理量省了不少事。如果你正在学 IIC用这颗传感器来练手是挺理想的选择——协议标准、代码量适中、资料也不少跑通了成就感特别足。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →