简介:面向单片机开发者的SHT20温湿度传感器驱动压缩包,聚焦IIC总线下的驱动控制与数据采集问题,适用于在嵌入式项目中接入SHT20、完成温度湿度实时读取的场景。资源共2个文件,包含1个h头文件和1个c源文件,压缩包整体仅1KB,结构精简;h头文件用于声明接口与寄存器定义,c源文件则实现具体IIC时序、命令发送与数据读取。驱动实现覆盖设备初始化、传感器参数配置(如分辨率与测量模式)、启动温湿度测量、原始数据读取、数据解码,以及基础错误处理等关键环节,并关注通信过程中的时序配合。由于SHT20返回的是编码数据,驱动内还包含温湿度换算算法,可直接得到工程可用的温度和湿度值。这套文件已有79人学习,适合刚接触IIC协议或传感器驱动开发的读者对照学习。借助这套驱动,能够快速理解SHT20与单片机之间的主从通信过程,也为后续自行移植驱动或扩展其他IIC传感器提供参考。
1. 项目概述:为什么我选了 STH20 而不是 DHT11
day06 是个分水岭。前五天我还在跟 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?
我对比过两者的差异,这直接决定了我的代码架构:
| 特性 | DHT11 | STH20 |
|---|---|---|
| 通信协议 | 单总线(自定义时序) | IIC(标准化协议) |
| 湿度精度 | ±5% RH | ±2% RH |
| 温度精度 | ±2°C | ±0.3°C |
| 分辨率 | 8bit / 8bit | 14bit / 14bit |
| 最大功耗 | 测量时约 3mA | 测量时约 200uA |
| 数据输出 | 直接输出温湿度值 | 原始码,需要公式换算 |
| 地址 | 无 | 0x40(7bit地址) |
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 设备都能驱动起来。
起始信号(START):SCL 保持高电平,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,用时间换稳定。
另外还有一个波形质量的问题。如果杜邦线接得很长,比如超过 20cm,IIC 时钟频率又快(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,用这颗传感器来练手是挺理想的选择——协议标准、代码量适中、资料也不少,跑通了成就感特别足。
本文还有配套的精品资源,点击获取