做物联网设备接入免不了要和MQTT打交道。最近在做一个4G Cat 1设备接入的项目模组用的是SIMCOM A7670C_FASL一开始只打算跑一路MQTT把温度、电压、信号强度这些数据传到阿里云。结果产品经理又提了新需求要支持远程参数配置和固件升级而且不能影响正常的数据上报。于是我把方案改成了双路MQTT——一个client专门走业务数据另一个client走设备管理和指令下发两条链路都挂在阿里云物联网平台上。A7670C_FASL这个模组内置MQTT AT命令不用自己移植协议栈串口发命令就能跑我把AT交互和状态机封装成了一套C代码整个调试过程大概花了三天。这篇文章就把这套方案的选型思路、AT命令流程和源码结构完整记录下来给正在用Cat 1模组接阿里云的朋友一个可以直接抄的作业。1. 这块模组到底能干什么A7670C_FASL与双路MQTT的选型逻辑1.1 Cat 1模组凭什么适合这类接入项目LTE Cat 1这个品类这几年在物联网终端里非常火原因很直接成本比Cat 4低速率又比NB-IoT高不少理论上行5Mbps、下行10Mbps跑MQTT这类小包协议绰绰有余。A7670C属于SIMCOM的Cat 1系列全网通LCC封装国内三大运营商的卡都能用。相比WiFi方案它不受网络环境限制插上SIM卡就能出网相比NB-IoT它对移动性的支持更好数据交互的实时性也更强。像共享设备、充电桩、农业监测这类需要在户外移动或者分散部署的场景Cat 1基本是性价比最优解。选型的时候我也对比过移远的EC20EC20是Cat 4模组性能没问题但价格和功耗都会高一些。而且EC20的MQTT命令体系是QMTCFG那一套和SIMCOM的CMQTT命令完全不同代码不能复用。如果你手头是EC20这篇文章里讲AT命令的部分就只能当思路参考不能直接照抄。1.2 A7670C_FASL的固件标识说明模组丝印上写的是A7670C_FASL这是A7670C系列里的一个变体标识实际使用时重点看固件版本不要只看丝印。我拿到手第一件事就是插电后发ATI查版本信息确认固件带MQTT扩展命令集。A7670C_FASL这个固件版本的特点是支持CMQTT开头的完整MQTT AT命令可以创建多个MQTT client每个client独立连接服务器、独立订阅topic。也就是说双路MQTT在硬件和固件层面是原生支持的不需要自己维护两套协议栈。有一点需要特别注意SIMCOM A7670C系列不同批次的固件可能会有差异同一个AT命令在不同固件上参数格式可能不同。调试前一定要去官网下载对应固件版本的《MQTT Application Note》把命令语法核对一遍再上手别直接拿网上的旧命令硬试。1.3 双路MQTT解决的实际问题为什么一定要双路一开始我也想偷懒一路MQTT既要高频上报业务数据又要处理下行指令。实际跑起来发现两个麻烦上行数据量一旦增大连续PUBLISH的时候下行的实时性很难保证因为AT串口是串行的发送长payload会占用通道另一个问题是逻辑耦合业务上报和指令处理混在一个回调里改起来很容易互相牵连。拆成两路之后每路职责非常清楚通道client_index用途典型QoS通道00物模型属性上报、事件上报QoS 0通道11远程配置下发、固件升级指令QoS 1这种拆法还有一个隐藏好处如果一路因为云端topic配置错误导致异常另一路还能保持在线设备不至于完全失联。对于远程维护场景留一条保底通道是非常值得的。1.4 与其他模组方案的取舍除了EC20市面上还有合宙Air724UG、有人物联的Cat 1模组等方案。每个方案都有自己的AT命令体系没有统一标准。选SIMCOM的原因主要是A7670C资料相对齐全MQTT命令语义简单直接踩坑之后能快速定位问题。而且SIMCOM的CMQTT命令把MQTT的connect、publish、subscribe直接映射成AT指令学习成本低适合要在短时间内出原型的情况。2. 硬件接线和串口调通通电之后先把这些基础做扎实2.1 供电和电平转换很多朋友拿到模组第一步就栽在供电上。A7670C的VBAT电压范围是3.4V到4.2V典型值3.8V峰值电流能到2A以上。千万别用USB转串口模块上的3.3V去给模组的VBAT供电带不动一发射就掉电压直接表现为模组反复重启或者网络注册失败。我调试时用的是5V/3A的开关电源经过DC-DC降压到3.8V供给模组。如果你买的是带底板的评估板板上通常已经做了电源转换和电平转换直接用USB线供电就能跑。如果是自己画板注意VBAT端的滤波电容要靠近模组引脚放置建议用钽电容加陶瓷电容组合。还有一个容易忽略的点是IO电平。A7670C的UART接口电平是1.8V很多USB转TTL模块是3.3V电平直接怼上去可能存在兼容风险。评估板一般都有电平转换芯片问题不大自己接线时最好确认一下两边电平必要时加电平转换电路。2.2 开机、串口和AT基本操作模组开机后串口默认波特率通常是1152008N1无流控。把USB转TTL模块的TX接到模组的RXRX接到模组的TXGND共地。上电后在串口工具里发AT回车能返回OK就说明通路没问题。我习惯先把这些基础AT命令跑一遍确认模组状态AT OK ATI A7670C_FASL_V02 ATCSQ CSQ: 24,99ATCSQ后面的第一个数字是信号强度24在4G网络里属于中等偏上的水平如果是0或者99就要检查天线和SIM卡了。注意模组必须接LTE主天线很多初玩者觉得“用网线内芯怼一下就行”结果信号一直很差实际上天线的接触和位置对效果影响非常明显。2.3 网络注册与PDP上下文激活MQTT连接之前模组必须已经注册上网络并且PDP上下文已经激活。网络注册状态用ATCGREG?查询状态0,1表示注册上网络PDP激活状态用ATCNACT?查询。不同运营商的APN设置也要处理实测常用的配置运营商APN中国移动cmnet中国联通3gnet中国电信ctnet如果模组没有自动激活PDP需要手动执行ATCGDCONT1,IP,cmnet OK ATCNACT1,cmnet OK有些固件在ATCNACT返回ERROR其实是PDP已经激活了用ATCNACT?查询就能看到状态。网络这块是MQTT连接的地基PDP不激活后面所有CMQTT命令都会卡在连接阶段而且报错信息往往只有CONNECT FAIL排查起来很烦。3. 阿里云端的设备、三元组和Topic一处写错就连不上平台3.1 创建产品和设备登录阿里云物联网平台控制台在公共实例里创建产品。这一步的关键参数是“节点类型”我选的“设备”连网方式选“蜂窝网络”。创建完产品之后在产品下添加设备平台会生成三元组ProductKey、DeviceName、DeviceSecret。控制台添加设备时如果你自定义DeviceName注意别用特殊字符建议全小写字母加数字后期拼Topic的时候不容易出错。我的规划是双路用两个身份所以在同一个产品下添加了两个设备dev001用于业务数据上报dev002用于管理指令。这样两个client在阿里云上就是完全独立的两个设备各自认证各自订阅topic互不干扰。3.2 三元组换算成MQTT连接参数A7670C模组内置的MQTT协议栈需要你提供标准MQTT连接参数而阿里云给的是三元组所以中间需要做一步换算。这一步非常关键连接失败八成是这里出的问题。我采用的换算方式是阿里云标准的一机一密签名流程clientId {deviceName}|securemode3,signmethodhmacsha256|username {deviceName}{productKey}password HMAC-SHA256({deviceSecret}, content)其中content按字符串拼接{clientId}{deviceName}{productKey}如果你不想在代码里实现HMAC-SHA256调试阶段可以先在PC上用Python把三个连接参数算出来直接写死在配置文件里。import hmac import hashlib product_key a1xxxxx device_name dev001 device_secret xxxxxxxx client_id f{device_name}|securemode3,signmethodhmacsha256| username f{device_name}{product_key} content client_id device_name product_key password hmac.new(device_secret.encode(), content.encode(), hashlib.sha256).hexdigest() print(clientId:, client_id) print(username:, username) print(password:, password)这里有几个细节需要注意。第一HMAC输出是小写十六进制字符串不是Base64很多人算出来连接鉴权失败就是输出的编码不对。第二签名内容拼接顺序是clientId在前接着是deviceName最后是productKey顺序不能变。第三我的clientId里没有带timestamp字段因为A7670C模组没有可靠的RTC时钟带timestamp反而要额外处理校时不带时间戳在公共实例上验证是可以通过的。如果你用的是新版企业版实例控制台文档里对clientId格式可能另有说明以文档为准。3.3 Topic规划与权限配置阿里云的Topic分系统Topic和自定义Topic。系统Topic像/sys/{productKey}/{deviceName}/thing/event/property/post用于上报属性这个默认就能发布不需要额外配置权限。我这里两路用的是同一个系统Topic但设备身份不同通道设备身份发布Topic订阅Topic通道0dev001/sys/a1xxxxx/dev001/thing/event/property/post/sys/a1xxxxx/dev001/thing/service/property/set通道1dev002/sys/a1xxxxx/dev002/thing/event/property/post/sys/a1xxxxx/dev002/thing/service/property/set如果你要用自定义Topic比如/a1xxxxx/dev001/user/mgmt需要在产品里先定义Topic类并勾选设备的发布、订阅权限。很多新手到了订阅那一步一直失败去控制台一看Topic类里根本没有定义这个topic属于权限没开。3.4 双路连接在云端的身份规划双路的身份规划有两种方式。一种是同一个产品下建两个设备就是我上面的方案好处是产品维度统一物模型一致代码里只需要改DeviceName和DeviceSecret。另一种是两个产品各一个设备适合两个业务方完全隔离的场景比如A团队只管数据上报B团队只管设备管理。我个人建议优先用同一个产品两个设备因为管理和维护都简单。但有一个坑必须提醒同一时刻、同一个设备证书只允许一个MQTT连接在线重复连接会出现互踢。所以双路必须用两个不同DeviceName千万不要试图用同一套三元组开两路连接。4. 双路MQTT的AT命令完整对话从CMQTTSTART到CMQTTPUB4.1 需要用到的SIMCOM MQTT AT命令A7670C_FASL这版固件上MQTT功能由一组CMQTT开头的AT命令完成核心命令如下命令作用ATCMQTTSTART启动MQTT服务ATCMQTTACCQ创建MQTT clientATCMQTTUSER设置MQTT用户名ATCMQTTPASS设置MQTT密码ATCMQTTTOPIC设置发布TopicATCMQTTCONNECT连接MQTT服务器ATCMQTTSUB订阅TopicATCMQTTPUB发布消息ATCMQTTDISC断开MQTT连接ATCMQTTREL释放MQTT clientATCMQTTSTOP停止MQTT服务每个命令都有细分的参数格式比如ATCMQTTTOPIC需要先传topic长度然后等待符号“”出现之后再发送topic内容。这个交互方式容易踩坑后面细说。4.2 第一路client业务数据发布流程这是整个AT对话最核心的部分。我按实际调试时的顺序列出来每一步都对应前面章节计算的参数ATCMQTTSTART OK ATCMQTTACCQ0,client_upload OK ATCMQTTUSER0,dev001a1xxxxx OK ATCMQTTPASS0,5f4dcc3b5a... OK ATCMQTTTOPIC0,44 /sys/a1xxxxx/dev001/thing/event/property/post OK ATCMQTTCONNECT0,a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com,1883,120,1 OK这一步默认情况下clean session我填1。连接成功后阿里云控制台的设备状态会变成在线。接下来就可以发消息了ATCMQTTTOPIC0,44 /sys/a1xxxxx/dev001/thing/event/property/post OK ATCMQTTPUB0,31,0,0 {temperature:25.6,voltage:3.85} OK注意payload长度是按字节算的不是按字符数。JSON里如果带中文一个中文字符在UTF-8下是三个字节长度很容易算错建议上报内容统一用英文键名和数字值避免编码问题。4.3 第二路client管理指令订阅流程第二路的流程和第一路基本一样区别是client_index要改成1确认使用另一套三元组ATCMQTTACCQ1,client_mgmt OK ATCMQTTUSER1,dev002a1xxxxx OK ATCMQTTPASS1,签名后的password OK ATCMQTTTOPIC1,44 /sys/a1xxxxx/dev002/thing/event/property/post OK ATCMQTTCONNECT1,a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com,1883,120,1 OK ATCMQTTSUB1,48,0 /sys/a1xxxxx/dev002/thing/service/property/set OK这里有一个细节如果先发CMQTTSUB但没有收到OK很可能Topic没有订阅权限或者Topic长度不对。订阅成功之后阿里云平台往下发指令时模组串口上才会出现对应的URC主动上报。4.4 下行消息的URC解析阿里云下行指令到达模组后不会直接打印payload而是先出现一段URC再通过专门的读消息命令把内容取出来。不同固件的URC格式不完全一样我的做法是在串口接收线程里按行匹配包含“CMQTT”关键字的行然后交给解析函数处理。这里提醒一句URC是主动上报的可能会有半包、粘包的情况。真正做产品时串口接收需要用环形缓冲区加状态机不能简单用一次读取完事。我的源码里就是用环形FIFO先把整行收齐再按行解析URC。4.5 断开重连的正确顺序MQTT连接断开后重连不是简单再发一次CMQTTCONNECT就行。如果旧client资源没释放再次CONNECT会失败。标准的重连顺序是ATCMQTTDISC0 OK ATCMQTTREL0 OK ATCMQTTACCQ0,client_upload OK ATCMQTTCONNECT0,...,1883,120,1 OK我实际测试过跳过CMQTTDISC直接重新ACCQ有时能成功但连续掉线几次之后就会出现资源泄漏最终连不上了。所以重连逻辑务必按DISC到REL到ACCQ再到CONNECT的流程走。5. 源码设计思路与核心代码解读完整工程怎么组织5.1 工程目录与模块划分整个工程按功能拆成了几个独立模块这样双路通道可以共用同一套逻辑simcom_a7670c_mqtt/ ├── main.c // 状态机主循环 ├── hal_uart.c // 串口驱动与环形FIFO ├── simcom_mqtt.c // CMQTT命令封装 ├── simcom_mqtt.h ├── aliyun_auth.c // 阿里云三元组转MQTT参数 ├── aliyun_auth.h └── mqtt_config.h // 双路通道配置分模块的主要原因是reconnect逻辑、发布逻辑都是双路共用的如果写在一起后面维护会非常痛苦。我把“通道配置”和“通道行为”分离新增一路只需要在配置结构体里加一组参数不用改任何业务逻辑。5.2 连接参数与数据结构设计mqtt_config.h里定义了双路通道的三元组和Topic信息#ifndef MQTT_CONFIG_H #define MQTT_CONFIG_H #define MQTT_SERVER_HOST a1xxxxx.iot-as-mqtt.cn-shanghai.aliyuncs.com #define MQTT_SERVER_PORT 1883 #define MQTT_KEEPALIVE 120 typedef struct { uint8_t client_idx; const char *device_name; const char *product_key; const char *device_secret; const char *pub_topic; const char *sub_topic; } mqtt_channel_cfg_t; #if 1 /* 双路配置通道0业务上报通道1管理指令 */ static const mqtt_channel_cfg_t g_mqtt_channels[] { { .client_idx 0, .device_name dev001, .product_key a1xxxxx, .device_secret xxxxxxxx, .pub_topic /sys/a1xxxxx/dev001/thing/event/property/post, .sub_topic /sys/a1xxxxx/dev001/thing/service/property/set, }, { .client_idx 1, .device_name dev002, .product_key a1xxxxx, .device_secret yyyyyyyy, .pub_topic /sys/a1xxxxx/dev002/thing/event/property/post, .sub_topic /sys/a1xxxxx/dev002/thing/service/property/set, }, }; #define MQTT_CH_MAX (sizeof(g_mqtt_channels) / sizeof(g_mqtt_channels[0])) #endif #endif这个设计的核心是所有和云端身份相关的参数都集中在配置表里业务代码只关心“第几路通道要发什么数据”。5.3 HMAC-SHA256签名模块如果不想在MCU里引入mbedTLS调试阶段最简单的方式是用Python计算好三个连接参数直接写成宏。但产品化肯定要在设备端算因为DeviceSecret不能明文丢在代码里让人一抓包就看出来。最稳妥的做法是在MCU里集成一个轻量级HMAC-SHA256实现只保留SHA256和HMAC功能不引入完整TLS库占用资源非常少#include stdio.h #include string.h #include sha256.h void aliyun_build_conn_params(const char *pk, const char *dn, const char *ds, char *cid, int cid_len, char *user, int user_len, char *pwd, int pwd_len) { char content[128]; snprintf(cid, cid_len, %s|securemode3,signmethodhmacsha256|, dn); snprintf(user, user_len, %s%s, dn, pk); snprintf(content, sizeof(content), %s%s%s, cid, dn, pk); hmac_sha256_hex(ds, content, pwd, pwd_len); }使用时有两点经验第一content缓冲区长度要留足deviceName加上几十字节的clientId扩展字段很容易超过64字节第二HMAC输出是小写hex不要转成大写阿里云校验区分大小写。5.4 双路状态机调度双路MQTT不能同时发AT命令因为底层是同一个串口。我的方案是让每路通道各自维护一个状态但真正发送AT命令的入口统一由调度主循环控制。核心状态机如下typedef enum { CH_IDLE 0, CH_NET_CHECK, CH_PDP_ACTIVE, CH_MQTT_START, CH_MQTT_ACCQ, CH_MQTT_CONNECT, CH_MQTT_READY, CH_MQTT_DISCONNECT, } channel_state_t; void mqtt_loop(void) { for (int i 0; i MQTT_CH_MAX; i) { switch (g_state[i]) { case CH_IDLE: /* 检查网络和PDP状态就绪后进入CH_MQTT_START */ if (net_ready()) g_state[i] CH_MQTT_START; break; case CH_MQTT_START: send_at(ATCMQTTSTART); g_state[i] CH_MQTT_ACCQ; break; case CH_MQTT_ACCQ: send_at_with_idx(ATCMQTTACCQ%d,\%s\, i, client_name); g_state[i] CH_MQTT_CONNECT; break; case CH_MQTT_CONNECT: if (mqtt_connected[i]) g_state[i] CH_MQTT_READY; else if (retry_cnt[i] 3) mqtt_reconnect(i); break; case CH_MQTT_READY: if (need_report[i]) mqtt_publish(i); break; default: break; } } }主循环每10毫秒跑一次通过系统tick做超时判断。AT命令发送统一走一个带超时的同步发送函数接收URC则由串口中断完成两者之间通过队列解耦。6. 实测数据、掉线重连和AT命令文档没写的坑6.1 实测连接耗时与稳定性我这边用了一张移动物联网卡在室内窗边信号强度CSQ在20到26之间。冷启动到网络注册大概需要5到8秒PDP激活后单路MQTT连接耗时在0.5到1秒左右双路顺序连接总耗时不超过2秒。连续运行7天一共掉线3次都是模组侧的TCP连接断开系统检测到MQTT_DISC的URC后触发重连平均4秒恢复在线。连接耗时的瓶颈基本不在模组而在阿里云域名解析和TCP握手。如果你直接填IP而不是域名连接会快一些但阿里云接入IP可能变化不建议写死。调试时用域名产品化时如果对入网速度敏感可以考虑加DNS缓存。6.2 我踩过的几个AT命令的坑第一个坑是ATCMQTTTOPIC的长度计算。最初我直接用strlen(topic)算长度topic全英文数字的时候没问题后来换了一个带中文的topicstrlen算出来是字节数而命令要求的是字符数两者不一致导致subscribe一直失败。我的经验是阿里云平台生成的自定义Topic尽量全用英文和数字避免非ASCII字符。第二个坑是模组掉线后直接重连报错。很多情况下模组侧并不知道TCP已经断了还会认为旧client还在。这时候要先发ATCMQTTDISC把旧连接断开再发ATCMQTTREL释放client资源最后再重新ACCQ和CONNECT。跳过资源释放连续掉线多几次就会出现client index被占用的情况。第三个坑是password的大小写。HMAC-SHA256输出的十六进制默认是小写字母我用Python算的时候没问题但有一次在MCU端实现的HMAC函数返回了全大写字符串结果阿里云一直报“鉴权失败”。这个问题控制台日志不会直接告诉你是大小写问题排查了好久。6.3 阿里云日志服务排查连接失败阿里云物联网平台控制台内置日志服务设备连接失败时在“监控运维-日志服务”里按DeviceName和时间段查询能看到每次连接尝试的详细结果。日志里如果出现“device auth check failed”基本就是三元组换算的password、username或clientId不对如果出现“connection refused”要先检查模组侧的网络状态和接入域名端口。记住一个排查顺序先看模组串口的AT返回确认CMQTTSTART和CMQTTACCQ都OK再看CMQTTCONNECT的返回值如果CONNECT返回错误或没有回OK最后去阿里云控制台日志服务看云端记录的失败原因。这个顺序能帮你快速把问题定位在模组侧还是云端配置侧。6.4 keepalive、QoS与低功耗调优双路MQTT的keepalive要分别设置我统一用的120秒。阿里云服务端的超时机制一般是1.5倍心跳时间120秒的keepalive意味着近3分钟才会判定离线对绝大多数场景足够了。如果你对“离线”状态的实时性有要求可以设到60秒但会增加少量流量。QoS方面阿里云只支持0和1不支持2。我通道0用QoS 0因为温度电压这类遥测数据即使丢一帧也无所谓通道1用QoS 1确保远程配置指令不丢失。实测QoS 1的消息在模组信号差的时候会有重传下行送达率明显提升。功耗方面双路长连接肯定比单路费电因为每路都要按keepalive周期发PINGREQ心跳。做电池供电设备时如果不需要管理通道一直在线可以让通道1在空闲时主动DISC需要下发指令时再建立连接。我的源码里保留了mqtt_channel_disconnect接口方便在低功耗模式下把管理通道动态启停。如果你后续想让这套代码支持更多通道扩展方式很简单在g_mqtt_channels数组里再增加一个配置项同时把client_index改成2就行。但要注意模组MQTT client的数量是有上限的A7670C_FASL具体能开几路要以你手上固件的应用笔记为准不要盲目叠加。我个人在实际项目里最多开到三路再多就会碰到内存和资源管理的问题。