公司动态

STM32F103 + EC800 4G Cat.1 MQTT远程温湿度监控实战

📅 2026/8/29 8:39:24
STM32F103 + EC800 4G Cat.1 MQTT远程温湿度监控实战
简介物联网远程监控场景中如何在没有WiFi覆盖的田间、仓库、机房等环境下实现设备数据稳定上云4G Cat.1模块与主流MCU的组合成为关键方案。MQTT协议凭借轻量、发布/订阅模型和实时双向通信能力成为物联网数据上传与指令下发的核心纽带。本文从物联网通信基础概念入手梳理了4G模块、MQTT协议的工作原理与技术价值并落脚于一个STM32F103搭配EC800 4G模块的实际项目——通过MQTT接入OneNET云平台定时上传温湿度数据同时支持手机App远程控制继电器和电机。该方案覆盖了从硬件接线、软件流程到平台配置的完整链路适合作为毕业设计或工程入门范本为类似远程监控与工业控制场景提供可复用的实践路径。 做物联网远程监控项目时4G Cat.1 模块与单片机的组合是绕不开的方案。最近把一个实际项目整理出来了STM32F103最小系统板 EC800-4G模块通过MQTT协议定时上传温湿度到OneNET云平台同时手机App下发控制命令远程开关继电器和电机。这个项目解决了在田间、机房、仓库等没有WiFi覆盖的场景下如何用一张SIM卡把设备数据送上网的问题也适合作为毕业设计或新人练手项目。下面我把硬件设计、软件流程、平台配置和调试过程中踩过的坑一次性讲清楚。1. 方案选型为什么是 STM32F103 EC800 MQTT OneNET1.1 为什么不选 ESP32 WiFi也不选 51 单片机我在决定方案前其实先对比过好几条路线。最容易被想到的是 ESP32 接 WiFi再连 MQTT网上类似“esp32温湿度”的例程一抓一大把。但 ESP32 方案有个致命前提现场必须有能上网的WiFi而且手机热点、企业WiFi的隔离策略都会让设备掉线。我这次要部署的场景是大棚和仓库现场没有稳定路由器所以直接放弃了 WiFi。51单片机做“温湿度检测报警系统”这类项目确实经典但性能瓶颈很明显RAM 只有 128 到 512 字节跑不了完整的 MQTT 报文解析也没有足够串口资源同时处理调试日志和 4G 模块通信。STM32F103 虽然也是老芯片但 20KB RAM、72MHz 主频、多个USART做这种数据采集和透传控制的任务绰绰有余。而且“stm32f103最小系统”实在太成熟了核心板十几块钱资料到处都是。NB-IoT 我也考虑过它的功耗低、穿透强但上行速率有限、下行时延偏高做周期上报还行做实时控制会觉得肉。相比之下EC800 这种 Cat.1 4G模块网络覆盖和速率都更均衡资费也不贵非常适合这个项目。如果你不想自己画板直接买带 EC800 的模组或者核心板剩下的精力全部放在业务逻辑上。1.2 EC800 模块和 MQTT 协议的基本认识EC800 是移远Quectel的 4G Cat.1 模块支持 LTE 网络国内三大运营商的卡都能用。它最大的特点是模块固件里直接内置了 MQTT 协议的 AT 指令也就是说STM32 不需要自己去移植一个 MQTT 协议栈只需要通过串口发 AT 指令模块就会帮你完成 TCP 连接、MQTT 握手、订阅主题、发布消息这些事。这一点非常关键它把项目难度从“嵌入式网络协议移植”降到了“串口收发字符串”。MQTT 协议本身是一个基于发布/订阅模型的轻量级消息协议。传统 TCP 是点对点通信你必须自己约定每条消息的格式HTTP 是请求/响应模型服务器想主动推数据给设备很别扭。MQTT 中间多了一个“代理服务器”也就是我们常说的 Broker。设备可以往一个叫“主题”的管道里发消息也可以订阅这个主题接收消息。客户端之间不需要知道对方是谁只要主题对得上就行。我一般给新手打一个比喻MQTT 的 Topic 就像微信群传感器往群里发一条温湿度消息手机 App 也在群里就能实时看到手机在群里发一条“开继电器”单片机在群里收到后就执行。OneNET 在这个项目里就扮演了“群主 服务器”的双重角色既做 MQTT Broker又负责数据存储、设备管理、API调用。1.3 整个项目的完整数据流搞清楚一条消息怎么从传感器走到手机再从手机回到继电器整个项目就理解了一半。上行链路温湿度传感器读取数据 → STM32F103 通过 GPIO 或 I2C 拿到温湿度值 → 拼接成 JSON 字符串 → 通过串口发送 MQTT 发布指令给 EC800 模块 → EC800 走 4G 网络把数据发到 OneNET 云平台 → OneNET 解析数据流并存档 → 手机 App 或网页展示实时温湿度曲线。下行链路手机 App 点击“开继电器” → 请求发送到 OneNET可以通过 MQTT 或 HTTP API → OneNET 把命令投递到设备的 MQTT 下行主题 → EC800 模组收到消息后通过串口以 URC 主动上报的形式推送给定 STM32 → STM32 解析 JSON找到控制字段 → 控制 GPIO 输出电平驱动继电器动作。所以这个项目其实是一个典型的“上行遥测 下行遥控”物联网闭环麻雀虽小五脏俱全。下面每一节我会按实际开发顺序展开。2. 硬件接线与系统设计电平、电源、传感器、执行器一个都不能错2.1 EC800 与 STM32F103 的串口接线含 PA9/PA10 细节先解决最容易被搞混的串口问题。STM32F103 有多个串口我最常用的两个USART1 对应 PA9TX、PA10RXUSART2 对应 PA2TX、PA3RX。很多新手第一次接线会问“stm32f103 pa9 pa10 哪个是 tx rx”答案是PA9 是 TXPA10 是 RX。串口接线必须交叉模块的 TX 接单片机的 RX模块的 RX 接单片机的 TX。如果你用 USART1那么 EC800 的 TXD 接 PA10RXD 接 PA9。如果你用 USART2就是 EC800 的 TXD 接 PA3RXD 接 PA2。我这次实际用 USART2 接 EC800USART1 留给了调试日志输出这样能同时看到日志和模块数据排查问题方便很多。电平匹配是一个容易被忽略的点。移远 EC800 的 UART 电平一般是 1.8V而 STM32F103 的 UART 是 3.3V TTL 电平。很多市售的 EC800 核心板或者模组把电平兼容问题处理好了可以直接对接 3.3V MCU。但如果你用的是独立模块就要看模块手册上的 VIO 电压。稳妥的做法是在单片机和模块之间加一个电平转换芯片或者用电阻分压把 3.3V 降到 1.8V。我自己的习惯是上电后先用万用表量模块的 VIO 引脚如果输出 1.8V 而模块手册又没明确说兼容 3.3V就老老实实加转换否则串口会随机丢数据。接线时还要把模块的 RESET 引脚接到单片机一个 GPIO 上方便死机时软复位PWRKEY 引脚也要接出来上电时给一段低电平脉冲让模块开机。模块的 NETLIGHT 网络指示灯接一个 LED看信号状态很直观。SIM 卡座用 Micro SIM 或 Nano SIM 转接卡座都行但一定要保证接触可靠我遇到过多次“没插好卡导致搜不到网”的假故障。2.2 供电设计EC800 的峰值电流问题EC800 的供电是这次项目里最容易翻车的环节。4G 模块发射瞬间电流很大实测峰值能到 2A 左右如果你用一个普通的 AMS1117-3.3V 去喂它电压会瞬间被拉垮模块就会随机重启表现为“能注册网但一发数据就死机”。正确的做法是给模块单独供电。EC800 的 VBAT 范围是 3.4V 到 4.2V推荐用 DC-DC 降压模块把输入电压降到 4V 左右输出电流能力至少 2A。如果项目用锂电池供电那 4.2V 锂电可以直接经过一个稳压二极管和滤波电容给模块供电。在模块 VBAT 引脚旁边必须放一个大容量的电解电容比如 470uF 到 1000uF和一个小容量的陶瓷电容0.1uF这样能吸收发射瞬间的电流尖峰。STM32F103 和传感器需要 3.3V可以从 4V 电源再经过一个单独的 LDO 稳压得到。所有 GND 必须共地这是常识但接线松动真的会遇到。我的建议是前期调试直接用一块支持 3A 输出的可调 DC-DC 小板把电压调到 4V确认整机工作正常后再做集成电源。否则一上来就纠结“为什么模块死机”非常浪费时间。2.3 温湿度传感器选型DHT11 和 SHT30 哪个更合适温湿度传感器在这个项目里我试过两种DHT11 和 SHT30各有特点。DHT11 是单总线协议只需要一根数据线价格便宜资料极多绝大多数入门教程都是拿它演示。但它精度一般温度 ±2℃、湿度 ±5%RH而且对时序要求很严格读取时必须精确控制微秒级延时稍微被中断打断就会读到错误数据。如果你的项目只是做“车间环境有没有超过40度”这种粗粒度监控DHT11 完全够用。SHT30 是 I2C 接口精度更高温度能到 ±0.2℃湿度 ±2%RH而且不需要手工操作时序只需要调用 I2C 读寄存器即可代码稳定性远好于 DHT11。价格也就几块钱我建议预算允许就直接上 SHT30。SHT30 的 I2C 地址是 0x44ADDR 引脚接地时SDA 和 SCL 都需要接上拉电阻到 3.3V一般用 4.7K 上拉即可。我这次最终用的是 SHT30因为整个系统的数据要上传到云平台做趋势分析精度高一点更有价值。文章后面代码示例我也按 SHT30 来讲如果你坚持用 DHT11网上现成的读取代码很多把 read 函数替换掉就行。2.4 本地控制外设继电器驱动与 PWM 调速“APP下发控制”总要控制点什么。最简单的负载就是一个继电器控制水泵、风扇、加热器。单片机的 GPIO 输出能力有限不能直接驱动继电器线圈需要加一个三极管比如 S8050或者达林顿管 ULN2003。继电器线圈两端要并联一个反向续流二极管1N4148否则断电瞬间的反向电动势可能烧掉三极管甚至单片机的引脚。如果你控制的不是简单的开关而是需要调速的设备比如风扇那就可以用 STM32F103 的 PWM 输出。STM32F103 的 TIM2/TIM3/TIM4 都支持 PWM 输出比如 TIM3 的 CH1 对应 PA6CH2 对应 PA7。配置好输出比较模式后改变 CCR 寄存器的值就能改变占空比从而控制风扇转速或 LED 亮度。热词里很多人搜“stm32f103的pwm输出配置”我在这里给一个基本思路72MHz 时钟经过预分频得到合适的计数频率比如 72MHz / 72 1MHz设置自动重装值为 1000这样 PWM 频率就是 1kHz占空比调节范围 0~1000正好对应 0.0%~100.0%APP 下发 50 就是占空比 50%。当 App 下发“relay1”“fan80”这样的指令时STM32 解析后直接操作 GPIO_SetBits 或 TIM_SetCompare 就能完成远程控制。本地最好再留一个按键和状态灯方便没有网络时也能手动开合继电器。3. STM32 软件实现定时采集、MQTT 上报与命令解析3.1 定时器与主循环设计不用 Delay用标志位软件框架是这项目能不能稳定跑起来的关键。我见过不少人直接在主循环里做一个大延时延时 5 秒后再去读传感器再去发 MQTT。这种做法最大的问题是当 STM32 在读取传感器或者处理 AT 指令时EC800 模块可能会主动上报下行数据URC如果程序没有及时去读串口消息就会丢失或者串口缓冲被积压之后的所有 AT 回复都会错位。我的做法是用定时器中断产生一个 1 秒的节拍中断里只置一个标志位主循环检测到这个标志位后再去做传感器采集和 MQTT 上报。这样主循环永远不会被一个 Delay 卡死串口中断可以实时接收模块数据。TIM2 的初始化代码大概是这样的void TIM2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_InitStructure.TIM_Prescaler 7199; // 72MHz / 7200 10kHz TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStructure.TIM_Period 9999; // 计数10000次产生1秒中断 TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 2; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_Cmd(TIM2, ENABLE); } volatile uint8_t timer_flag 0; uint8_t tick_count 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); timer_flag 1; } }主循环里就可以这样写while(1) { if (timer_flag) { timer_flag 0; tick_count; if (tick_count 5) { // 5秒执行一次 tick_count 0; SHT30_ReadData(temp, humi); MQTT_PublishTempHumidity(temp, humi); } } ProcessMQTTReceive(); // 处理EC800上报的下行数据 }这个结构非常稳定它把“周期性任务”和“异步通信事件”分开了。如果你想改成 10 秒上报一次就把tick_count 10不用动其他代码。3.2 读取 SHT30 温湿度传感器I2C 通信与数据处理SHT30 的读取逻辑很简单。需要先向传感器发送一个测量命令然后等待一小段时间比如 15ms再读取 6 个字节的数据温度高字节、温度低字节、CRC湿度高字节、湿度低字节、CRC。我这里不校验 CRC因为做的是工程验证但对数据做了基本合法性判断防止偶尔读到的 0xFF 这种垃圾值被上传到云平台。uint8_t SHT30_ReadData(float *temp, float *humi) { uint8_t buf[6] {0}; uint8_t cmd[2] {0x2C, 0x06}; // 高精度测量命令 I2C_Start(); I2C_WriteByte(0x44 1); // 设备地址 I2C_WriteByte(cmd[0]); I2C_WriteByte(cmd[1]); I2C_Stop(); delay_ms(15); I2C_Start(); I2C_WriteByte(0x44 1 | 0x01); // 读模式 buf[0] I2C_ReadByte(ACK); buf[1] I2C_ReadByte(ACK); buf[2] I2C_ReadByte(ACK); buf[3] I2C_ReadByte(ACK); buf[4] I2C_ReadByte(ACK); buf[5] I2C_ReadByte(NACK); I2C_Stop(); if (buf[0] 0xFF buf[1] 0xFF) return 0; // 数据无效 *temp -45.0 175.0 * ((buf[0] 8) | buf[1]) / 65535.0; *humi 100.0 * ((buf[3] 8) | buf[4]) / 65535.0; return 1; }使用 I2C 通信时要注意SCL 和 SDA 两根线都不要接反而且都必须有上拉电阻。我见过不少人把 SDA 和 SCL 接反结果读出来的数据全乱。接线前一定对着模块丝印确认。如果你用的是 DHT11则必须注意读取过程中的时序保护。DHT11 的每位数据由 50us 低电平开始紧接着的高电平长度决定该位是 0 还是 1。读取期间如果发生串口中断就可能把 70us 长短的高电平判断错。我建议在读取 DHT11 之前把全局中断关掉读完再打开。SHT30 就没有这个烦恼这也是我推荐它的原因之一。3.3 串口处理与 EC800 的 MQTT 初始化流程STM32F103 与 EC800 的交互全部走串口 AT 指令。这里非常重要的一点是串口接收必须用中断不能在主循环里轮询等待。EC800 可能在任何时刻上报下行数据比如“QMTSUB: 0,0,”主题名,payload”这些信息如果没被及时接收就会丢。我一般用环形缓冲接收串口数据再在主循环里按行解析uint8_t uart_rx_buf[512]; uint16_t uart_rx_head 0; uint16_t uart_rx_tail 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART2); uart_rx_buf[uart_rx_head] ch; if (uart_rx_head 512) uart_rx_head 0; } }然后写一个UART_ReadLine(char *line)函数从环形缓冲里取出一行以\r\n结尾的字符串主循环调用它去判断收到的 AT 回复。如果一次没有取出完整数据就继续等。初始化 EC800 的过程一般是这样发送AT等待回复OK。发送ATE0关闭回显。发送ATCSQ查询信号强度。返回值第一个数字在 10 以上就说明信号可用。发送ATCEREG?查询网络注册状态。返回CEREG: 0,1表示已注册上网络。发送ATCGACT1激活 PDP 上下文。发送ATQMTOPEN0,xxx.xxx.xxx.xxx,1883打开 MQTT 连接。这里的地址是在 OneNET 平台里查到的 MQTT Broker 地址不同云平台给的地址不一样。发送ATQMTCONN0,clientId,username,password建立 MQTT 连接。三要素的拼接规则我后面第四部分会讲。每发送一条 AT 指令后都必须等待模块回复 OK 或者报错再发下一条。千万不要用串口助手一次性把指令全部粘贴进去模块来不及处理很容易把缓冲区冲爆然后返回一堆莫名其妙的 ERROR。3.4 数据上报JSON 构造与 QMTPUB 发布OneNET 的数据展示是基于“数据流模板”的。你在平台里定义了temp和humi两个属性那么设备上报的 JSON 就必须包含这两个字段。我常用的上报 payload 是这种格式{temp:25.6,humi:60.3}如果你在 OneNET 平台里配置的是复杂结构比如分组和报警阈值那就需要按照产品模型定义的标准格式类似{temp:{value:25.6},humi:{value:60.3}}具体格式以你平台当前版本的产品模型文档为准。我建议在第一步配置平台时就固定字段名后面程序不要随意改不然数据和云端模板对不上会出现“设备显示在线但数据不刷新”的情况。上报时先在代码里用sprintf拼出 JSON 字符串char payload[128]; sprintf(payload, {\temp\:%.1f,\humi\:%.1f}, temp, humi);然后调用发布函数char cmd[256]; sprintf(cmd, ATQMTPUB0,0,0,0,\$sys/%s/%s/thing/property/post\,%s\r\n, pid, deviceName, payload); UART_SendString(cmd);注意EC800 的ATQMTPUB指令格式不同固件版本可能略有差异主要表现为参数个数。我用的方式是把整个 payload 放进指令行里模块收到后自动发布。有些版本要求先发ATQMTPUB0,0,0,0,topic,length收到后再发 payload。所以调试时第一件事就是看模块手册里 QMTPUB 指令的语法。发布频率也要控制。调试期每 5 秒发布一次还好办正式部署建议至少 30 秒一次。一是省流量二是减少对云平台的 API 压力三是避免因为频繁 MQTT 收发导致模块不稳。3.5 接收 APP 下发的控制命令与本地执行当 OneNET 平台下发命令时EC800 模块会通过串口主动上报URC 格式类似QMTSUB: 0,0,$sys/pid/device/thing/property/set,22 {relay:1}第一行表示有 22 字节的 MQTT 消息到达第二行就是实际 payload。STM32 的串口解析逻辑要能识别QMTSUB:开头的事件然后把下一行数据缓存到 buffer 里继续判断 JSON 内容。我推荐直接在 STM32F103 里加入 cJSON 库虽然会消耗一点代码空间和 RAM但解析 JSON 的稳定性比自己写strstr强太多了。比如可以用 cJSON 这么写#include cJSON.h void ParseControlCmd(char *payload) { cJSON *json cJSON_Parse(payload); if (!json) return; cJSON *relay_item cJSON_GetObjectItem(json, relay); if (relay_item cJSON_IsNumber(relay_item)) { if (relay_item-valueint 1) { GPIO_SetBits(GPIOB, GPIO_Pin_12); // 继电器开 } else { GPIO_ResetBits(GPIOB, GPIO_Pin_12); // 继电器关 } } cJSON_Delete(json); }注意 cJSON_Parse 会动态分配内存解析完必须调用 cJSON_Delete 释放掉否则长时间运行后堆碎片会增加最终导致 malloc 失败。STM32F103 的 RAM 虽然紧张但简单场景下 20KB 足够跑 cJSON。控制执行完之后最好再发布一条状态回执比如{relay_state:1}这样 App 端可以确认“命令已经执行成功”。这种状态同步机制在正式产品里几乎不可或缺否则用户点了开关按钮不知道到底开没开。4. OneNET 平台配置与 APP 远程控制链路4.1 在 OneNET 创建产品、设备拿到 MQTT 三元组OneNET 平台端的配置是整个项目里看起来最简单、但最容易出错的环节。因为平台每隔一段时间会升级菜单名称和接入协议可能变化。我建议做项目前先打开 OneNET 平台当前版本的“设备接入”文档确认三要素的拼接规则。大体流程是注册平台账号 → 创建产品 → 选择“MQTT”协议 → 添加设备 → 平台生成产品ID、设备ID、鉴权信息有时也叫设备密钥或密码。我实际项目里的做法是把这三个信息放到 STM32 程序头部宏定义#define ONENET_PID 5xxxxxxxxxxx // 产品ID #define ONENET_DID 6xxxxxxxxxxx // 设备ID #define ONENET_AUTH my_device_key // 鉴权信息MQTT 连接时EC800 的ATQMTCONN参数通常是这样拼接的clientIdONENET_PID;ONENET_DID中间用分号隔开usernameONENET_PIDpasswordONENET_AUTH如果你调不通请优先检查这个三元组。特别是分号是英文分号不能写成中文分号鉴权信息的大小写也必须完全一致。很多 MQTT 连接失败不是代码问题而是这里差了一个字母。Broker 地址和端口同样在平台文档里查一般是 IP 加 1883 端口。不要照抄我下面这行代码里的 IP因为平台的地址可能调整过ATQMTOPEN0,183.230.40.96,1883在实际项目里要去平台后台查看当前的 MQTT 接入地址是多少再填进去。4.2 数据流模板与主题命名OneNET 新版平台用的是“产品模型”你需要在模型里定义属性比如属性名类型单位说明tempfloat℃温度humifloat%RH湿度relay_stateint无继电器状态上行发布的主题一般是$sys/{产品ID}/{设备ID}/thing/property/post下行订阅主题一般是$sys/{产品ID}/{设备ID}/thing/property/set。注意$sys三个字母之前有个美元符号不能省略产品ID和设备ID也要替换成你自己的。我见过很多人卡在主题上设备上报的 topic 写错了一个斜杠云平台一直收不到数据。所以我在程序里习惯把主题定义成宏#define TOPIC_POST $sys/ ONENET_PID / ONENET_DID /thing/property/post #define TOPIC_SET $sys/ ONENET_PID / ONENET_DID /thing/property/set这种写法一目了然也方便以后换平台时统一修改。4.3 APP 下发控制官方调试器和自建 App 两条路开发初期不建议一上来就写 App。先用 OneNET 平台自带的调试工具或者官方 App 测试上行数据和下行命令。在设备详情页里你可以手动给设备下发一条 JSON 命令如果 STM32 那边收到了用串口日志打印出来就说明整条链路是通的。这一步确认没问题后再考虑做自定义 App。想做自己的 App最简单的路线是用 uniapp 写一个跨平台客户端使用 MQTT.js 或者 uniapp 的 MQTT 插件直接连接 OneNET 的 MQTT Broker。App 订阅同一个下行主题的确认主题并发布控制命令。因为 OneNET 本身已经实现了 Broker 功能你不需要单独搭服务器。热词里有人搜“mqtt vue3”“uniapp使用mqtt”说明这条路线确实有不少人在用。如果你需要更复杂的业务比如用户登录、设备权限、历史数据报表那就得加一个后端服务。热词里有“springboot mqtt 监听”和“springboot2.7 整合 mqtt”说明这也是常见做法。后端的思路是用 Spring Boot 写一个 MQTT 客户端订阅 OneNET 的设备数据主题把数据存进数据库APP 需要控制设备时调用后端 API后端再用 MQTT 发布下行命令。这样做的好处是 App 不直接暴露云平台密钥安全性好很多也方便多用户权限管理。不过对 STM32F103 这一端来说无论 App 是直接用 MQTT 连平台还是通过后端转发最终的下行消息都是到那一个 set 主题。所以单片机侧代码不需要改这一节你只需要理解链路不用纠结 App 具体实现了。5. 常见问题与排查技巧实录5.1 4G 模块搜不到网、MQTT 连接一直失败这类问题在项目里出现频率最高我整理成一个速查表现象可能原因排查方法ATCSQ 返回 99,99天线没接好或天线损坏重新插拔天线检查天线是否焊牢ATCSQ 返回 0~5信号太差把天线移到窗边或加延长线ATCEREG? 返回 0,2 或 0,3正在找网或被拒绝检查SIM卡是否插好、是否欠费、是否支持4GATCEREG? 返回 0,1已注册OK可以进入下一步MQTT 连接返回 ERROR三元组拼接错误核对产品ID;设备ID、username、passwordMQTT 连接提示超时Broker地址或端口错误从平台文档重新查地址检查1883端口实践经验拿到模块后第一步不是接单片机而是用 USB 转 TTL 模块直接接电脑用串口助手手动发 AT 指令。先确认模块能注册网络、能连 MQTT再接 STM32。这样可以快速隔离“是模块问题还是单片机程序问题”。另外SIM 卡槽的金手指有时候会氧化接触不良表现就是“时好时坏”。遇到模块一会儿注册成功一会儿掉线先换个 SIM 卡座试试。APN 一般默认用运营商的默认配置如果不行可以手动设置ATCGDCONT1,IP,cmnet5.2 定时采集不准、程序卡死、上报周期漂移如果你的定时上报周期明显不对或者运行一段时间后程序就卡住了多半是代码里某个阻塞函数太长。我遇到过的情况是读取 SHT30 时 I2C 引脚没接好程序在等待 ACK 时死循环或者发送 AT 指令后一直等待 OK 回复但模块已经死机程序只能干等。解决思路是给所有等待操作加超时。比如串口等待一行回复时设置一个 2 秒的时限超时就放弃本次等待继续主循环。这样即使模块出现异常单片机也不会死锁。我习惯把串口等待封装成int UART_WaitForLine(char *line, uint16_t timeout_ms) { uint32_t start GetTickMs(); while (GetTickMs() - start timeout_ms) { if (UART_ReadLine(line)) return 1; } return 0; }定时器中断里只做标志位操作绝对不做耗时任务。如果你发现程序有时候卡顿可以检查是否在中断里调用了 sprintf、printf 这类阻塞函数。5.3 APP 下发命令收不到或者收到后执行不了下行命令收不到最常见的原因是订阅没成功。发完ATQMTSUB之后一定要看模块返回的是不是 OK。订阅主题写错了也不会报错但平台下发消息时模块收不到。我建议在代码里加一条日志把订阅的主题也打印出来调试时一眼能看出来。另一种情况是设备不在线。OneNET 平台一般只对在线设备下发消息如果设备已经离线App 侧可能会提示“设备不在线”或“命令超时”。先确认 STM32 串口日志里 EC800 是否保持 MQTT 连接不要只看模块的电源灯。收到命令但执行不了通常是 JSON 解析问题。一是字段名对不上平台下发的是{relay:1}代码里找的却是{switch:1}二是数据量太大缓冲区截断。EC800 一次最多能接收的 MQTT payload 长度有限如果 App 下发了一长串 JSON 带了很多额外字段ST 的串口缓冲可能没接完整导致 JSON 解析失败。我的经验是下发命令尽量精简控制类命令只放必要字段例如{relay:1}。如果确实需要很多参数可以在 OneNET 产品模型里拆成多个属性单独控制。5.4 断线重连与历史数据缓存4G 网络在移动过程中可能出现短暂信号不好或者 MQTT 连接因为长时间空闲被服务端断开。项目要稳定运行必须做断线重连机制。我实现的重连逻辑是这样的主循环里每隔 30 秒检查一次 MQTT 连接状态发送 ATQM本文还有配套的精品资源点击获取