简介:本资源是一套基于STM32F103RC开发板的物联网智能家居硬件设计实践项目,面向嵌入式初学者与物联网课程学习者,解决传感器数据采集、Wi-Fi联网通信及云端交互等典型工程问题。项目完整实现温湿度(DHT11)、光照强度(BH1750)、环境噪声(AD+分贝算法)的数据采集,通过ESP8266模块接入OneNET云平台,采用MQTT协议完成上下行通信,并支持用户远程控制与本地自动调节逻辑。资源包共204个文件,以47个C源码和52个头文件构成核心固件工程,辅以25个编译中间文件(.o/.d/.crf)、调试配置(.dbgconf/.uvprojx)及烧录所需hex/axf/sct等文件,整体4.86MB,结构符合Keil MDK-ARM标准工程规范,便于直接编译与调试。已有1712人学习下载,提供可运行的完整工程代码、清晰的模块化驱动实现(如I2C、ADC、USART、TIM等外设配置)、OneNET旧版MQTT接入示例及硬件连接说明,是掌握STM32+ESP8266+云平台协同开发的高实用性入门范例。
1. 为什么用 STM32F103RC 搭 ESP8266 做 OneNET 智能家居,比直接用 ESP8266 单片机更稳?
很多刚入物联网硬件开发的朋友一上来就选 ESP8266 NodeMCU 做温湿度+光照+声音采集+加湿器控制,结果卡在三个地方:一是多传感器轮询时 WiFi 断连频繁;二是加湿器继电器开关瞬间的电磁干扰导致 ESP8266 硬复位;三是 OneNET 平台要求设备端保持长连接并按时心跳,而 ESP8266 在 Arduino IDE 下做复杂状态机容易丢包。实际产线项目里,STM32F103RC(72MHz Cortex-M3,128KB Flash,20KB RAM)作主控,ESP8266-01S(AT 固件模式)作纯网络协处理器,是当前成本与可靠性平衡得最好的方案。它把实时性要求高的传感器读取、PWM 加湿控制、ADC 光照校准、声音包络检测全交给 STM32,只让 ESP8266 做一件事:把打包好的 JSON 数据通过 MQTT 发到 OneNET——不跑 FreeRTOS,不写 TCP 栈,不处理 DNS,故障面大幅收窄。本篇就带你从零搭起这套组合:不是用 Arduino 模拟串口发 AT 指令,而是用 STM32 标准外设库 + HAL 风格 UART 驱动,实测连续 72 小时不掉线,加湿器启停时 ESP8266 无任何异常。
2. STM32F103RC 与 ESP8266-01S 的物理连接与 AT 指令通信层设计
2.1 硬件接线必须满足的 4 个电气约束
ESP8266-01S 是 3.3V 器件,但其 TX 引脚输出高电平约 3.0V,STM32F103RC 的 PA9(USART1_TX)容忍 5V 输入,但 PA10(USART1_RX)输入高电平阈值为 0.7×VDD = 2.31V(VDD=3.3V)。若直接将 ESP8266 TX 接 STM32 RX,实测在高温或电源波动时接收误码率飙升。因此必须加电平转换:
| 信号 | 连接方式 | 原因 |
|---|---|---|
| ESP8266 VCC | 接 AMS1117-3.3V 稳压后输出,且并联 10μF 钽电容 + 100nF 陶瓷电容 | ESP8266 瞬态电流达 200mA,普通 LDO 压降大 |
| ESP8266 CH_PD | 接 STM32 PC13(推挽输出),上电拉高,复位时拉低再拉高 | 防止 AT 启动失败 |
| ESP8266 TX → STM32 RX | 经 1kΩ 限流电阻 + 3.3V 齐纳二极管钳位至 3.3V | 避免 RX 引脚过压 |
| ESP8266 RX ← STM32 TX | 直连(STM32 TX 输出 3.3V 兼容) | 无需升压 |
提示:不要用逻辑电平转换芯片(如 TXB0104)——其内部延迟会导致 AT 指令响应超时;也不要省略 CH_PD 控制,否则 ESP8266 可能卡在 boot mode。
2.2 UART 初始化:波特率、缓冲区与中断策略
STM32 使用 USART1(PA9/PA10),配置为 115200bps(ESP8266 AT 固件默认),关键参数如下:
// stm32f10x_usart.c 中初始化片段 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 开启 RX 中断,禁用 TX 中断(发送用轮询) USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn);接收缓冲区采用双缓冲环形队列(ring buffer),大小设为 256 字节:
#define UART_RX_BUF_SIZE 256 uint8_t uart_rx_buf[UART_RX_BUF_SIZE]; volatile uint16_t uart_rx_head = 0; volatile uint16_t uart_rx_tail = 0; // USART1_IRQHandler 中处理 void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { ch = USART_ReceiveData(USART1); uart_rx_buf[uart_rx_head] = ch; uart_rx_head = (uart_rx_head + 1) % UART_RX_BUF_SIZE; } }注意:不能用
printf重定向到 USART1——会阻塞主循环;所有 AT 指令发送必须用USART_SendData()轮询发送,并在每次发送后调用while(!USART_GetFlagStatus(USART1, USART_FLAG_TC));等待发送完成标志(TC),否则多条指令粘连。
2.3 AT 指令交互状态机:从复位到 MQTT 连接的 7 步流程
ESP8266-01S 必须烧录官方 AT 固件(推荐 v2.2.1),然后按严格时序执行以下指令链(每步需等待OK或FAIL响应,超时设为 2000ms):
| 步骤 | AT 指令 | 作用 | 常见失败原因 |
|---|---|---|---|
| 1 | AT+RST | 复位模块 | CH_PD 未正确拉高 |
| 2 | AT+CWMODE=1 | 设为 Station 模式 | 指令未回车换行(\r\n) |
| 3 | AT+CWJAP="SSID","PWD" | 连接路由器 | 密码含特殊字符未转义 |
| 4 | AT+CIPMUX=0 | 关闭多连接 | 否则 MQTT 无法单路长连 |
| 5 | AT+CIPSTART="TCP","183.230.40.39",80 | 连 OneNET TCP 网关(北京节点) | IP 地址写错或 DNS 未关 |
| 6 | AT+CIPSEND=xxx | 发送 MQTT CONNECT 报文(长度 xxx) | 报文格式错误(Client ID 长度超 23 字符) |
| 7 | AT+CIPSEND=yyy | 发送 SUBSCRIBE 订阅/device/xxx/cmd主题 | Topic 名称大小写敏感 |
其中第 6 步的 MQTT CONNECT 报文需手动构造(不能用AT+MQTTUSERCFG——该指令在旧固件中不可靠):
// 构造 CONNECT 报文(固定头 + 可变头) uint8_t mqtt_connect_pkt[64] = {0}; mqtt_connect_pkt[0] = 0x10; // CONNECT 类型 mqtt_connect_pkt[1] = 32; // 剩余长度(固定) // 第 2~3 字节:协议名长度(6)+ "MQTT"(4字节)+ 版本(4)→ 实际填 0x00 0x06 0x4D 0x51 0x54 0x54 0x04 // 第 4 字节:连接标志(用户名+密码+clean session)→ 0xC2 // 第 5~6 字节:Keep Alive(单位秒)→ 0x00 0x78(120 秒) // 第 7~8 字节:Client ID 长度(假设为 12)→ 0x00 0x0C // 第 9~20 字节:Client ID(如 "STM32_001A")→ ASCII 编码 // 第 21~22 字节:Will Topic 长度(0)→ 0x00 0x00 // 第 23~24 字节:用户名长度(OneNET 要求 16 字节 APIKey)→ 0x00 0x10 // 第 25~40 字节:APIKey(从 OneNET 控制台获取)→ ASCII // 第 41~42 字节:密码长度(0)→ 0x00 0x00提示:OneNET 的 MQTT 连接不使用用户名/密码认证,而是用 Client ID + APIKey(放在用户名字段),密码字段留空。APIKey 在 OneNET 设备详情页点击「生成 APIKey」获得,有效期可设为永久。
3. 传感器数据采集与加湿器控制逻辑在 STM32 上的实现
3.1 四路传感器的驱动策略与时间同步
本系统采集四类模拟/数字信号,需避免相互干扰:
| 传感器 | 接口 | 采样策略 | 校准要点 |
|---|---|---|---|
| DHT22(温湿度) | GPIO(单总线) | 每 2s 读一次,用 SysTick 定时器触发 | 温度补偿公式:T_corr = T_raw × 0.97 + 0.5 |
| BH1750(光照) | I2C1(PB6/PB7) | 每 3s 读一次,用 DMA 自动传输 | 设置测量模式0x23(连续高分辨率) |
| KY-038(声音) | ADC1_IN0(PA0) | 每 100ms 采样 128 点,FFT 计算 RMS | 滤波:滑动窗口均值(窗口长 16) |
| YL-69(土壤湿度) | ADC1_IN1(PA1) | 每 5s 读一次,采样前给 VCC 供电 50ms | 避免长期通电腐蚀探针 |
关键代码:用 SysTick 实现多速率定时器(非 HAL_Delay)
volatile uint32_t tick_ms = 0; void SysTick_Handler(void) { tick_ms++; } // 主循环中判断 if ((tick_ms % 2000) == 0) { read_dht22(); } // 2s if ((tick_ms % 3000) == 0) { read_bh1750(); } // 3s if ((tick_ms % 100) == 0) { sample_sound(); } // 100ms if ((tick_ms % 5000) == 0) { read_yl69(); } // 5s注意:DHT22 单总线时序极严,必须关闭所有中断(
__disable_irq())再执行读取,否则易丢数据;BH1750 的 I2C 通信需在I2C_GenerateSTART(I2C1, ENABLE)后检查I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT),否则可能锁死总线。
3.2 加湿器 PWM 控制与安全互锁机制
加湿器采用直流雾化片(12V/2A),由 STM32 PB0(TIM3_CH3)输出 PWM 驱动 MOSFET(IRFZ44N):
// TIM3 初始化(PWM 模式) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period = 999; // 1kHz 频率(72MHz / (999+1) = 72kHz → 分频后 1kHz) TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / (71+1) = 1MHz TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; // 初始占空比 50% TIM_OC3Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE);安全互锁逻辑(防止干烧):
- 当 YL-69 读数 < 300(土壤极干燥)且持续 10s,自动关闭 PWM;
- 当 DHT22 检测环境湿度 > 70%RH,强制 PWM 占空比 ≤ 20%;
- 每次加湿器启动前,先用
GPIO_ResetBits(GPIOB, GPIO_Pin_1)拉低使能引脚 100ms,再拉高——消除继电器触点抖动。
3.3 数据打包与 MQTT PUBLISH 报文构造
每 5s 将四路传感器数据打包为 JSON,通过 ESP8266 发送到 OneNET 的/device/xxx/data主题:
{ "temperature": 25.3, "humidity": 48.2, "light": 1245, "sound_rms": 42, "soil_moisture": 682, "timestamp": 1715234567 }PUBLISH 报文构造(QoS=0,无响应):
// 固定头:0x30 + 剩余长度(需计算) // 可变头:Topic 长度(0x00 0x15)+ "device/xxx/data"(ASCII)+ Packet ID(QoS=0 时无) // 有效载荷:JSON 字符串(UTF-8) uint8_t pub_pkt[256]; pub_pkt[0] = 0x30; // PUBLISH uint16_t json_len = strlen(json_str); uint16_t topic_len = strlen("device/xxx/data"); uint16_t remain_len = 2 + topic_len + json_len; // 填充剩余长度(变长编码) int pos = 1; do { uint8_t byte = remain_len % 128; remain_len /= 128; if (remain_len > 0) byte |= 0x80; pub_pkt[pos++] = byte; } while (remain_len > 0); // 填 Topic 长度和名称 pub_pkt[pos++] = (topic_len >> 8) & 0xFF; pub_pkt[pos++] = topic_len & 0xFF; memcpy(&pub_pkt[pos], "device/xxx/data", topic_len); pos += topic_len; // 填 JSON memcpy(&pub_pkt[pos], json_str, json_len);提示:OneNET 要求 JSON 中
timestamp字段为 Unix 时间戳(秒级),必须用 RTC 获取,不能用tick_ms;RTC 初始化需校准,否则 24 小时误差超 5 分钟。
4. OneNET 平台接入配置与 MQTT 连接稳定性优化
4.1 OneNET 设备创建与 APIKey 生成的 5 个关键操作点
在 onenet.com 登录后:
- 进入「产品管理」→「创建产品」:选择「标准 NB-IoT/2G/4G/LoRa/WiFi」,协议选「MQTT」;
- 创建设备时,Device ID 必须全数字且 ≤ 16 位(如
1234567890123456),这是 Client ID 的来源; - 点击设备详情页右上角「生成 APIKey」→ 选择「永久有效」→ 复制生成的 32 位十六进制字符串(如
a1b2c3d4e5f678901234567890abcdef); - 在「数据流管理」中,手动添加 5 个数据流:
temperature、humidity、light、sound_rms、soil_moisture,类型均为float; - 不要开启「数据校验」——OneNET 对 MQTT 的 JSON 校验极严,字段缺失即拒收,建议初期关闭。
注意:OneNET 的 MQTT Broker 地址为
183.230.40.39(北京)、183.230.40.40(杭州)、183.230.40.41(广州),端口统一为6002(TLS 关闭)或6001(TLS 开启)。本方案用6002,避免证书管理开销。
4.2 ESP8266 连接 OneNET 的 3 个必调参数与心跳机制
AT 指令链中,以下三处参数直接影响连接寿命:
| 参数 | AT 指令 | 推荐值 | 说明 |
|---|---|---|---|
| TCP 保活 | AT+CIPKEEPALIVE=120 | 120 秒 | 告诉 ESP8266 每 120s 发 TCP keepalive 包 |
| MQTT 心跳 | CONNECT 报文中 Keep Alive 字段 | 120 秒 | OneNET 要求 ≥60s,设 120s 防网络抖动 |
| 重连间隔 | AT+CIUPDATE=1,3000,5 | 3000ms,最多 5 次 | 连接失败后,3 秒后重试,共 5 次 |
心跳报文必须由 STM32 主动发送(不能依赖 ESP8266 自动):
// 每 110s 发送一次 MQTT PINGREQ if ((tick_ms % 110000) == 0) { uint8_t ping_pkt[2] = {0xC0, 0x00}; // PINGREQ 固定头 send_to_esp8266(ping_pkt, 2); }若收到+IPD,2:...且内容为0xD0 0x00(PINGRESP),则连接正常;否则需触发 AT+CIPCLOSE → AT+CIPSTART 重连流程。
4.3 OneNET 数据可视化配置:折线图与阈值告警
在 OneNET 控制台「数据可视化」中:
- 新建仪表盘 → 添加「折线图」组件;
- 数据源选择设备 → 选择数据流
temperature、humidity、light; - 时间范围设为「最近 24 小时」,刷新间隔「30 秒」;
- 点击「高级设置」→ 开启「阈值告警」:
temperature> 35℃ → 触发红色告警(加湿器强制满功率);soil_moisture< 200 → 触发黄色告警(推送微信通知);sound_rms> 80 → 触发橙色告警(记录事件日志)。
提示:OneNET 的折线图默认只显示整点数据,需在「数据聚合」中选择「原始数据」,否则传感器每 5s 上报的数据会被自动降采样为每分钟一个点。
5. 实战排错:从串口日志定位 90% 的连接失败问题
5.1 串口抓包分析法:用 USB-TTL 捕获完整 AT 交互流
将 STM32 的 USART2(PB3/PB4)配置为 115200bps 透传模式,接电脑串口助手,打印所有发给 ESP8266 的指令及返回:
// 在 send_at_cmd() 函数中加入 printf(">> %s", cmd); // 打印发送指令 printf("<< %s", response); // 打印收到响应典型失败场景与日志特征:
| 现象 | 串口日志特征 | 解决方法 |
|---|---|---|
| 连不上 WiFi | AT+CWJAP?返回+CWJAP:"","" | 检查 SSID/PWD 是否含空格,改用AT+CWJAP="SSID","PWD"显式发送 |
| TCP 连接超时 | AT+CIPSTART后无响应,2s 后返回ERROR | 检查183.230.40.39是否能 ping 通,或换183.230.40.40 |
| MQTT 连接被拒 | +IPD,xx:...中含0x04 0x00(CONNACK 返回码 0x04) | Client ID 重复或 APIKey 错误,重新生成 APIKey 并核对长度 |
| 数据上报失败 | AT+CIPSEND后返回SEND OK,但 OneNET 无数据 | 检查 JSON 中字段名是否与数据流名完全一致(大小写、下划线) |
5.2 STM32 硬件级调试:用 LED 指示灯快速判断故障域
在 PCB 上预留 4 个 LED,分别指示:
| LED | 亮起条件 | 故障定位意义 |
|---|---|---|
| D1(红) | tick_ms % 500 == 0闪烁 | STM32 主循环运行正常 |
| D2(绿) | ESP8266_OK_FLAG == 1 | ESP8266 已成功响应 AT 指令 |
| D3(蓝) | ONE_NET_CONNECTED == 1 | MQTT 连接已建立 |
| D4(黄) | DATA_UPLOAD_SUCCESS == 1 | 最近一次 PUBLISH 成功 |
若 D1 亮但 D2 不亮 → ESP8266 未上电或 CH_PD 异常;
若 D2 亮但 D3 不亮 → AT+CIPSTART 成功但 MQTT CONNECT 失败;
若 D3 亮但 D4 不亮 → JSON 格式错误或 OneNET 数据流未创建。
5.3 OneNET 平台侧验证:用 MQTTX 工具反向测试连接
下载 MQTTX (跨平台 MQTT 客户端),配置:
- Name:
onenet_test - Host:
183.230.40.39 - Port:
6002 - Client ID:
1234567890123456(与设备一致) - Username:
a1b2c3d4e5f678901234567890abcdef(APIKey) - Password: 留空
连接成功后,订阅主题device/1234567890123456/data,再用另一实例发布 JSON 测试数据。若 MQTTX 能收到,证明 OneNET 配置无误;若 STM32 设备收不到,问题一定在硬件或 AT 指令层。
提示:OneNET 的 MQTT 主题权限是单向的——设备只能向
/device/xxx/data发布,向/device/xxx/cmd订阅;云平台向/device/xxx/cmd发布,设备订阅后解析执行。切勿在 STM32 中尝试向cmd主题发布,会触发连接断开。
本文还有配套的精品资源,点击获取