拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103+EC200S 4G DTU实战:MQTT接入阿里云与远程LED控制

简介面向单片机开发者的STM32F103EC200S 4G项目例程以MQTT协议将温度数据实时上传至阿里云物联网平台同时接收云端JSON指令控制LED灯适用于物联网课程设计、毕业设计以及4G DTU产品原型验证。项目代码基于KEIL标准库编写兼容STM32F103系列根据芯片型号调整Flash容量后即可复用程序中对单片机与EC200S模块的接线均有清晰定义且关键函数附带注释方便读者理解数据采集、组网上云与指令解析的完整链路。压缩包总计112个文件、1.83MB以C/H源代码和KEIL工程文件为主体另外包含hex固件、PDF说明、BMP调试参考图、文本配置及清理脚本等结构划分直观便于按需查阅和二次开发。目前已有159人学习下载整体难度适中对有一定单片机基础、希望快速搭建4G物联网方案的开发者是性价比较高的参考资源。1. 4G DTU 方案选型STM32F103 与 EC200S 组合为什么还不过时STM32F103 加 EC200S-4G模块本质上是一个价格敏感、需求明确的 4G DTU 方案温度数据通过传感器进单片机再经 EC200S 的 4G 网络用 MQTT 协议推到阿里云物联网平台云端下发的 JSON 格式指令经过同一条链路回到单片机让指定 GPIO 驱动 LED。市面上的 DTU 盒子很多但遇到“上报频率要 5 秒一次”“指令要透传成自定义协议”这类需求盒子里固件往往改不动。自己用 STM32F103 驱动 EC200S既保留协议控制权又能直接操作底层 GPIO。这套方案适合做设备远程监控、农业四情、冷链运输的嵌入式或物联网工程师目标是快速复制链路、填对连接参数、跑通第一次收发并且知道失败时该看哪条日志。2. STM32F103 最小系统与 EC200S 的硬件连接和 AT 拨号硬件连接是第一道坑。EC200S 的 UART 电平不是 3.3V而是 1.8V如果直接把 STM32F103 的 PA9/PA10 接上去刚开始 AT 指令能通一进入 TCP 透传、数据量上来就会随机丢字。原因不在代码而在电平和供电。这一章先把最小系统和串口分配讲清楚再把 AT 拨号链路走通。2.1 最小系统晶振、Boot 和串口1/串口3使用差异用 STM32F103C8T6 做底板时最少需要 8MHz 晶振加两个负载电容、BOOT0 下拉到地、NRST 上拉VDDA/VSSA 做去耦。串口选择上我习惯用 USART1 接 EC200S用 USART3 做调试输出。为什么不是反过来STM32F103 的 USART1 挂在 APB2 上时钟 72MHz分频精度更好USART3 挂在 APB1 上时钟 36MHz115200 波特率没问题但把 EC200S 的 AT 波特率提到 460800 时USART3 的分频误差会明显变大。另外 USART1 的 RX/TX 是 PA10/PA9和 JTAG 部分引脚复用默认状态不冲突USART3 在 PB11/PB10如果这些引脚被 ADC 或 PWM 占用就要做重映射。功能STM32F103 引脚备注EC200S TX - STM32 RXPA10 (USART1_RX)模块输出 1.8V需电平转换EC200S RX - STM32 TXPA9 (USART1_TX)注意电平方向调试串口 TXPB10 (USART3_TX)只输出日志调试串口 RXPB11 (USART3_RX)可接收调试命令LED 控制输出PA1开漏或三极管驱动温度模拟量输入PA0ADC1_IN0这就是“stm32f103 串口1和串口3使用差异”的典型场景同一个波特率设置时钟源、中断优先级、引脚冲突都不一样。业务串口和调试串口分开MQTT 数据流和日志才不会互相干扰。2.2 EC200S 供电、电平转换和 SIM 卡电路EC200S 工作电压 3.4V 到 4.3V推荐 3.8V瞬间发射电流能到 2A。用 AMS1117-3.3 给 MCU 供电可以但 4G 模组的电源必须单独走 DC-DC 或锂电池并且靠近模组 VBAT 放两个 100µF 钽电容和 22µF 陶瓷电容。电平转换用 TXS0102 或 2N7001T如果只用单向通信也可以用三极管搭反相电路但注意 EC200S 的 RX 通常需要 1.8V 上拉。SIM 卡要加 ESD 防护串联 22Ω 电阻并预留 SIM_DET 检测脚这样在代码里能区分“没插卡”和“没信号”。注意EC200S 的串口电平不是 3.3V也不是 5V任何“直接飞线”的做法都会在高速透传时丢包。2.3 用 AT 指令完成网络注册和获取 IP下面是我在调试串口敲的最小组顺序不能乱ATE0 ATCPIN? ATCSQ ATCREG? ATCGDCONT1,IP,cmnet ATCGACT1 ATQIACT1 ATQIACT?参数说明ATE0关闭回显减少串口干扰ATCPIN?返回值是READY才能继续ERROR说明 SIM 卡没识别ATCSQ第一项是信号强度20 以上算健康10 以下长连接基本稳不住ATCREG?返回0,1或1,5才是已注册网络cmnet是中国移动默认 APN联通、电信请改成运营商对应的默认 APN。ATQIACT1激活 PDP 上下文最后一条ATQIACT?会显示模块拿到的 IP 地址。这一步通过后模块已经有了可用数据链路。2.4 建立 TCP 链路到阿里云 MQTT brokerSTM32 不能像 PC 一样直接 socket需要先让 EC200S 建立 TCP 连接。常见做法是用模块的 AT 指令打开一个 TCP clientATQIDNSCFG1,8.8.8.8 ATQIOPEN1,0,TCP,broker域名或IP,1883,12345,0ATQIOPEN的第一个参数是 contextID必须和前面ATCGDCONT使用的 ID 一致第二个参数是连接 ID后续ATQISEND收发数据都用它TCP是服务类型broker 地址可以用阿里云平台分配的 MQTT endpoint 域名部分模组固件只认 IP需要用 DNS 解析后再填。端口 1883 是标准 MQTT 明文端口本地端口可以随机。最后一个参数是收据模式EC200S 不同固件版本取值不同建议选 buffer access 模式后面用ATQISEND和ATQIRD收发数据这样能明确知道每次收到的数据长度避免透传模式下串口粘包。返回CONNECT OK后TCP 链路就建好了。3. MQTT 协议在 STM32 上的移植与报文封装TCP 链路通上之后MQTT 报文由 STM32F103 自己组装。我不用 AT 指令自带的 MQTT 功能因为要定制 topic 和 payload还要在收到下发指令后直接操作 GPIO。手写 MQTT 报文不复杂难的只是协议状态和超时管理。3.1 读懂 MQTT 报文固定报头与剩余长度MQTT 控制报文分为固定报头、可变报头和有效载荷。固定报头第一个字节的高 4 位是报文类型低 4 位是标志位。常用类型如下控制报文报文类型值典型标志CONNECT0x100CONNACK0x200PUBLISH0x30Qos 标志在 bit1/bit2SUBSCRIBE0x82通常为 0x2SUBACK0x900PINGREQ0xC00PINGRESP0xD00DISCONNECT0xE00固定报头第二个字段是剩余长度编码规则是每个字节低 7 位存放长度值最高位表示是否还有后续字节。比如长度 321先取 321 对 128 取余得到 65加上最高位 0x80 变成 0xC1再取 321 整除 128 得到 2所以编码结果是0xC1 0x02。STM32F103 内存有限建议把发送缓冲区设成 512 字节MQTT 报文长度控制在 200 字节以内。3.2 裁剪一个轻量级 MQTT 客户端网上能搜到不少“mqtt协议在stm32上的移植”代码多半是基于 paho 嵌入式 C 或自研小栈。我的建议是不追求完整实现按需裁剪。一个最小客户端只需要 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ 四个功能收到 CONNACK、SUBACK、PINGRESP 时更新状态。接口可以收敛成下面几个函数typedef struct { uint8_t *buf; uint16_t buf_len; uint16_t (*write)(uint8_t *data, uint16_t len); uint16_t (*read)(uint8_t *data, uint16_t len); uint32_t (*get_ms)(void); } mqtt_client_t; int mqtt_encode_connect(uint8_t *buf, const char *client_id, const char *username, const char *password); int mqtt_encode_publish(uint8_t *buf, const char *topic, const uint8_t *payload, uint16_t plen, uint8_t qos); int mqtt_encode_subscribe(uint8_t *buf, uint16_t pid, const char *topic, uint8_t qos); int mqtt_encode_pingreq(uint8_t *buf);逻辑说明write和read接口由串口层提供底层走 EC200S 的ATQISEND和ATQIRDget_ms返回毫秒时间戳用于心跳和超时判断。编码函数只负责把报文写入buf返回报文总长度由调用方执行发送。这样移植时不依赖具体 RTOS 或裸机循环。3.3 CONNECT 和 PUBLISH 报文生成最核心的 CONNECT 报文代码如下static uint16_t encode_len(uint8_t *dst, uint32_t len) { uint8_t i 0; do { uint8_t d len % 128; len / 128; if (len) d | 0x80; dst[i] d; } while (len); return i; } static uint16_t append_str(uint8_t *dst, const char *s) { uint16_t len strlen(s); dst[0] len 8; dst[1] len; memcpy(dst[2], s, len); return len 2; } int mqtt_encode_connect(uint8_t *buf, const char *client_id, const char *username, const char *password) { uint8_t vhead[128]; uint16_t vlen 0; uint16_t p 0; // 协议名 MQTT 和协议级别 5 vhead[vlen] 0x00; vhead[vlen] 0x04; memcpy(vhead[vlen], MQTT, 4); vlen 4; vhead[vlen] 0x05; // 连接标志: 0xC2 表示启用用户名校验、密码校验、清理会话 vhead[vlen] 0xC2; // Keep Alive 60 秒 vhead[vlen] 0x00; vhead[vlen] 0x3C; vlen append_str(vhead[vlen], client_id); vlen append_str(vhead[vlen], username); vlen append_str(vhead[vlen], password); buf[0] 0x10; uint8_t n encode_len(buf[1], vlen); memmove(buf[1 n], vhead, vlen); return 1 n vlen; }参数说明连接标志0xC2二进制是1100 0010bit7 表示后面有 usernamebit6 表示有 passwordbit1 表示 clean session。阿里云平台要求这两个字段都非空所以这里没有留可选分支。Keep Alive 设成 60 秒但实际工程里建议 30 秒主动发一次 PINGREQ防止运营商 NAT 把链路断开。PUBLISH 报文的生成同样简单但要区分 QoSint mqtt_encode_publish(uint8_t *buf, const char *topic, const uint8_t *payload, uint16_t plen, uint8_t qos) { uint8_t vhead[256]; uint16_t vlen 0; uint16_t p 0; p append_str(vhead[p], topic); // QoS1 下需要包标识符demo 里固定写 0x0001 if (qos 0) { vhead[p] 0x00; vhead[p] 0x01; } memcpy(vhead[p], payload, plen); p plen; vlen p; buf[0] 0x30 | (qos 1); uint16_t n encode_len(buf[1], vlen); memmove(buf[1 n], vhead, vlen); return 1 n vlen; }这里buf[0] 0x30 | (qos 1)在 QoS1 时得到0x32对应 MQTT 的 PUBLISH 且需确认。包标识符一般用自增计数或随机数阿里云对同一连接内这两个字节没有严格限制但发送端不能重复用同一个值太快。发布消息后如果使用 QoS1必须等 PUBACK 才能发下一条否则会加重 4G 上行负担。3.4 用 PINGREQ 维持心跳EC200S 的 TCP 连接看起来是通的但基站可能已经回收资源。裸机主循环里要周期发送 PINGREQif (now - last_ping 30000) { int len mqtt_encode_pingreq(buf); uart_write(buf, len); last_ping now; ping_retry; }如果连续两个心跳周期没收到 PINGRESP不应继续等而应主动关闭 TCP 重连。判断逻辑放在接收解析里收到 PINGRESP 就把ping_retry清零。4. STM32F103 接入阿里云物联网平台MQTT 参数与 JSON Topic 设计阿里云物联网平台的三元组是 ProductKey、DeviceName、DeviceSecret。连接 broker 时要把它换成标准的 MQTT 协议字段。很多人在这一步卡住因为密码不是直接填 DeviceSecret而是用 HMAC-SHA1 算法签出来的。4.1 生成阿里云 MQTT 连接参数阿里云控制台“设备详情”页通常会给出 MQTT 连接参数示例开发前期最快的方式是先把这些参数复制到aliyun_config.h。当要自己生成密码时常见做法是用下面这套 python 逻辑import base64 import hmac import hashlib import urllib.parse product_key a1XXXXXX device_name temp01 device_secret 9c0dXXXXXXXXXX timestamp 789 client_id f{device_name}|securemode3,signmethodhmacsha1,timestamp{timestamp}| username f{device_name}{product_key} content fclientId{client_id}deviceName{device_name}productKey{product_key}timestamp{timestamp} sign hmac.new( device_secret.encode(), content.encode(), hashlib.sha1 ).digest() password urllib.parse.quote_plus(base64.b64encode(sign).decode()) print(client_id :, client_id) print(username :, username) print(password :, password)逻辑说明client_id里用管道符分隔了设备名和签名参数这里的timestamp必须和签名内容里使用的一致。password是先用 DeviceSecret 对拼接内容做 HMAC-SHA1再做 Base64 编码最后做 URL 编码。不同 SDK 生成的字符串可能略有差异如果某一端连不上先检查/是否被正确转义。对 STM32F103 来说不需要在 MCU 里跑 HMAC-SHA1开发阶段预先算好参数烧进固件即可。注意一机一密设备每次上线都可以用固定签名参数只要 DeviceSecret 不泄露动态计算签名不是必须项。4.2 物模型属性上报和属性设置的 Topic 与 JSON 格式在阿里云控制台定义物模型时我建了两个属性Temperature类型为 float 只读LEDSwitch类型为 bool 可写。上报 topic 使用/sys/{productKey}/{deviceName}/thing/event/property/postpayload 按官方 JSON 格式封装{ id: 1001, version: 1.0, params: { Temperature: 25.6 }, method: thing.event.property.post }下发属性设置的 topic 是/sys/{productKey}/{deviceName}/thing/service/property/set云端下发的数据是 JSON 格式示例{ method: thing.service.property.set, id: 123, params: { LEDSwitch: 1 }, version: 1.0 }这里params.LEDSwitch就是要解析出来的值。MCU 收到后把它映射到 PA1 的输出状态1 表示亮0 表示灭。4.3 用 cJSON 在 STM32F103 上组包和解包组包用 cJSON 非常方便cJSON *root cJSON_CreateObject(); cJSON_AddStringToObject(root, id, 1001); cJSON_AddStringToObject(root, version, 1.0); cJSON *params cJSON_AddObjectToObject(root, params); cJSON_AddNumberToObject(params, Temperature, 25.6); cJSON_AddStringToObject(root, method, thing.event.property.post); char *payload cJSON_PrintUnformatted(root); mqtt_publish(topic, payload, strlen(payload), 1); free(payload); cJSON_Delete(root);解析下发的 JSON 时注意 payload 不一定以\0结尾最好用cJSON_ParseWithLength老版本没有这个函数就先拷贝到静态数组再cJSON_ParsecJSON *root cJSON_ParseWithLength(buf, len); cJSON *method cJSON_GetObjectItem(root, method); if (strcmp(method-valuestring, thing.service.property.set) 0) { cJSON *params cJSON_GetObjectItem(root, params); cJSON *led cJSON_GetObjectItem(params, LEDSwitch); int onoff led-valueint; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, onoff ? GPIO_PIN_RESET : GPIO_PIN_SET); } cJSON_Delete(root);这段代码的逻辑是先取method字段确认这是属性设置而不是别的事件再取params.LEDSwitch的值。cJSON 在 STM32F103 上会调用 malloc高频上报会产生内存碎片建议把上报周期控制在 1 秒以上并定期重启或使用静态内存池版本。4.4 属性设置的下行回复阿里云平台下发的属性设置是服务调用设备端需要显式回复。回复 topic 是/sys/{productKey}/{deviceName}/thing/service/property/set_replypayload 要携带原请求的id{ code: 0, id: 123 }不回复会导致控制台一直显示设备不在线或操作超时。回复成功后云端才认为指令执行完成。5. 温度上报与 LED 下发控制的完整 MQTT 收发流程代码写到这里相当于把协议栈和业务逻辑都塞进状态机。温度上报不是简单延时4G 网络可能丢 TCP 包MQTT 要处理确认才能避免数据堆积。5.1 温度采样与滤波温度采集用 NTC 分压加 ADC 采样。连续采样 16 次去掉最大值最小值后取平均再把 ADC 值换算成温度和电阻float read_temperature(void) { uint32_t sum 0; uint16_t min_v 0xFFFF; uint16_t max_v 0; for (int i 0; i 16; i) { uint16_t v adc_read(); sum v; if (v min_v) min_v v; if (v max_v) max_v v; } uint16_t avg (sum - min_v - max_v) / 14; float voltage avg * 3.3f / 4096.0f; float r_ntc 10.0f * voltage / (3.3f - voltage); return 1.0f / (1.0f / 298.15f 1.0f / 3950.0f * logf(r_ntc / 10.0f)) - 273.15f; }参数说明这里假定 NTC 是 10K B 值 3950电源用 3.3V。r_ntc计算公式来自 NTC 分压电路分母里的3.3f - voltage如果接近 0说明短路或断路应该在代码里加阈值判断。logf需要连接浮点数学库编译时加-lm。5.2 上报状态机与 QoS1 确认上报链路我用状态机管理状态包括空闲、连接中、就绪、等待发布确认typedef enum { DTU_IDLE, DTU_CONNECTING, DTU_READY, DTU_WAIT_PUBACK } dtu_state_t; dtu_state_t state DTU_IDLE; uint32_t next_report 0; while (1) { mqtt_parse_rx(); if (state DTU_READY HAL_GetTick() next_report) { mqtt_publish(topic, payload, len, 1); state DTU_WAIT_PUBACK; next_report HAL_GetTick() 5000; } if (state DTU_WAIT_PUBACK mqtt_puback_flag) { mqtt_puback_flag 0; state DTU_READY; } mqtt_keepalive(); }逻辑说明只有在DTU_READY状态才允许发布下一条数据。mqtt_puback_flag由 MQTT 解析模块在收到 PUBACK 时置位。如果 10 秒没收到 PUBACK就认为链路异常把状态切回DTU_IDLE触发重连。5.3 接收缓冲区与 JSON 解析控制 LEDEC200S 如果工作在 buffer access 模式收到 TCP 数据会先报QIURC: recv,连接IDMCU 再用ATQIRD连接ID,长度读取。读回来的数据要放进环形缓冲区MQTT 解析模块逐字节处理。处理函数如下static int handle_property_set(char *buf, int len) { cJSON *root cJSON_ParseWithLength(buf, len); if (!root) return -1; cJSON *method cJSON_GetObjectItem(root, method); if (strcmp(method-valuestring, thing.service.property.set) ! 0) { cJSON_Delete(root); return 0; } cJSON *params cJSON_GetObjectItem(root, params); cJSON *led cJSON_GetObjectItem(params, LEDSwitch); int onoff led-valueint; GPIO_WriteBit(GPIOA, GPIO_Pin_1, onoff ? Bit_RESET : Bit_SET); char *id cJSON_GetObjectItem(root, id)-valuestring; char reply[64]; snprintf(reply, sizeof(reply), {\code\:0,\id\:\%s\}, id); mqtt_publish(reply_topic, reply, strlen(reply), 0); cJSON_Delete(root); return 0; }这里的strcmp判断必须是完整匹配不能用strncmp加短匹配否则会把thing.service.property.set_reply也当成处理对象。LEDSwitch在阿里云物模型里是布尔型cJSON 解析后valueint为 0 或 1不能直接用valuestring。5.4 串口日志与异常识别调试阶段最有用的是在 USART3 打时间戳和状态printf([%lu] state%d csq%d puback%d\r\n, HAL_GetTick(), state, csq_value, mqtt_puback_flag);日志里如果看到state长时间停在DTU_WAIT_PUBACK说明上行 TCP 数据到了 broker但确认没回来如果state经常回DTU_IDLE要先查信号再查模块是否被 reset。把日志改成固定格式后放现场跑一晚上第二天回来翻串口文件基本能定位是网络问题还是协议问题。6. 调稳这套 4G DTU 方案的具体技巧现场设备和开发板最大的区别是环境不确定。同样的固件在办公室能跑一天到工业现场可能两小时掉线一次。这里分享三个我常用的稳定性技巧。6.1 先看 CSQ 再看代码排查任何 4G 上传问题第一步是敲ATCSQ。返回值第一项如果是 99说明模块没找到网络如果是 10 以下说明信号差TCP 长连接必然会频繁断开。弱信号环境里把上报周期从 5 秒拉长到 30 秒MQTT Keep Alive 从 60 秒改到 30 秒能让模块早一点发现链路假死而不是在基站侧默默超时。信号值无法通过代码优化只能从天线和电源下手。6.2 MQTT 掉线重连用指数退避掉线后立即重连很可能导致模块和基站之间反复冲突。我用指数退避第一次 3 秒第二次 6 秒第三次 12 秒最多 60 秒封顶。重连前先关闭旧 socket再做ATQIACT检查网络再执行 MQTT CONNECT。整个过程要独立做成一个dtu_reconnect函数避免主循环里到处散落重连代码。只要 DeviceSecret 不换每次重连都使用预先算好的 MQTT 参数不需要重新跑 HMAC-SHA1。6.3 在 buffer access 模式下组包透传模式看似简单但串口数据没有帧边界MQTT 报文长度超过串口缓冲时会被拆成多段。我建议用 buffer access 模式每次ATQIRD返回明确长度然后按 MQTT 固定报头的剩余长度字段判断是否收完。解析时把 URC 上报的QIURC: recv,0当成一次接收事件优先处理数据读取再处理业务上报。把ATCSQ、重连次数和QIURC三路日志同时挂上现场问题基本半小时内能定位。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门