ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103与EC200U通过MQTT接入OneNET云平台实战指南

STM32F103与EC200U通过MQTT接入OneNET云平台实战指南 简介面向需要快速实现设备上云的嵌入式开发者资源提供了一套STM32F103与移远EC200U 4G模块的完整通信例程基于MQTT协议定时上传温湿度数据至ONENET平台并支持手机APP远程下发控制命令工程采用KEIL标准库编写若换用同系列其他型号只需调整芯片型号与FLASH容量。代码中已定义硬件接线并包含定时器、ADC、I2C等标准外设驱动文件方便理解数据采集、4G通信与云平台对接的完整链路。压缩包共244个文件约101.37MB以C/H源码、编译输出文件、hex/axf固件为核心另有接线图、说明图片及配套APK安装包结构清晰便于按模块查阅。目前已有234人学习代码对关键逻辑注释详细并提供清理编译残余的脚本可帮助开发者快速理清EC200U的AT指令交互、MQTT协议数据上报与ONENET平台消息下发流程可直接作为环境监测、智能家居等场景的物联网实战参考。1. STM32F103 与 EC200U 4G MQTT 上云链路这套资料解决什么问题这套例程解决的问题很具体用 STM32F103 标准库工程驱动移远 EC200U 4G Cat.1 模块通过 MQTT 协议将温湿度数据定时上报到中国移动 OneNET 云平台再从“中移和物”APP 下发控制指令完成反向控制。4G Cat.1 比 NB-IoT 带宽更高、比 WiFi 方案省去现场配网适合农业大棚、机房监控、冷链运输这类直接插卡运行的设备。代码基于 KEIL 标准库ADC、I2C、TIM、USART 驱动都已初始化换同系列 MCU 只需改 Target 设置里的芯片型号和 Flash 容量。适合一个月内要交原型项目的开发工程师也适合想完整走通“传感器→4G模块→MQTT→云平台→APP”链路的入门者。下面先从工程文件入手把编译环境和接线确认清楚。2. KEIL 工程文件结构、Flash 容量配置与 EC200U 接线定义拿到压缩包后第一件事不是编译而是先分清哪些是源码、哪些是上一位开发者留下的编译残留。工程根目录下的 CSTX.axf、Test.axf、CSTX_sct.Bak 都是旧编译产物CSTX.axf 是最终链接出的可执行镜像CSTX_sct.Bak 是分散加载文件的备份真正承担功能的代码是 stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_tim.c、stm32f10x_rcc.c、stm32f10x_flash.c 这 5 个标准库外设文件再加上用户层的 main.c 和网络驱动部分。2.1 外设文件分工与数据采集链路这套工程的外设驱动是按标准库文件拆开的每个 c 文件对应一个外设模块改动时不用在整个工程里翻找宏定义。下表是这几个文件在本项目里的实际作用文件外设在本工程中的用途stm32f10x_rcc.cRCC 时钟配置系统时钟与各外设时钟使能stm32f10x_tim.cTIM 定时器生成定时采集与 MQTT 发布节拍stm32f10x_adc.cADC 采样读取模拟量传感器或电池电压stm32f10x_i2c.cI2C 主机读取数字温湿度传感器如 SHT30、AHT20stm32f10x_flash.c片内 Flash保存设备 ID、APIKey 等掉电不丢参数数据链路分两条温湿度传感器如果走 I2C挂在 PB6/PB7 对应的 I2C1 上TICK 中断周期唤醒读取如果传感器输出是模拟电压则接到 PA0/PA1 复用为 ADC1 通道再在应用层做电压到温度的量程换算。定时器中断只负责置位发送标志不让 I2C 或 MQTT 的耗时操作进入中断上下文这样模块在收发数据时不会阻塞采集节拍。2.2 KEIL 芯片型号、Flash 容量与调试器切换工程默认基于 STM32F103 开发实际用的是什么具体型号要打开 KEIL 的 Options for Target 确认。常见的 F103 型号引脚和 Flash 容量不同直接烧录可能造成启动文件与芯片容量不匹配。在魔术棒 Device 标签页选择对应芯片再切到 Target 标签页检查 IROM1 的 Size。芯片型号Flash 容量启动文件IROM1 SizeSTM32F103C8T664KBstartup_stm32f10x_md.s0x10000STM32F103RCT6256KBstartup_stm32f10x_hd.s0x40000STM32F103ZET6512KBstartup_stm32f10x_hd.s0x80000如果换了芯片却只改 Device 型号、忘记同步启动文件编译可能通过但链接生成的镜像地址会越界。正点原子和野火的工程模板里标准库版本一般把启动文件放在工程组列表的最前面替换时直接移除旧 startup 文件并添加对应型号即可。调试器选择在 Debug 标签页右侧下拉框使用 ST-Link 选 ST-Link Debugger使用 J-Link 选 J-LINK/J-TRACE Cortex。选对后还要进入 Settings 确认 SW 模式下的连接速度F103 常见配置为 4MHz 或 1.8MHz线材较长时降速能减少下载失败。压缩包里的“清除KEIL编译残余双击鼠标.bat”解决的是中间产物堆积问题内容等价于手动删除 Listings、Objects 目录以及 .o、.crf、.axf、.hex、.map 这类文件。脚本写法注意保留源码目录echo off rem 清理 KEIL 编译中间产物不影响源码 del /s /q *.o *.d *.crf *.axf *.hex *.map *.lst *.dep *.bak 2nul for /d %%i in (Listings Objects) do ( if exist %%i rd /s /q %%i ) echo 中间产物已清理源码目录未动 pause这段脚本用del /s /q递归删除当前目录及子目录下的编译生成文件2nul把“文件未找到”提示吞掉for /d遍历 Listings 和 Objects 两个目录并整目录删除。KEIL5 创建工程时有的项目不生成 Listings删除时不存在也不会报错。清理后重新编译能避免旧目标文件导致的“明明改了代码却不生效”问题。2.3 STM32F103 与 EC200U 的接线、供电和调试通道EC200U 是 LTE Cat.1 模组峰值发射电流接近 2A直接挂在 STM32 的 3.3V LDO 上大概率出现启动后反复重启或注册网络失败。常见接法是外部 5V 电源经过 MP1584 降压到 4V 供给模块 POWER 引脚MCU 用 3.3V 独立供电两边只共地。串口电平方面EC200U 的 UART 接口与 3.3V TTL 兼容可以直接连 STM32 的 USART1。STM32F103 引脚外设功能EC200U 引脚说明PA9USART1_TXURXDMCU 发送给模块PA10USART1_RXUTXD模块发送给 MCUPB10USART3_TX接 USB-TTL RX可选调试日志输出5V 外部电源DC-DC 降压 4VVCC模块供电需 2A 余量GND地GND必须共地否则电平基准不一致PA9/PA10 接线时注意是交叉连接PA9 接模块的 URXDPA10 接模块的 UTXD接反的现象是发 AT 无任何回应。调试口建议单独留一路 USART3 接 CH340 到电脑看 log因为 ST-Link 的虚拟串口和 SWD 调试同时使用时部分板卡会互相干扰。3. EC200U AT 指令序列与 MQTT 连接建立串口处理、返回码与参数选择EC200U 对外交互完全走 AT 指令难点不在指令本身而在串口数据怎么可靠接收、怎么从返回串里判断成功失败、以及 MQTT 连接参数和 OneNET 平台怎么对齐。这三件事每一件都比“把模块通电”重要下面按处理顺序展开。3.1 USART1 初始化与环形缓冲接收项目里 EC200U 挂在 USART1波特率建议 115200。F103 的 USART1 挂载在 APB2 总线上时钟频率由 RCC 配置决定如果误把 APB2 外设当成 APB1 使能初始化后数据会乱码。先做最小初始化// usart1_ec200u.c void EC200U_UART_Init(void) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; // PA9 TX - EC200U URXD gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_10; // PA10 RX - EC200U UTXD gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); usart.USART_BaudRate 115200; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, usart); nvic.NVIC_IRQChannel USART1_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }USART1 的 TX/RX 引脚都从 GPIOA 引出注意GPIO_Mode_AF_PP是复用推挽输出RX 脚必须配置成浮空输入而不能复用推挽NVIC 中断优先级这里给 1 组 0 子优先级如果工程里同时有 TIM 中断把高优先级留给定时器串口中断稍低不会丢数据因为 RXNE 标志在中断没来得及响应时会一直挂着。模块返回的 AT 响应是一串不定长文本不能只在中断里做一遍strstr。最稳妥的做法是收到一个字节就存入环形缓冲区主循环里再做字符串匹配// ringbuf.c 简化版 #define RING_SIZE 512 static uint8_t ring[RING_SIZE]; static volatile uint16_t rhead 0, rtail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ring[rhead] USART_ReceiveData(USART1); rhead (rhead 1) % RING_SIZE; } } int at_send_and_wait(const char *cmd, uint32_t timeout_ms) { rhead rtail 0; // 先清空上次残留数据 while (*cmd) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, *cmd); } uint32_t t0 GetTickMs(); while (GetTickMs() - t0 timeout_ms) { if (ring_find(rhead, rtail, OK) 0) return 0; if (ring_find(rhead, rtail, ERROR) 0) return -1; } return -2; // 超时 }at_send_and_wait的参数含义是cmd 为要发送的完整 AT 指令必须以\r\n结尾部分模块只认\r所以拼接指令时统一用字符串数组按下述方法生成timeout_ms 是等待返回的最大毫秒数。返回值 0 表示在环形缓冲区里匹配到了期望关键字-1 表示模块回了 ERROR-2 表示超时。该写法中ring_find是一个在 rhead 与 rtail 区间内逐字节匹配子串的函数实际工程里还要加保护当 rhead 与 rtail 相等且缓冲区连续收到大量数据时要么丢弃旧数据要么扩大缓冲区。3.2 AT 指令握手顺序与网络注册判断EC200U 从冷启动到能连 MQTT必须按固定顺序执行以下指令跳过任何一步都会在后续步骤报错。常见顺序表如下步骤AT 指令预期返回说明1ATOK模块是否在线2ATE0OK关闭回显便于解析返回3ATCSQCSQ: 20,0信号强度与误码率4ATCEREG?CEREG: 0,10,1 表示已注册 home 网络5ATCGDCONT1,IP,cmnetOK设置运营商 APN6ATCGACT1,1OK激活 PDP 上下文7ATQMTOPEN1,183.230.40.40,6002QMTOPEN: 1,0打开到 OneNET 的 TCP 连接8ATQMTCONN1,设备ID,产品ID,APIKeyQMTCONN: 1,0,0MQTT 会话建立其中 ATCSQ 的第一个字段范围是 0 到 31越小信号越差99 表示无信号。ATCEREG? 返回的第二个字段 0 表示未注册1 表示已注册本网5 表示漫游3 表示网络拒绝卡没开通物联网业务时通常会卡在 3。ATCGDCONT 里的 cmnet 是中国移动默认 APN电信卡要换 ctlte联通卡换 wonet这里按 SIM 卡运营商改。提示模块刚上电 3 秒内可能对 AT 指令无响应主程序中要循环发 AT 直到收到 OK 再继续不能靠固定 delay 强等。3.3 MQTT 会话参数与 KeepAlive 对齐ATQMTOPEN 的第二个参数是 OneNET 经典多协议接入的 MQTT 服务器地址端口 6002这是旧版 OneNET 多协议接入口如果在新版 OneNET Studio 里创建设备接入地址和端口不同需要从控制台设备详情里复制。ATQMTCONN 的四个参数中clientIdx 固定为 1表示模块内部的 MQTT 客户端编号第二个参数是设备 ID第三个是产品 ID第四个是设备 APIKey这三个值必须和 OneNET 平台上创建产品时生成的完全一致。MQTT 协议的 CleanSession 和 KeepAlive 参数在模块里默认即可OneNET 会在一定周期内未收到数据时断开连接所以应用层要保证定时发送数据包。这套例程的做法是在 TIM2 中断里置位发送标志主循环每 10 秒读取一次温湿度并调用 MQTT 发布发布本身也就是一次心跳不需要额外维护心跳包。4. OneNET 接入、温湿度数据上云与 APP 下行控制实现OneNET 平台侧要做的准备工作分三步创建产品、创建设备、记录 APIKey。这些值会被写进 MCU 代码里作为 MQTT 连接的三要素。平台侧配置错误时 MCU 端往往表现为 ATQMTCONN 返回错误码而不是直接显示中文提示。4.1 OneNET 产品、设备与 APIKey 生成登录 OneNET 控制台进入多协议接入添加一个 MQTT 产品记录产品 ID。然后在设备列表中添加设备系统会生成设备 ID 和 APIKey。APIKey 是设备级的鉴权密钥相当于 MQTT 密码泄露后任何客户端都能以该设备身份上下行数据项目管理中要单独保管。设备创建完成后数据流名称自己定义代码里上报的 id 字段必须与平台数据流名称完全一致否则上报成功但数据流里看不到点。比如平台数据流叫 temp代码里必须用id:temp大小写、空格都不能差。4.2 温湿度 JSON 组装与 MQTT 发布OneNET 经典 MQTT 方式上报数据主题固定为$dp负载是 JSON 格式。温湿度采集结果先拼成字符串再封装到 QMTPUB 指令里// mqtt_publish.c static void MQTT_Publish_Temp_Humi(float temp, float humi) { char payload[160]; char cmd[224]; const char *topic $dp; snprintf(payload, sizeof(payload), {\datastreams\:[ {\id\:\temp\,\datapoints\:[{\value\:%.1f}]}, {\id\:\humi\,\datapoints\:[{\value\:%.1f}]} ]}, temp, humi); snprintf(cmd, sizeof(cmd), ATQMTPUB1,0,0,0,\%s\,\%s\\r\n, topic, payload); at_send_and_wait(cmd, 5000); }这段代码做过两处特殊处理浮点数格式化用%.1f保留一位小数避免%f在部分嵌入式 printf 库中输出过长的位数payload 拼接时把内部的双引号都加了反斜杠转义因为整个 JSON 是作为 AT 指令的一个字符串参数传出去的。ATQMTPUB 的参数依次是客户端编号 1、消息 ID 0、QoS 0、retain 0、主题、负载QoS 为 0 表示最多发送一次OneNET 侧不返回确认网络抖动时可能丢数据改为 1 则模块会等待平台 ack但耗时更长根据项目对实时性和可靠性的权衡选择。4.3 下行命令订阅与 APP 控制实现反向控制链路比上报多一个订阅动作。OneNET 经典接入的下行命令主题是$creq/设备IDMCU 需要先订阅平台推送命令后模块返回QMTRECV事件再从事件里剥出 JSON。static void MQTT_Subscribe_Cmd(const char *device_id) { char cmd[96]; snprintf(cmd, sizeof(cmd), ATQMTSUB1,1,\$creq/%s\,0\r\n, device_id); at_send_and_wait(cmd, 2000); }订阅之后APP 端下发格式如{cmd_id:123,data:{led:1}}的命令模块串口会出现以 QMTRECV 开头的行。主循环里检测到这个关键字后把{之后的内容当作 JSON 解析这里不引入 cJSON 库也能快速判断关键字段if (ring_find(rhead, rtail, QMTRECV) 0) { char *body strchr((char *)ring, {); if (body) { if (strstr(body, \led\:1)) GPIO_SetBits(GPIOC, GPIO_Pin_13); if (strstr(body, \led\:0)) GPIO_ResetBits(GPIOC, GPIO_Pin_13); } rhead rtail 0; // 处理完清缓冲区 }该解析方式只适用于字段少、格式固定的控制场景项目里如果命令类型超过 3 种建议把{到}的内容拷贝到独立数组并用 cJSON 解析避免在主循环里多次调用strstr造成缓冲区数据被覆盖。APP 绑定设备时输入设备 ID 和 APIKey绑定成功后 APP 端能看到设备数据流控制指令会经平台实时推到$creq/设备ID主题。5. 收尾排查下载失败、注册失败与数据流不显示的实用检查清单工程跑不起来时按“硬件→网络→平台”三层排查比盲目改代码有效。这里把使用这套例程最容易遇到的三个问题整理成可操作的检查点。5.1 下载失败先查 BOOT 与调试口冲突ST-Link 或 DAP 下载报错时先确认 BOOT0 和 BOOT1 都在低电平BOOT0 拉高会进入系统存储器模式调试器无法正常访问 Flash。PA13/PA14 被代码复用为重映射功能也会导致 SWD 断开如果初始化代码里不小心把这两个引脚配置成普通 GPIO下载器会报 RDDI-DAP Error。此时按住复位键再点下载或通过串口 ISP 擦除整个 Flash 恢复。5.2 CSQ 与 CEREG 返回码定位网络问题模块返回CSQ: 99,99表示读不到信号优先检查天线是否拧紧、SIM 卡是否插到位、模块供电是否稳定CEREG: 0,3表示网络拒绝换一张确认开通物联网服务的卡测试。如果ATCGACT1,1一直不返回 OK把 SIM 卡装进手机确认能上网排除欠费和卡槽接触不良。返回码含义处理建议CSQ: 0信号极弱检查天线位置与馈线CSQ: 31信号极强正常范围CEREG: 0,2正在搜索等待 10 秒重查CEREG: 0,3网络拒绝检查 SIM 卡与套餐QMTOPEN: 1,-1TCP 连接失败检查 IP/端口与 PDP 激活5.3 数据流为空的最后一步确认平台数据流里没有点先确认 ATQMTPUB 返回的是QMTPUB: 1,0而不是负值然后核对 payload 里的 id 是否与 OneNET 数据流名称完全一致多一个空格或大小写不匹配都不会显示。最后查一下 OneNET 设备详情页的在线状态模块断电后平台侧要等 KeepAlive 超时才离线不要据此误判链路异常。假如QMTPUB: 1,0已返回且平台仍无点重点核对$dp主题是否写错成了$DPOneNET 的主题名区分大小写必须全小写重发后再刷新数据流页面。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进