简介: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 的从机地址是 0x40(7 位地址,读写时左移一位)。它不像 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; }原始码值右移两位是因为低两位是状态位,计算时直接丢弃,转换公式里分母用 16384(2^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 电流让传感器温度比环境高 5~15°C。这个功能在冷链运输、机房冷通道这类高湿度环境里非常实用——传感器结露后湿度读数会长时间卡在 100%,单纯靠通风无法快速恢复,加热几十秒就能让敏感膜恢复正常响应。代价是温度读数不再反映环境温度,所以加热期间得放弃上报温度,等关闭加热并稳定 2 秒后再恢复正常测量。代码里只需在初始化后追加一条写寄存器操作,而每次上电默认该位是 0。
3. ESP8266 透传链路:AT 固件版本、接线与 TCP 长连
3.1 选模块和固件:ESP-01 与 ESP-12F 的适用边界
市面上贴着 ESP8266 标签的模块至少有三种形态:ESP-01、ESP-12F、以及集成 USB 转串口的 NodeMCU 开发板。做量产产品选 ESP-12F,PCB 天线、四层屏蔽、引脚间距适合贴片;做快速原型选 NodeMCU,毕竟 Arduino IDE 直接开发 NodeMCU 的管脚定义、烧录方式资料最全,能省掉 USB 转 TTL 的接线。但注意 NodeMCU 的板载 LDO 在 Wi-Fi 发射瞬间压降明显,要带 STM32F103ZE 这种大芯片时最好独立供电,共地不共电源。
AT 固件版本直接影响透传行为。老款安信可 AT 固件用AT+CIPMODE=1进入透传模式后,退出要发+++,而且前后必须各留 1 秒静默时间。乐鑫官方新版 AT 在透传模式下退出命令改成+++后跟回车,部分版本还支持AT+CIPATTA自动附着 TCP。我倾向于在工程包里锁死固件版本,把 AT 固件的日期和 SDK 版本写进说明文档,否则用户刷了不同版本固件,后面所有 AT 指令的返回值解析逻辑都可能翻车。
3.2 STM32 与 ESP8266 之间的连接原理图要点
| STM32F103ZE 引脚 | ESP8266 模块引脚 | 说明 |
|---|---|---|
| PA2 (USART2_TX) | RXD | 注意交叉连接,电平都是 3.3V |
| PA3 (USART2_RX) | TXD | 串口 2 用于 Wi-Fi,串口 1 留给调试 |
| 3.3V (外部 LDO) | VCC | ESP8266 峰值电流可达 300mA |
| GND | GND | 必须共地 |
| 任意空闲 GPIO | RST | 可选,用于异常时硬件复位 |
| 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", // 关闭回显 "AT+CWMODE=1\r\n", // 1=Station模式 "AT+CWJAP=\"myssid\",\"mypass\"\r\n", // 连接Wi-Fi,需替换 "AT+CIPSTART=\"TCP\",\"139.9.123.45\",8080\r\n", // 建立TCP "AT+CIPMODE=1\r\n", // 进入透传模式 "AT+CIPSEND\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/'>' }AT+CWMODE=1的返回值是OK,连接 Wi-Fi 的AT+CWJAP在信号不好时会返回ERROR或CONNECT FAIL,必须重试。进入透传模式后,串口收到的所有数据都会原封不动发给服务器,反之服务器下发的内容直接到达串口——这意味着你的采集逻辑和网络收发逻辑必须通过中断或 DMA 解耦,不能在主循环里用阻塞式uart2_wait_reply,否则服务器数据到达时会撑爆接收缓冲。建议做法是开启串口 2 空闲中断,把透传数据导入环形队列,主循环只做传感器采集和队列发送。
3.4 断线检测与自动重连的常见做法
透传模式的最大弱点是链路断了你还不知道。ESP8266 在 TCP 连接断开时会有UNLINK或在一段静默后直接返回ERROR提示,但前提是它感知到了断开。如果路由器掉电、远端服务器崩溃,模块可能一直保持在半开状态。常见做法是应用层加心跳——每 30 秒发送一个 4 字节的心跳帧,服务器 90 秒没收到就判定链路死亡并主动断开 TCP,ESP8266 收到 FIN 后才会退出透传模式,这时 STM32 重新执行AT+CIPSTART和AT+CIPMODE=1。也可以让 STM32 定期发AT+CIPSTATUS查询链路状态,但查询本身会打断透传数据流,不建议在数据频繁上报时用。
4. 数据协议设计:从裸数据到可解析的上行帧
4.1 定长二进制帧还是 JSON:取决于网关还是直连
很多初学者的工程包直接把温湿度拼成"temp=25.3&humi=60.1"这种字符串往 TCP 里塞,服务端拿到后用strtok分割。这在小规模调试时没问题,但一旦接入正式物联网平台,或者需要转发给多个业务系统,这种非结构化字符串会在解析层埋下灾难。我建数据帧时不推荐 JSON,因为 F103ZE 的 SRAM 有 64KB 不假,但 STM32 上做 JSON 序列化要引入 cJSON 库,Flash 占用和解析开销都是负担。定长二进制帧是嵌入式到云端传输里更常见的选择,固定偏移、固定长度,解析代码十行内搞定。
下面定义一个 10 字节上行帧,所有多字节字段采用大端序:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 2 | 帧头 | 固定 0xAA 0x55 |
| 2 | 1 | 设备类型 | 0x01 表示温湿度节点 |
| 3 | 1 | 设备地址 | 设备 ID 低 8 位,可扩展 |
| 4 | 2 | 温度 | 补码表示的整数,单位 0.01°C |
| 6 | 2 | 湿度 | 无符号整数,单位 0.01%RH |
| 8 | 1 | 帧序号 | 0~255 循环,用于乱序检测 |
| 9 | 1 | CRC-8 | 从帧头到帧序号字节的 CRC |
4.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 的 PA2(TX)线上,同时抓取进入 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 做本地显示,都是顺手的事。
本文还有配套的精品资源,点击获取