☰
SHT20温湿度传感器GPIO模拟I2C驱动实战与避坑指南
2026/10/15 16:14:51 网站建设 项目流程

简介:这份资源面向使用STM32(Cortex-M3内核)进行嵌入式开发的工程师与学习者,提供通过GPIO模拟I2C协议驱动SHT20数字温湿度传感器的完整软件实现。SHT20具备±2%湿度与±0.3°C温度测量精度,支持I2C接口、低功耗与快速响应,常用于环境监测、智能家居与物联网设备。资源包共3个文件,包含C源码、头文件与中文技术手册PDF,压缩包约449KB,源码负责初始化、命令发送与数据读取,头文件定义常量与函数原型,手册则讲解工作原理、通信协议、接口定义及故障排查。目前已有2304人学习下载。读者可据此理解GPIO模拟I2C的时序实现,掌握传感器通信流程,并借助中文手册快速完成调试与项目集成,适合需要脱离硬件I2C外设、灵活复用引脚的嵌入式开发者参考。

1. SHT20 温湿度传感器程序:为什么 GPIO 模拟 I2C 反而是更稳的落地路径

SHT20 这颗温湿度传感器在嵌入式圈子里属于“老熟人”,I2C 接口、供电范围宽、精度够用,价格也友好。但真正把它塞进项目里,很多人第一反应是翻 MCU 的硬件 I2C 外设,配置引脚复用、时钟、DMA,结果卡在总线死锁、时序不匹配、引脚被占用这些玄学问题上。我自己的血泪经验是:当硬件 I2C 外设被其他从机占满,或者 MCU 本身 I2C 外设少得可怜时,用两个普通 GPIO 模拟 I2C 反而是更可控的方案——时序完全由你说了算,出问题能单步调试,不用跟外设寄存器黑匣子较劲。

这篇笔记就围绕“SHT20 温湿度传感器程序(GPIO 模拟 I2C)+ 中文手册”这个方向,把选型理由、时序实现、参数配置、中文手册里容易看漏的细节,以及实际调试中翻车的几个点讲清楚。适合正在用 8 位或 32 位 MCU 做环境采集、想快速跑通 SHT20 又不想被硬件 I2C 绑死的嵌入式开发者。读完你能拿到一套可复现的 GPIO 模拟 I2C 驱动框架,知道 SHT20 的测量命令怎么发、CRC 怎么校验、温湿度怎么换算,也能避开我踩过的那些坑。

2. SHT20 与 GPIO 模拟 I2C:先搞懂时序再动手写代码

2.1 SHT20 的 I2C 地址与命令集,中文手册里最容易看漏的三处

SHT20 的 7 位 I2C 地址是 0x40,写操作时左移一位变成 0x80,读操作是 0x81。这个在中文手册的“通信协议”章节里通常写得很清楚,但有三处细节容易被忽略。

第一处是测量命令的保持时间。SHT20 支持两种测量模式:保持主机模式(Hold Master)和非保持主机模式(No Hold Master)。保持模式下,传感器在测量期间会拉低 SCL 线,主机必须等待;非保持模式下,传感器不拉低 SCL,主机需要自己轮询或延时等待测量完成。中文手册里对这两种模式的时序图是分开画的,如果只看命令表不看时序图,很容易发错命令后读回全 0 或全 1。

第二处是分辨率配置。SHT20 默认分辨率是 12 位湿度 + 14 位温度,但可以通过“用户寄存器”写入配置字节来改成 8 位湿度 + 12 位温度。这个配置字节的 bit0 是分辨率选择位,bit6 是片上加热器开关。中文手册里用户寄存器的表格通常和命令表隔了好几页,翻的时候容易漏。

第三处是 CRC 校验的多项式。SHT20 的 CRC 是 8 位校验,多项式 0x131,初始值 0x00。中文手册里会给出一个 CRC 计算流程图,但不会直接给代码。很多人图省事跳过 CRC,结果在长线缆或电机干扰环境下读回的数据偶尔跳变,查半天查不出原因。

提示:中文手册里“电气特性”章节的供电电压范围是 2.1V 到 3.6V,但实际用 3.3V 供电时,如果 MCU 的 GPIO 是 5V tolerant,上拉电阻接到 5V 会超过 SHT20 的绝对最大电压。上拉电阻必须接到 3.3V。

2.2 GPIO 模拟 I2C 的四个基础时序函数:起始、停止、写字节、读字节

GPIO 模拟 I2C 的核心就是把 I2C 的时序用软件延时和引脚电平翻转拼出来。SHT20 支持标准模式 100kHz 和快速模式 400kHz,但 GPIO 模拟时我一般跑 100kHz 左右,留足余量,避免因为 MCU 主频波动或中断打断导致时序违规。

先定义两个 GPIO 引脚:SCL 和 SDA。SDA 需要双向,所以初始化时配置为开漏输出(如果 MCU 支持)或输出/输入动态切换。下面是一套基于常见 MCU 的伪代码框架,实际移植时只需要替换 GPIO 操作宏和延时函数。

// gpio_i2c.h - GPIO 模拟 I2C 基础宏定义 // 假设 SCL 接 PA5,SDA 接 PA6,实际按你的板子改 #define I2C_SCL_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define I2C_SCL_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define I2C_SDA_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_6) #define I2C_SDA_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_6) #define I2C_SDA_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) // 延时函数,100kHz 下约 5us 半周期 // 具体值用示波器或逻辑分析仪校准 static void i2c_delay(void) { volatile uint32_t i = 10; // 根据主频调整 while (i--) { } } // 起始条件:SCL 高时 SDA 由高变低 void i2c_start(void) { I2C_SDA_HIGH(); I2C_SCL_HIGH(); i2c_delay(); I2C_SDA_LOW(); // SCL 高期间 SDA 拉低 = 起始 i2c_delay(); I2C_SCL_LOW(); // 钳住总线,准备发数据 i2c_delay(); } // 停止条件:SCL 高时 SDA 由低变高 void i2c_stop(void) { I2C_SDA_LOW(); I2C_SCL_HIGH(); i2c_delay(); I2C_SDA_HIGH(); // SCL 高期间 SDA 拉高 = 停止 i2c_delay(); } // 写一个字节,返回从机 ACK 状态:0=ACK,1=NACK uint8_t i2c_write_byte(uint8_t data) { uint8_t i; for (i = 0; i < 8; i++) { if (data & 0x80) I2C_SDA_HIGH(); else I2C_SDA_LOW(); data <<= 1; i2c_delay(); I2C_SCL_HIGH(); // 拉高 SCL,从机采样 SDA i2c_delay(); I2C_SCL_LOW(); i2c_delay(); } // 第 9 个时钟:释放 SDA,读从机 ACK I2C_SDA_HIGH(); // 主机释放 SDA i2c_delay(); I2C_SCL_HIGH(); i2c_delay(); uint8_t ack = I2C_SDA_READ(); // 0 表示从机拉低 = ACK I2C_SCL_LOW(); i2c_delay(); return ack; } // 读一个字节,ack=0 发送 ACK,ack=1 发送 NACK uint8_t i2c_read_byte(uint8_t ack) { uint8_t i, data = 0; I2C_SDA_HIGH(); // 主机释放 SDA,准备读 for (i = 0; i < 8; i++) { data <<= 1; I2C_SCL_HIGH(); i2c_delay(); if (I2C_SDA_READ()) data |= 0x01; I2C_SCL_LOW(); i2c_delay(); } // 发送 ACK 或 NACK if (ack) I2C_SDA_HIGH(); // NACK else I2C_SDA_LOW(); // ACK i2c_delay(); I2C_SCL_HIGH(); i2c_delay(); I2C_SCL_LOW(); i2c_delay(); I2C_SDA_HIGH(); // 释放总线 return data; }

这段代码里最关键的是i2c_delay()的取值。延时太短,时序超过 400kHz,SHT20 可能不响应;延时太长,测量一次要等很久。我一般先用逻辑分析仪抓 SCL 频率,调到 90kHz 到 110kHz 之间。如果没有逻辑分析仪,可以先用一个较大的延时值(比如 20us),确保能通信后再逐步减小。

另一个容易翻车的地方是 SDA 方向切换。写字节时 SDA 是输出,读字节前必须把 SDA 切成输入。上面代码里用I2C_SDA_HIGH()在开漏模式下等效于释放总线,但如果你的 MCU 不支持开漏,就需要在读写切换时改寄存器方向位。这个在中文手册的 GPIO 章节里不会写,得看 MCU 的参考手册。

2.3 SHT20 测量流程:发命令、等测量、读数据、算温湿度

SHT20 的典型测量流程分四步:发送测量命令、等待测量完成、读取原始数据、换算成物理量。以非保持主机模式为例,温度测量命令是 0xF3,湿度测量命令是 0xF5。

// sht20.c - SHT20 驱动核心逻辑 #define SHT20_ADDR_W 0x80 #define SHT20_ADDR_R 0x81 #define SHT20_CMD_TEMP 0xF3 #define SHT20_CMD_HUMI 0xF5 #define SHT20_CMD_USER_W 0xE6 #define SHT20_CMD_USER_R 0xE7 // 读取温湿度原始值,temp_raw 和 humi_raw 为输出 uint8_t sht20_read_raw(uint16_t *temp_raw, uint16_t *humi_raw) { uint8_t buf[3]; // 读温度 i2c_start(); if (i2c_write_byte(SHT20_ADDR_W)) { i2c_stop(); return 1; } if (i2c_write_byte(SHT20_CMD_TEMP)) { i2c_stop(); return 2; } i2c_stop(); // 非保持模式下,等待测量完成,14 位温度约 85ms delay_ms(85); i2c_start(); if (i2c_write_byte(SHT20_ADDR_R)) { i2c_stop(); return 3; } buf[0] = i2c_read_byte(0); // ACK buf[1] = i2c_read_byte(0); // ACK buf[2] = i2c_read_byte(1); // NACK,最后一个字节 i2c_stop(); // CRC 校验 if (sht20_crc8(buf, 2) != buf[2]) return 4; *temp_raw = (buf[0] << 8) | buf[1]; *temp_raw &= 0xFFFC; // 低 2 位是状态位,清零 // 读湿度,流程同上 i2c_start(); if (i2c_write_byte(SHT20_ADDR_W)) { i2c_stop(); return 5; } if (i2c_write_byte(SHT20_CMD_HUMI)) { i2c_stop(); return 6; } i2c_stop(); delay_ms(30); // 12 位湿度约 29ms i2c_start(); if (i2c_write_byte(SHT20_ADDR_R)) { i2c_stop(); return 7; } buf[0] = i2c_read_byte(0); buf[1] = i2c_read_byte(0); buf[2] = i2c_read_byte(1); i2c_stop(); if (sht20_crc8(buf, 2) != buf[2]) return 8; *humi_raw = (buf[0] << 8) | buf[1]; *humi_raw &= 0xFFFC; return 0; } // CRC-8 校验,多项式 0x131,初始值 0x00 uint8_t sht20_crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0x00; uint8_t i, j; for (i = 0; i < len; i++) { crc ^= data[i]; for (j = 0; j < 8; j++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; } // 温湿度换算 // 温度 = -46.85 + 175.72 * temp_raw / 65536 // 湿度 = -6.0 + 125.0 * humi_raw / 65536 void sht20_convert(uint16_t temp_raw, uint16_t humi_raw, float *temp, float *humi) { *temp = -46.85f + 175.72f * temp_raw / 65536.0f; *humi = -6.0f + 125.0f * humi_raw / 65536.0f; if (*humi > 100.0f) *humi = 100.0f; if (*humi < 0.0f) *humi = 0.0f; }

这段代码里有几个参数需要根据中文手册确认。温度换算公式里的 -46.85 和 175.72 是 14 位分辨率下的系数,如果改成 12 位分辨率,系数会变。湿度换算的 -6.0 和 125.0 是 12 位分辨率下的系数。中文手册的“换算公式”章节会给出不同分辨率对应的系数表,改分辨率时一定要同步改公式。

CRC 校验函数里的多项式 0x31 是 0x131 去掉最高位的结果,因为 CRC-8 只取低 8 位。这个在中文手册的 CRC 流程图里不会明说,但对照流程图走一遍就能推出来。

注意:非保持模式下,测量等待时间要留足余量。中文手册标称 14 位温度最大 85ms,但实际如果传感器刚从低温环境进入高温环境,测量时间可能略长。我一般延时 100ms 保险。

3. 把 SHT20 驱动塞进项目:初始化、周期采集与数据滤波

3.1 GPIO 与延时初始化:上拉电阻、开漏配置和延时校准

在把驱动集成到项目之前,硬件和底层初始化要先确认三件事。

第一是上拉电阻。SHT20 的 SDA 和 SCL 都需要上拉,典型值 4.7kΩ 到 10kΩ。如果 MCU 的 GPIO 配置为开漏输出,外部上拉是必须的;如果配置为推挽输出,虽然也能通信,但总线冲突时可能烧引脚。我一般统一用开漏加外部 4.7kΩ 上拉,接到 3.3V。

第二是 GPIO 模式。SCL 始终是输出,SDA 在写数据时是输出,读数据时是输入。如果 MCU 支持开漏输出,SDA 配置为开漏输出,读之前写 1 释放总线即可。如果不支持开漏,就需要在读写切换时改方向寄存器。下面是一个常见的初始化代码。

// gpio_i2c_init - 初始化 GPIO 模拟 I2C 引脚 void gpio_i2c_init(void) { GPIO_InitTypeDef gpio; // 使能 GPIO 时钟,按你的 MCU 改 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCL 推挽输出,SDA 开漏输出 gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // 总线空闲状态:SCL 和 SDA 都拉高 I2C_SCL_HIGH(); I2C_SDA_HIGH(); }

第三是延时校准。i2c_delay()里的循环次数需要根据 MCU 主频调整。一个粗略的估算方法是:如果 MCU 跑 72MHz,一个空循环大约几个时钟周期,循环 10 次大概 1us 左右。但编译器优化等级会影响这个值,所以最好用示波器测 SCL 频率。没有示波器的话,可以先用一个已知的 I2C 从机(比如 OLED 屏)测试通信是否正常,再换 SHT20。

3.2 周期采集任务:定时器触发、状态机与超时重试

在实际项目里,SHT20 不会只读一次,通常是周期采集。我一般用定时器每 2 秒触发一次采集,采集过程用状态机拆成“发命令、等测量、读数据、换算”四个状态,避免在主循环里阻塞太久。

// sht20_task.c - 周期采集状态机 typedef enum { SHT20_IDLE = 0, SHT20_MEAS_TEMP, SHT20_WAIT_TEMP, SHT20_READ_TEMP, SHT20_MEAS_HUMI, SHT20_WAIT_HUMI, SHT20_READ_HUMI, SHT20_DONE } sht20_state_t; static sht20_state_t state = SHT20_IDLE; static uint32_t state_tick = 0; static uint16_t temp_raw, humi_raw; static float temperature, humidity; // 每 1ms 调用一次 void sht20_task(void) { switch (state) { case SHT20_IDLE: // 等待定时器触发,这里用 tick 判断 if (state_tick == 0) { i2c_start(); if (i2c_write_byte(SHT20_ADDR_W) == 0 && i2c_write_byte(SHT20_CMD_TEMP) == 0) { i2c_stop(); state = SHT20_WAIT_TEMP; state_tick = 100; // 等 100ms } else { i2c_stop(); state_tick = 10; // 失败重试 } } else { state_tick--; } break; case SHT20_WAIT_TEMP: if (state_tick > 0) { state_tick--; break; } i2c_start(); if (i2c_write_byte(SHT20_ADDR_R) == 0) { uint8_t buf[3]; buf[0] = i2c_read_byte(0); buf[1] = i2c_read_byte(0); buf[2] = i2c_read_byte(1); i2c_stop(); if (sht20_crc8(buf, 2) == buf[2]) { temp_raw = ((buf[0] << 8) | buf[1]) & 0xFFFC; state = SHT20_MEAS_HUMI; state_tick = 0; break; } } i2c_stop(); state = SHT20_IDLE; state_tick = 100; // 失败后等 100ms 重试 break; case SHT20_MEAS_HUMI: i2c_start(); if (i2c_write_byte(SHT20_ADDR_W) == 0 && i2c_write_byte(SHT20_CMD_HUMI) == 0) { i2c_stop(); state = SHT20_WAIT_HUMI; state_tick = 40; // 湿度等 40ms } else { i2c_stop(); state = SHT20_IDLE; state_tick = 100; } break; case SHT20_WAIT_HUMI: if (state_tick > 0) { state_tick--; break; } i2c_start(); if (i2c_write_byte(SHT20_ADDR_R) == 0) { uint8_t buf[3]; buf[0] = i2c_read_byte(0); buf[1] = i2c_read_byte(0); buf[2] = i2c_read_byte(1); i2c_stop(); if (sht20_crc8(buf, 2) == buf[2]) { humi_raw = ((buf[0] << 8) | buf[1]) & 0xFFFC; sht20_convert(temp_raw, humi_raw, &temperature, &humidity); state = SHT20_DONE; break; } } i2c_stop(); state = SHT20_IDLE; state_tick = 100; break; case SHT20_DONE: // 数据就绪,可以在这里做滤波或上报 state = SHT20_IDLE; state_tick = 2000; // 2 秒后再采 break; } }

这个状态机的关键点是每个状态都有超时重试。I2C 总线在干扰环境下可能偶尔 NACK,如果一直卡在某个状态,整个采集任务就挂了。我一般给每个状态设一个最大重试次数,超过就复位状态机重新开始。

3.3 温湿度数据滤波:滑动平均与中值滤波的取舍

SHT20 的原始数据本身比较稳,但在电机、继电器、无线模块附近,偶尔会有跳变。我一般用滑动平均加中值滤波的组合:先取 5 次采样,去掉最大最小值,剩下 3 个取平均。这样既能抑制脉冲干扰,又不会让响应太慢。

// 滑动平均 + 中值滤波 #define FILTER_SIZE 5 static float temp_buf[FILTER_SIZE]; static float humi_buf[FILTER_SIZE]; static uint8_t filter_idx = 0; void sht20_filter(float temp_in, float humi_in, float *temp_out, float *humi_out) { temp_buf[filter_idx] = temp_in; humi_buf[filter_idx] = humi_in; filter_idx = (filter_idx + 1) % FILTER_SIZE; // 复制到临时数组排序 float t[FILTER_SIZE], h[FILTER_SIZE]; uint8_t i, j; for (i = 0; i < FILTER_SIZE; i++) { t[i] = temp_buf[i]; h[i] = humi_buf[i]; } // 冒泡排序,数据量小,够用 for (i = 0; i < FILTER_SIZE - 1; i++) { for (j = 0; j < FILTER_SIZE - 1 - i; j++) { if (t[j] > t[j + 1]) { float tmp = t[j]; t[j] = t[j + 1]; t[j + 1] = tmp; } if (h[j] > h[j + 1]) { float tmp = h[j]; h[j] = h[j + 1]; h[j + 1] = tmp; } } } // 去掉最大最小,取中间 3 个平均 *temp_out = (t[1] + t[2] + t[3]) / 3.0f; *humi_out = (h[1] + h[2] + h[3]) / 3.0f; }

这个滤波器的参数 FILTER_SIZE 可以根据实际场景调。如果环境变化慢,可以加到 7 或 9;如果需要快速响应,减到 3。注意滤波后的数据会有一定滞后,做报警逻辑时要用原始数据判断突变,用滤波数据做显示。

4. SHT20 调试避坑:从 NACK 到数据跳变的排查记录

4.1 上电后一直 NACK:上拉电阻和地址搞错是重灾区

现象:SHT20 上电后,发起始条件有 ACK,但发完地址字节后从机不拉低 SDA,读回 NACK,后续命令全部失败。

原因通常有两个。一是上拉电阻没接或阻值太大。SHT20 的 SDA 和 SCL 必须上拉,如果 MCU 内部上拉只有 40kΩ 左右,加上线缆电容,上升沿会变缓,100kHz 下可能采不到高电平。二是地址搞错。SHT20 的 7 位地址是 0x40,写地址是 0x80,读地址是 0x81。有些人直接拿 0x40 当写地址发,从机不响应。

解决:先用万用表量 SDA 和 SCL 对 3.3V 的电阻,确认在 4.7kΩ 到 10kΩ 之间。然后用逻辑分析仪抓波形,看地址字节是不是 0x80。如果没有逻辑分析仪,可以写一个简单的扫描程序,遍历 0x00 到 0xFE 的写地址,看哪个地址有 ACK。

4.2 读回数据全是 0xFF 或 0x00:测量命令和等待时间不对

现象:地址有 ACK,命令也发出去了,但读回的温度原始值是 0xFFFC 或 0x0000,换算出来温度是 -46.85°C 或 125°C 附近。

原因:一是发了保持主机模式命令但没等 SCL 被拉低就继续操作。保持模式下,传感器测量期间会拉低 SCL,主机如果不等,读回的就是无效数据。二是非保持模式下等待时间不够。14 位温度需要 85ms,如果只等 10ms 就读,传感器还没转换完。三是命令字节发错,比如把 0xF3 写成 0xF5,读回的是湿度原始值,换算成温度自然不对。

解决:确认你用的是保持模式还是非保持模式。保持模式下,发完命令后要等 SCL 被从机拉低再拉高,或者直接延时 100ms。非保持模式下,延时 100ms 再读。命令字节对照中文手册的命令表再核对一遍。

4.3 数据偶尔跳变:CRC 没开和线缆干扰

现象:大部分时候数据正常,但偶尔温度跳 2°C 到 3°C,湿度跳 5% 以上,过一会儿又恢复。

原因:一是没做 CRC 校验,干扰导致某一位翻转,读回的数据错了但没被发现。二是 SDA 和 SCL 走线太长,和电机线、继电器线捆在一起,耦合了干扰。三是电源纹波大,SHT20 的供电脚没有加去耦电容。

解决:打开 CRC 校验,校验失败的样本直接丢弃,不要用。SDA 和 SCL 走线尽量短,远离功率线,必要时用屏蔽线或双绞线。SHT20 的 VDD 和 GND 之间加一个 100nF 陶瓷电容,靠近传感器引脚放置。

4.4 总线死锁:SCL 被从机拉低不释放

现象:程序跑一段时间后,SCL 一直低电平,主机发什么从机都不响应,复位 MCU 才能恢复。

原因:I2C 总线在通信过程中被中断打断,时序错乱,从机进入异常状态,拉低 SCL 不放。或者主机在从机还没释放 SDA 时就发了停止条件,导致从机状态机卡死。

解决:在 I2C 初始化时加一个总线恢复函数:如果检测到 SCL 或 SDA 被拉低,主机发送 9 个时钟脉冲,让从机把剩余数据移完,然后发停止条件。另外,在 I2C 读写函数里加超时机制,如果某个操作超过一定时间没完成,强制复位 GPIO 和状态机。

// I2C 总线恢复:发送 9 个时钟脉冲 void i2c_bus_recover(void) { uint8_t i; I2C_SDA_HIGH(); // 释放 SDA for (i = 0; i < 9; i++) { I2C_SCL_LOW(); i2c_delay(); I2C_SCL_HIGH(); i2c_delay(); } i2c_start(); i2c_stop(); }

4.5 中文手册里的“典型应用电路”不能照抄

现象:按照中文手册的典型应用电路接,上拉电阻接到 5V,结果 SHT20 通信不稳定,甚至发热。

原因:中文手册的典型应用电路有时是通用 I2C 从机的参考图,上拉电压可能标的是 5V。但 SHT20 的供电范围是 2.1V 到 3.6V,如果 MCU 是 5V 系统,上拉电阻接到 5V,SHT20 的 SDA 和 SCL 引脚会承受 5V 电压,超过绝对最大额定值。

解决:上拉电阻必须接到 SHT20 的 VDD,也就是 3.3V。如果 MCU 是 5V 系统,需要加电平转换电路,或者用开漏输出加上拉到 3.3V,但 MCU 的 GPIO 要设置成开漏模式,不能推挽输出 5V。

5. 进阶技巧:用 GPIO 模拟 I2C 同时挂多颗 SHT20 与低功耗采集

5.1 多颗 SHT20 共总线:地址冲突与分时供电

SHT20 的 I2C 地址固定是 0x40,不能通过引脚改地址。如果要在同一组 GPIO 上挂多颗 SHT20,有两种做法。一是用 I2C 多路复用器,比如 TCA9548A 这类芯片,把总线分成多路,每路挂一颗。二是分时供电,每颗 SHT20 的 VDD 单独用一个 GPIO 控制,采集时只给目标传感器上电,其他传感器断电,避免地址冲突。

分时供电的代码框架如下:

// 多颗 SHT20 分时供电采集 #define SHT20_PWR1_PIN GPIO_Pin_0 #define SHT20_PWR2_PIN GPIO_Pin_1 void sht20_select(uint8_t idx) { // 先全部断电 GPIO_ResetBits(GPIOB, SHT20_PWR1_PIN | SHT20_PWR2_PIN); delay_ms(10); // 给目标传感器上电 if (idx == 0) GPIO_SetBits(GPIOB, SHT20_PWR1_PIN); else GPIO_SetBits(GPIOB, SHT20_PWR2_PIN); delay_ms(20); // 等传感器上电稳定 }

分时供电的缺点是每次切换要等上电稳定,采集周期会变长。如果对实时性要求高,还是用多路复用器更合适。

5.2 低功耗采集:测量间隙断电与 GPIO 状态保持

电池供电的场景下,SHT20 的功耗要控制。SHT20 测量时电流约 300uA,空闲时约 0.15uA。如果每 2 秒采一次,平均电流很小。但 GPIO 模拟 I2C 的引脚如果一直输出高电平,上拉电阻上会有持续电流。我一般采集完成后把 SCL 和 SDA 都拉低,SHT20 的 VDD 也断电,下次采集前再重新初始化。

// 采集完成后进入低功耗 void sht20_sleep(void) { I2C_SCL_LOW(); I2C_SDA_LOW(); // 如果 VDD 可控,断开传感器供电 GPIO_ResetBits(GPIOB, SHT20_PWR_PIN); } // 唤醒并重新初始化 void sht20_wakeup(void) { GPIO_SetBits(GPIOB, SHT20_PWR_PIN); delay_ms(20); gpio_i2c_init(); i2c_bus_recover(); // 防止上次断电时时序错乱 }

注意 SCL 和 SDA 拉低后,如果上拉电阻还在,会有电流流过。如果上拉电阻接到可控电源,可以把上拉电源也断掉。但大多数情况下,上拉电阻的电流在微安级,对电池寿命影响不大。

5.3 验证方法:用已知温湿度环境交叉比对

驱动写完后,怎么确认读出来的数据是准的?我一般用三个方法交叉验证。一是用手捂住传感器,看湿度是否快速上升、温度是否缓慢上升。二是用另一颗已知准确的温湿度模块(比如同一批次的 SHT20 或者经过校准的模块)放在同一环境,对比读数。三是用冰水混合物(0°C)和沸水(100°C,注意海拔影响)做两点校准,看温度误差是否在 ±0.5°C 以内。

如果读数偏差大,先检查换算公式的分辨率系数是否匹配,再检查 CRC 是否通过,最后检查电源电压是否在 2.1V 到 3.6V 之间。SHT20 的精度在 25°C 时是 ±0.3°C,湿度 ±3%,超出这个范围就要找原因。

我自己在多个项目里用 GPIO 模拟 I2C 驱动 SHT20,最深的教训是:不要迷信硬件 I2C,也不要迷信中文手册的典型电路。时序用逻辑分析仪抓一遍,上拉电阻用万用表量一遍,CRC 校验打开,超时重试加上,这套组合拳下来,SHT20 基本不会给你掉链子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询