
简介面向嵌入式与物联网初学者的 STM32 上云例程基于 STM32F103C8T6 主控与 ESP8266 WiFi 模块使用 AT 指令完成 MQTT 协议对接将设备数据发布至阿里云物联网平台并支持订阅云端下行控制指令。压缩包共包含 134 个文件以 .c/.h 源码为主体并带有 Keil 工程文件、启动文件、链接脚本及编译生成的 hex 烧录文件便于直接打开工程查看或烧录验证整体大小 3.17MB。已有 8393 人学习下载。源码中明确给出了阿里云的 Broker 地址、设备三元组信息、发布与订阅 Topic 等宏定义读者可直观理解 MQTT 接入参数的配置方式同时涉及串口驱动、ESP8266 AT 指令解析、定时器及 LCD 显示等模块覆盖了嵌入式设备联网的常见环节适合作为课程设计、毕业设计或职业培训的参考工程。 搞嵌入式这几年我其实一直有一个观点能让STM32联网上云的项目十个里有八个都是靠ESP8266AT指令这套老组合跑起来的。它不像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字符串比如ATCWJAPwifi名字,密码它就会去连接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的报文里。第四步通过串口把报文数据交给ESP8266ESP8266在内部完成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的RXDRXD接STM32的TXDGND共地。我用的STM32串口1PA9为TXPA10为RX接线就是STM32F103C8T6ESP8266PA9 (USART1_TX)RXDPA10 (USART1_RX)TXD3.3VVCCGNDGNDGNDCH_PDENCH_PD必须拉高否则模块不工作GPIO0悬空或通过10K电阻上拉让ESP8266进入Flash Boot正常运行模式而不是下载模式。我见过不下三个项目出问题都是因为CH_PD没接。电平和通信电压这块平时用经典51单片机习惯5V的人要格外注意ESP8266的IO口不兼容5VSTM32F103的供电虽然是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相当于产品线IDDeviceName是设备名字DeviceSecret就是设备端的密钥。但MQTT服务器要求设备在连接时提供username和password阿里云又不直接让你把DeviceSecret放连接报文里所以规定了一个签名算法HMAC-SHA256。具体规则是这样的把clientId、deviceName、productKey拼成一个字符串格式固定为clientId{设备的clientId}deviceName{设备名}productKey{产品key}然后以DeviceSecret当作密钥对这个字符串做HMAC-SHA256运算运算结果转成十六进制小写就是MQTT的password。此外MQTT连接报文里的clientId并不是随便填的阿里云要求格式为自定义ClientId|securemode3,signmethodhmacsha256|其中securemode3表示安全模式signmethodhmacsha256表示签名算法。这里的自定义ClientId我一般直接用设备名方便排查。3.3 MQTT连接参数推导为了让后面代码直接可用举个具体的例子。假设我申请的设备参数如下ProductKeya1EfGhIjKlMDeviceNamedevice01DeviceSecret8b0b1c2d3ef4567890abcdef12345678自定义ClientIddevice01那么签名用的原始字符串就是clientIddevice01deviceNamedevice01productKeya1EfGhIjKlM用8b0b1c2d3ef4567890abcdef12345678做密钥对上面这串做HMAC-SHA256得到的结果十六进制字符串就是password。最终MQTT连接的三个参数是参数值clientIddevice01|securemode3,signmethodhmacsha256|usernamedevice01a1EfGhIjKlMpasswordHMAC-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的初始化步骤我习惯分为四步走复位与模式设置ATRST然后延时1秒接着发ATCWMODE1把模块设为Station模式也就是让它作为客户端去连路由器不能开热点。连接Wi-FiATCWJAPMyWiFi,12345678。如果返回WIFI CONNECTED然后WIFI GOT IP说明连网成功如果返回FAIL先查密码和路由器信号。查询IP确认联网ATCIFSR能看到模块被分配到的局域网IP通常以192.168开头。建立TCP连接这一步是MQTT的前置。发送ATCIPSTARTTCP,a1EfGhIjKlM.iot-as-mqtt.cn-shanghai.aliyuncs.com,1883返回CONNECT OK就说明TCP链路已经通了。注意ATCIPSTART里的服务器地址不是随便写的必须是你的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位十六进制字符串那这个假定始终安全。连接标志0xC21100 0010逐位看就是bit71包含username、bit61包含password、bit11Clean 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秒用ATCIPSTATUS查询TCP连接状态返回值如果得不到CONNECT OK就认为链路断了。检查到断开后先把mqttConnected置0然后从ATCIPSTART开始重新建立TCP连接再重新发CONNECT报文最后重新订阅下行Topic。如果连续3次TCP都建立失败调用ATRST重启ESP8266模块并把所有初始化流程重跑一遍。实际上重启模块是解决ESP8266一切奇奇怪怪问题的万能药。心跳保活方面因为MQTT协议层KeepAlive设了60秒我就在代码里设一个50秒的定时器到点发送一个PINGREQ报文两个字节固定是0xC0 0x00。收到PINGRESP0xD0 0x00就算正常没必要额外处理如果连续几次没收到就按上面的流程断开重连。5. 调试实录与避坑指南5.1 常用调试指令速查在程序还没彻底跑通时我不会直接用STM32接ESP8266而是先把ESP8266用USB转TTL模块单独接电脑用串口助手逐条敲AT指令。用熟了再写进代码定位问题会快非常多。调试阶段最常用的几条指令指令作用AT测试模块是否响应返回OK代表通信正常ATRST软复位模块ATCWMODE1设置Station模式ATCWJAPssid,pwd连接Wi-FiATCIFSR查询模块IPATCIPSTATUS查询TCP/UDP连接状态ATCIPSTARTTCP,域名,1883建立TCP连接ATCIPMODE1开启透传模式ATCIPSEND进入透传发送状态退出透传模式注意前后要各停顿1秒常用的一个流程先ATCIPMODE1开启透传然后ATCIPSEND之后所有串口数据都会直接作为TCP数据发给服务器。这样调试MQTT报文时尤其方便你可以把CONNECT数据包用十六进制发送器发出去看云平台反应。5.2 高频问题排查表这一节直接上硬货是我实际调试过程中遇到并解决的问题现象排查方向ESP8266发送AT无响应检查TXD/RXD是否接反检查CH_PD是否拉高检查供电是否稳定降低波特率到9600试试ATCWJAP返回FAIL确认Wi-Fi密码确认模块天线是否可靠2.4G频段路由器是否开启可以靠近路由器试ATCIPSTART返回ERROR或DNS FAIL确认服务器域名是否正确确认模块已获取IP换用阿里云MQTT服务器的IP地址直连作为临时排查手段服务器立即断开连接大量情况是clientId格式不对或password签名值计算错误重点排查报文负载部分的十六进制发送CONNECT后无CONNACK检查末尾是否缺少MQTT报文的固定头/剩余长度字段用Wireshark抓包对比上报数据偶尔丢失观察Wi-Fi信号强度考虑把QoS改成1在JSON前后增加延迟让ESP8266有充分时间发送其中一个我印象特别深的坑是password签名计算时阿里云要求签名字符串里clientId不能带|securemode3,signmethodhmacsha256|这一段后缀但MQTT连接报文里又必须带上这段。换句话说签名用的是裸clientId而连接用的是带安全参数的后缀。刚开始我拿带后缀的clientId去签名服务器一直报认证失败查了整整一个下午才发现。另一个常见坑是串口助手和MCU发送十六进制报文时把字符串的ASCII码当十六进制发了。比如MQTT CONNECT报文的第一个字节协议约定是0x10在串口助手里如果打开HEX发送就要填10但如果你按字符模式发DASCII码0x44服务器肯定不认。写代码时也要注意buf数组里存的是字节值而不是字符。5.3 烧录与调试工具的经验工程编译和烧录我用的是Keil MDK加ST-Link选择STM32F103C8作为目标芯片。如果你在烧录时遇到error: no stm32 target found!之类的提示别慌大概率不是芯片挂了先按这个顺序排查检查ST-Link和开发板的接线SWDIO接SWDIO、SWCLK接SWCLK、GND接GND3.3V最好不接。把板子完全断电再重新上电按住复位键的同时点击下载等开始下载的瞬间松手。检查Keil里Debug设置是否选对了ST-Link和接口SW模式。排除ST-Link驱动问题插上电脑看设备管理器是否正常识别。如果确实遇到调试器连不上的极端情况我的习惯是给板子通过BOOT0拉高进入ISP模式用串口把程序赶紧刷进去再说毕竟项目节点不等人。调试期间串口工具我推荐用支持HEX发送和定时发送的比如SSCOM或者XCOM。用它们模拟STM32发MQTT报文给阿里云能快速确认是软件问题还是报文问题。你甚至可以在调试时把CONNECT报文一段段发观察阿里云在哪一步跟你有交互比闭着眼睛写代码再看日志高效得多。写在最后的一点经验其实这套STM32ESP8266AT指令MQTT阿里云的组合用到最后你会发现真正难的不是代码本身而是对“每一层协议各干各的活”的理解。AT指令负责管Wi-Fi和TCPMQTT报文负责跟云平台说话你把这两条线捋顺了换AnyCloud、OneNET乃至自建MQTT服务器也就是改改域名和签名算法的事。我后来接其他平台基本就是复用同一套代码框架只改了连接参数和Topic。这也是我强烈建议新手先别急着用一键SDK老老实实从AT指令和MQTT报文开始调通一次的原因。对于这套源码框架后面我还可以再拆一篇专门讲Alink JSON属性上报和下发指令的解析细节这次先到这儿希望能帮你把手上的板子真正连上云端。本文还有配套的精品资源点击获取