简介:面向嵌入式与物联网初学者的 STM32 上云例程,基于 STM32F103C8T6 主控与 ESP8266 WiFi 模块,使用 AT 指令完成 MQTT 协议对接,将设备数据发布至阿里云物联网平台,并支持订阅云端下行控制指令。压缩包共包含 134 个文件,以 .c/.h 源码为主体,并带有 Keil 工程文件、启动文件、链接脚本及编译生成的 hex 烧录文件,便于直接打开工程查看或烧录验证,整体大小 3.17MB。已有 8393 人学习下载。源码中明确给出了阿里云的 Broker 地址、设备三元组信息、发布与订阅 Topic 等宏定义,读者可直观理解 MQTT 接入参数的配置方式;同时涉及串口驱动、ESP8266 AT 指令解析、定时器及 LCD 显示等模块,覆盖了嵌入式设备联网的常见环节,适合作为课程设计、毕业设计或职业培训的参考工程。 搞嵌入式这几年,我其实一直有一个观点:能让STM32联网上云的项目,十个里有八个都是靠ESP8266+AT指令这套老组合跑起来的。它不像ESP32那样集成了Wi-Fi,也不像加个以太网芯片那么重,但对绝大多数做单片机出身的朋友来说,ESP8266用AT指令驱动,手写一遍MQTT连接流程,是理解物联网数据链路最直接、最踏实的一条路。
我之前做的一个设备接入项目,就是STM32采集传感器数据,通过ESP8266以AT指令方式按MQTT协议上报到阿里云物联网平台。整个链路跑通之后,我在串口助手里看到Alink JSON数据包被云端成功接收的那一刻,感觉像是打通了任督二脉。今天这篇就把完整的程序源码思路、踩坑过程、以及MQTT连接参数的推导方法拆开揉碎聊一遍,针对的是已经会点STM32、但第一次接触ESP8266上云的朋友。看完之后你会很清楚:什么是AT指令控制模块、什么是MQTT报文、阿里云那个复杂的三元组签名到底怎么算、以及程序断线之后怎么自动重连。
1. 项目背景与整体技术思路
1.1 这套组合能做什么
先给一个直观的画面。你的STM32主控负责干活——读温度、读湿度、采集开关状态、控制继电器,这些都是单片机擅长的。但单片机的短板也很明显:它没有网络协议栈,不能直接连路由器。这时候ESP8266就相当于给单片机插上了一根网线,只不过两者的交流方式是串口。
那我为什么不用STM32直接驱动ESP8266的SDK,非要走AT指令这么一条"原始"的路线?因为AT指令足够通用。AT指令是设备商预留在固件里的通用控制接口,只要ESP8266出厂带了AT固件,你不用关心它的内部实现,只需要往串口丢ASCII字符串,比如AT+CWJAP="wifi名字","密码",它就会去连接Wi-Fi。这种方式的优点是开发周期极短、逻辑简单、问题容易定位,非常契合中小型嵌入式项目的交付节奏。
用这套方案做出来的东西,小到智能插座、温湿度监测器,大到环境监测终端、农业大棚控制柜,都能在阿里云物联网平台上以MQTT协议通信。整个流程的核心就是:STM32采集 → 串口发AT指令 → ESP8266转发MQTT报文 → 阿里云平台接收处理。
1.2 数据链路全景:从采集到上云
画一下完整的数据流向,你会更清楚每一行代码的意义。
第一步,STM32通过ADC、I2C或者SPI把传感器数字量读进来,换算成实际物理量,比如温度25.6℃、湿度60%。第二步,STM32将这些数据整理成JSON格式,比如{"Temperature":25.6,"Humidity":60},之所以用JSON,是因为阿里云物联网平台的Alink协议规定消息体是这种格式。第三步,MCU按照MQTT协议规范,将JSON打包进一个叫PUBLISH的报文里。第四步,通过串口把报文数据交给ESP8266,ESP8266在内部完成TCP封装,发往云端。
这里必须强调一个常被新手搞混的概念:MQTT是应用层协议,TCP是传输层协议。ESP8266负责建立TCP连接,这是它的老本行;而MQTT的CONNECT、PUBLISH、SUBSCRIBE这些报文,需要STM32自己按格式拼出来,通过已建立的TCP通道发给云平台。把这两层的关系理清了,后面的代码就不会乱。
2. 硬件准备与接线细节
2.1 硬件清单
做这个项目我用的是一套非常常见的硬件,几乎每个嵌入式玩家的抽屉里都能翻出来:
- STM32F103C8T6最小系统板,也就是大家口中的"蓝板",成本低、资料多
- ESP8266-01S或者ESP-12F模块,推荐ESP-12F,因为天线和PCB走线更合理,信号稳定得多
- 一个USB转TTL模块(调试ESP8266时会用到)
- 杜邦线若干、面包板一个
- 一个靠谱的3.3V稳压电源模块(这里划重点,后面说为什么)
不需要太多花哨的器件。如果你只想先复现代码逻辑,用串口打印调试信息,连传感器都可以先不接,用程序里模拟的假数据上云。
2.2 接线图与电平注意事项
STM32和ESP8266就是标准的串口对接。ESP8266的TXD接STM32的RXD,RXD接STM32的TXD,GND共地。我用的STM32串口1(PA9为TX,PA10为RX),接线就是:
| STM32F103C8T6 | ESP8266 |
|---|---|
| PA9 (USART1_TX) | RXD |
| PA10 (USART1_RX) | TXD |
| 3.3V | VCC |
| GND | GND |
| GND | CH_PD(EN) |
CH_PD必须拉高,否则模块不工作;GPIO0悬空或通过10K电阻上拉,让ESP8266进入Flash Boot正常运行模式,而不是下载模式。我见过不下三个项目出问题都是因为CH_PD没接。
电平和通信电压这块,平时用经典51单片机习惯5V的人要格外注意:ESP8266的IO口不兼容5V,STM32F103的供电虽然是3.3V,但部分引脚在上电瞬间可能会有毛刺。稳妥的做法是串口线上串接1K电阻分压,防止反向电压灌入ESP8266。我实际测量过,不接电阻在大部分情况下也能跑,但遇到模块死机重启,先怀疑这个。
2.3 供电与复位处理
ESP8266在Wi-Fi发射瞬间电流能跑到300mA左右,如果用STM32板载的AMS1117-3.3V给它供电,掉压严重时会直接导致模块反复重启。我的做法是给ESP8266单独用一节AMS1117-3.3V模块从5V电源转出来供电,STM32和ESP8266之间只走信号线,并且共地。
上电时序上,ESP8266冷启动自检大概需要200ms左右,程序启动后不要立刻发AT指令,先延时300~500ms再初始化。如果发现串口发出的第一条AT没有回应,大概率就是上电时序没等够。
3. 阿里云物联网平台侧配置
3.1 产品创建与设备添加
这部分是在网页上操作的,跟代码分开,但难道不小。登录阿里云物联网平台控制台之后:
- 点击"创建产品",产品名称随意,比如"STM32_Device";所属品类选"自定义";节点类型选"设备";连网方式选"Wi-Fi";数据格式一定要选"Alink JSON"。
- 产品创建好后,在产品下"添加设备":DeviceName可以自定义,比如
device01,也可以留空让平台自动生成。 - 添加完设备,点击"查看",会看到三个关键参数:ProductKey、DeviceName、DeviceSecret。这三个就是要写进STM32程序里的"身份证",千万保存好。
还有一个可选但建议做的步骤:在产品定义的"功能定义"里添加自定义属性,比如温度Temperature、湿度Humidity。这样平台会自动生成标准的Topic,设备上报的数据也能在控制台的可视化面板里直接看,省得自己解析。
3.2 搞清三元组和签名
设备三元组,听起来玄乎,本质就是设备在云端的账号密码。ProductKey相当于"产品线ID",DeviceName是设备名字,DeviceSecret就是设备端的密钥。但MQTT服务器要求设备在连接时提供username和password,阿里云又不直接让你把DeviceSecret放连接报文里,所以规定了一个签名算法:HMAC-SHA256。
具体规则是这样的:把clientId、deviceName、productKey拼成一个字符串,格式固定为:
clientId{设备的clientId}deviceName{设备名}productKey{产品key}然后以DeviceSecret当作密钥,对这个字符串做HMAC-SHA256运算,运算结果转成十六进制小写,就是MQTT的password。此外,MQTT连接报文里的clientId并不是随便填的,阿里云要求格式为:
自定义ClientId|securemode=3,signmethod=hmacsha256|其中securemode=3表示安全模式,signmethod=hmacsha256表示签名算法。这里的自定义ClientId,我一般直接用设备名,方便排查。
3.3 MQTT连接参数推导
为了让后面代码直接可用,举个具体的例子。假设我申请的设备参数如下:
- ProductKey:
a1EfGhIjKlM - DeviceName:
device01 - DeviceSecret:
8b0b1c2d3ef4567890abcdef12345678 - 自定义ClientId:
device01
那么签名用的原始字符串就是:
clientIddevice01deviceNamedevice01productKeya1EfGhIjKlM用8b0b1c2d3ef4567890abcdef12345678做密钥,对上面这串做HMAC-SHA256,得到的结果(十六进制字符串)就是password。
最终MQTT连接的三个参数是:
| 参数 | 值 |
|---|---|
| clientId | device01|securemode=3,signmethod=hmacsha256| |
| username | device01&a1EfGhIjKlM |
| password | HMAC-SHA256签名结果(64位十六进制小写) |
| 服务器地址 | a1EfGhIjKlM.iot-as-mqtt.cn-shanghai.aliyuncs.com |
| 端口 | 1883 |
注意服务器地址里的region后缀,我这里是华东2(上海),如果你的设备区域不同,比如是新加坡或者美西,域名后缀要跟着变。这个域名在阿里云文档和产品详情页里都能查到。
4. 源码实现:AT指令驱动到MQTT通信
4.1 串口驱动与AT收发框架
STM32端的代码,第一步是串口。我使用的是标准库,配置USART1为115200-8-N-1,开启接收中断。为了不让接收中断过于频繁,我采用了串口接收空闲中断+DMA的做法:DMA接收满一定字节数,或者总线空闲时,认为一帧AT响应结束。如果你用的是HAL库,直接使用HAL_UARTEx_ReceiveToIdle_DMA也一样。
发送AT指令的框架函数大概是这样的:
uint8_t UART1_SendString(char *str) { while (*str) { USART_SendData(USART1, *str++); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } return 0; }这里一个大坑:AT指令必须以\r\n结尾,我们平时按回车习惯是\n,导致指令没被ESP8266解析。我的做法是在发送函数外层封装一层:
void AT_SendCommand(const char *cmd) { UART1_SendString((char *)cmd); UART1_SendString("\r\n"); HAL_Delay(200); // 等待ESP8266返回 }延时200ms是经验值,AT指令响应时间不一,连接Wi-Fi时可能更久,建议在实际调试中根据返回情况调整。
4.2 ESP8266联网流程
这是整段代码里最“流水账”但最重要的部分。ESP8266的初始化步骤,我习惯分为四步走:
- 复位与模式设置:
AT+RST,然后延时1秒;接着发AT+CWMODE=1,把模块设为Station模式,也就是让它作为客户端去连路由器,不能开热点。 - 连接Wi-Fi:
AT+CWJAP="MyWiFi","12345678"。如果返回WIFI CONNECTED然后WIFI GOT IP,说明连网成功;如果返回FAIL,先查密码和路由器信号。 - 查询IP确认联网:
AT+CIFSR,能看到模块被分配到的局域网IP,通常以192.168开头。 - 建立TCP连接:这一步是MQTT的前置。发送
返回AT+CIPSTART="TCP","a1EfGhIjKlM.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883CONNECT OK就说明TCP链路已经通了。
注意,AT+CIPSTART里的服务器地址不是随便写的,必须是你的ProductKey加固定前缀,和3.3节表格里那个地址一样。
4.3 MQTT报文拼包与收发
TCP建立之后,现在要处理MQTT协议栈。MQTT的控制报文格式是:固定报头(1字节)+ 剩余长度(变长字节)+ 可变报头 + 负载。
对于CONNECT报文,格式为:
- 固定报头第一个字节是
0x10,表示这是CONNECT控制报文。 - 剩余长度是"可变报头+负载"的字节数。
- 可变报头里包含协议名"MQTT"(长度00 04)、协议级别04、连接标志(这里固定为
0xC2,表示使用Username/Password认证且开启Clean Session)、KeepAlive(两字节,我设的是60秒)。 - 负载依次是clientId、username、password三个UTF-8编码字符串,每个字符串前面要加两个字节的长度。
这段拼包代码是整个工程里最需要小心的,一个字节错了,服务器直接断开连接。我把它封装成一个函数,核心部分就像这样:
uint16_t MQTT_BuildConnectPacket(uint8_t *buf, const char *clientId, const char *username, const char *password) { uint16_t index = 0; uint8_t varHeader[] = {0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, 0xC2, 0x00, 0x3C}; uint16_t varHeaderLen = sizeof(varHeader); uint16_t clientIdLen = strlen(clientId); uint16_t usernameLen = strlen(username); uint16_t passwordLen = strlen(password); uint16_t payloadLen = 2 + clientIdLen + 2 + usernameLen + 2 + passwordLen; uint16_t remainingLen = varHeaderLen + payloadLen; buf[0] = 0x10; // 剩余长度编码,此处假定小于128,只用1字节 buf[1] = remainingLen; memcpy(&buf[2], varHeader, varHeaderLen); index = 2 + varHeaderLen; buf[index++] = clientIdLen >> 8; buf[index++] = clientIdLen & 0xFF; memcpy(&buf[index], clientId, clientIdLen); index += clientIdLen; buf[index++] = usernameLen >> 8; buf[index++] = usernameLen & 0xFF; memcpy(&buf[index], username, usernameLen); index += usernameLen; buf[index++] = passwordLen >> 8; buf[index++] = passwordLen & 0xFF; memcpy(&buf[index], password, passwordLen); index += passwordLen; return index; }这段代码里有几个关键点细细说一下:
- 剩余长度字段在MQTT协议里是变长编码,如果剩余长度小于128可以只写1字节。我上面的代码直接假定小于128并只写了
buf[1] = remainingLen,在密码长度为64、clientId和username都不长时是成立的。如果你的阿里云密码签名结果也是固定64位十六进制字符串,那这个假定始终安全。 - 连接标志
0xC2=1100 0010,逐位看就是bit7=1(包含username)、bit6=1(包含password)、bit1=1(Clean Session)。为什么用Clean Session?因为如果使用持久会话,云端可能会缓存之前的订阅关系,在设备侧逻辑简单、断开重连频繁的场景下,Clean Session可以减少状态同步的烦恼。 - KeepAlive设为60秒,意味着在60秒内设备必须至少给服务器发一个报文,否则服务器认为设备掉线。后面会讲对应的PINGREQ心跳。
CONNECT报文发出去后,STM32会收到服务器的CONNACK响应,固定报头是0x20,剩余长度是2,两个字节的可变报头里第二字节为0就表示连接成功。我在代码里通过状态机来等待这个返回,收到正确报文后才进入下一步订阅或者发布,而不是发送后盲目往下走。这一点非常关键,否则你根本不知道到底是密码错了还是报文拼错了。
4.4 属性上报与指令下行
设备连接成功后,最常用的操作是属性上报。上报时用的Topic是:
/sys/{ProductKey}/{DeviceName}/thing/event/property/postPayload按照Alink JSON规范:
{"id":"1","version":"1.0","method":"thing.event.property.post","params":{"Temperature":25.6,"Humidity":60}}这个JSON字符串会作为PUBLISH报文的负载发送。PUBLISH报文拼接相对简单,固定报头第一字节QoS0时是0x30,可变报头是Topic字符串(两个字节长度+Topic内容),负载就是上面的JSON字符串。QoS选择0就够了,一来大多数传感器上报场景允许偶尔丢数据,二来QoS0不需要等待ACK,实现最简单;如果你做的是控制类的指令下发,下行可以使用QoS1,保证命令可靠到达。
指令下行则要用平台定义的属性设置Topic:
/sys/{ProductKey}/{DeviceName}/thing/service/property/set设备需要订阅这个Topic,云平台下发指令后,ESP8266会把收到的MQTT数据吐到串口,STM32解析后执行对应动作。解析的思路其实不难:在串口接收缓冲中不断寻找"params"这个关键字,再往后就是实际属性值。正式产品里建议引入cJSON库,但在验证阶段自己写个字符串查找也能跑。
4.5 断线重连与心跳保活
网络世界没有“永久在线”这回事,路由器重启、信号波动、服务器空闲断开,任何一个因素都会让链路断掉。断线重连我采用的是最简单可靠的"轮询检测+全流程重跑"策略:
定义一个变量mqttConnected,初始为0。在主循环里:
- 每5秒用
AT+CIPSTATUS查询TCP连接状态,返回值如果得不到CONNECT OK,就认为链路断了。 - 检查到断开后,先把
mqttConnected置0,然后从AT+CIPSTART开始重新建立TCP连接,再重新发CONNECT报文,最后重新订阅下行Topic。 - 如果连续3次TCP都建立失败,调用
AT+RST重启ESP8266模块,并把所有初始化流程重跑一遍。实际上重启模块是解决ESP8266一切"奇奇怪怪"问题的万能药。
心跳保活方面,因为MQTT协议层KeepAlive设了60秒,我就在代码里设一个50秒的定时器,到点发送一个PINGREQ报文(两个字节,固定是0xC0 0x00)。收到PINGRESP(0xD0 0x00)就算正常,没必要额外处理;如果连续几次没收到,就按上面的流程断开重连。
5. 调试实录与避坑指南
5.1 常用调试指令速查
在程序还没彻底跑通时,我不会直接用STM32接ESP8266,而是先把ESP8266用USB转TTL模块单独接电脑,用串口助手逐条敲AT指令。用熟了再写进代码,定位问题会快非常多。
调试阶段最常用的几条指令:
| 指令 | 作用 |
|---|---|
AT | 测试模块是否响应,返回OK代表通信正常 |
AT+RST | 软复位模块 |
AT+CWMODE=1 | 设置Station模式 |
AT+CWJAP="ssid","pwd" | 连接Wi-Fi |
AT+CIFSR | 查询模块IP |
AT+CIPSTATUS | 查询TCP/UDP连接状态 |
AT+CIPSTART="TCP","域名",1883 | 建立TCP连接 |
AT+CIPMODE=1 | 开启透传模式 |
AT+CIPSEND | 进入透传发送状态 |
+++ | 退出透传模式(注意前后要各停顿1秒) |
常用的一个流程:先AT+CIPMODE=1开启透传,然后AT+CIPSEND,之后所有串口数据都会直接作为TCP数据发给服务器。这样调试MQTT报文时尤其方便,你可以把CONNECT数据包用十六进制发送器发出去看云平台反应。
5.2 高频问题排查表
这一节直接上硬货,是我实际调试过程中遇到并解决的问题:
| 现象 | 排查方向 |
|---|---|
ESP8266发送AT无响应 | 检查TXD/RXD是否接反;检查CH_PD是否拉高;检查供电是否稳定;降低波特率到9600试试 |
AT+CWJAP返回FAIL | 确认Wi-Fi密码;确认模块天线是否可靠;2.4G频段路由器是否开启;可以靠近路由器试 |
AT+CIPSTART返回ERROR或DNS FAIL | 确认服务器域名是否正确;确认模块已获取IP;换用阿里云MQTT服务器的IP地址直连作为临时排查手段 |
| 服务器立即断开连接 | 大量情况是clientId格式不对或password签名值计算错误,重点排查报文负载部分的十六进制 |
| 发送CONNECT后无CONNACK | 检查末尾是否缺少MQTT报文的固定头/剩余长度字段;用Wireshark抓包对比 |
| 上报数据偶尔丢失 | 观察Wi-Fi信号强度;考虑把QoS改成1;在JSON前后增加延迟让ESP8266有充分时间发送 |
其中一个我印象特别深的坑是:password签名计算时,阿里云要求签名字符串里clientId不能带|securemode=3,signmethod=hmacsha256|这一段后缀,但MQTT连接报文里又必须带上这段。换句话说,签名用的是裸clientId,而连接用的是带安全参数的后缀。刚开始我拿带后缀的clientId去签名,服务器一直报认证失败,查了整整一个下午才发现。
另一个常见坑是串口助手和MCU发送十六进制报文时,把字符串的ASCII码当十六进制发了。比如MQTT CONNECT报文的第一个字节协议约定是0x10,在串口助手里如果打开"HEX发送",就要填10;但如果你按字符模式发"D"(ASCII码0x44),服务器肯定不认。写代码时也要注意,buf数组里存的是字节值,而不是字符。
5.3 烧录与调试工具的经验
工程编译和烧录,我用的是Keil MDK加ST-Link,选择STM32F103C8作为目标芯片。如果你在烧录时遇到error: no stm32 target found!之类的提示,别慌,大概率不是芯片挂了,先按这个顺序排查:
- 检查ST-Link和开发板的接线,SWDIO接SWDIO、SWCLK接SWCLK、GND接GND,3.3V最好不接。
- 把板子完全断电再重新上电,按住复位键的同时点击下载,等开始下载的瞬间松手。
- 检查Keil里Debug设置是否选对了ST-Link和接口(SW模式)。
- 排除ST-Link驱动问题,插上电脑看设备管理器是否正常识别。
如果确实遇到调试器连不上的极端情况,我的习惯是给板子通过BOOT0拉高进入ISP模式,用串口把程序赶紧刷进去再说,毕竟项目节点不等人。
调试期间,串口工具我推荐用支持HEX发送和定时发送的,比如SSCOM或者XCOM。用它们模拟STM32发MQTT报文给阿里云,能快速确认是软件问题还是报文问题。你甚至可以在调试时把CONNECT报文一段段发,观察阿里云在哪一步跟你有交互,比闭着眼睛写代码再看日志高效得多。
写在最后的一点经验
其实这套STM32+ESP8266+AT指令+MQTT+阿里云的组合,用到最后你会发现,真正难的不是代码本身,而是对“每一层协议各干各的活”的理解。AT指令负责管Wi-Fi和TCP,MQTT报文负责跟云平台说话,你把这两条线捋顺了,换AnyCloud、OneNET乃至自建MQTT服务器,也就是改改域名和签名算法的事。我后来接其他平台,基本就是复用同一套代码框架,只改了连接参数和Topic。这也是我强烈建议新手先别急着用一键SDK,老老实实从AT指令和MQTT报文开始调通一次的原因。对于这套源码框架,后面我还可以再拆一篇专门讲Alink JSON属性上报和下发指令的解析细节,这次先到这儿,希望能帮你把手上的板子真正连上云端。
本文还有配套的精品资源,点击获取