STM32+EC800Z实现4G直连MQTT:从AT状态机到端云全链路 简介本资源是一套面向嵌入式初学者与物联网开发者的STM32实战项目源码聚焦于温湿度传感器通过4G模块接入OneNet云平台的完整通信实现。项目基于STM32F1系列单片机采用EC800Z 4G模组以标准AT指令方式完成网络注册、TCP连接、MQTT协议接入及DHT11数据上报适用于智能农业、环境监测等低功耗广域网应用场景。压缩包共8个文件4个.h头文件定义串口驱动、DHT11读取、4G模组工作模式及IAP升级指令4个.c源文件实现对应功能逻辑总大小仅13KB结构精简、模块职责清晰便于理解底层通信流程与资源调度机制。目前已有1437人学习下载读者可直接获取可编译运行的工程框架、标准化的AT指令封装逻辑、稳定可靠的DHT11时序处理代码以及适配OneNet平台的MQTT报文构造范例是掌握嵌入式设备联网调试与云平台对接的高实用性入门参考。1. 一个温湿度传感器凭什么能自己“拨号上网”——STM32 EC800Z 实现端到云全链路入网不靠WiFi、不接路由器插电即连MQTT服务器你有没有遇到过这种场景在野外变电站巡检点部署温湿度监测节点现场没WiFi、没以太网口、甚至没220V供电只有一块锂电池和一块太阳能板。这时候传统方案要么加个4G网关再拉线要么换用NB-IoT模组但要等运营商网络覆盖——而这个项目直接让一颗STM32F103C8T6成本不到¥8 一块EC800Z移远EC20的低成本工业级4G variant组合完成从传感器采样、AT指令拨号、PPP拨号建立IP连接、MQTT协议栈初始化、JSON数据封装、定时上报、断线重连的全部动作。它不是Demo是实测在-25℃~70℃工业环境下连续运行18个月的入网固件它不依赖任何上位机或配置工具所有参数APN、服务器地址、topic、心跳间隔固化在Flash指定扇区掉电不丢更关键的是它把EC800Z这个“黑匣子”彻底拆开——不是简单调AT指令而是实现了带超时重传的AT命令状态机、PPP协商失败自动降级重试、MQTT CONNECT报文手动拼装与ACK校验。如果你正卡在“单片机怎么真正用4G发数据”这一步而不是停留在“ATCGATT1返回OK就以为连上了”的玄学阶段这份源码就是你缺的那张电路图。2. 硬件架构与通信链路从STM32 GPIO到MQTT Broker的七层穿透2.1 硬件连接拓扑为什么EC800Z必须走UART2而非USART1EC800Z模块与STM32的物理连接并非随意指定。本项目采用UART2PA2/PA3作为主AT通道原因有三第一EC800Z的TXD引脚输出电平为3.3V CMOS但其RXD引脚要求输入耐压≥5V工业级设计而STM32F103的USART1挂载在APB2总线上其IO口默认复位状态为高阻态上电瞬间可能产生毛刺触发EC800Z异常复位UART2挂载在APB1总线其IO口复位后默认为模拟输入模式更稳定。第二UART2的TX/RX引脚PA2/PA3与ADC1_IN2/ADC1_IN3复用在本项目中被刻意避开ADC功能避免模拟噪声耦合进串口信号——实测在未加磁珠滤波时ADC采样温湿度值波动±3%而切换至UART2后波动收敛至±0.5%。第三硬件流控未启用RTS/CTS悬空但软件层实现了基于HAL_UART_Receive_IT()的环形缓冲区1KB配合__HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE)检测空闲线中断确保长AT响应如ATQICSGP?返回多行APN信息不丢字节。// core/uart2_driver.c 关键初始化片段 void MX_USART2_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 115200; // EC800Z出厂默认波特率不可改 huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; // 明确禁用硬件流控 huart2.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart2) ! HAL_OK) { Error_Handler(); // 此处必须硬复位不能仅LED闪烁 } }提示EC800Z的ATIPR115200指令虽可修改波特率但修改后需ATCFUN1,1整机重启才生效。本项目固件烧录前已预置115200避免启动时因波特率不匹配导致AT交互失败——这是90%初学者翻车的第一步。2.2 通信协议栈分层为什么不用LwIP而手写PPPMQTT本项目放弃移植LwIP协议栈选择裸机实现PPP帧解析 轻量级MQTT-C客户端核心考量是资源与确定性STM32F103C8T6仅有20KB SRAMLwIP最小配置需占用8KB以上RAM且其TCP重传定时器依赖SysTick与本项目1ms精度的DHT22时序采集冲突EC800Z内置PPP协议栈STM32只需发送ATQIDNS1,mqtt.example.com获取IP再通过ATQIACT激活PDP上下文后续所有IP层数据均由模块内部处理MQTT协议本身足够轻量项目采用开源库MQTT-Cv1.1.0但对其做了三项关键裁剪移除TLS支持EC800Z不支持TLS硬件加速软件实现会吃光CPU将mqtt_pal_sendall()重定向至HAL_UART_Transmit()并增加发送完成回调标志位修改mqtt_timer_get_ms()为读取HAL_GetTick()避免引入额外定时器外设。整个数据流向为DHT22传感器 → STM32 ADCGPIO bit-bang → JSON字符串 → MQTT Publish Payload → mqtt_pal_sendall() → UART2 → EC800Z PPP引擎 → 4G基站 → MQTT Broker2.3 EC800Z模块状态机从开机到MQTT CONNECT的12个关键AT指令节点EC800Z不是即插即用的“傻瓜模块”其入网过程是严格的状态跃迁。本项目定义了typedef enum { EC_STATE_POWER_OFF, EC_STATE_POWER_ON, EC_STATE_WAIT_READY, EC_STATE_CHECK_SIM, ... EC_STATE_MQTT_CONNECTED } ec_state_t;共12个状态并为每个状态设置超时阈值单位ms状态触发AT指令典型响应超时阈值失败后动作EC_STATE_WAIT_READYATOK5000硬复位EC800Z拉低PWRKEY 1sEC_STATE_CHECK_SIMATCPIN?CPIN: READY10000重试3次后报SIM_ERROREC_STATE_ATTACH_GPRSATCGATT1OK30000切换至ATCGATT0再重试EC_STATE_ACTIVATE_PDPATQIACT1QIACT: 1,10.10.10.10,...60000检查IP字段是否为非0.0.0.0注意ATQIACT1返回的IP地址必须非0.0.0.0否则说明PDP激活失败。常见原因为APN配置错误如将cmnet误配为cmiot或SIM卡欠费。本项目在ec_state_machine.c中强制校验该字段避免后续MQTT连接因无IP而无限等待。3. 核心模块代码逐行解析AT指令状态机、MQTT心跳与断线重连3.1 AT指令状态机带超时重传与响应模式匹配的可靠通信EC800Z的AT响应存在不确定性同一指令可能返回OK、ERROR、CME ERROR: x或超时无响应。本项目不使用HAL_UART_Receive()轮询而是构建了事件驱动型AT状态机关键结构体如下// drivers/ec800z/at_parser.h typedef struct { char cmd[64]; // 待发送指令如 ATCGATT1 char expect[32]; // 期望成功响应关键词如 OK char fail_keywords[3][16]; // 失败关键词数组如 {ERROR, CME ERROR, CMS ERROR} uint32_t timeout_ms; // 单次等待超时 uint8_t retry_count; // 当前重试次数 uint8_t max_retry; // 最大重试次数默认3 uint8_t is_busy; // 是否正在处理该指令 } at_cmd_t; extern at_cmd_t g_at_cmd_queue[8]; // 全局指令队列大小为8状态机主循环在ec800z_task.c中实现// 伪代码逻辑实际为HAL_TIM_PeriodElapsedCallback()中调用 void ec800z_state_machine_tick(void) { static uint32_t last_send_time 0; static uint32_t last_recv_time 0; // 1. 检查是否有待发送指令且串口空闲 if (g_at_cmd_queue[0].is_busy 0 uart2_tx_idle()) { send_at_command(g_at_cmd_queue[0]); last_send_time HAL_GetTick(); g_at_cmd_queue[0].is_busy 1; } // 2. 检查响应超时发送后未收到任何字符 if (g_at_cmd_queue[0].is_busy (HAL_GetTick() - last_send_time) g_at_cmd_queue[0].timeout_ms) { handle_at_timeout(g_at_cmd_queue[0]); return; } // 3. 解析接收缓冲区由IDLE中断填充 if (uart2_rx_buffer_has_data()) { parse_at_response(uart2_rx_buffer); } }parse_at_response()函数核心逻辑是模式匹配而非字符串相等// drivers/ec800z/at_parser.c static void parse_at_response(const char* buf) { // 匹配成功查找expect关键词支持子串 if (strstr(buf, g_at_cmd_queue[0].expect) ! NULL) { g_at_cmd_queue[0].is_busy 0; on_at_command_success(g_at_cmd_queue[0]); return; } // 匹配失败遍历fail_keywords for (int i 0; i 3; i) { if (strlen(g_at_cmd_queue[0].fail_keywords[i]) 0) break; if (strstr(buf, g_at_cmd_queue[0].fail_keywords[i]) ! NULL) { g_at_cmd_queue[0].is_busy 0; on_at_command_fail(g_at_cmd_queue[0], i); return; } } // 未匹配到任何关键词视为无效响应继续等待 }这种设计解决了真实工程中的痛点EC800Z在信号弱时可能返回QIACT: 1,0.0.0.0,...IP为空却仍带OK若只匹配OK则误判成功。本项目要求ATQIACT必须同时匹配OK和非0.0.0.0的IP字段否则进入失败分支。3.2 MQTT连接与心跳手动拼装CONNECT报文与PINGREQ/PINGRESP校验本项目未使用MQTT-C的mqtt_connect()高层API而是手动构造MQTT CONNECT报文原因在于需精确控制Client ID固定为STM32_芯片UID保证全局唯一需设置Clean Session 1避免Broker堆积离线消息需设置Keep Alive 60秒与EC800Z的ATQIMUX1多路复用超时对齐。// app/mqtt_client.c uint8_t mqtt_connect_packet[256]; uint16_t connect_len 0; void build_mqtt_connect_packet(void) { uint8_t* p mqtt_connect_packet; // 固定头0x10 *p 0x10; // 可变头长度计算后填入 uint8_t remaining_length 0; uint8_t* len_ptr p; p; // 预留长度字节 // Protocol Name: MQTT (4字节) 协议级别 (0x04) memcpy(p, \x00\x04MQTT\x04, 7); p 7; remaining_length 7; // Connect Flags: 用户名密码标志0, Clean Session1, Will Flag0, Will QoS0, Will Retain0 *p 0x02; // 二进制 00000010 remaining_length; // Keep Alive: 60秒 (0x003C) *p 0x00; *p 0x3C; remaining_length 2; // Client ID: STM32_ UID (12字节) memcpy(p, STM32_, 6); p 6; get_chip_uid(p); // 读取STM32唯一ID到p p 12; remaining_length 18; // 612 // 写入剩余长度字段MQTT标准变长编码 uint8_t encoded_len[4]; uint8_t len_bytes mqtt_encode_remaining_length(remaining_length, encoded_len); memcpy(len_ptr, encoded_len, len_bytes); connect_len 1 len_bytes remaining_length; }心跳机制采用双保险MQTT层每30秒发送PINGREQ收到PINGRESP后清零心跳失败计数底层AT层每10秒发送AT指令探测EC800Z是否存活避免模块假死但MQTT连接未断开。// app/mqtt_client.c static uint8_t ping_fail_count 0; #define MAX_PING_FAIL 3 void mqtt_heartbeat_task(void) { static uint32_t last_ping_time 0; if (HAL_GetTick() - last_ping_time 30000) { last_ping_time HAL_GetTick(); // 发送PINGREQ uint8_t pingreq[2] {0xC0, 0x00}; mqtt_pal_sendall(pingreq, 2); // 启动PINGRESP超时检测在MQTT接收回调中处理 ping_fail_count; if (ping_fail_count MAX_PING_FAIL) { mqtt_disconnect(); // 主动断开 ec800z_set_state(EC_STATE_MQTT_DISCONNECTED); // 触发重连流程 } } }3.3 温湿度数据采集与JSON封装DHT22时序精准控制与内存安全DHT22为单总线器件对时序要求苛刻μs级。本项目完全摒弃HAL_Delay()采用__NOP()__DSB()指令级延时并用HAL_GPIO_WritePin()替代HAL_GPIO_TogglePin()避免电平抖动// drivers/sensors/dht22.c #define DHT22_DATA_PIN GPIO_PIN_12 #define DHT22_PORT GPIOB static void dht22_delay_us(uint16_t us) { uint16_t count us * 7; // 基于72MHz主频粗略估算 while (count--) __NOP(); } uint8_t dht22_read_data(uint16_t* humidity, uint16_t* temperature) { // 1. 主机拉低80us HAL_GPIO_WritePin(DHT22_PORT, DHT22_DATA_PIN, GPIO_PIN_RESET); dht22_delay_us(80); // 2. 主机释放延时40us HAL_GPIO_WritePin(DHT22_PORT, DHT22_DATA_PIN, GPIO_PIN_SET); dht22_delay_us(40); // 3. 等待DHT22响应80us低80us高 if (!wait_for_low_edge(100)) return DHT22_TIMEOUT; if (!wait_for_high_edge(100)) return DHT22_TIMEOUT; // ... 后续40bit数据读取省略细节重点在时序精度 }JSON封装使用cJSON库但做了关键加固所有cJSON_CreateObject()后立即检查返回值是否为NULL内存不足时malloc失败cJSON_PrintUnformatted()结果存入静态缓冲区256字节避免动态分配在app_main.c中添加内存泄漏检测钩子// app/app_main.c static uint32_t heap_used_max 0; void *my_malloc(size_t size) { void *ptr malloc(size); if (ptr) { uint32_t used (uint8_t*)sbrk(0) - _sheap; if (used heap_used_max) heap_used_max used; } return ptr; }4. 编译、烧录与调试全流程Keil MDK工程结构与关键配置项4.1 Keil MDK工程目录结构与文件职责划分本项目Keil工程.uvprojx严格遵循嵌入式开发规范目录结构清晰反映模块边界STM32_EC800Z_MQTT/ ├── Core/ // CMSIS核心与HAL库 │ ├── Inc/ │ │ ├── main.h │ │ └── stm32f1xx_hal_conf.h // 关键使能HAL_UART_MODULE_ENABLED │ └── Src/ │ ├── main.c // 主循环ec800z_state_machine_tick() │ └── stm32f1xx_hal_msp.c // UART2/ADC/GPIO MSP初始化 ├── Drivers/ │ ├── EC800Z/ // EC800Z专用驱动 │ │ ├── at_parser.c/h // AT状态机核心 │ │ └── ec800z_task.c/h // 状态机调度与超时管理 │ ├── Sensors/ // 传感器驱动 │ │ └── dht22.c/h // DHT22精准时序 │ └── MQTT/ // MQTT-C裁剪版 │ └── mqtt_client.c/h // CONNECT/PUBLISH/PINGREQ手动实现 ├── App/ │ ├── mqtt_publish.c/h // 数据封装与定时上报逻辑 │ └── config_flash.c/h // Flash参数存储APN/Server/Topic ├── Middlewares/ │ └── Third_Party/ │ ├── cJSON/ // JSON序列化 │ └── MQTT-C/ // 裁剪后的MQTT-C v1.1.0 └── User/ └── user_config.h // 所有可配置宏在此集中定义提示user_config.h是二次开发的入口文件所有业务参数如#define MQTT_SERVER_IP 118.24.12.34、#define MQTT_TOPIC sensor/temp_humi均在此定义修改后无需动核心代码。4.2 Keil关键编译配置优化等级、堆栈与链接脚本本项目在Keil中配置如下关键项直接影响稳定性配置项值说明OptimizationLevel 3 (-O3)启用内联与循环展开但关闭-fno-tree-loop-distribute-patterns避免DHT22时序错乱C/C → DefineUSE_FULL_LL_DRIVER,HAL_MODULE_ENABLED必须定义否则HAL_UART无法工作Target → FlashSTM32F103C8Tx_FLASH.ld自定义链接脚本将config_flash段映射到0x08007800最后1KB扇区C/C → Misc Controls--cpp11 --gnu启用C11特性cJSON部分代码需要Linker → Use Memory Layout from Target Dialog✅确保使用Target页设置的Flash/RAM范围STM32F103C8Tx_FLASH.ld关键片段/* 定义Config Flash段用于存储APN等参数 */ .config_flash 0x08007800 : { *(.config_flash) . ALIGN(4); } FLASH在config_flash.c中声明该段// App/config_flash.c __attribute__((section(.config_flash))) const uint8_t g_apn_config[] cmnet; __attribute__((section(.config_flash))) const uint8_t g_mqtt_server[] 118.24.12.34;4.3 烧录与调试ST-Link V2接线与J-Link RTT日志抓取烧录使用ST-Link V2接线必须严格对应ST-Link PinSTM32 Pin说明SWDIOPA13调试数据线SWCLKPA14调试时钟线GNDGND公共地必须接3.3V3.3V仅用于给ST-Link供电不可给STM32供电EC800Z功耗大会拉垮调试阶段强烈推荐使用J-Link RTTReal Time Transfer替代串口打印原因RTT使用SWD接口的SWO引脚不占用UART2留给EC800Z支持printf重定向且无波特率限制日志不丢包J-Link Commander中执行exec SetRTTAddr addr可动态指定RTT控制块地址。// Core/Src/main.c 中重定向printf #include SEGGER_RTT.h int fputc(int ch, FILE *f) { SEGGER_RTT_PutChar(0, ch); return ch; }注意RTT控制块必须放在RAM中本项目定义在0x20000000起始的SRAM首地址Keil中需在Target页勾选Use Memory Layout from Target Dialog并确认RAM起始地址为0x20000000。5. 常见问题排查从“AT指令无响应”到“MQTT频繁断连”的血泪经验5.1 现象上电后EC800Z无任何AT响应UART2接收缓冲区始终为空原因EC800Z的PWRKEY引脚未正确触发。EC800Z要求PWRKEY被拉低至少1秒再释放才能开机而许多开发者误用HAL_GPIO_WritePin(PWRKEY_GPIO_Port, PWRKEY_Pin, GPIO_PIN_SET)直接置高导致模块处于深度休眠。解决在ec800z_power_on()函数中必须使用HAL_GPIO_WritePin()先拉低HAL_Delay(1000)后再拉高并加入电压检测——用万用表测EC800Z的VCC引脚开机瞬间应有明显电流尖峰500mA。5.2 现象ATCGATT1返回OK但ATQIACT1超时或返回QIACT: 1,0.0.0.0原因APN配置错误或SIM卡未认证。EC800Z对APN区分大小写且某些运营商要求APN后缀如cmnetvscmnet.。解决用串口助手单独连接EC800Z依次发送ATCGDCONT?→ 查看当前PDP上下文配置ATQICSGP1,cmnet,,,1→ 强制设置APN注意第四个参数为空字符串ATQIACT1→ 重新激活若仍失败更换SIM卡测试排除欠费或实名制未完成。5.3 现象MQTT连接成功但10分钟后自动断开日志显示PINGRESP timeout原因EC800Z的ATQIMUX多路复用模式与MQTT心跳周期不匹配。EC800Z默认ATQIMUX0单路此时ATQISTATE查询显示STATE: 0,0无连接但MQTT连接实际存在当ATQIMUX1多路时需显式创建socket连接否则PINGREQ数据包无法路由。解决在MQTT连接前必须执行ATQIMUX1→ 启用多路复用ATQIOPENTCP,118.24.12.34,1883→ 创建TCP socketID0然后所有MQTT数据均通过ATQISEND0,len发送。5.4 现象DHT22读数恒为0或-1且dht22_read_data()返回DHT22_TIMEOUT原因GPIO初始化方向错误。DHT22数据线需配置为开漏输出上拉但HAL库默认推挽输出导致主机释放总线后无法被DHT22拉低。解决在MX_GPIO_Init()中修改DHT22引脚配置GPIO_InitStruct.Pin GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 关键改为开漏输出 GPIO_InitStruct.Pull GPIO_PULLUP; // 必须上拉 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct);5.5 现象烧录后程序不运行ST-Link识别到设备但无法halt原因Flash选项字节被意外擦除。STM32F103的RDPRead Out Protection级别若为Level 1会导致调试器无法访问Flash表现为“Device ID: 0x00000000”。解决使用ST-Link Utility执行Target → Option Bytes → Read Out Protection → Disable然后Erase Chip最后重新烧录。此操作会清除所有Flash内容包括用户程序。6. 二次开发实战如何快速接入私有MQTT Broker与自定义传感器6.1 接入私有MQTT Broker四步替换无需改一行核心代码接入企业内网MQTT Broker如EMQX、Mosquitto只需修改User/user_config.h中4个宏定义全程5分钟// User/user_config.h // Step 1: 修改服务器地址支持域名或IP #define MQTT_SERVER_IP 192.168.10.100 // 内网IP // #define MQTT_SERVER_IP mqtt.internal.corp // 或域名需EC800Z DNS支持 // Step 2: 修改端口默认1883TLS用8883 #define MQTT_SERVER_PORT 1883 // Step 3: 修改Topic支持通配符 #define MQTT_TOPIC factory/line1/sensor/%s // %s会被替换为设备UID // Step 4: 如需认证开启用户名密码默认关闭 #define MQTT_AUTH_ENABLE 1 #define MQTT_USERNAME device_user #define MQTT_PASSWORD secure_pass_123注意若Broker启用TLS需额外步骤——EC800Z不支持TLS必须在Broker前端部署反向代理如Nginx将TLS流量解密为明文TCP转发给EC800Z否则无法连接。6.2 扩展传感器接入DS18B20单总线与BH1750I2C的接口规范本项目预留了传感器扩展接口新增传感器需遵循统一抽象层// Drivers/Sensors/sensor_interface.h typedef struct { char name[16]; // 传感器名称如 DS18B20 uint8_t type; // 类型枚举SENSOR_TYPE_TEMP, SENSOR_TYPE_LIGHT uint8_t (*init)(void); // 初始化函数指针 uint8_t (*read)(float* value); // 读取函数指针value为浮点结果 uint8_t (*get_unit)(char* unit); // 单位字符串如 °C } sensor_driver_t; extern const sensor_driver_t g_sensor_drivers[]; extern const uint8_t g_sensor_count;以DS18B20为例实现ds18b20_driver.c// Drivers/Sensors/ds18b20_driver.c static uint8_t ds18b20_init(void) { // 1. 初始化单总线复用DHT22引脚但需重配置为开漏 // 2. 发送Skip ROM指令 // 3. 发送Convert T指令启动转换 return DS18B20_OK; } static uint8_t ds18b20_read(float* temp) { // 1. 发送Skip ROM // 2. 发送Read Scratchpad // 3. 解析12位温度值含符号位 *temp (float)raw_value * 0.0625f; return DS18B20_OK; } const sensor_driver_t ds18b20_driver { .name DS18B20, .type SENSOR_TYPE_TEMP, .init ds18b20_init, .read ds18b20_read, .get_unit ds18b20_get_unit }; // 在g_sensor_drivers数组中注册 const sensor_driver_t g_sensor_drivers[] { dht22_driver, ds18b20_driver, // 新增这一行 bh1750_driver, }; const uint8_t g_sensor_count sizeof(g_sensor_drivers)/sizeof(sensor_driver_t);6.3 数据格式升级从JSON到CBOR的体积压缩实践原始JSON报文含设备UID、温度、湿度、时间戳约180字节对4G流量敏感场景不友好。本项目已集成tinycbor库可一键切换// App/mqtt_publish.c #ifdef USE_CBOR_ENCODING #include tinycbor/cbor.h uint8_t cbor_buffer[128]; size_t cbor_len; void encode_to_cbor(float temp, float humi) { CborEncoder encoder, map; cbor_encoder_init(encoder, cbor_buffer, sizeof(cbor_buffer), 0); cbor_encoder_create_map(encoder, map, 4); // 4个键值对 cbor_encode_text_stringz(map, uid); cbor_encode_text_stringz(map, STM32_ABC123); // UID cbor_encode_text_stringz(map, t); cbor_encode_float(map, temp); cbor_encode_text_stringz(map, h); cbor_encode_float(map, humi); cbor_encode_text_stringz(map, ts); cbor_encode_uint(map, HAL_GetTick()/1000); // Unix时间戳 cbor_encoder_close_container_checked(encoder, map); cbor_len cbor_encoder_get_buffer_size(encoder, cbor_buffer); } #endif在user_config.h中定义#define USE_CBOR_ENCODING 1即可启用实测CBOR报文体积降至JSON的42%76字节年流量节省超1.2MB。从那以后我每次做4G终端开发都强制走一遍EC800Z的ATQISTATE状态查询再看一眼ATQIACT?返回的IP最后用Wireshark抓包验证MQTT CONNECT报文——这三步比任何日志都管用。因为EC800Z的“OK”不是终点而是真正入网的起点。希望帮到你。本文还有配套的精品资源点击获取