news 2026/10/22 9:22:59

STM32+ESP8266+EMQX+Android:MQTT智能家居全链路实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+ESP8266+EMQX+Android:MQTT智能家居全链路实践

简介:这是一套基于STM32、ESP8266、EMQX和Android的智能家居系统完整源码包,面向物联网开发者、嵌入式学习者和移动端应用爱好者,帮助理解从硬件控制到云端消息分发的完整链路。项目以STM32作主控,负责采集传感器数据并执行控制逻辑,ESP8266提供Wi-Fi连接,EMQX承担MQTT消息代理,Android端则作为用户远程操作界面,四者协同实现设备自动化与远程管理。资源共339个文件,涵盖了Keil工程源码(.c/.h)、编译产物(.axf/.hex)、Android项目(.kt/.xml)、前端配置(.js/.vue)、硬件相关文档(.md)及图片资源(.png)等,压缩包约37.76MB,目录结构清晰。已有565人学习下载,读者可从中学习STM32外设驱动编写、MQTT协议接入方法、Android网络通信与UI设计,以及嵌入式与移动端联调思路,是一份有参考价值的全栈式智能家居实战资料。

1. 用STM32、ESP8266、EMQX和Android拼一套智能家居:这条路能走通

某开发者手边摆着四样东西:一块STM32主控板、一个ESP8266模块、一台跑着EMQX的服务器、一部Android手机。他想做的是把这四样拼成一套能远程控灯、读温湿度、看设备状态的系统。折腾了两个月,设备要么连不上家里路由,要么手机按钮按下去十分钟没反应。后来他把通信逻辑全部交给MQTT协议,一周就把样机跑通。我拆的这份源码包,正好是这条被验证过的完整链路:STM32管硬件控制,ESP8266管无线桥接,EMQX管消息转发,Android管用户交互。适合想从零搭一套跨端智能家居系统的开发者,也适合想搞懂嵌入式、物联网、移动端三层怎么协作的入门者。

2. 先把四层链路的分工看清:谁在采集、谁在转发、谁在派发

2.1 嵌入式主控侧:STM32F10x标准外设库与资源分配

从源码包的文件列表能直接看出,这套工程用的是STM32F10x标准外设库,而不是HAL库。stm32f10x_tim.c、stm32f10x_rcc.c、stm32f10x_usart.c这一类文件,是官方早期主推的库结构:每个外设对应一个源文件,初始化时需要手动设置结构体参数。相比HAL库,它少了一层抽象,代码量小,排查问题时能直接看到寄存器级操作,对于这种功能不算复杂的智能家居样机反而更好改。

主控侧要承担的任务大致有四块:读取人体红外、温湿度这类传感器;根据指令控制继电器或调光模块;把设备状态上报给ESP8266;接收来自ESP8266的下行指令。因此资源分配上,至少要留两路串口——一路给ESP8266做数据桥接,一路给调试串口打印日志。文件里同时出现了stm32f10x_adc.c和stm32f10x_i2c.c,说明传感器可能走ADC模拟量或I2C数字量,很多温湿度模块走I2C,门磁、光照走ADC。stm32f10x_can.c意味着工程还预留了CAN总线扩展口,部分开发者会用来对接更多墙面面板,但初跑系统时不需要动这块。

初始化流程一般是:先配置RCC时钟树,再初始化GPIO复用,最后配置USART。下面这段是USART初始化时最容易被忽略的部分,也是后面排查串口不通的关键:

void USART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = baud; 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); USART_Cmd(USART1, ENABLE); }

这段代码先把TX脚配置为复用推挽输出、RX脚配置为浮空输入,然后设置波特率、8位数据位、1位停止位、无校验。ESP8266模块一般工作在115200,调试串口建议用9600或115200,保持两者分开。之所以强调这一步,是因为很多人直接把TX/RX对应接反,Wi-Fi模块收不到STM32发的数据,这是第一个血泪教训。如果发现串口中断里处理不过来,可以用USART_ITConfig只开接收中断,发送侧用阻塞轮询,保证数据不丢。

2.2 无线桥接侧:ESP8266的AT透传模式

再讲ESP8266。这个模块的工作很简单:把STM32的串口数据“塞进”Wi-Fi网络,再把网络上收到的指令“倒出来”给串口。要让链路可靠,最关键的是选对工作模式。源码包需要ESP8266运行在AT固件模式下,连接家里的路由器,然后建立一条TCP长连接指向EMQX所在主机的1883端口,再进入透传模式。

AT+CWMODE=1 # Station模式,仅连接外部热点 AT+CWJAP="MyWiFi","password" # 连接路由器,改成你自己的SSID和密码 AT+CIPMUX=0 # 单连接模式 AT+CIPSTART="TCP","192.168.1.100",1883 # 连接EMQX的1883端口 AT+CIPMODE=1 # 开启透传模式 AT+CIPSEND # 进入发送状态

这里要注意透传模式的含义:从AT+CIPSEND成功之后,ESP8266会把串口收到的全部字节直接发到TCP连接上,不再解析AT命令。也就是说STM32端写MQTT协议报文时,要自己组包——CONNECT、PUBLISH、SUBSCRIBE,这些报文的每一个字节都由主控代码生成。如果在透传模式下想退出,需要发送特殊序列+++,但实际项目中我一般直接让STM32重启模块。

这个约束决定了STM32端需要实现一个轻量级MQTT客户端。源码工程中通常会把MQTT协议状态机写在一个单独的模块里,不占用主循环太多时间。有人会问,为什么不接路由器后先在ESP8266里跑MQTT固件,非要用AT透传?区别在于:MQTT固件让ESP8266自己做协议解析,STM32只管发裸数据;而AT透传方案把协议逻辑放到STM32侧,ESP8266只是一个网卡。项目选后者,便于调试——你可以用串口助手直接看透传的数据内容,遇到问题能立刻判断是数据没发出去还是协议组错了。

2.3 消息中心侧:EMQX为什么会比自定义TCP协议更顺手

第三部分是EMQX。它是整个系统的消息中心,STM32设备状态发给它,Android客户端订阅它。如果不用MQTT,直接让STM32和Android点对点TCP通信,会面临两个问题:一是手机网络环境不稳定,连接随时可能被基站和路由器回收;二是设备多起来以后,点对点连接数会爆炸。MQTT把这些事全部标准化了。

EMQX上的关键配置有三个:监听端口1883、允许匿名连接、保留消息功能。开发阶段,允许匿名连接能让联调省很多事。保留消息则很有用——设备把当前的开关状态发布到topic,Android一上线就能立刻拿到最后一条状态,不需要等设备再次上报。设计主题时,我一般会按“设备类型/房间/属性/动作”来划分。比如控制卧室灯的指令走house/bedroom/light/cmd,灯的当前状态走house/bedroom/light/state,传感器温度走house/sensor/temperature。这样在EMQX的Dashboard上直接能看到数据流,排错时比看一坨二进制帧快得多。

Topic路径方向载荷示例
house/bedroom/light/cmdAndroid→设备{"action":"on"}
house/bedroom/light/state设备→Android{"state":"on"}
house/gateway/name设备→Android{"id":"dev01","online":true}
house/sensor/temperature设备→Android{"value":25.3}

这套主题设计的好处是权限隔离很方便:如果以后接入了多个房间,只需要给每个房间分配不同前缀,Android端按前缀订阅,EMQX端可以在Dashboard上看到每个主题的流量,定位哪条链路断了也就半小时以内的事。

3. 把源码包还原成可运行工程:MDK、EMQX、Android Studio三步落位

3.1 嵌入式工程重建:把标准外设库文件和MDK工程结构对上

拿到源码包后第一件事不是打开IDE,而是理清文件归属。stm32SmartHome.axf是编译后的镜像文件,.babelrc和gradlew.bat说明Android部分是从Gradle工程打包时带出来的。在嵌入式部分,关键是把下面几类文件放对位置:启动文件、CMSIS核心文件、标准外设库源文件和应用层代码。MDK里一般新建四个组:Startup、CMSIS、StdPeriph_Driver、User。

启动文件决定芯片型号,如果源码包里的启动文件对应的是STM32F103C8T6中等容量,而你用的是STM32F103ZET6大容量,需要在Project对话框中更换启动文件以及相应的容量宏。否则最常见的问题是编译能过、下载后不跑,卡在HardFault。还有一个容易翻车的点:标准外设库要求定义STM32F10X_MD或STM32F10X_HD,这个宏在C/C++编译选项里,缺了它,外设库代码会以错误的方式编译。

应用层里,main.c会初始化外设、轮询按键状态、处理传感器数据、把状态打包成MQTT消息。其余模块分别维护自己的状态机,比如ESP8266连接逻辑可以封装成两个函数:一个负责建链,一个负责任务轮转。顺序大致是:先初始化RCC和GPIO,再初始化USART,再初始化和EMQX相关的网络链路。实际工程中,我习惯把串口接收放到中断,再用环形缓冲区缓存,这样主循环不会被阻塞。下面是一个典型的主循环骨架:

int main(void) { SystemInit(); USART1_Init(115200); USART2_Init(9600); GPIO_Key_Init(); GPIO_Light_Init(); Net_Init(); // 发送AT指令,建立TCP链路 while (1) { Key_Scan(); Sensor_Read(); Net_Publish_Status(); Net_Receive_Handler(); Delay_ms(20); } }

Net_Receive_Handler从串口环形缓冲区读取数据并解析MQTT消息,调用相应控制函数。这套骨架的优点是每一个模块都可以分别测试,比如先屏蔽Net_Publish_Status,用串口助手手动喂数据,验证控制逻辑是否正常。编译完会生成.axf文件,下载时不要只在“Program Algorithm”里漏掉Flash容量配置,否则也会出现下载后无反应的情况。源码包里的.babelrc可能与Android工程中WebView相关资源有关,不是嵌入式部分的必备项,忽略它不影响跑通主流程。

3.2 部署EMQX:从安装到订阅发布联调

EMQX的安装不复杂。在Linux服务器上准备好预编译包,解压后进入bin目录启动。启动命令一般是./emqx start,然后打开http://服务器IP:18083进入Web管理界面,首次登录会要求修改默认密码。联调阶段最省心的配置是开放1883端口并允许匿名接入,生产环境再改回账号密码认证。

启动完先不用碰硬件,直接在服务器上用命令行客户端发一条消息验证broker是否正常。这里以mosquitto_pub和mosquitto_sub为例,它们是MQTT联调的标准工具:

# 终端1:订阅 mosquitto_sub -h 127.0.0.1 -p 1883 -t 'house/#' -v # 终端2:发布 mosquitto_pub -h 127.0.0.1 -p 1883 -t 'house/bedroom/light/cmd' -m '{"action":"on"}'

如果终端1立刻打印出消息,说明EMQX链路是通的。接下来才轮到ESP8266和STM32。主题设计上,house/#这个通配订阅非常实用,调试时能看全局流量,而Android端则订阅自己关心的具体主题。如果你想验证QoS和保留消息,可以加参数:mosquitto_pub -r表示保留消息,-q 1指定QoS级别。设备状态上报一般用QoS 1,控制指令用QoS 0也可以。QoS 2在资源受限的单片机上会带来额外的协议开销,智能家居场景基本不需要。

3.3 Android端:把SmartHomeDashboard工程和MQTT客户端接起来

Android部分以SmartHomeDashboard名称出现在源码包里。用Android Studio导入这个文件夹时,要选Open an existing project,而不是直接打开gradlew.bat。导入后先同步Gradle,确认依赖能拉下来。MQTT客户端一般用Paho库,在build.gradle里添加依赖:

dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' }

连接EMQX的代码示范如下:

String broker = "tcp://192.168.1.100:1883"; String clientId = "android_" + System.currentTimeMillis(); MqttClient client = new MqttClient(broker, clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(false); options.setConnectionTimeout(10); options.setKeepAliveInterval(30); client.connect(options); client.setCallback(new MqttCallback() { @Override public void messageArrived(String topic, MqttMessage message) { runOnUiThread(() -> updateUI(topic, new String(message.getPayload()))); } }); client.subscribe(new String[]{"house/bedroom/light/state", "house/sensor/#"}, new int[]{1, 1});

这段代码里,cleanSession设为false能配合EMQX保留离线消息,但要注意重连时clientId不能重复。同一个clientId在服务端只会保留一个会话,新连接会把旧连接踢下线。keepAliveInterval设30秒,低于运营商会回收半开连接的时间。UI层只需要在消息到达时刷新开关状态,在按钮点击时发布指令即可,整个Android端最重要的逻辑就是重连和消息回调,其他都是视图层的绑定。

4. 串口、Wi-Fi、MQTT三处高频踩坑:现象、原因、解决

这里我自己也翻过车,以下几条是从实际联调里一条条记下来的,每一条都对应一个具体现象,排查顺序和参数配置都写在里面。

4.1 ESP8266上电后一直输出ERROR,AT命令无响应

现象:ESP8266模块上电后串口助手收到一堆乱码,偶尔能看到ERROR,发AT没有反应。

原因:大概率是供电问题。ESP8266在Wi-Fi联网瞬间的峰值电流可以到300mA甚至更高,如果直接用USB转TTL的3.3V供电,电压会被拉低到3.0V以下,模块直接复位。另一个低频原因是串口波特率不对,部分固件默认115200,但也有固件改成9600。还有人在接线时把TXD/RXD接成了同向,模块收不到任何指令。

解决:给ESP8266单独供电,用一个输出能力至少500mA的3.3V稳压芯片。接线时确认TXD连接对端RXD、RXD连接对端TXD,RST引脚上拉到3.3V,EN(CH_PD)也上拉。上电后等2秒再发AT。如果用调试器板上自带的3.3V口,功率通常不够,最好都从电源模块取电。这个现象排查顺序是:先看电压,再看接线,最后看波特率。电压问题占了七成。

4.2 手机App显示在线,但按下开关设备没有动作

现象:MQTT连接成功,App界面状态正常,但点击按钮后STM32控制的继电器纹丝不动。

原因:这类问题最隐蔽,因为链路看起来是通的。常见原因有三种:第一,主题拼写不一致,App发布的house/bedroom/light/cmd和STM32订阅的不一样;第二,QoS级别不一致,订阅方得到的QoS是发布方和订阅方的最小值,如果一端用了QoS 2、另一端用QoS 0,实际投递可能以QoS 0进行,稍微有点延迟就容易丢失;第三,ESP8266透传模式下,STM32收到的消息中带了额外的\r\n或IP包头。

解决:先做拆链路排查。在EMQX Dashboard上开一个WebSocket订阅house/#,或者用mosquitto_sub看设备是否真的收到消息。如果没有消息,则是发布端主题问题;如果有消息但设备不动,怀疑STM32解析问题。解析方面常见一个坑:STM32端用strstr匹配JSON时没有处理尾部回车,建议在接收缓冲区里先把\r\n替换成\0,再逐行解析。我在协议层加了一条自定义帧格式:以0xAA开头、0x55结尾、长度字段在中间,这样解析简单很多,也避免了一帧数据被拆成两段串口帧时解析失败。

4.3 继电器偶发失灵,多点一次会执行两次

现象:按下一次开灯,灯有时候亮,有时候不亮;再按一次反而可能触发两次切换。

原因:最初怀疑是GPIO配置问题,用逻辑分析仪抓引脚波形才发现,问题出在串口中断处理太慢。执行命令的那段逻辑直接写在中断回调里,一旦传感器周期上报数据和下行控制指令同时到达,串口数据被覆盖,控制帧丢了一半,最后GPIO电平乱跳。另一个常见原因是继电器线圈没有加续流二极管,断开瞬间产生反向电动势,干扰STM32供电,严重时直接复位。

解决:把串口接收改成DMA,或者至少把接收数据放进环形缓冲区,解析放到主循环里。控制指令执行最好加一条防抖策略——收到一帧完整消息后,延时50ms再判断同一topic是否重复。硬件上,继电器线圈两端并联一个1N4007二极管,方向一定要反接,负极接电源正极,否则会烧驱动三极管。从那以后我在布局上强制把继电器区和主控区拉开,不再共用一小块地。

4.4 EMQX后台能看到设备连接,但Android永远收不到遗嘱消息

现象:设备突然断电后,EMQX Dashboard上仍然显示设备在线,Android端无法感知到离线。

原因:遗嘱主题确实设置了,但keepalive间隔设得太长,设备掉线后要等至少一个保活周期才会被broker判定离线。另一种常见状况是设备端没有发送遗嘱消息内容,或者遗嘱发布到的topic,Android订阅时用了不同的通配符路径,导致消息到了却对不上号。再有一种情况是ClientID重复,新连接挤掉旧连接后,broker处理遗嘱消息的时机变得不可预测。

解决:在CONNECT报文中同时携带遗嘱topic、遗嘱消息和keepalive值。将保活时间设置在15到30秒之间,避免因网络地址转换老化导致长连接被运营商回收。遗嘱消息建议用QoS 1发布,因为QoS 0在broker推送过程中可能被直接丢弃。设置完成后,可以刻意给设备断电验证:切断电源后20秒内,EMQX后台里设备状态应变为离线,Android端同时收到{"online":false}的遗嘱消息。这个验证建议每次改配置后都强制跑一遍,花不了两分钟。

5. 验证这套系统是否真的通:用模拟客户端把全链路先跑通

在动手焊板子之前,有一个性价比很高的验证套路:让PC上同时扮演“真实设备”和“Android App”。具体做法是准备两个MQTT客户端,一个作为fake_device向topic发布温度与设备状态,一个作为fake_app订阅状态和控制指令。我一般用一段不到三十行的Python脚本完成:

import paho.mqtt.client as mqtt DEVICE_ONLINE = "house/gateway/status" def on_message(client, userdata, msg): print("app收到:", msg.topic, msg.payload.decode()) app = mqtt.Client() app.connect("192.168.1.100", 1883) app.subscribe("house/#") app.on_message = on_message dev = mqtt.Client() dev.connect("192.168.1.100", 1883) dev.publish(DEVICE_ONLINE, '{"online":true}', qos=1, retain=True) dev.loop_start() app.loop_forever()

跑这个脚本前,先确认EMQX已经启动;脚本跑起来后,如果app端打印出online状态,说明EMQX的转发和保留消息都正常。再去检查真实硬件,这时候如果硬件还连不上,问题的范围就缩小到嵌入式端,而不是被层层网络问题裹住。验证链路时我建议顺便抓一次包,监听1883端口,重点看CONNACK报文里的return code,以及PUBLISH报文的topic是否与预期一致。若设备连上后又被broker立即断开,多半是ClientID重复;若连接正常但消息看不见,回到主题路径上来。

从那以后,我每次改ESP8266透传或STM32组包逻辑,都会强制先跑一遍这套fake_app/fake_device验证链路,确认协议没问题才碰真硬件,省下的排障时间远超写脚本的那十几分钟。如果你手头正好有这份源码包,按这个顺序拆开跑一遍,比我在这里说一百句都管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 9:22:42

OpenGL纹理加载实战:SOIL库编译、避坑与RAII封装

简介:SOIL-master_soil_ 是开源 OpenGL 图像加载库 SOIL 的完整源码包,面向需要在 Visual Studio 等环境下开发图形应用,或希望深入理解图像纹理加载机制的开发者。该库支持 BMP、GIF、JPEG、PNG、TGA 等多种常见格式,借助 SOIL_l…

作者头像 李华
网站建设 2026/10/22 9:22:29

OpenMMLab新手实战:从环境配置到训练自己的目标检测模型

OpenMMLab:一键搞定深度学习训练(新手向)说到深度学习训练,很多新手的第一个反应是恐惧:环境装不明白,数据格式搞不懂,训练循环写了一周还在报错,最后连"模型到底有没有在学东西…

作者头像 李华
网站建设 2026/10/22 9:21:54

一站式AI运营源码系统:从内容生成到多平台自动发布的全链路实践

做内容运营的人应该都有这种体会:每天生产几十条内容已经够累了,还要一个个平台登录、复制粘贴、配图、改格式,忙完一圈发现大部分时间都耗在搬运上,真正用来思考选题和优化文案的时间反而没剩下多少。我前后折腾了几个版本之后&a…

作者头像 李华
网站建设 2026/10/22 9:21:26

Flink CDC 实战:DataStream API 与 Flink SQL 两条路线怎么选

1. 为什么CDC这件事值得单独拎出来聊 做数据集成的人,迟早会碰到一个绕不开的场景:业务库里的数据一直在变,下游的数仓、缓存、检索服务却只能靠定时任务一遍遍全量拉取。跑批窗口越拉越长,数据库压力越来越大,业务方还…

作者头像 李华
网站建设 2026/10/22 9:21:02

设备管理系统实战:数据模型、Flask实现与落地避坑

简介:这套设备管理系统压缩包提供了一个完整的设备全生命周期管理解决方案,面向企业设备管理岗位与信息化建设人员,用于解决设备台账混乱、维护不及时、报修流程繁琐等实际问题。系统基于ASP.NET平台开发,涵盖了设备登记、实时追踪…

作者头像 李华
网站建设 2026/10/22 9:20:39

Kotlin外卖App课程设计全流程:数据库、服务端到Android联调避坑指南

简介:一款基于Kotlin编写的Android外卖应用完整工程,包含客户端、服务端与数据库三部分,定位清晰,是面向移动开发课程设计的综合实战项目,适合本科或专科学生参考学习。项目覆盖界面交互、逻辑处理与数据存储等移动应用…

作者头像 李华