ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于STM32和ESP8266的SHT20温湿度采集上云方案

基于STM32和ESP8266的SHT20温湿度采集上云方案 简介STM32F103ZE与ESP8266、SHT20组合的物联网温湿度监测工程面向嵌入式初学者和物联网开发者解决通过Wi-Fi远程采集环境温湿度的常见需求。项目以STM32F103ZE为主控通过I2C读取SHT20传感器数据再经串口驱动ESP8266模块将数据上传至网络或云端架构清晰适合综合实践。压缩包共117个文件约778KB以51个h头文件与48个c源文件为核心包含外设驱动、Keil工程配置、hex烧录文件和bat脚本等完整度较高。已有285人浏览/学习。工程代码覆盖时钟配置、串口与I2C初始化、ESP8266网络连接、数据协议处理等关键环节并带有gizwits_protocol相关模块便于理解设备对接云平台的典型流程同时涉及ADC、DMA等外设方便二次扩展。无论是课程设计、毕业设计还是智能家居与小型环境监测项目都可将此源码作为可扩展的参考起点。1. 为什么这套组合至今仍是低成本温湿度上云的标准答案拿到“STM32F103ZE_ESP8266模块SHT20温湿度模块”这个工程包命名第一反应是典型的课设或小型工业采集节点主控选了大容量 F103ZE通信交给 ESP8266传感器用 I2C 接口的 SHT20。比起 DHT11 这类单总线器件SHT20 的数字输出和出厂校准意味着算法层几乎零补偿比起直接上 4G 模组ESP8266 的成本和功耗又低一个量级。这套结构常被新手当作“STM32 连 Wi-Fi 透传”的练手项目但真要在产线上跑坑全藏在固件版本、I2C 时序和断线重连里。F103ZE 在 F1 家族里属于大容量型号512KB Flash、64KB SRAM跑一个裸机采集任务绰绰有余哪怕要挂 RTOS 也还有余量。很多人会问为什么不用 C8T6答案很简单ZE 引出的 PB6/PB7 硬件 I2C 和多余串口让你在调 ESP8266 时不用跟引脚复用较劲。这篇文章就沿着“传感器采集 → 数据帧封装 → Wi-Fi 透传 → 上云排错”这条链路把每一个环节的参数和命令讲到能直接抄作业。2. SHT20 采集I2C 命令、分辨率与 CRC 校验一次讲清楚2.1 别急着上硬件 I2C先把 SHT20 的寄存器地图背下来SHT20 的从机地址是 0x407 位地址读写时左移一位。它不像 DHT11 那样有严格时序要求只要 I2C 时钟在 10kHz 到 400kHz 之间都能正常工作。关键是要记住触发器命令0xF3 触发温度测量无时钟拉伸、0xF5 触发湿度测量无时钟拉伸而带时钟拉伸的版本是 0xE0 和 0xE5。这两种模式的区别在于测量期间总线是否被从机拉低——时钟拉伸模式下主控只需发送命令后等待释放代码更简单但某些软件模拟 I2C 在对接时容易漏掉对 SCL 低电平的侦测。STM32F103 的硬件 I2C 一代确实有各种历史遗留问题比如总线错误后很难自动恢复。我的习惯是直接用 GPIO 模拟 I2C把时序控制权完全握在手里尤其是读 SHT20 这类慢速传感器模拟方式反而更省心。下面是模拟 I2C 读取温湿度的核心逻辑注意 SHT20 每次测量需要等待转换完成温度 14 位分辨率下典型转换时间是 85ms湿度 12 位下是 29ms别傻等。// 模拟I2C读取SHT20返回0成功-1为设备无应答 #define SHT20_ADDR_W 0x80 // 0x40 1 #define SHT20_ADDR_R 0x81 uint8_t sht20_read_measure(uint8_t cmd, uint16_t *raw) { uint8_t buf[3]; i2c_start(); if (i2c_send_byte(SHT20_ADDR_W) ! 0) { i2c_stop(); return -1; } if (i2c_send_byte(cmd) ! 0) { i2c_stop(); return -1; } i2c_stop(); delay_ms(100); // 覆盖14位温度/12位湿度的最长转换时间 i2c_start(); if (i2c_send_byte(SHT20_ADDR_R) ! 0) { i2c_stop(); return -1; } buf[0] i2c_read_byte(ACK); buf[1] i2c_read_byte(ACK); buf[2] i2c_read_byte(NACK); i2c_stop(); *raw ((uint16_t)buf[0] 8) | buf[1]; return 0; } float sht20_calc_temp(uint16_t raw) { return -46.85f 175.72f * (raw 2) / 16384.0f; } float sht20_calc_humi(uint16_t raw) { float rh -6.0f 125.0f * (raw 2) / 16384.0f; if (rh 0) rh 0; if (rh 100) rh 100; return rh; }原始码值右移两位是因为低两位是状态位计算时直接丢弃转换公式里分母用 163842^14匹配 12/14 位有效数据。校验字节 buf[2] 暂未参与验证但如果你要往工业级方向做这个字节必须用来做 CRC-8 校验细节放在本文最后一章展开。2.2 分辨率配置与测量节奏的取舍SHT20 的用户寄存器地址是 0xE7读回后 bit7 和 bit0 控制分辨率。默认上电是 12 位湿度、14 位温度这是最推荐的组合。有人为了省功耗把分辨率降到 8 位/11 位但 SHT20 在低分辨率模式下的噪声明显增大尤其在 25°C 附近的恒温场景跳动幅度反而比 DHT11 更难看。实际测试下来14 位温度 12 位湿度的组合在 1Hz 采样率下功耗也没高到哪去没理由牺牲精度。测量节奏方面常见做法是每 2 秒采一次并通过 Wi-Fi 上报。这个间隔是经验值ESP8266 的 TCP 连接和 TLS 握手的耗时会占掉 300ms 到 1s如果传感器采集频率高于 Wi-Fi 上报频率数据会堆积不如直接让采集周期和上报周期同步。需要现场快速响应时可以把 SHT20 的转换等待时间从 100ms 缩短到刚好覆盖最大转换周期再配合串口 DMA 释放 CPU。2.3 加热器寄存器凝露场景的不起眼救星0xE6 是写用户寄存器命令bit2 置 1 会开启内部加热器消耗约 3mA 电流让传感器温度比环境高 515°C。这个功能在冷链运输、机房冷通道这类高湿度环境里非常实用——传感器结露后湿度读数会长时间卡在 100%单纯靠通风无法快速恢复加热几十秒就能让敏感膜恢复正常响应。代价是温度读数不再反映环境温度所以加热期间得放弃上报温度等关闭加热并稳定 2 秒后再恢复正常测量。代码里只需在初始化后追加一条写寄存器操作而每次上电默认该位是 0。3. ESP8266 透传链路AT 固件版本、接线与 TCP 长连3.1 选模块和固件ESP-01 与 ESP-12F 的适用边界市面上贴着 ESP8266 标签的模块至少有三种形态ESP-01、ESP-12F、以及集成 USB 转串口的 NodeMCU 开发板。做量产产品选 ESP-12FPCB 天线、四层屏蔽、引脚间距适合贴片做快速原型选 NodeMCU毕竟 Arduino IDE 直接开发 NodeMCU 的管脚定义、烧录方式资料最全能省掉 USB 转 TTL 的接线。但注意 NodeMCU 的板载 LDO 在 Wi-Fi 发射瞬间压降明显要带 STM32F103ZE 这种大芯片时最好独立供电共地不共电源。AT 固件版本直接影响透传行为。老款安信可 AT 固件用ATCIPMODE1进入透传模式后退出要发而且前后必须各留 1 秒静默时间。乐鑫官方新版 AT 在透传模式下退出命令改成后跟回车部分版本还支持ATCIPATTA自动附着 TCP。我倾向于在工程包里锁死固件版本把 AT 固件的日期和 SDK 版本写进说明文档否则用户刷了不同版本固件后面所有 AT 指令的返回值解析逻辑都可能翻车。3.2 STM32 与 ESP8266 之间的连接原理图要点STM32F103ZE 引脚ESP8266 模块引脚说明PA2 (USART2_TX)RXD注意交叉连接电平都是 3.3VPA3 (USART2_RX)TXD串口 2 用于 Wi-Fi串口 1 留给调试3.3V (外部 LDO)VCCESP8266 峰值电流可达 300mAGNDGND必须共地任意空闲 GPIORST可选用于异常时硬件复位3.3V 经 10kΩ 上拉CH_PD (EN)拉高使能不可悬空接线图看起来简单真正容易踩的坑是 VCC 供电。STM32F103ZE 的板载 3.3V LDO 通常输出能力只有 150mA 左右ESP8266 在 Wi-Fi 发射瞬间电流会冲到 250mA 以上共用 LDO 会导致模块反复重启。正确做法是给 ESP8266 单独配一个输出 500mA 以上的 3.3V LDO比如 AMS1117-3.3 用 5V 输入或者直接上 MP1584 降压模块。选串口 2 而不是串口 1是为了把调试日志和 AT 指令通道分开——接串口 1 的话每次看调试信息都得把 Wi-Fi 模块的 TX 断开操作体验极其痛苦。3.3 初始化序列从串口配置到 TCP 长连的完整 AT 流程ESP8266 上电后会先输出一串乱码这是 Boot ROM 的 74880 波特率启动日志属于正常现象紧接着模块自动切换到 AT 固件设定的波特率。下面是一套经过验证的初始化序列用 USART2 以 115200 8N1 发送每发一条命令须等待模块返回OK或ERROR再发下一条。// 每条AT命令通过串口2发送等待回复超时2000ms const char *at_cmds[] { AT\r\n, // 同步握手 ATE0\r\n, // 关闭回显 ATCWMODE1\r\n, // 1Station模式 ATCWJAP\myssid\,\mypass\\r\n, // 连接Wi-Fi需替换 ATCIPSTART\TCP\,\139.9.123.45\,8080\r\n, // 建立TCP ATCIPMODE1\r\n, // 进入透传模式 ATCIPSEND\r\n, // 开始透传返回 }; for (int i 0; i sizeof(at_cmds)/sizeof(at_cmds[0]); i) { uart2_send_string(at_cmds[i]); while (!uart2_wait_reply(2000)); // 等待OK/ERROR/ }ATCWMODE1的返回值是OK连接 Wi-Fi 的ATCWJAP在信号不好时会返回ERROR或CONNECT FAIL必须重试。进入透传模式后串口收到的所有数据都会原封不动发给服务器反之服务器下发的内容直接到达串口——这意味着你的采集逻辑和网络收发逻辑必须通过中断或 DMA 解耦不能在主循环里用阻塞式uart2_wait_reply否则服务器数据到达时会撑爆接收缓冲。建议做法是开启串口 2 空闲中断把透传数据导入环形队列主循环只做传感器采集和队列发送。3.4 断线检测与自动重连的常见做法透传模式的最大弱点是链路断了你还不知道。ESP8266 在 TCP 连接断开时会有UNLINK或在一段静默后直接返回ERROR提示但前提是它感知到了断开。如果路由器掉电、远端服务器崩溃模块可能一直保持在半开状态。常见做法是应用层加心跳——每 30 秒发送一个 4 字节的心跳帧服务器 90 秒没收到就判定链路死亡并主动断开 TCPESP8266 收到 FIN 后才会退出透传模式这时 STM32 重新执行ATCIPSTART和ATCIPMODE1。也可以让 STM32 定期发ATCIPSTATUS查询链路状态但查询本身会打断透传数据流不建议在数据频繁上报时用。4. 数据协议设计从裸数据到可解析的上行帧4.1 定长二进制帧还是 JSON取决于网关还是直连很多初学者的工程包直接把温湿度拼成temp25.3humi60.1这种字符串往 TCP 里塞服务端拿到后用strtok分割。这在小规模调试时没问题但一旦接入正式物联网平台或者需要转发给多个业务系统这种非结构化字符串会在解析层埋下灾难。我建数据帧时不推荐 JSON因为 F103ZE 的 SRAM 有 64KB 不假但 STM32 上做 JSON 序列化要引入 cJSON 库Flash 占用和解析开销都是负担。定长二进制帧是嵌入式到云端传输里更常见的选择固定偏移、固定长度解析代码十行内搞定。下面定义一个 10 字节上行帧所有多字节字段采用大端序偏移长度字段说明02帧头固定 0xAA 0x5521设备类型0x01 表示温湿度节点31设备地址设备 ID 低 8 位可扩展42温度补码表示的整数单位 0.01°C62湿度无符号整数单位 0.01%RH81帧序号0~255 循环用于乱序检测91CRC-8从帧头到帧序号字节的 CRC4.2 帧构造代码与 CRC 实现uint8_t frame[10]; float temp 25.37f, humi 60.12f; int16_t temp_i (int16_t)(temp * 100); // 2537 uint16_t humi_i (uint16_t)(humi * 100); // 6012 frame[0] 0xAA; frame[1] 0x55; frame[2] 0x01; frame[3] dev_addr; frame[4] (uint8_t)(temp_i 8); frame[5] (uint8_t)(temp_i 0xFF); frame[6] (uint8_t)(humi_i 8); frame[7] (uint8_t)(humi_i 0xFF); frame[8] seq; frame[9] crc8_compute(frame, 9); // 多项式0x31初值0xFF温度用int16_t存是因为温区可能覆盖负值乘以 100 保留两位小数。湿度是单极性的所以uint16_t足够。帧序号解决上报乱序问题——TCP 本身保证有序但经过 MQTT 桥接或日志转发时丢帧和重排都可能发生服务端根据帧序号就能识别缺口。CRC-8 的多项式建议直接用 SHT20 数据手册里的 0x31同 CRC-8/MAXIM这样传感器的逐字节校验和帧校验能共用一张查表代码。初值用 0xFF 还是 0x00 要和接收端约定一致这里选 0xFF 更符合多数工业协议的习惯。crc8_compute 函数的具体实现不是本项目的关键用查表法的话 256 字节的表放在 Flash 里几乎不占资源。4.3 上报频率与合并策略一个易被忽略的服务器压力点假定采集周期 2 秒、每帧 10 字节单设备上行带宽约 40bps这点流量对服务器不算压力但 1000 台设备同时 2 秒一帧就是 5000 帧/秒Nginx 后面的业务服务很容易被小包洪峰打满。建议在采集端做合并每 6 次采集12 秒合成一帧批量数据帧类型扩展为 0x02温度字段改成最多 6 组连续值。代价是实时性变差但对机柜温度这类缓变物理量完全够用。合并逻辑可以放在 STM32 端也可以让服务器端做区别在于前者省流量后者灵活看具体场景。5. 进阶验证技巧用串口抓包确认透传数据完整性5.1 抓包接线与分路监听调 ESP8266 串口透传时最头疼的问题是看不到数据究竟卡在哪一段是 STM32 没发出帧还是 ESP8266 没推到 TCP还是服务器收到后解析错。常见做法是用一个 USB-TTL 工具并联在 STM32 的 PA2TX线上同时抓取进入 ESP8266 前的数据流然后在服务器端用tcpdump抓以太网侧流量两相对照就能定位。抓包工具的波特率要设为与 STM32 串口 2 相同的 115200。并联监听不会影响原有通信但要注意 USB-TTL 的 RX 线没有负载效应问题直接用杜邦线跨接到 PA2 上即可。5.2 用 CRC 回读验证传感器链路最后一招用于确认 SHT20 的测量数据是否可靠。SHT20 在返回的第三个字节里带 CRC-8 校验覆盖前两个数据字节。把上面 sht20_read_measure 函数里读到的 buf[2] 用同样的 CRC 算法做比对不一致就丢弃本次测量并计数。实际工程中 CRC 出错率极低但如果发生原因通常是 I2C 线过长或电源纹波噪声干扰。做产品验证时建议连续运行 24 小时统计 CRC 失败次数超过 0.1% 就要查硬件设计或布线。把这套逻辑内联到温度读取函数里比盲目信任传感器更接近一个可交付的代码库该有的行为。我把 crc8_compute 实现放在最后方便直接剪切使用SHT20 官方例程里的crc8_check是逐位计算版本查表版在嵌入式平台运行更快static const uint8_t crc8_table[256] { 0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97, // 完整查表省略生成多项式0x31初值0xFF }; uint8_t crc8_compute(const uint8_t *data, uint16_t len) { uint8_t crc 0xFF; for (uint16_t i 0; i len; i) { crc crc8_table[crc ^ data[i]]; } return crc; }查表的填充方法是用0x31多项式逐位生成把 256 个结果写进数组。这里用初值 0xFF与 SHT20 数据手册一致同时兼顾了帧校验。抓包确认透传数据帧的最后一个字节值与 STM32 计算值相同整条链路的数据可信度就闭环了。后续如果要升级把这套帧格式通过 MQTT 网关转发或尝试在 STM32 侧接一个 OLED 做本地显示都是顺手的事。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进