STM32F103+EC800-4G+MQTT上云OneNET:从硬件到报文全链路解析 简介这是一份基于STM32F103单片机驱动EC800-4G模块通过MQTT协议上传温湿度数据至OneNET平台的完整例程适合正在做嵌入式物联网项目或课程设计的开发者参考。代码采用Keil标准库编写当前工程基于STM32F103若使用同系列其他型号只需调整芯片型号与Flash容量即可复用降低了二次开发门槛。压缩包共234个文件以C源文件、H头文件、编译过程文件O/CRF及KEIL工程配置uvprojx/uvoptx为主同时包含编译生成的HEX/AXF烧录文件与BMP接线示意图整体约6.97MB。目前已有291人学习下载工程可直接参考使用。代码内对单片机与模块的接线做了明确注释并附有清除编译残留的批处理脚本便于快速构建工程同时提供了MQTT连接、在线状态判断等关键环节的实现思路可在此基础上接入其他传感器适合作为物联网项目的基础框架。1. 这套STM32F103EC800-4GMQTT上云工程到底解决什么问题做过设备上云的人都知道把单片机的数据送到云平台最卡壳的不是采集而是“通信链路怎么打通”。很多套件给的例程要么是WiFi方案的要么只给你一段裸代码拿到手里根本不知道哪根线接哪。这份资源给的是STM32F103 EC800-4G MQTT ONENET 的完整工程从串口AT指令拨号到TCP建链再到MQTT报文组帧、温湿度数据上送一条链路全部打通。它适合正在做农业大棚、机房环境监控、设备远程监测的开发者尤其是需要用4G模块做点对点远程传输脱离WiFi依赖的场子。EC800-4G是国产Cat.1模组不需要自己写协议栈通过串口AT指令就能完成网络注册和TCP连接真正麻烦的是参数对不对、时序稳不稳。这份资源把这两块都做了。2. 硬件与工程搭建最小系统、5V转3.3V与St-Link调试三道关很多人在Keil工程里一编译就想去点Download结果下载完板子没反应回头查硬件一堆坑。这里把拿到资源后的硬件准备和调试链路梳理一遍。2.1 stm32f103最小系统跑起来的最低物料清单STM32F103C8T6是这份工程的默认目标也是市面上最常见、最便宜的F103芯片。最小系统的搭建并不复杂但每一条引脚处理都有讲究缺一个元件轻则下载不了重则程序跑飞。元件参数/接法作用8MHz无源晶振并联2个20pF负载电容连接OSC_IN/OSC_OUT提供HSE时钟源BOOT0下拉10kΩ电阻到GND确保从主Flash启动NRST上拉10kΩ并联100nF到GND防止复位引脚悬空误触发VDD每个VDD脚加100nF去耦电容电源入口加10uF钽电容保证MCU供电稳定VDDA串联10Ω后接3.3V隔离模拟域噪声别看元件少BOOT0如果不拉低程序下进去完全跑不起来这是新手第一个玄学问题。晶振的两个负载电容推荐用20pF位置尽量靠近OSC_IN和OSC_OUT走线不要太长否则起振不稳会导致定时器时间漂移。NRST的上拉电阻一定要接STM32F103的复位脚中断触发阈值比较灵敏悬空时静电干扰就可能造成复位。2.2 stm32f103 5V转3.3V电路LDO选型与共地血泪经验系统供电如果从USB的5V进来需要转成3.3V给MCU和模块用。常见做法是用AMS1117-3.3价格便宜、电路简单输入输出端各加10uF电容就能稳定工作。注意AMS1117在高压差、大电流下发热明显如果板子功耗不大MCUDHT11几个LED完全够用如果还要给EC800-4G模块供电就不要从这颗LDO上取电模块的峰值电流接近2A1117根本扛不住电压一被拉低模块就会不断重启表现为“串口完全收不到AT返回”。EC800-4G的VBAT供电必须单独走电源最稳妥的方式是直接接5V输入或外接稳压源在模块VBAT引脚附近加470uF电解电容和100nF陶瓷电容。这里有一条共地血泪经验MCU的GND和模块的GND必须连在一起否则串口通信就是乱码或者完全没反应。很多人给模块单独供电之后觉得电源“隔离”了挺好实际上数字信号的参考电压就是GND地不共逻辑电平就对不上这是排查串口问题时的第一优先级。2.3 串口打印与定时器3调试的两只眼睛这份工程里串口1用于printf调试输出串口2接EC800-4G模块串口分配是标准的PA9/PA10和PA2/PA3不用改硬件就能直接跑。先把串口1的调试输出打通后面所有AT交互和MQTT报文才能看得见。// usart1_printf.c 重定向fputc到串口1 int fputc(int ch, FILE *f) { while ((USART1-SR USART_SR_TXE) 0); // 等待发送寄存器空 USART1-DR (uint8_t)ch; return ch; } // 初始化串口1和串口2均为115200 8N1 void Debug_UART_Init(void) { USART_InitTypeDef usart; GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA9 TX设置为复用推挽输出 gpio.GPIO_Pin GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // PA10 RX设置为浮空输入 gpio.GPIO_Pin GPIO_Pin_10; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); // 串口1参数115200 8N1 usart.USART_BaudRate 115200; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_Mode USART_Mode_TX | USART_Mode_RX; USART_Init(USART1, usart); USART_Cmd(USART1, ENABLE); }重定向fputc之后随便在代码里写printf(temp: %.1f\r\n, temp);调试信息就会从串口1出来。串口2的初始化和串口1基本一样只是引脚换到PA2/PA3注意RX引脚要配置成浮空输入TX配置成复用推挽输出。定时器3在工程里承担了采集时基我一般会在CubeMX里把TIM3配成1Hz更新中断DHT11每秒钟采样一次LED翻转指示系统运行状态。这样看程序是否卡死瞄一眼LED就行比看电池快。2.4 Keil MDK工程搭建与St-Link调试全流程拿到资源里的keil工程第一件事是确认自己的MDK版本和芯片Pack是否匹配。STM32F1系列的Device Family Pack需要单独安装MDK5装好默认不带旧芯片的器件支持必须手动安装ST官方提供的Keil.STM32F1xx_DFP.pack选择Pack版本时可以优先选当前MDK版本推荐的版本太新或太旧都不影响编译但影响器件选型和调试器兼容性。St-Link调试器的接线只有四根线SWDIO、SWCLK、GND、3.3V不需要接复位脚。在Keil里打开Options for TargetDebug页选择ST-Link Debugger然后点Settings把Port选成SWMax Clock建议降到4MHz兼容性最好。Flash Download页勾选Reset and Run意思就是烧录完成后自动复位运行不然每次下载完还得手动按复位键很影响调试节奏。工程编译链接后点Download如果提示No Target Connected先检查St-Link的USB线是不是只有电源线没有数据线这种线最坑再排查SWDIO和SWCLK两个引脚是否焊接虚连。如果连上了但下载后程序不跑就回到BOOT0检查跳帽或电阻拉低后重新复位一次。串口1能看到printf输出就说明STM32F103的最小系统和调试链路全部打通了下一步才能让EC800-4G正常工作。3. EC800-4G拨号SIM卡检测、网络注册与TCP链路建立EC800-4G模块上电后默认是自动入网的但你不知道它到底有没有注册上网络也不知道APN配没配对。这一章按“检测SIM卡—查信号—注册网络—激活PDP—建TCP”的顺序把整个拨号过程拆成可验证的步骤。3.1 为什么选EC800-4G而不是ESP8266或W5500资源选EC800-4G这块Cat.1模组是有原因的。ESP8266方案成本最低但依赖WiFi路由现场没有无线网络就全盘抓瞎W5500是纯有线以太网方案稳定性好但需要布网线设备一旦移动就要改接线。EC800-4G属于Cat.1模块插上SIM卡就能通过运营商的公网接入物联网信号覆盖广移动场景也能用数据走运营商网络也比WiFi更适合工业环境。选它做这个工程的通信核心是兼顾成本、稳定性和接入便捷度的结果。3.2 EC800-4G上电与AT指令通道先确认模块还活着模块接线确认无误后通电注意不要直接上电立刻发AT。EC800系列模块开机需要2到3秒的初始化时间刚上电就发指令大概率得不到响应。正确做法是等2.5秒后发送AT命令收到OK就说明串口链路和模块启动都正常。// ec800_at_send.c 向模块发送AT指令并等待响应 uint8_t EC800_SendAT(char *cmd, char *expect, uint16_t timeout_ms) { uint8_t buf[256] {0}; uint16_t len 0, timer 0; USART2_ClearRxBuffer(); // 清空串口2接收缓冲区 USART2_SendString(cmd); // 发送AT指令记得带\r\n while (timer timeout_ms) { if (USART2_RxBuffer_Available()) { buf[len] USART2_GetChar(); if (strstr(buf, expect) ! NULL) // 找到期望的关键字 return 1; } delay_ms(1); timer; } return 0; // 超时未收到期望响应 } // 上电后依次检测模块状态 void EC800_PowerOn_Check(void) { delay_ms(2500); // 模块上电自检 if (EC800_SendAT(AT\r\n, OK, 2000)) printf(EC800 module ready\r\n); else printf(EC800 no response\r\n); // 检查供电和波特率 }这里最关键的一点是USART2_RxBuffer_Available的判断。串口接收要用中断或DMA方式持续往缓冲区放数据不能在一个while循环里用阻塞等待串口接收标志因为AT响应没有固定长度阻塞式接收很容易丢字节。检查expect用strstr函数匹配子串而不是strcmp比较整个字符串原因是AT指令的响应后面带\r\nCOM开头等前缀不同子串匹配更可靠。timeout_ms一般给2000到3000毫秒AT指令本身响应很快慢的是网络相关指令比如ATCGACT可能要跑到5秒要单独加大超时。3.3 网络注册与SIM卡状态检查模块活着之后要做三件事插卡是否识别、信号强度是否达标、网络是否注册成功。ATCPIN? 排查SIM卡状态返回READY表示SIM卡识别正常返回ERROR或NO SIM就要检查卡有没有插好、触点是不是氧化了ATCSQ 查信号强度返回格式是CSQ: 20,0第一个数字0到31大于10基本可用小于5基本处于断网边缘ATCEREG? 查网络注册状态返回CEREG: 0,1表示已注册到网络如果返回0,2表示正在搜索网络0,3表示注册被拒绝这三个指令在EC800_SendAT里分别跑一遍把结果printf出来。信号强度如果稳定在10以上一般就能继续往下走。很多人在这卡很久原因是SIM卡用的是普通手机卡插到模组里能识别但模组没有正确激活数据业务。没关系下一步把APN设置好就能解决。网络状态检查里有个常见翻车场景MCU复位后程序里立刻执行ATCPIN?这时候EC800模块因为供电重新上电还在自检指令根本发不进去。解决方法是程序启动时先做一次“模块握手”循环发AT超时后再发重试三次每次间隔2秒直到返回OK再继续。这套重试逻辑在后续所有AT流程里都适用尤其是网络相关指令一定不能只发一次。3.4 APN设置与PDP激活移动、联通、电信物联网卡的分水岭运营商对不同SIM卡分配的APN不一样很多情况下模块默认的APN并不正确导致网络显示已注册但PDP无法激活。这时需要手动设置APN。中国移动的设备卡通常用cmiot电信设备卡是ctnet联通设备卡一般是uninet。注意网上有一些很老的教程让你设置cmnet那是手机卡公网APN用在设备卡上反而激活失败。// ec800_net.c 设置APN并激活PDP上下文 // 移动物联网卡示例 USART2_SendString(ATCGDCONT1,\IP\,\cmiot\\r\n); // 返回OK后查询确认 USART2_SendString(ATCGDCONT?\r\n); // 激活PDP上下文 USART2_SendString(ATQIACT1\r\n); // 查询PDP激活状态返回QIACT: 1,1表示激活成功 USART2_SendString(ATQIACT?\r\n);激活PDP之后模块才真正拿到了内网IP地址这个状态下才能走TCP或MQTT连接。判断激活是否生效看ATQIACT?的返回值第二个参数是0说明激活失败是1说明激活成功。激活失败时先查CSQ信号是否够强再确认APN有没有写错。还有一个很多人忽略的点SIM卡对应的流量套餐必须开通物联网卡的数据服务有些卡刚发货时后台默认是“未激活”状态卡虽然识别但数据业务完全不可用逻辑上就不是代码能解决的问题只能去运营商后台开卡。3.5 建立TCP连接到ONENETATQIOPEN的参数踩坑PDP激活成功后就能用EC800的ATQIOPEN指令建立TCP连接。ONENET平台的明文MQTT接入地址是183.230.40.96端口6002注意这里的端口不是88838883是证书加密端口裸TCP连过去会失败或一直超时。// 建立TCP连接到ONENET MQTT明文端口 char cmd[100]; sprintf(cmd, ATQIOPEN1,0,\TCP\,\183.230.40.96\,6002,0,0\r\n); USART2_SendString(cmd); // 等待异步返回结果成功会收到: CONNECT OK // 失败会收到: QIOPEN: 0,ERROR 或超时ATQIOPEN执行后模块会异步返回结果不像AT指令那样直接回OK。连接成功会返回CONNECT OK失败返回QIOPEN: 0,ERROR。这一步如果失败优先检查PDP是否还处于激活状态模块空闲时会自动释放PDP建议在建立TCP前重新执行ATQIACT1。另外ATQIOPEN里的6个参数分别代表连接索引、socket类型、协议、目标IP、端口、访问模式、保活标志照抄即可不需要研究每个参数但连接索引从1开始后面MQTT过程中的所有发送、关闭操作都要指向这个索引号。TCP链路打通之后串口2收到的数据就是从ONENET平台返回的MQTT协议报文了下一步才是MQTT层的接入和工作。4. MQTT上云ONENET的避坑记录鉴权参数、报文组帧与心跳这一整章是踩坑重灾区。TCP通了不代表MQTT能上云ONENET平台对MQTT的接入参数、Topic格式和报文格式都有自己的一套要求任何一个字符不对平台只会给你一个无情的关闭连接。4.1 ONENET产品与设备创建先把三个值记到手在ONENET平台创建一个产品协议类型选择MQTT创建完成后能得到产品ID。接着在产品下添加设备设备名称是设备的唯一标识自定义一个比如stm32_dev01。设备添加后平台会生成一个鉴权信息这个值需要自己在添加设备时填建议填一串私有的字符串不要跟设备名重复。这三个值分别对应MQTT报文里的clientID、username和password。ONENET平台要求这三个参数在CONNECT报文里的位置非常固定clientID填产品ID下划线设备名格式是“产品ID_设备名”username就是产品IDpassword就是设备的鉴权信息。很多人拿着平台生成的APIKey往password里填结果全部连接失败因为APIKey是平台对API接口的访问秘钥跟MQTT设备鉴权完全是两回事。这三个值必须在代码里填到mqtt_client.c的头文件配置区工程里这份资源已经留出了对应位置。平台参数MQTT报文位置示例值产品IDclientID和username481xxxxx设备名clientID的一部分stm32_dev01组合后481xxxxx_stm32_dev01鉴权信息passwordabc123xyz自定义字符串设备创建后不需要记APIKey只需要记住产品ID、设备名、鉴权信息这三个字段。如果设备接入时一直报“鉴权失败”去平台对比一下设备名称的大小写MQTT的clientID区分大小写平台生成的设备名是什么代码里就必须原样填什么。4.2 MQTT报文黑匣子CONNECT、PUBLISH、SUBSCRIBE结构MQTT协议报文说穿了就是固定头加上可变头和负载。固定头的第一个字节是报文类型和标志位第二个字节是剩余长度。剩余长度小于128时直接用一个字节表示对CONNECT报文来说剩余长度等于可变头和负载的总字节数。这就绕开了“MQTT黑匣子”的玄学焦虑。// mqtt_pkt.c 构造MQTT CONNECT报文 uint16_t mqtt_build_connect(uint8_t *buf, char *client_id, char *username, char *password) { uint16_t i 0; uint16_t remaining_len; // 可变头协议名 MQTT级别4标志位C2心跳60秒 uint8_t vheader[] {0x00, 0x04, M, Q, T, T, 0x04, 0xC2, 0x00, 0x3C}; uint16_t client_len strlen(client_id); uint16_t user_len strlen(username); uint16_t pass_len strlen(password); remaining_len 10 2 client_len 2 user_len 2 pass_len; buf[i] 0x10; // CONNECT报文类型 buf[i] (uint8_t)remaining_len; // 剩余长度128直接用1字节 memcpy(buf[i], vheader, 10); i 10; buf[i] 0x00; buf[i] (uint8_t)client_len; memcpy(buf[i], client_id, client_len); i client_len; buf[i] 0x00; buf[i] (uint8_t)user_len; memcpy(buf[i], username, user_len); i user_len; buf[i] 0x00; buf[i] (uint8_t)pass_len; memcpy(buf[i], password, pass_len); i pass_len; return i; // 返回整个报文长度供ATQISEND的len参数使用 }CONNECT报文里的标志位0xC2二进制是1100 0010其中bit7为0表示当前连接不允许继续保留会话bit1为1表示开启Clean Session设备每次上线都不继承历史会话状态。对于低功耗或长连接的数据上报场景Clean Session打开是最省心的。心跳周期放在可变头的最后两个字节单位是秒0x003C即60秒OneNET平台要求的保活时间必须在50到180秒之间太短会频繁断开重连太长又会让平台误判设备离线。PUBLISH报文比CONNECT简单得多固定头用0x30QoS0、不保留剩余长度等于2加Topic长度加负载长度Topic在前负载在后。SUBSCRIBE报文的固定头是0x82剩余长度后面跟报文ID和Topic列表。这三个报文组好之后用EC800的透传模式通过ATQISEND把字节流直接发到TCP服务器上不用考虑分包模块底层会处理TCP传输。4.3 数据上报把DHT11读数塞进PUBLISH帧ONENET的MQTT数据上报Topic路径是$sys/产品ID/设备名/dp/post/json负载是一个JSON格式的键值对。DHT11读到的温湿度是浮点数需要先格式化成字符串再填进报文。// mqtt_publish.c 发布温湿度数据 uint8_t mqtt_publish_temp_humi(uint8_t *buf, char *topic, float temp, float humi) { uint16_t i 0; char payload[64]; int payload_len; // 组装JSON负载{temp:23.5,humi:60.0} payload_len sprintf(payload, {\temp\:%.1f,\humi\:%.1f}, temp, humi); uint16_t topic_len strlen(topic); buf[i] 0x30; // PUBLISH, QoS0 buf[i] (uint8_t)(2 topic_len payload_len); // 剩余长度 buf[i] 0x00; buf[i] (uint8_t)topic_len; memcpy(buf[i], topic, topic_len); i topic_len; memcpy(buf[i], payload, payload_len); i payload_len; // 将buf整体通过EC800_TransparentSend发到TCP连接 EC800_TransparentSend(buf, i); return i; }这里有几个容易翻车的点。第一个是JSON字符串的字段名ONENET平台的数据流名称就是JSON里冒号前面的key如果要展示成平台仪表盘的“温度”“湿度”数据流key必须和平台数据流名称保持一致比如平台数据流叫tempJSON里就写temp不能写temperature。第二个是浮点格式化用%.1f保留一位小数DHT11本身的精度就是整数保留一位小数属于经验上的友好处理平台上显示23.5比23更好看。第三个是Topic路径里的设备名必须和CONNECT报文中的clientID里的设备名完全一致这个错位会导致平台返回设备不存在数据直接丢弃。设备离线重连时平台会发送一个订阅请求要求设备重新订阅命令下行Topic。如果设备端的循环逻辑里没有对重连后重新SUBSCRIBE做处理平台命令就下不来设备只会“在线”而收不到任何指令。这个场景的排查要从平台端看设备上下线日志而不是盯着串口输出。4.4 避坑记录MQTT接入中高频踩坑汇总把开发这类工程遇到过的真实问题整理成一份排查清单按现象、原因、解决的顺序写在一起以后对着这张表查问题能省一半抓头发的时间。现象1CONNECT报文发出去后平台立刻关闭连接设备反复重连。原因大概率是CONNECT报文里的clientID、username、password三项参数中有一个与平台配置不一致尤其容易填错password把APIKey填成了鉴权信息。解决到平台把产品ID、设备名、鉴权信息三个值重新核对一遍直接改代码里的配置区后重新编译下载。如果设备名前带空格或过了小写也要一并修正。现象2TCP已经CONNECT OK但MQTT报文似乎发不出去串口看不到平台响应。原因是EC800的AT指令模式与透明传输模式没有切换或者发送时机不对模块处于AT指令模式时对收到的字节流不做TCP转发。解决确认使用了ATQISEND发送数据发送完成后立即执行ATQISEND0的“数据发送结束”指令模块才能把缓冲区的数据真正推到服务器发送结束指令没发数据一直停在模块缓冲区平台当然什么都收不到。现象3平台设备显示在线但数据流一直没有新数据。原因是PUBLISH报文的Topic路径写错比较常见的是少写了$sys前缀或者设备名写成了产品名。解决每次上线后用平台的API调试工具查看最近数据如果返回设备不存在检查Topic字符串如果返回请求成功但没有数据点检查JSON里的key是否和平台数据流名称一致。现象4设备上报周期变长后平台判定离线但串口上看程序还在跑。原因是MQTT的心跳超时或连接被平台主动清理。设备的保活时间设置超过平台上限或实际TCP连接长时间空闲被NAT踢掉。解决把CONNECT报文里的心跳时间设为平台允许范围内的值一般60秒最为稳妥同时打开EC800模块的TCP保活设置在ATQIOPEN参数里把最后一位设成2表示启用KeepAlive机制让模块自己在TCP层维持心跳避免被NAT设备清理。现象5重连后平台能上线但命令下发收不到。原因是设备重新连接后没有重新订阅命令Topic平台的命令下行依赖设备订阅关系连接断开后订阅关系自动清除。解决在MQTT连接成功的流程后面无条件执行一次SUBSCRIBE指令把命令下行Topic重新订阅一遍。5. 上云后的进阶玩法可视化曲线、指令下发与手动验证习惯数据成功上送ONENET后这套工程才真正具备了产品雏形。ONENET控制台自带可视化能力不需要自己写前端创建应用、拖几个控件数据就能变成图表。进入平台的可视化页面添加一个“曲线图”控件数据流选择temp和humi刷新周期设置成10秒很快就能看到曲线在实时滚动。整个配置过程十五分钟以内就能完成比从零写仪表盘省太多事。指令下发是设备从“上传数据”升级到“远程控制”的关键一步。设备端要订阅$sys/产品ID/设备名/cmd/request/这个Topic平台下发指令时设备RA能收到格式为“cmd/request/指令ID”的响应。收到后解析JSON比如下发一个继电器控制指令设备端的逻辑可以是这样先识别指令ID再提取控制字段执行后提交一份“cmd/response/指令ID”的确认数据。这个机制在工程里已经预留了处理函数可以把串口2收到的指令直接透传给RS485总线接电表、PLC、变频器都通用相当于让STM32F103成为一个4G网关的“下载端”。调试时最值得养成的习惯是“先手动再自动”。每次烧录程序前先用PC的串口调试助手连到EC800模块的串口按顺序手动执行AT检查模块、ATQIACT激活PDP、ATQIOPEN建TCP、手动粘贴组好的CONNECT报文确认平台设备在线后再粘贴PUBLISH报文看数据是否上送。这一遍手动流程跑通再烧单片机程序联调能极大缩短排错时间。手动验证这套流程血泪教训是烧录后串口2没有任何输出时第一时间先去量模块的电源和GND而不是去翻代码。从那以后我每次做4G上云项目都强制自己走一遍“手动拨号—手动MQTT—自动循环”的三段式调试这份资源里的工程文件已经把上面所有流程都包含在了源码里改一下产品ID和设备配置就能跑起来希望帮到你。本文还有配套的精品资源点击获取