ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103+EC20接入阿里云MQTT实战:AT指令与签名计算

2026/9/15 10:41:37 拓冰建站 浏览量
STM32F103+EC20接入阿里云MQTT实战:AT指令与签名计算 简介面向STM32F103单片机与移远EC20 4G模组开发的实战工程包以KEIL标准库编写实现了通过MQTT协议将设备数据上传至阿里云物联网平台覆盖串口驱动、网络连接、报文封装与发布等关键环节适合物联网嵌入式学习者直接参考或二次开发。压缩包共171个文件、约4.82MB以C源码、H头文件、uvprojx工程文件为主另含hex固件、map/lst编译信息、PDF说明以及清除编译残余的bat脚本目录结构清晰便于对照工程与文档快速验证。代码内对单片机与EC20的接线引脚均有定义注释详细针对不同STM32F103型号只需调整芯片型号与FLASH容量即可沿用同时保留编译中间文件与链接映射方便在学习中排查问题。资源包已有339人学习对于正在搭建4G上云链路或入门阿里云物联网平台的开发者具备较高参考价值。1. 为什么是 STM32F103 EC20 阿里云 MQTT很多人做设备上云第一反应是找 STM32 的 MQTT 协议栈代码实际上走 4G 模块时协议栈在模组内部MCU 只负责通过串口下发 AT 指令。这个例程里的 STM32F103 最小系统配合移远 EC20用 KEIL 标准库初始化 RCC、定时器、ADC、Flash 等外设再把采集到的数据通过 MQTT 发布到阿里云物联网平台。工程干净串口1 和串口3 分工明确能直接在 KEIL 里编译下载适合刚开始接触 4G 模组和云平台接入的嵌入式工程师。如果你手里是 STM32F103C8T6 或 RCT6只需要在 KEIL 里改一下芯片型号和 Flash 容量代码依然适用。2. 串口1、串口3 的接线与 KEIL 标准库工程要点STM32F103 连接 EC20 的物理链路看着简单实际坑在电源和串口复用。EC20 发射瞬间电流可达 2A如果直接从 STM32 板载 AMS1117 供电模组会在注册网络时反复重启。这个例程把 USART1 留作日志USART3 用于 AT 指令两个串口的任务完全分开调试的时候不会互相干扰。下面这段接线表是代码里已经定义好的对照自己的板子检查即可。2.1 串口1、串口3 与 EC20 的接线表STM32F103 引脚外设功能对端模块引脚方向说明PA9/PA10USART1 TX/RX调试串口或 USB-TTL主控到宿主机输出运行日志PB10USART3 TXEC20 RXMCU 到模块AT 指令发送PB11USART3 RXEC20 TX模块到 MCU响应与 URC 接收3.3V电平参考EC20 电平兼容-一般可直连PWRKEY普通 GPIOEC20 PWRKEYMCU 到模块拉低 500ms 开机VBAT-外部 4V/3A 电源-不能和 MCU 共用 LDO实际例程也把 PWRKEY、RESET 等控制引脚定义为普通 GPIO。PWRKEY 需要拉低 500ms 到 1s 触发开机许多借用 STM32F103 最小系统的设计直接在 GPIO 初始化时把这个引脚设置成推挽输出开机延时后拉低再释放。RESET 引脚同样需要 GPIO 控制但例程里一般不主动使用因为 EC20 掉电后重新配置 AT 参数的成本更低。2.2 标准库工程里值得留意的文件项目根目录有 STM32EC20.axf、modify1.bat、mb_gbk2uni.c 等文件。STM32EC20.axf 是 KEIL 编译产物不需要手动处理。modify1.bat 用来清除 KEIL 编译残余当出现cannot open file、符号重复定义这类奇怪错误时双击运行再重新编译比手动删 Object 文件夹更快。mb_gbk2uni.c 是 GBK 到 unicode 的转换模块主要处理中文显示和编码MQTT 数据里如果不涉及中文可以暂时不去动它。工程里的 stm32f10x_rcc.c、stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_adc.c 不是摆设。RCC 负责系统时钟和 GPIO 时钟定时器可以用来生成周期上报节拍ADC 负责读取模拟量Flash 则存放设备配置。也就是说例程已经把一个最小设备的时基、数据采集和存储都搭好了你要改的只是传感器数据和云平台参数。下面这段 USART3 初始化代码是标准库常见写法注意它把 PB10 配成复用推挽PB11 配成浮空输入。浮空输入能够让 RX 保持高阻态串口空闲时电平由模块 TX 引脚决定不会引入额外上拉或下拉干扰。void USART3_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; /* 开启 GPIOB 和复用时钟USART3 本身在 APB1 上 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); /* PB10: USART3 TX复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); /* PB11: USART3 RX浮空输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); /* 115200, 8N1无硬件流控 */ 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(USART3, USART_InitStructure); USART_Cmd(USART3, ENABLE); }串口3挂在 APB1 上PCLK1 默认是系统时钟的一半。STM32F103RCT6 的 SystemInit 会把 PCLK1 设为 36MHz标准库的 RCC_GetClocksFreq 在计算波特率寄存器时会自动用 36MHz所以上面代码不需要手动改 USART3 的时钟倍频。但如果你复用了别人的工程而这个工程把 APB1 分频设成了 4那 115200 会偏低现象是 AT 指令明明发出去EC20 总是回 ERROR 或乱码。排查时先用串口1 打印 USART3 收到的原始字节确认字节流以 0x0D 0x0A 结尾而不是单个 0x0A。代码里变量命名和注释都属于标准库风格GPIO 初始化后没有检查参数就立即使能串口这在 STM32F103 上问题不大因为引脚资源固定。如果你想把 AT 通道换到 USART2需要同时改 RCC 时钟源和引脚不能只换 GPIO_Pin 宏。3. EC20 的 MQTT AT 指令从 QMTOPEN 到 QMTPUBEC20 内置 MQTT 协议栈所以 STM32F103 端不需要移植 paho mqtt 代码。需要做的是把阿里云平台的主机地址、设备三元组和 Topic 通过 AT 指令写进模组然后处理返回的 URC。下面这段流程在串口1 上能看到全过程日志也是排错时最重要的信息源。3.1 MQTT 相关 AT 指令与预期返回步骤指令预期返回说明查卡ATCPIN?CPIN: READYSIM 卡有效查 PS 注册ATCGREG?CGREG: 0,14G 分组域注册配置协议版本ATQMTCFGversion,0,4OKMQTT 3.1.1打开连接ATQMTOPEN0,xxxx.mqtt.iothub.aliyuncs.com,1883OK随后 QMTOPEN: 0,0异步结果不能只看 OK建立会话ATQMTCONN0,clientId,username,passwordOK随后 QMTCONN: 0,0,0连接成功发布消息ATQMTPUB0,0,1,0,/sys/...,{params:...}OK随后 QMTPUB: 0,0QoS1 收 ACK订阅下行ATQMTSUB0,0,/sys/.../thing/service/property/set,1OK随后 QMTSUB: 0,0,0,1云端命令这里最容易踩的坑是ATQMTOPEN。它是异步指令发送后立刻收到OK但真正的 socket 建立是在QMTOPEN: 0,0这个 URC 返回之后。如果收到 OK 就开始ATQMTCONN大概率会得到CME ERROR: 516或直接无响应。正确做法是先等待QMTOPEN: 0,0再等 500ms 让模块处理 DNS 和 TCP 栈。注意不要在收到OK后立即发送ATQMTCONN必须等QMTOPEN: 0,0出现。这个 URC 可能晚到数秒初次拨号时更慢。不同固件版本对ATQMTCFGversion的支持不同。如果在你的模块上这条指令返回ERROR可以先查固件是否内置 MQTT 协议部分 4G 模块的低成本版本只支持 TCP/IP没有 MQTT AT 命令集。移远 EC20 常规版本的 MQTT 指令是完整保留的但出厂固件可能默认关闭需要通过ATQMTCFGversion显式打开。我一般建议先用 USB-TTL 直接连模块把本章表格里的指令一行行敲进去确认全部返回后再让 STM32 接管。3.2 设备端命令发送与发布函数下面这段代码是常见做法适合把 AT 指令收发封装到底层业务层只关心返回的字符串。uint8_t EC20_SendAT(const char *cmd, const char *expect, uint32_t timeout_ms) { uint8_t buf[512]; uint32_t len 0; USART3_Clear(); USART3_SendString(cmd); while (timeout_ms--) { if (USART3_ReceiveByte(buf[len])) { len; buf[len] 0; if (strstr((char *)buf, expect)) return 1; if (strstr((char *)buf, ERROR)) return 0; } DelayMs(1); } return 0; }参数说明cmd 字符串必须以\r\n结尾例如ATCGREG?\r\nexpect 是你要等的关键字QMTOPEN: 0,0就属于 URCtimeout_ms 建议给到 3 秒以上网络注册和 MQTT 握手都慢。strstr匹配 expect 后立即返回所以 expect 要尽量精确避免用0这种短字符串误匹配。实际使用中USART3 的中断里会把每个字节放进一个环形缓冲USART3_ReceiveByte从缓冲取数。上面的循环体虽然看起来是阻塞查询但因为有超时上限不会把整个系统卡死。如果你在自己的工程里直接抄这个函数记得在中断服务函数中处理ORE标志否则高波特率下容易丢字节导致strstr一直匹配不到QMTOPEN。有了这个函数再封装 MQTT 发布int EC20_MQTTPublish(const char *topic, const char *payload, uint8_t qos) { char cmd[320]; snprintf(cmd, sizeof(cmd), ATQMTPUB0,0,%d,0,\%s\,\%s\\r\n, qos, topic, payload); return EC20_SendAT(cmd, QMTPUB: 0,0, 5000); }ATQMTPUB的完整参数是client_idx,msg_id,qos,retain,topic,payload。这里 client_idx 固定为 0msg_id 传 0 表示由模块自动分配qos 推荐设备上报用 1retain 保留消息通常关闭。当前这个函数直接在指令行里拼接 payload适合几十字节的属性上报。如果以后要上报很长的 JSON建议改用ATQMTPUBEX它支持分两次输入先给 payload 长度再通过串口发送内容避免 AT 输入缓冲溢出。4. 阿里云物联网平台接入参数与属性上行 Topic之前项目中经常看到有人把 ProductKey、DeviceName、DeviceSecret 直接当成 MQTT 的 username 和 password 填进 ATQMTCONN登录平台一直报连接被拒绝。原因是阿里云要求 password 是 DeviceSecret 参与的 HMAC-SHA1 签名而不是原始密钥。下面把签名规则和 Topic 格式拆开讲。4.1 用 Python 计算 MQTT 连接参数阿里云设备接入的 MQTT 连接参数由三元组生成。我一般会先在本机跑一段 Python把结果写进 STM32F103 工程的iot_config.h而不是拿设备端硬算签名。这样模块上电后直接用算好的字符串代码也简单。import hmac, hashlib, time # 从阿里云控制台复制三元组 device_name device1 product_key a1ApGJpXXXX device_secret 8e2f4b5cXXXX # timestamp 必须是当前毫秒时间戳不能写死 timestamp str(int(time.time() * 1000)) client_id fstm32_ec20|securemode3,timestamp{timestamp},signmethodhmacsha1| # 签名内容按 clientId/deviceName/productKey/timestamp 顺序拼接 content ( clientId client_id deviceName device_name productKey product_key timestamp timestamp ) password hmac.new( device_secret.encode(), content.encode(), hashlib.sha1 ).hexdigest() print(clientId:, client_id) print(username:, device_name) print(password:, password)这个签名规则可以这样理解阿里云服务端收到 CONNECT 报文后会把 clientId 和 username 里的三元组字段拆出来与 timestamp 一起按 clientId、deviceName、productKey、timestamp 的顺序拼接再用 DeviceSecret 做 HMAC-SHA1。任意一个字段与设备端不一致签名验证就失败。所以 clientId 里的securemode3和signmethodhmacsha1不能省timestamp 也不能在设备端写死。4.2 AT 指令填入与 Topic 映射将上面的输出代入第三章的 AT 指令就是ATQMTCONN0,stm32_ec20|securemode3,timestamp1736901234567,signmethodhmacsha1|,device1,2e3f6f9d0b09...为了可读性我把真实连接参数用省略号代替。注意 AT 指令里的双引号需要原样保留有些串口助手会自动把\吃掉导致模块收到错误的 CONNECT 报文。排查时优先在串口1 打印ATQMTCONN完整内容确认没有空格。上传数据使用的 Topic 与物模型绑定方向Topic用途上行属性/sys/{productKey}/{deviceName}/thing/event/property/post设备上报实时数据上行响应/sys/{productKey}/{deviceName}/thing/event/property/post_reply平台返回处理结果下行设置/sys/{productKey}/{deviceName}/thing/service/property/set云端修改属性下行响应/sys/{productKey}/{deviceName}/thing/service/property/set_reply设备应答payload 使用标准物模型 JSON下面是一段温度湿度上报的示例{ id: 1, version: 1.0, params: { temperature: 25.6, humidity: 68.2 } }STM32F103 端通常用sprintf直接构造这段 JSON/* 温度、湿度由 ADC 采样后换算得到 */ sprintf(payload, {\id\:\%d\,\version\:\1.0\,\params\:{\temperature\:%.1f,\humidity\:%.1f}}, message_id, temperature, humidity);注意version字段固定为1.0id是请求标识平台不回显也可以但如果一个时刻有多条上报建议自增。params里的 key 必须在产品的物模型里定义否则平台返回参数错误。例程里mb_gbk2uni.c就是处理字符串编码的如果你在产品里添加了中文属性名需要先统一转成 UTF-8。我更建议全部用 ASCII 属性标识能省掉整个编码转换模块。5. 下载失败、串口乱码与 QoS 选型5.1 下载器和 boot 引脚设置例程拿到的工程进入 KEIL 后第一件事不是改代码而是确认 Debug 选项选的是 J-Link 还是 ST-Link。ST-Link 在stm32f103 dap下载失败 boot1这种场景里很常见表现为下载时提示Cannot access target一般不是工程配置问题而是芯片 boot1 引脚被拉高。把 boot1 恢复为 0boot0 也回到 0再上电重新连接即可。如果芯片内部程序已经跑飞导致无法连接可以 boot01、boot10 进入系统存储器做一次全片擦除。5.2 串口波特率与网络注册串口1 和串口3 的使用差异在例程中也有体现。USART1 挂载 APB2USART3 挂载 APB1USART3 的波特率偏差更大。如果出现串口1 打印正常、串口3 收到的 AT 响应偶尔少字节先用逻辑分析仪看模块 TX 引脚有没有完整输出再检查 MCU 的 RX 引脚是否配置成复用浮空输入。遇到过不少移植工程把 PB11 配置成上拉输入EC20 空闲电平是 0上拉后会把空闲电平拉高导致起始位误判。网络注册是 4G 模块最挑剔的部分。接入例程后如果QMTOPEN卡住按以下顺序检查排查项命令或做法预期SIM 卡ATCPIN?CPIN: READY信号强度ATCSQCSQ: 25,31分组域注册ATCGREG?CGREG: 0,1基站附着ATCEREG?CEREG: 0,1CSQ返回99,99时说明模块没有找到网络这时 AT 指令调得再对也没用。EC20 的 PWRKEY 开机后要有 2~3 秒的等待我一般在main函数里延时 3 秒再发ATCPIN?。日志里看到CFUN: 1的 URC 后再进 MQTT 流程比直接轮询成功率高。5.3 QoS 选择与确认最后谈 QoS。对于温度、电量这类周期性属性建议用 QoS0 加上报时间戳平台端按时间序覆盖对于告警类事件用 QoS1 确保不丢。EC20 的QMTPUB: 0,0是模块收到平台 PUBACK 后的 URC看到它才能确认消息落到了阿里云。QoS2 在模组和平台双端都会多一次确认实际物联网项目里用得少除非你的业务对重复严格敏感。本文还有配套的精品资源点击获取