简介这是面向单片机开发者的一份IIC通信实战驱动围绕SHT20温湿度传感器展开解决MCU通过IIC协议读取温湿度数据的核心问题。压缩包仅含2个文件以bsp_sht20.h头文件和bsp_sht20.c源文件构成整体大小约1KB典型的精简驱动结构适合初学者快速移植与理解。已有79人学习适合正在学习单片机外设驱动或需要快速集成SHT20传感器的读者。驱动文件覆盖初始化、配置命令、数据采集、数据解析与错误处理等完整流程头文件中封装了寄存器地址与函数接口源文件则按IIC时序实现了温湿度测量与返回数据解码可直接嵌入工程使用。借助这份代码开发者能省去翻阅数据手册和调试底层时序的精力更专注上层业务逻辑也能以此为模板扩展到其他IIC传感器驱动开发。1. 单片机 day06 的 IICSTH20这条链路在解决什么问题很多单片机学习路线会把 day06 定在从 GPIO 点灯跨到通信协议的那一天而 IIC 往往是第一个要啃的协议。它线少、帧简单却把时序、应答、上拉电阻这些黑匣子一次全塞进来STH20 又恰好是一个把 IIC 用到极致的温湿度传感器——寄存器不多、命令简单但几毫秒的等待没处理好读数就能从头到尾都是 0xFF。这里要写的就是把“IIC STH20 驱动文件”这条链路完整走通从硬件接线到驱动分层再到换算和排查整个过程不依赖特定芯片型号照着移植即可。先说结论STH20 这类传感器在 IIC 上没有什么复杂寄存器表核心就三件事——地址对、命令对、等待够。绝大多数 day06 翻车现场恰恰卡在第三件事发完测量命令就急着收数据收回来的是上一次测量的旧值或者干脆是全 1。这篇文章按驱动文件为主线先把 IIC 底层和 STH20 的时序讲清楚再给出一份可直接抄的最小代码最后用四个真实踩坑案例把 0xFF、卡死、湿度跳变这类问题逐个拆开。适合谁看正在跑某系列单片机学习例程的初学者以及想把零散 IIC 代码整理成模块的从业者。前者能照着一路复现后者能看到驱动文件该怎么拆分、超时和错误码怎么设计。按照这套方式做完后面再换任何 IIC 传感器基本都能省掉大半调试时间。2. 硬件层先别急着写代码STH20 的引脚、IIC 地址、上拉电阻写驱动之前先把硬件摸清。软件 IIC 本身不难难的是传感器挂在总线上之后的电气行为。很多“代码看起来完全正确但读不到数据”的问题最终都能顺着 SCL、SDA 的电平找到原因。STH20 模块大多数是四根引脚VCC、GND、SCL、SDA部分模块还会引出额外的地址脚但默认不用接。下面从地址、上拉、接线三个角度把硬件层定下来。2.1 STH20 地址怎么记7 位地址 0x40 与写入/读取字节的换算IIC 协议里的地址分 7 位地址和 8 位地址两种写法这是新手第一个容易混的地方。STH20 的 7 位地址是 0x40数据手册里有时直接写成 8 位写地址 0x80、读地址 0x81。换算规则就是 7 位地址左移一位最低位补读写标志写为 0读为 1。地址形式值说明7 位地址0x40硬件 IIC 外设配置时使用8 位写地址0x80软件 IIC 发送地址帧时使用8 位读地址0x81软件 IIC 读取数据时使用驱动文件里建议三种写法都留宏避免不同环境下传参传错#define STH20_ADDR_7BIT 0x40 #define STH20_ADDR_W 0x80 #define STH20_ADDR_R 0x81逻辑说明软件 IIC 是手动拼地址字节所以发送时直接用 0x80 和 0x81如果 MCU 带硬件 IIC 外设初始化从机地址时往往只填 7 位 0x40读写位由外设自动处理。常见错误是把 0x80 填进硬件 IIC 的地址寄存器导致地址帧变成 0x100总线上一片静默。这个宏拆开写就是提醒自己在不同模式下使用不同的常量。提示如果手里模块的丝印不是 STH20 而是兼容型号先按数据手册核对地址。部分兼容芯片把地址脚引出接高或接低会改变 7 位地址默认悬空或接低为 0x40。2.2 上拉电阻怎么选4.7k 还是 10k模块和裸片的差别IIC 总线是开漏结构SCL 和 SDA 本身不会主动输出高电平必须靠外部上拉电阻把电平拉高。模块上通常已经焊好了上拉电阻但裸片或自己画的板子上往往没有。如果缺上拉最典型的现象是主机发送正常但从机不回 ACK读回来的数据全是 0xFF因为 SDA 根本没被拉低的能力。上拉阻值选择取决于总线电容和工作频率。标准模式 100kHz 下4.7k 到 10k 都能用快速模式 400kHz 建议用 1k 到 4.7k阻值太大会让边沿变缓数据传输容易出错。阻值太小则会增大灌电流对 GPIO 和传感器都不友好。给一个经验表格总线频率总线电容推荐上拉100kHz较短连线200pF4.7k~10k100kHz长线或多个设备200pF2.2k~4.7k400kHz短连线1k~4.7k我一般会在裸片方案里先按 4.7k 起步预留焊盘可以换 2.2k。这里没有后悔药可吃因为如果一开始用了 10k等挂上第二个设备、总线电容翻倍之后再换电阻布线往往已经定死改起来要动板子。模块用户则要先确认板载上拉是否已经存在。判断方法很简单万用表量 SCL 到 VCC 之间的电阻有几十千欧以下的值就说明已经有上拉如果测出来是开路就需要自己在模块外面并联两颗电阻。2.3 接线和供电的 3 个坑长杜邦线、5V 供电、SCL/SDA 接反硬件接线看着简单但三个坑基本覆盖了 day06 阶段的大部分返工。第一杜邦线太长。面包板实验里十几厘米的杜邦线很常见但长线会带来寄生电容波形边沿变缓400kHz 下很容易出现第一个字节正确、后续字节乱码。解决方法是把软件 IIC 时钟降下来比如把半周期延时从 1.25us 提到 5us也就是把总线速率降到 100kHz 左右或者直接换短线、用排线缩短回路。第二5V 供电。STH20 这类传感器本质是 3.3V 器件很多模块板载了稳压电路可以接 5V但如果是裸片或板载稳压被跳线旁路5V 供电会烧毁传感器。更隐蔽的问题是上拉模块的 SCL/SDA 上拉电阻如果接到了 5V而 MCU 的 IO 不是 5V 容忍长期使用会损伤引脚。稳妥做法是统一 3.3V 供电MCU 和传感器共地。第三SCL 与 SDA 接反。这个错误在代码层面不会报任何错误但表现很怪地址帧能发出去传感器却始终不回 ACK或者回了一个非预期地址的 ACK。排查时先看原理图再用逻辑分析仪看波形SCL 线上应该是规律的方波SDA 线上才有数据变化如果两个波形反了先换线再查代码。3. STH20 驱动文件骨架初始化、测量时序、温湿度换算驱动文件的组织方式直接决定这个传感器代码能不能复用到下一个项目。day06 例程里最常见的写法是把所有 IIC 操作堆在 main.c 里跑通没问题但换一块板子就要重写。我更推荐把驱动分成三层IIC 底层、STH20 器件层、应用调用层。下面按这个层次把驱动文件的骨架定下来并详细说明测量时序和换算公式。3.1 驱动文件按三段拆开IIC 底层、传感器命令、应用层一个可复用的驱动文件至少应该拆成三个文件或三个模块模块职责典型文件IIC 底层起始、停止、读写字节、应答iic_sw.c / iic_hw.cSTH20 器件层封装测量命令、读取原始值、换算物理量sth20.c应用层按业务周期调用、处理错误码user_th_sensor.c这样拆的核心原因有两个。第一IIC 底层和传感器逻辑解耦以后把软件 IIC 换成硬件 IIC只需要替换底层几个函数器件层完全不用动。第二传感器逻辑和业务逻辑分开温湿度数据最终是给屏幕、串口、云端还是控制逻辑用都不是驱动文件该管的事。器件层的对外接口建议只保留两个初始化函数和读取温湿度函数。初始化可以做得很轻因为 STH20 上电后默认就可以测量不一定需要额外配置寄存器读取函数则返回一个错误码同时通过结构体指针带出温度值和湿度值。接口越窄移植时越省事。/* sth20.h 对外接口 */ typedef struct { float temperature_c; float humidity_rh; } sth20_data_t; uint8_t sth20_init(void); uint8_t sth20_read_temp_humi(sth20_data_t *data);逻辑说明初始化函数里可以先执行一次软复位命令把传感器内部状态复位到已知状态再等待 10ms 左右让它稳定。实际项目中如果发现首次读取数据明显异常问题往往就出在没做软复位。读取函数内部负责完整的测量流程应用层只需要定时调用。3.2 STH20 的测量命令与等待时序为什么不能发完命令立刻读数STH20 的测量命令不多核心是温湿度两个测量命令外加软复位。以兼容 SHT20 指令集的模块为例常用命令如下命令功能说明0xE3测量温度时钟拉伸模式SCL 被传感器拉低直到测量完成0xE5测量湿度时钟拉伸模式同上0xF3测量温度无时钟拉伸模式主机需自行等待测量时间0xF5测量湿度无时钟拉伸模式同上0xFE软复位复位传感器内部状态很多 day06 例程默认用 0xF3 和 0xF5也就是无时钟拉伸模式。这个模式下主机发送测量命令后传感器会释放总线自己慢慢测量。如果主机不等它完成就发起读操作读到的可能是旧数据也可能是空闲电平。测量时间取决于分辨率14 位分辨率下典型测量时间在 60ms 到 80ms最大不超过 100ms。正确的等待时序是发命令 → 等至少 80ms → 发起读操作。等待时间宁可给足也不要精确卡在典型值上。如果选用 0xE3 和 0xE5 时钟拉伸模式传感器会在测量期间把 SCL 拉低主机检测到 SCL 释放后再读数据时序上更自动但软件 IIC 下不容易检测这个状态所以我不太建议在移植性要求高的驱动里用它。3.3 温湿度原始值换算公式ADC 原始值转物理量整数运算还是浮点STH20 测量完成后读回来的是两个字节的原始值也就是 ADC 结果。如果传感器内部是 14 位 ADC原始值范围是 0 到 16383有的数据手册按 16 位方式描述高两位恒为 0。换算公式如下float sth20_raw_to_temp(uint16_t raw) { return -46.85f 175.72f * raw / 65536.0f; } float sth20_raw_to_humi(uint16_t raw) { return -6.0f 125.0f * raw / 65536.0f; }逻辑说明温度公式的 -46.85 和 175.72 是数据手册给出的线性化参数湿度公式的 -6 和 125 同理。分母用 65536 还是 65535 是另一个老生常谈的坑。严格按手册的标定公式分母取 65536 更常见有些驱动写成 65535两者在满量程下最多差 0.1% RH 左右肉眼基本看不出来但在湿度 100% 边界校准时会体现出差别。我的习惯是统一用 65536并在注释里标明出处免得后来的人乱改。如果 MCU 没有浮点单元比如某些入门级 8 位单片机每次都用 float 计算会拖慢速度。可以改成定点运算先把 175.72 和 125 放大 100 倍变成整数最后再手动除。但温湿度传感器本身不是高速场景性能不敏感优先保证代码可读性就好。3.4 驱动头文件必须有的宏地址、命令、超时、错误码驱动文件的头文件不只是放函数声明更重要的是把常量集中定义出来。初学者最容易犯的毛病是把地址、命令字散落在 .c 文件各个函数里想调参数时全局搜索半天。一个合格的 sth20.h 至少应该有下面这些内容/* 地址 */ #define STH20_ADDR_7BIT 0x40 #define STH20_ADDR_W 0x80 #define STH20_ADDR_R 0x81 /* 命令 */ #define STH20_CMD_T_MEAS_NOHOLD 0xF3 #define STH20_CMD_RH_MEAS_NOHOLD 0xF5 #define STH20_CMD_T_MEAS_HOLD 0xE3 #define STH20_CMD_RH_MEAS_HOLD 0xE5 #define STH20_CMD_SOFT_RESET 0xFE /* 时序参数 */ #define STH20_MEAS_TIMEOUT_MS 100 #define STH20_MEAS_DELAY_MS 80 #define STH20_RESET_DELAY_MS 10 /* 错误码 */ #define STH20_OK 0 #define STH20_ERR_NO_ACK 1 #define STH20_ERR_TIMEOUT 2 #define STH20_ERR_CRC 3 #define STH20_ERR_PARAM 4逻辑说明超时时间 100ms 是按最大测量时间留出的余量不推荐卡在 60ms 或 80ms因为传感器在低温或老化后测量时间会变长超时太紧会让设备在临界条件下不断报错。错误码从 0 开始是非零即错的惯例方便应用层快速判断返回值。预留 ERR_CRC 是因为后面讲到的第三字节 CRC 校验在正式产品里应该在驱动里做掉一开始就把错误码位置留好后面补代码不用改接口。4. 用软件模拟 IIC 跑通 STH20最小可运行代码与关键参数软件模拟 IIC 是理解协议最直接的方式。硬件 IIC 把时序细节藏在外设里出了问题反而难排查软件 IIC 每个时钟沿都由代码控制只要逻辑正确行为就是可预判的。这一章给出一个平台无关的软件 IIC 实现再接上 STH20 读取代码最后说明几个关键参数怎么调。4.1 软件 IIC 的起始、停止、应答位GPIO 模拟的基准写法软件 IIC 的底层操作一共就四件事起始、停止、写一个字节、读一个字节外加应答位的产生与检测。下面的写法把 GPIO 操作抽成平台函数实际使用时替换成自己芯片的库函数即可。/* 平台函数由具体芯片的 GPIO 库实现 */ static void iic_scl_write(uint8_t level); /* 控制 SCL 电平 */ static void iic_sda_write(uint8_t level); /* 控制 SDA 电平 */ static uint8_t iic_sda_read(void); /* 读取 SDA 电平 */ static void iic_sda_set_output(void); /* SDA 切到输出模式 */ static void iic_sda_set_input(void); /* SDA 切到输入模式 */ static void iic_delay_us(uint32_t us); /* 微秒延时 */ void iic_start(void) { iic_scl_write(1); iic_sda_set_output(); iic_sda_write(1); iic_delay_us(5); iic_sda_write(0); /* SCL 为高时 SDA 下降沿 - 起始 */ iic_delay_us(5); iic_scl_write(0); iic_delay_us(5); } void iic_stop(void) { iic_scl_write(0); iic_sda_set_output(); iic_sda_write(0); iic_delay_us(5); iic_scl_write(1); iic_delay_us(5); iic_sda_write(1); /* SCL 为高时 SDA 上升沿 - 停止 */ iic_delay_us(5); } uint8_t iic_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { iic_scl_write(0); if (dat 0x80) iic_sda_write(1); else iic_sda_write(0); dat 1; iic_delay_us(3); iic_scl_write(1); iic_delay_us(5); iic_scl_write(0); iic_delay_us(3); } /* 检测从机 ACK释放 SDA拉高 SCL 后读电平 */ iic_sda_set_input(); iic_scl_write(0); iic_delay_us(2); iic_scl_write(1); iic_delay_us(4); uint8_t ack (iic_sda_read() 0) ? 1 : 0; iic_scl_write(0); iic_delay_us(3); iic_sda_set_output(); return ack; } uint8_t iic_read_byte(uint8_t send_ack) { uint8_t dat 0, i; iic_sda_set_input(); for (i 0; i 8; i) { dat 1; iic_scl_write(0); iic_delay_us(4); iic_scl_write(1); iic_delay_us(4); if (iic_sda_read()) dat | 1; iic_scl_write(0); iic_delay_us(3); } /* 主机发送 ACK 或 NACK */ if (send_ack) { iic_sda_set_output(); iic_sda_write(0); } else { iic_sda_set_output(); iic_sda_write(1); } iic_scl_write(0); iic_delay_us(2); iic_scl_write(1); iic_delay_us(4); iic_scl_write(0); iic_delay_us(3); iic_sda_set_input(); return dat; }逻辑说明起始条件是 SCL 高电平时 SDA 产生下降沿停止条件是 SCL 高电平时 SDA 产生上升沿。写字节时数据在 SCL 低电平期间变化、高电平期间采样这是 IIC 协议的基本约束。ACK 检测必须在发送完第 8 个时钟后把 SDA 切成输入否则主机会把自己的输出电平读回来永远读到高电平误判为从机无应答。参数说明这里的 5us 延时对应约 100kHz 总线速率。如果系统时钟跑得很快GPIO 翻转本身只有几十纳秒必须靠延时函数保证时序宽度。如果调试时发现接收数据错位先把两个 5us 都翻倍确认信号完整性后再往下压。读字节时数据采样点放在 SCL 高电平的中间位置也就是延时一半后再读能避开边沿附近的抖动区。4.2 STH20 读取温湿度完整流程写命令、等待测量完成、连读三个字节有了底层 IICSTH20 的器件层就很简单了。完整流程分两步先写测量命令再等测量完成后读数据。每个测量命令后面跟的是三个字节数据高位、数据低位、CRC 校验字节。CRC 字节在初学阶段可以先读出来但不用校验产品代码里再算。uint8_t sth20_write_cmd(uint8_t cmd) { uint8_t ack; iic_start(); ack iic_write_byte(STH20_ADDR_W); ack iic_write_byte(cmd); iic_stop(); return (ack 2) ? 0 : 1; /* 两次应答都有才算成功 */ } uint8_t sth20_read_16bit(uint16_t *raw) { uint8_t buf[3]; iic_start(); if (!iic_write_byte(STH20_ADDR_R)) { iic_stop(); return 1; } buf[0] iic_read_byte(1); /* 数据高位主机回 ACK */ buf[1] iic_read_byte(1); /* 数据低位主机回 ACK */ buf[3] iic_read_byte(0); /* CRC 字节主机回 NACK */ iic_stop(); *raw ((uint16_t)buf[0] 8) | buf[1]; return 0; } uint8_t sth20_read_temp_humi(sth20_data_t *data) { uint16_t raw_t, raw_h; if (data 0) return STH20_ERR_PARAM; if (sth20_write_cmd(STH20_CMD_T_MEAS_NOHOLD)) return STH20_ERR_NO_ACK; delay_ms(STH20_MEAS_DELAY_MS); if (sth20_read_16bit(raw_t)) return STH20_ERR_NO_ACK; if (sth20_write_cmd(STH20_CMD_RH_MEAS_NOHOLD)) return STH20_ERR_NO_ACK; delay_ms(STH20_MEAS_DELAY_MS); if (sth20_read_16bit(raw_h)) return STH20_ERR_NO_ACK; style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />