1. 项目缘起与整体设计思路
1.1 为什么选STM32加OneNet这套组合
这个项目最早是我帮一个学弟做毕业设计时接手的,需求很明确:做一个能远程控制家里灯光、风扇、窗帘,还能实时看温湿度数据的智能家居系统。市面上现成的方案不少,但要么太贵,要么太封闭,改不动。最后定下来的技术路线是STM32做本地主控,ESP8266负责联网,数据上OneNet云平台,手机端通过MQTT协议下发指令和接收状态。
为什么是STM32而不是ESP32或者树莓派?这里有几个很实际的考量。STM32的GPIO操作足够底层,外设资源丰富,做多路继电器控制、PWM调光、ADC采集都很顺手,而且工业级的稳定性是经过验证的。ESP32虽然自带WiFi,但引脚资源紧张,做多路控制时经常要外扩IO,反而麻烦。树莓派跑Linux系统,功耗高、启动慢,对于“通电就要立刻工作”的家居场景并不合适。STM32加ESP8266的组合,本质上是把“实时控制”和“网络通信”两个职责拆开,各司其职,稳定性反而更好。
OneNet作为云平台的选择也很关键。它提供免费的设备接入额度,MQTT协议支持完善,数据可视化组件可以直接拖拽生成,对于个人项目和小型商用场景完全够用。相比自己搭MQTT服务器,省去了公网IP、服务器运维、证书配置这一堆麻烦事。你只需要在平台上创建产品、添加设备、拿到设备ID和鉴权信息,剩下的就是让ESP8266把数据发上去。
1.2 系统架构的分层设计
整个系统我把它分成四层来看,这样排查问题时思路会清晰很多。
第一层是感知与执行层,包括DHT11温湿度传感器、光敏电阻、继电器模块、LED灯带这些。它们直接和STM32的GPIO、ADC引脚相连,负责采集环境数据和执行开关动作。
第二层是本地控制层,核心就是STM32最小系统板。它跑一个主循环,定时读取传感器数据,根据预设阈值或者云端下发的指令控制继电器。这里我用了状态机的方式管理各个设备,避免逻辑混乱。
第三层是网络通信层,ESP8266通过UART和STM32通信,跑AT指令集或者刷MQTT固件。我实测下来,刷MQTT固件的方式更稳定,因为AT指令在处理长连接和断线重连时容易出问题。
第四层是云端与应用层,OneNet负责数据存储、转发和可视化,手机端可以用微信小程序或者MQTTX这类客户端来订阅和发布消息。
注意:分层设计的好处是,当网络出问题时,本地控制逻辑完全不受影响。我试过把ESP8266拔掉,STM32依然能根据温湿度自动控制风扇,这一点在演示时特别加分。
1.3 关键器件的选型对比
| 器件 | 选型 | 理由 | 替代方案 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 资源够用、资料多、价格低 | STM32F407,性能更强但成本高 |
| 联网 | ESP8266-01S | 便宜、AT指令成熟、社区大 | ESP32,但引脚少 |
| 温湿度 | DHT11 | 单总线、驱动简单 | SHT30,精度更高但贵 |
| 继电器 | 5V低电平触发 | 兼容STM32的3.3V逻辑 | 固态继电器,寿命长但贵 |
| 电源 | AMS1117-3.3 | 简单可靠 | MP1584,效率更高 |
这个选型表是我踩过几次坑之后定下来的。最早用DHT22,精度确实好,但价格是DHT11的三倍,对于演示项目来说没必要。继电器一开始买了高电平触发的,结果STM32上电瞬间引脚默认高电平,继电器会“啪”地吸合一下,后来换成低电平触发才解决。
2. 核心细节解析与实操要点
2.1 STM32的GPIO配置与继电器驱动
STM32的GPIO操作是这个项目的基础,但很多人在这里翻车。F103的GPIO有八种模式,控制继电器要用推挽输出,速度选50MHz就够了。初始化代码大概长这样:
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);这里有个细节:上电瞬间的电平状态。STM32复位后GPIO默认是浮空输入,但如果你在初始化之前引脚被外部电路拉高,继电器就可能误动作。我的做法是在硬件上给继电器控制引脚加一个下拉电阻,软件上初始化后立刻输出高电平(低电平触发继电器),确保上电时设备是关闭状态。
驱动继电器时,STM32的单个GPIO最大输出电流是20mA,而继电器线圈通常需要60-80mA。所以不能直接驱动,必须加三极管或者光耦。我用的是S8050三极管加1K基极电阻,继电器线圈两端反并联一个1N4148续流二极管,防止断电时反向电动势打坏三极管。
实操心得:如果你用光耦隔离,记得光耦的输入端也要加限流电阻。我试过直接把STM32引脚接光耦,结果光耦没坏,STM32的引脚倒是烧了一个。后来算了一下,光耦输入端压降约1.2V,STM32输出3.3V,需要串一个(3.3-1.2)/0.01=210Ω的电阻,取220Ω刚好。
2.2 DHT11的时序控制与数据校验
DHT11是单总线器件,对时序要求很严格。STM32的主频是72MHz,一个时钟周期约13.9ns,而DHT11的“1”和“0”是靠高电平持续时间区分的:26-28us表示“0”,70us表示“1”。用延时函数来卡这个时间,误差不能太大。
我的做法是用SysTick做微秒级延时,代码如下:
void delay_us(uint32_t us) { uint32_t temp; SysTick->LOAD = 9 * us; SysTick->VAL = 0; SysTick->CTRL = 0x01; do { temp = SysTick->CTRL; } while ((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL = 0x00; SysTick->VAL = 0; }读取流程是:主机拉低总线至少18ms,然后拉高20-40us,等待DHT11响应。DHT11会拉低80us,再拉高80us,然后开始传40位数据。每一位都以50us低电平开始,高电平持续时间决定是0还是1。
数据格式是:湿度整数+湿度小数+温度整数+温度小数+校验和。校验和等于前四个字节相加的低八位。我遇到过校验失败的情况,后来发现是两次读取间隔太短,DHT11需要至少1秒的间隔。改成2秒读一次之后,再没出过问题。
注意:DHT11的湿度小数部分在普通版本里一直是0,只有温度小数有分辨率。如果你需要更高精度,直接上SHT30,I2C接口,代码更简单,精度±2%RH。
2.3 ESP8266的固件刷写与MQTT配置
ESP8266-01S出厂一般是AT固件,但AT指令在处理MQTT长连接时不够稳定。我建议刷MQTT透传固件,比如安信可的AT固件或者NodeMCU固件。刷写需要USB转TTL模块,接线是:ESP8266的TX接TTL的RX,RX接TX,CH_PD接3.3V,GPIO0接地进入下载模式。
刷写工具用官方的Flash Download Tool,选择8Mbit的固件,地址填0x00000。刷完之后,GPIO0悬空,重新上电,模块就进入正常工作模式。
MQTT配置的关键参数有这几个:
| 参数 | 值 | 说明 |
|---|---|---|
| 服务器地址 | mqtts.heclouds.com | OneNet的MQTT接入点 |
| 端口 | 1883 | 非加密端口,加密用8883 |
| 设备ID | 平台生成 | 在OneNet设备详情页查看 |
| 产品ID | 平台生成 | 同上 |
| 鉴权信息 | 自定义 | 创建设备时设置的密钥 |
连接指令是:
AT+MQTTCONN=0,"mqtts.heclouds.com",1883,1然后订阅主题:
AT+MQTTSUB=0,"$sys/产品ID/设备ID/cmd/request/#",1发布数据用:
AT+MQTTPUB=0,"$sys/产品ID/设备ID/dp/post/json",1,0,0,"{\"temp\":25}"实操心得:ESP8266的AT指令对引号和逗号很敏感,建议用串口助手先手动测试一遍,确认每条指令都能返回OK,再写到STM32的代码里。我一开始直接在代码里拼字符串,结果因为少了一个逗号,调试了一下午。
2.4 OneNet平台的产品创建与数据流配置
OneNet的界面这几年改版了好几次,但核心逻辑没变:先创建产品,再添加设备,然后定义数据流。
创建产品时,协议选MQTT,数据格式选JSON,行业选智能家居。产品创建好后,进入设备管理,添加设备。设备名称随便填,但设备ID和鉴权信息要记下来,后面ESP8266连接要用。
数据流是OneNet的核心概念,你可以把它理解成“一类数据的通道”。比如温度是一个数据流,湿度是另一个,继电器状态又是一个。在产品的“数据流模板”里提前定义好,设备上报数据时就会自动匹配。
数据上报的JSON格式:
{ "temp": 25.6, "humi": 60.2, "relay1": 1, "relay2": 0 }OneNet收到后会自动解析成对应的数据流。你可以在“数据可视化”里拖拽仪表盘、开关、折线图来展示这些数据。
注意:OneNet的免费版有设备数和数据存储时间的限制,个人项目够用,但商用要提前评估。另外,数据流名称不要用中文,虽然平台支持,但ESP8266的AT指令处理中文容易乱码。
3. 实操过程与核心环节实现
3.1 硬件搭建与接线检查
硬件连接是整个项目最容易出问题的环节。我整理了一份接线表,照着接基本不会错:
| STM32引脚 | 连接对象 | 备注 |
|---|---|---|
| PA9 | ESP8266 RX | UART1 TX |
| PA10 | ESP8266 TX | UART1 RX |
| PB0 | 继电器1 IN | 低电平触发 |
| PB1 | 继电器2 IN | 低电平触发 |
| PB5 | DHT11 DATA | 加4.7K上拉 |
| PA0 | 光敏电阻 | ADC采集 |
| 3.3V | ESP8266 VCC | 必须3.3V |
| 5V | 继电器 VCC | 继电器用5V |
ESP8266的供电是重点。它的峰值电流能到300mA,而STM32板子上的3.3V稳压器通常只能给100-200mA。我一开始直接从STM32的3.3V取电,结果ESP8266一连接WiFi就重启。后来单独用一个AMS1117-3.3给它供电,问题解决。
实操心得:ESP8266的CH_PD引脚必须接高电平,否则模块不工作。我见过有人忘了接,然后一直怀疑是固件问题。另外,ESP8266的TX引脚是3.3V电平,直接接STM32的RX没问题,但STM32的TX是3.3V,接ESP8266的RX也没问题,两者电平兼容。
3.2 STM32主程序的逻辑框架
主程序我用了一个简单的状态机加定时器调度的结构。SysTick每1ms中断一次,在中断里维护几个计数器,分别控制传感器读取、数据上报、指令处理的节奏。
volatile uint32_t tick_1s = 0; volatile uint32_t tick_2s = 0; volatile uint32_t tick_100ms = 0; void SysTick_Handler(void) { tick_1s++; tick_2s++; tick_100ms++; } int main(void) { SystemInit(); GPIO_Config(); USART1_Init(115200); USART2_Init(115200); DHT11_Init(); ESP8266_Init(); while (1) { if (tick_100ms >= 100) { tick_100ms = 0; ESP8266_Process(); // 处理串口数据 } if (tick_1s >= 1000) { tick_1s = 0; DHT11_Read(&temp, &humi); Local_Control(temp, humi); // 本地自动控制 } if (tick_2s >= 2000) { tick_2s = 0; MQTT_Publish_Data(temp, humi, relay_state); } } }这个框架的好处是,每个任务的时间片是独立的,不会因为某个任务耗时过长而影响其他任务。比如DHT11读取需要20ms左右,放在1秒的任务里完全没问题。
3.3 MQTT数据上报与指令下发的完整流程
数据上报的流程是:STM32通过UART2给ESP8266发AT指令,ESP8266把数据打包成MQTT报文发到OneNet。指令下发则是反过来的:OneNet把指令推给ESP8266,ESP8266通过UART2传给STM32,STM32解析后执行。
上报数据的AT指令拼接:
void MQTT_Publish_Data(float temp, float humi, uint8_t relay) { char json[128]; sprintf(json, "{\"temp\":%.1f,\"humi\":%.1f,\"relay\":%d}", temp, humi, relay); char cmd[256]; sprintf(cmd, "AT+MQTTPUB=0,\"$sys/%s/%s/dp/post/json\",1,0,0,\"%s\"\r\n", PRODUCT_ID, DEVICE_ID, json); USART2_SendString(cmd); }指令下发的解析:
void ESP8266_Process(void) { if (USART2_RxFlag) { USART2_RxFlag = 0; if (strstr((char*)USART2_RxBuffer, "relay1")) { if (strstr((char*)USART2_RxBuffer, "\"1\"")) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 关闭继电器 } else { GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 打开继电器 } } memset(USART2_RxBuffer, 0, USART2_RxLen); } }这里有个坑:ESP8266返回的数据里可能包含多条消息,如果只用一次strstr可能会漏掉。我的做法是把接收缓冲区开大一点(512字节),每次处理完清空,并且在解析时用while循环找所有匹配项。
注意:OneNet下发的指令格式是固定的,你可以在平台的“设备调试”里手动发一条指令测试。我试过发
{"relay1":1},ESP8266收到后原样传给STM32,STM32解析后控制继电器,整个链路延迟在200ms左右,完全满足家居控制的需求。
3.4 手机端控制界面的快速搭建
手机端我没有自己写APP,直接用OneNet的“应用管理”里的“数据可视化”功能。拖一个开关组件,绑定到relay1数据流,设置开值为1、关值为0。再拖两个仪表盘显示温度和湿度,一个折线图显示历史数据。
如果你想要更灵活的控制,可以用微信小程序。OneNet提供了微信小程序的SDK,你只需要在小程序里调用它的API,就能读取设备数据和下发指令。我试过用MQTTX这个桌面客户端来测试,连接参数和ESP8266一样,订阅$sys/产品ID/设备ID/cmd/request/#主题,就能收到设备上报的数据。
实操心得:OneNet的数据可视化组件有缓存,有时候你改了数据流名称,界面上还是显示旧的。这时候刷新一下页面,或者退出重新登录就好了。另外,开关组件的“开”和“关”对应的值要和你STM32里定义的一致,我定义的是1开0关,结果平台上默认是0开1关,导致控制反了,改过来就好了。
4. 常见问题与排查技巧实录
4.1 ESP8266连接OneNet失败的排查步骤
这是问得最多的问题,我整理了一个排查流程:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| AT无返回 | 波特率不对 | 尝试115200和9600 |
| 返回ERROR | 指令格式错误 | 检查引号和逗号 |
| 连接超时 | 网络不通 | 先用AT+PING测试 |
| 鉴权失败 | 设备ID或密钥错 | 重新复制平台信息 |
| 频繁掉线 | 供电不足 | 单独给ESP8266供电 |
我遇到过一次连接超时,排查了半天发现是路由器开了AP隔离,ESP8266和手机不在同一个网段。关掉AP隔离就好了。还有一次是OneNet的服务器地址填错了,应该是mqtts.heclouds.com,我填成了mqtt.heclouds.com,少了s。
注意:ESP8266的AT固件版本不同,指令可能有差异。建议用
AT+GMR查看版本号,然后去安信可的官网下载对应的AT指令手册。我用的版本是1.7.4,MQTT指令和1.6.x有些区别。
4.2 DHT11数据读取失败的常见原因
DHT11读失败通常有三个原因:时序不对、上拉电阻缺失、读取间隔太短。
时序问题最常见。STM32的延时函数如果被中断打断,就会导致时序错乱。我的做法是在读取DHT11时关闭全局中断,读完再打开。代码如下:
void DHT11_Read(float *temp, float *humi) { __disable_irq(); // 读取时序 __enable_irq(); }上拉电阻方面,DHT11的DATA引脚必须接一个4.7K到10K的上拉电阻到3.3V。我试过不接,有时候能读出来,有时候读出来全是0。接上之后一次都没失败过。
读取间隔方面,DHT11的 datasheet 写的是“两次读取间隔不小于1秒”,但实测下来2秒更稳。我一开始设的500ms,失败率大概30%,改成2秒后降到0。
4.3 继电器误动作与上电抖动的解决
继电器误动作是硬件调试时的经典问题。现象是STM32一上电,继电器就“啪”地吸合一下,然后才恢复正常。
原因是STM32复位后,GPIO处于浮空输入状态,引脚电平不确定。如果继电器是低电平触发,而引脚恰好被外部电路拉低,继电器就会吸合。
解决方法有两个:硬件上在继电器控制引脚加一个10K的下拉电阻到地,确保上电时引脚是低电平;软件上在GPIO初始化之后,立刻输出高电平。我两个都做了,再没出现过误动作。
实操心得:如果你用的是高电平触发的继电器,那就加上拉电阻,软件初始化后输出低电平。总之原则就是:上电时让继电器处于关闭状态。
4.4 OneNet数据上报频率与流量控制
OneNet免费版对数据上报频率有限制,好像是每秒不超过1次。我一开始设的500ms上报一次,结果平台返回“请求过于频繁”。改成2秒一次就正常了。
另外,数据包的大小也要控制。JSON格式虽然可读性好,但比二进制格式占空间。如果你上报的数据项很多,可以考虑用简写,比如t代替temp,h代替humi。不过对于个人项目来说,2秒一次、每次100字节左右,一个月也就几百MB流量,完全在免费额度内。
4.5 常见问题速查表
| 问题 | 排查方向 | 快速解决 |
|---|---|---|
| STM32程序不跑 | 晶振、复位、BOOT | 检查BOOT0接地 |
| 串口无输出 | 波特率、TX/RX接反 | 交换TX/RX试试 |
| ESP8266反复重启 | 供电电流不足 | 单独3.3V供电 |
| MQTT连接失败 | 地址、端口、鉴权 | 用MQTTX先测试 |
| 数据上报成功但平台不显示 | 数据流未定义 | 在平台添加数据流 |
| 手机端控制延迟大 | 网络质量 | 检查WiFi信号强度 |
这个表是我在调试过程中一点点攒出来的,基本上覆盖了90%的问题。剩下的10%通常是硬件虚焊或者模块本身坏了,换一个就好了。
5. 项目扩展与个人经验分享
5.1 从单设备到多设备的扩展思路
这个项目最初只控制两个继电器,后来我把它扩展到了8路。扩展的方法很简单:在STM32上多定义几个GPIO,在OneNet上多定义几个数据流,在JSON里多拼几个字段。但要注意,ESP8266的AT指令有长度限制,一次最多发256字节。如果你要上报的数据太多,可以分两次发,或者用更紧凑的格式。
另一个扩展方向是场景联动。比如“回家模式”:打开客厅灯、打开空调、关闭窗帘。这个逻辑可以放在STM32里,也可以放在OneNet的“触发器”里。我选择放在STM32里,因为响应更快,而且断网也能用。
5.2 本地控制与云端控制的优先级处理
这是一个容易被忽略但很重要的问题:当本地自动控制和云端手动控制同时发生时,听谁的?
我的做法是云端优先。具体来说,STM32维护一个“手动模式”标志位。当收到云端指令时,置位这个标志,并启动一个5分钟的定时器。在定时器到期之前,本地自动控制不生效。定时器到期后,自动恢复本地控制。
这样做的好处是,用户手动关灯后,不会因为光线变暗又被自动打开。5分钟的时间也足够用户离开房间。
5.3 我在这个项目里踩过的坑
第一个坑是电源。我一开始用USB线给整个系统供电,结果ESP8266一工作,STM32就复位。后来用万用表测了一下,USB线压降太大,5V到了板子上只剩4.3V。换了一根粗一点的线就好了。
第二个坑是AT指令的换行符。ESP8266的AT指令必须以\r\n结尾,我只发了\r,结果模块一直不返回。后来看手册才发现要\r\n。
第三个坑是OneNet的设备ID和产品ID搞混。这两个都是长字符串,长得有点像。我复制的时候复制错了,导致鉴权一直失败。后来在平台上看清楚了,产品ID是以“产品”开头的,设备ID是以“设备”开头的。
第四个坑是DHT11的供电。DHT11的工作电压是3.3V到5.5V,我接的3.3V,但数据线也接的3.3V上拉。后来发现DHT11在3.3V下响应速度会慢一点,改成5V供电、3.3V上拉之后,读取更稳定了。
5.4 这个项目还能怎么玩
如果你已经跑通了这个基础版本,可以试试这几个方向:
- 接入语音助手:用OneNet的API对接一些开放的语音平台,实现语音控制。
- 增加红外遥控:加一个红外接收头,学习家里的空调遥控码,实现空调控制。
- 做数据记录与分析:把OneNet的数据导出到本地,用Python做温湿度趋势分析。
- 改用ESP32:如果你需要更快的处理速度或者蓝牙功能,可以把ESP8266换成ESP32,代码改动不大。
我个人觉得,这个项目最大的价值不在于功能有多复杂,而在于它打通了“传感器-主控-云端-手机”的完整链路。你把这个链路跑通了,后面加什么功能都只是在这个骨架上添砖加瓦。我后来做鱼缸监控、做温室大棚,用的都是同一套架构,只是换了传感器和执行器。
最后分享一个小技巧:在调试MQTT通信时,用MQTTX这个客户端先连上OneNet,手动发几条指令,确认平台侧没问题,再去调STM32和ESP8266。这样可以排除掉一半的变量,省很多时间。