做物联网的老哥们应该都有体会,MCU联网上云这件事,说难不难,说简单也不简单。STM32、AT32、GD32这类单片机要连华为云IoTDA,没有Linux系统,没有现成的SDK,如果从头写MQTT协议栈,再考虑断线重连、JSON解析、时间戳鉴权这些东西,没个一两周拿不下来。最近项目里用了WF24这个串口WiFi模块,把"连接华为云 + 上报数据 + 订阅下发消息"整套流程用AT指令跑通了,整个过程不到半天。这篇文章把这套完整AT指令序列、华为云侧参数生成方法、还有我踩过的坑全部整理出来,给正准备用串口WiFi模块上云的朋友做参考。
这篇东西适合谁看?最典型的是做单片机设备接云的开发人员,比如智能家居设备、工业数据采集终端、农业传感器网关这类场景。不管你是用STM32还是用国产MCU,只要串口能发AT指令,就能照搬这套方法。如果你还没来得及接触华为云IoTDA,也不怕,华为云侧的准备工作我会一步一步拆开讲,参数怎么算、为什么这么算,都会交代清楚。
1. 方案总览:WF24为什么能省掉IoT开发的半条命
1.1 从单片机到华为云,为什么我选AT指令方案
先说清楚一个基本问题:WF24到底在系统里扮演什么角色。
WF24是一款基于ESP8266方案、出厂预烧AT指令固件的串口WiFi模块。它的工作方式很简单——你的主控MCU通过UART串口向模块发送文本形式的AT指令,模块就去执行WiFi连接、TCP/IP通信、MQTT消息收发这些脏活累活,执行完再把结果通过串口返回给MCU。对于MCU来说,连华为云就变成了"往串口写几个字符串"这么简单的事。
我见过很多项目一上来就想用SDK方案,把ESP8266的SDK移植到自己的MCU上跑。这个思路在资源充足的项目里没问题,但对大多数纯单片机方案来说,代价实在太大:RAM和Flash不够用、协议栈出问题很难调试、项目排期根本不允许。相比之下AT指令方案有3个明显优势:
- 开发周期短。不需要通读MQTT协议规范,不需要处理TCP粘包分包,串口助手直接就能调试。
- 耦合度低。MCU只做业务逻辑,网络部分的BUG基本被模块厂商挡掉了,后续想换别的WiFi模块,只要AT指令兼容,代码改动量很小。
- 可观测性强。串口上每一帧AT指令交互都是明文可见的,排查问题的时候直接把串口日志甩给对接方,沟通效率翻倍。
1.2 整体链路里谁负责什么
这个方案的完整数据链路是这样的:
MCU主控 (STM32等) <--UART--> WF24模块 <--WiFi--> 路由器 <--公网--> 华为云IoTDA <--API--> 应用端/APPMCU负责采集数据、生成JSON负载、解析下发命令并执行对应动作。WF24负责WiFi连接、MQTT协议栈、与华为云保持长连接。华为云IoTDA负责设备鉴权、Topic路由、产品模型校验、命令转发。三者的分工非常清晰。
对比一下如果不用WiFi模块,自己用4G模块或者无线网卡做同样的接入,要么资费不划算,要么要写大量网络协议代码。串口WiFi模块这种方案胜在便宜、成熟、上手门槛低,非常适合快速验证和中小批量产品。
2. 华为云IoTDA侧准备:没有这一步,后面全是无用功
2.1 创建产品与注册设备
想在AT指令里顺利连上华为云,首先得在IoTDA平台上把设备"户口"建好。进了华为云控制台,搜索"设备接入 IoTDA"服务,然后按照下面几个步骤操作:
- 在"产品"页面点击"创建产品"。协议类型选MQTT,数据格式选JSON,产品名称按你的设备来,比如"智能温控器"。
- 产品创建成功后,进入产品详情,添加服务。华为云的产品模型是"服务 + 属性"的树形结构,比如服务ID叫temperature,下面定义属性value、unit等。这一步虽然不是连接必需的,但后续标准属性上报能不能成功,跟产品模型里的属性定义是否匹配有直接关系。
- 在产品下"注册设备"。设备标识码(node_id)可以自己填,比如
temp_sensor_01;认证方式选"密钥",密钥可以自动生成也可以自己指定。注册完成后,记录两个关键信息:设备ID(device_id)和设备密钥(secret)。
需要说明的是,华为云设备ID通常长这样:65a2b3c4d5e6f7_temp_sensor_01,前面的乱序字符串是产品ID,下划线后面是你填的设备标识码。设备密钥是字符串形式,类似一个密码。这两个信息决定了后面所有鉴权参数的计算。
2.2 关键参数生成:ClientId、Username、Password是怎么来的
华为云IoTDA的MQTT接入认证,不像一些云平台那样直接用设备ID和密钥做用户名密码。它要求设备在MQTT的CONNECT报文里填三个自定义参数:ClientId、Username、Password。这三个参数不是随便填的,生成规则如下:
- ClientId:
{device_id}_0_{timestamp} - Username:
{device_id} - Password:
HMAC-SHA256(密钥, timestamp)换算成十六进制小写字符串
这里的timestamp时间戳是一个很关键的参数,它既不是Unix毫秒时间戳,也不是标准日期时间字符串,而是UTC+8时区下的"年月日时"组合数值,格式为YYYYMMDDHH。比如当前时间是北京时间2025年6月1日下午14点30分,那timestamp就是2025060114。注意,它精确到小时,有效期为当前这个小时间段,过了这个小时就需要重新计算。这也是很多新手在AT调不通时最容易忽略的原因——你上午生成的参数,下午才拿来连,当然失败。
再说说ClientId中间那个_0_。这个0表示密码校验算法类型,华为云目前默认使用SHA256算法,所以固定填0,不要乱改。
这里我写一个Python脚本,用来快速计算这三个参数:
import hmac import hashlib from datetime import datetime, timedelta device_id = "65a2b3c4d5e6f7_temp_sensor_01" # 替换成你的设备ID secret = "your_device_secret" # 替换成你的设备密钥 # 华为云要求的是北京时区(UTC+8)的当前小时 beijing_time = datetime.utcnow() + timedelta(hours=8) timestamp = beijing_time.strftime("%Y%m%d%H") client_id = f"{device_id}_0_{timestamp}" username = device_id password = hmac.new( secret.encode("utf-8"), timestamp.encode("utf-8"), hashlib.sha256 ).hexdigest() print("timestamp:", timestamp) print("ClientId :", client_id) print("Username :", username) print("Password :", password)脚本跑完,把输出的ClientId、Username、Password记下来,后面AT指令里直接填。
2.3 接入地址和端口怎么填
华为云IoTDA每个实例都有一个独立的MQTT接入地址,在控制台"设备接入 IoTDA"实例详情页能看到,格式类似:
xxxxxxxx.iot-mqtts.cn-north-4.myhuaweicloud.com注意不同地域的后缀不一样,比如北京四是cn-north-4,上海一是cn-east-3。端口方面,IoTDA开放了两个MQTT端口:1883是明文MQTT端口,8883是TLS加密端口。AT指令方案里,如果选择1883端口,AT+MQTTUSERCFG里的scheme参数填0即可,配置最省事;如果选择8883端口,scheme要填1,并且需要把CA证书写入模块Flash,操作繁琐不少。我建议项目验证阶段直接用1883端口跑通业务逻辑,正式产品务必切换TLS方案保证链路安全。
3. WF24硬件接线与WiFi连接
3.1 硬件接线与串口参数
WF24模块的硬件连接很简单,核心是给模块供电和引出UART。模块供电必须用3.3V,绝不能让5V直接怼到VCC引脚上,否则模块大概率当场烧毁。串口电平同理,模块的TX/RX是3.3V电平,如果主控板是5V引脚,中间要加电平转换芯片或者用分压电阻,否则轻则通信乱码,重则损坏模块串口。
以下是标准接线对应关系:
| WF24引脚 | 接主控/调试器 | 注意事项 |
|---|---|---|
| VCC | 3.3V电源 | 严禁5V;电流需大于300mA |
| GND | 电源地 | 必须与主控共地 |
| TXD | 主控RXD | 模块发送,主控接收 |
| RXD | 主控TXD | 模块接收,主控发送 |
| EN | 3.3V或复位控制 | 悬空可能导致模块不稳定 |
| GPIO0 | 悬空或接高电平 | 低电平会进入下载模式 |
首次调试我建议先不接主控,直接用USB转TTL工具把模块接到电脑,串口助手调试最直观。串口参数固定为波特率115200、数据位8、停止位1、无校验(115200-8-N-1),发送指令时勾选"发送新行",也就是每条指令末尾自动追加\r\n回车换行。
3.2 WiFi连接AT指令实测
模块上电后,串口助手先发送AT,如果模块正常会返回OK。这一步是判断模块是否正常工作、波特率是否正确的最快方法。我遇到过一个情况:发送AT没反应,后来发现是USB转TTL工具的质量问题导致电平不稳,换了一个工具就正常了。
确认模块通信正常后,依次执行WiFi连接指令:
AT+CWMODE=1 AT+CWJAP="你的WiFi名称","你的WiFi密码"AT+CWMODE=1的作用是把模块设为Station模式,也就是作为终端设备去连接路由器。AT+CWJAP执行后,如果成功会先返回WIFI CONNECTED,再返回WIFI GOT IP,最后返回OK。从这两条返回信息就能判断WiFi的连路和IP分配都完成了。
连接路由器之后,用AT+CIFSR或者AT+CIPSTA?查询模块拿到的IP地址:
AT+CIFSR +CIFSR:STAIP,"192.168.1.100" +CIFSR:STAMAC,"xx:xx:xx:xx:xx:xx" OK看到IP之后,说明局域网通信已经通了。如果执行AT+CWJAP返回ERROR或者FAIL,优先检查WiFi密码是否填对、路由器是否开启了MAC地址过滤、WiFi是不是5G频段。需要提醒的是,WF24这类ESP8266模块只支持2.4G频段,如果家里路由器开了双频合一,搜不到2.4G网络的话,可以先用手机热点测试排除问题。
3.3 让模块上电自动连WiFi
实际产品中,设备不可能每次开机都靠人来发AT指令连WiFi,所以需要开启自动连接功能:
AT+CWAUTOCONN=1 AT+CWQAP第一行设置模块上电自动连接上次保存的WiFi,第二行是主动断开当前连接。执行完后重启模块,模块会自己连接WiFi。这一步看似无关紧要,但如果不设置,每次上电都要MCU手动重新发一遍AT+CWJAP,非常容易在量产设备上漏掉。
4. MQTT连接与数据上报
4.1 AT+MQTTUSERCFG配置三项鉴权参数
WiFi通了之后,真正的重头戏来了:配置MQTT参数并连接华为云。
乐鑫AT指令集里,MQTT配置指令是AT+MQTTUSERCFG,完整格式为:
AT+MQTTUSERCFG=<linkID>,<scheme>,<client_id>,<username>,<password>,<cert_key_ID>,<CA_ID>,<path>各参数含义如下:
| 参数 | 含义 | 本文取值 |
|---|---|---|
| linkID | MQTT连接编号 | 0 |
| scheme | 传输方式,0为TCP明文,1为TLS加密 | 0(验证阶段) |
| client_id | MQTT客户端ID | 第2节计算出的ClientId |
| username | MQTT用户名 | 设备ID |
| password | MQTT密码 | 第2节计算出的Password |
| cert_key_ID / CA_ID | 证书索引 | 0 |
| path | 证书路径 | 留空 |
假设第2节脚本算出来的三个参数分别是temp_sensor_01_0_2025060114、65a2b3c4d5e6f7_temp_sensor_01和a1b2c3d4e5f6...,那配置指令就长这样:
AT+MQTTUSERCFG=0,0,"temp_sensor_01_0_2025060114","65a2b3c4d5e6f7_temp_sensor_01","a1b2c3d4e5f60718293a4b5c6d7e8f9012345",0,0,""注意指令里的双引号是ASCII引号,严格区分大小写,密码里的十六进制字母是小写。填错任何一个字符,后面连接大概率会被华为云拒绝。
4.2 AT+MQTTCONN连接华为云
参数配置完成,执行连接指令:
AT+MQTTCONN=0,"xxxxxxxx.iot-mqtts.cn-north-4.myhuaweicloud.com",1883,1各字段含义:0是连接编号,第二个双引号里填华为云接入地址,1883是端口,最后的1表示自动重连。如果连接成功,模块会返回:
OK这里没有多余信息,一个OK就代表MQTT连接已经建立。为了确认连接确实活着,可以查询连接状态:
AT+MQTTSTATE? +MQTTSTATE: 0 OK返回值0表示MQTT连接正常。如果连不上,返回的是ERROR或+MQTTSTATE: 1。这时重点排查三件事:板子能不能Ping通外网、华为云地址和端口是否写对、鉴权三参数是否过期或计算错误。
4.3 属性上报:AT+MQTTPUB和标准Topic格式
MQTT连上之后,最核心的操作就是上报数据。华为云IoTDA的标准属性上报Topic是固定的:
$oc/devices/{device_id}/sys/properties/report{device_id}要替换成你的完整设备ID。上报数据的JSON格式也是固定的,需要在services数组里带上产品模型里定义的服务ID和属性值。假设我建了一个服务ID为temperature、属性名为value的产品模型,上报一条25.6摄氏度的数据,指令如下:
AT+MQTTPUB=0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/properties/report","{"services":[{"service_id":"temperature","properties":{"value":25.6}}]}",0,0AT+MQTTPUB的参数分别是连接编号、Topic、负载数据、QoS等级、是否保留。QoS填0即可,保留标志填0。执行成功后返回OK。
这里有个细节要特别提醒:在串口助手里发送上面这条指令,JSON内部的双引号直接写就行,不需要加反斜杠转义。但如果是在C语言等代码里拼接AT指令字符串,双引号外面套双引号就必须转义,这是很多从串口助手调试转到MCU代码时容易踩的坑。
数据上报成功后,回到华为云控制台,在设备详情的"设备影子"或"设备日志"页面就能看到最新上报的属性值。我实测从AT指令返回OK到控制台显示数据,延迟通常在几百毫秒以内。
5. 订阅消息与命令下发
5.1 订阅Topic怎么选
跟平台主动下发数据相关的Topic,华为云IoTDA主要有两类:
第一类是命令下发,平台通过下面这个Topic向设备下发命令:
$oc/devices/{device_id}/sys/commands/request_id={request_id}注意request_id是由平台生成的一串唯一标识,每次下发命令都会变化。设备想接收所有命令,需要订阅的通配Topic是:
$oc/devices/{device_id}/sys/commands/#执行订阅指令:
AT+MQTTSUB=0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/commands/#",0第二个参数是Topic,最后一个0是QoS等级。订阅成功返回OK。
第二类是消息下发,Topic格式为:
$oc/devices/{device_id}/sys/messages/down如果需要接收平台主动推送的二进制或JSON消息,就订阅这条。两条Topic都订阅也可以,互不冲突。
5.2 收到下发消息如何解析
订阅成功后,只要华为云平台往设备下发命令,模块串口会自动弹出一条上行通知,格式类似:
+MQTTSUBRECV: 0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/commands/request_id=abc123","{"command_name":"set_temperature","service_id":"temperature","paras":{"target":30}}"这条异步通知里,第一部分是消息来自哪个Topic,第二部分是命令负载。负载JSON里的command_name是你在平台产品模型里定义的命令名,paras是命令携带的参数。MCU收到这条数据后,需要做两件事:解析出request_id,因为响应用得着;解析出命令名和参数,然后执行对应动作。
有的AT固件版本返回长度字段,格式类似:
+MQTTSUBRECV: 0,"topic",56,"data"不要被这个差异吓到,本质上都是同一份数据,只是多了个payload长度前缀。MCU解析时先按逗号切分,定位到最后一个双引号字段即可。
还有一点要注意:这个通知是模块自动推送的,不依赖MCU主动查询。所以MCU串口接收要启用中断或者DMA,避免在长时间阻塞其他任务时丢失串口数据。
5.3 从设备回写命令响应
华为云对命令下发的协议要求是:设备收到命令后应当回复响应,这样控制台才能显示命令执行状态。命令响应的Topic是把下发Topic里最后的request_id=xxx替换成响应格式:
$oc/devices/{device_id}/sys/commands/response/request_id=abc123响应数据格式如下:
{"result_code":0,"response_name":"set_temperature","paras":{"result":0}}对应的AT指令是:
AT+MQTTPUB=0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/commands/response/request_id=abc123","{"result_code":0,"response_name":"set_temperature","paras":{"result":0}}",0,0result_code填0表示成功,非0表示失败。这样一套完整的命令下发链路就算闭环了。我建议在平台侧调试时故意下发一条设备不认识的命令,观察设备端串口日志,很多同学通过这种逆向方式快速搞清了Topic和JSON格式的边界。
6. 完整联调脚本与问题排查
6.1 一键跑通的完整AT指令序列
为了方便大家复现,我把整套流程的AT指令按照执行顺序整理成一份可以直接对照操作的清单。假设WiFi名是MyWiFi、密码是12345678,华为云接入地址是xxxxxxxx.iot-mqtts.cn-north-4.myhuaweicloud.com。
AT AT+CWMODE=1 AT+CWJAP="MyWiFi","12345678" AT+CIFSR AT+MQTTUSERCFG=0,0,"temp_sensor_01_0_2025060114","65a2b3c4d5e6f7_temp_sensor_01","a1b2c3d4e5f60718293a4b5c6d7e8f9012345",0,0,"" AT+MQTTCONN=0,"xxxxxxxx.iot-mqtts.cn-north-4.myhuaweicloud.com",1883,1 AT+MQTTSTATE? AT+MQTTSUB=0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/commands/#",0 AT+MQTTPUB=0,"$oc/devices/65a2b3c4d5e6f7_temp_sensor_01/sys/properties/report","{"services":[{"service_id":"temperature","properties":{"value":25.6}}]}",0,0把这段指令按顺序发送,前一条返回正确后再发下一条。这是最稳妥的联调方式,不建议一口气全部复制粘贴发送,因为WiFi连接和MQTT连接都需要时间,发太快容易乱套。
6.2 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| AT指令无响应 | 串口波特率不对/模块未复位 | 检查115200配置;手动复位模块重新上电 |
| AT+CWJAP返回ERROR | WiFi名或密码错误/路由限制 | 改写指令;换手机热点测试 |
| AT+MQTTCONN返回ERROR | 网络不通/地址端口写错 | 用AT+PING测试公网连通性;检查接入地址 |
| 连接成功后10秒左右断开 | 鉴权参数错误/时间戳过期 | 重新跑Python脚本生成参数 |
| MQTTPUB返回OK但平台无数据 | 产品模型属性不匹配 | 检查服务ID、属性名是否和模型一致 |
| 订阅命令后平台下发无反应 | 设备离线或订阅Topic不对 | 确认设备在线;核对commands/#写法 |
| 命令重复收到 | QoS1重传 | 平台下发用QoS0,或设备侧按request_id去重 |
6.3 再补几个避坑心得
最后分享几条这个方案里最容易被忽略的经验。
时间戳过期问题是华为云AT接入里最隐蔽的一个坑。我第一次调的时候,在电脑上生成参数后,慢慢悠悠接线、发AT指令,结果连一次失败一次,排查了半天才发现是时间戳已经过了那个整点小时。后来我把参数生成脚本写成了每次连接前自动执行,再也不手动复制时间戳了。
模块供电的余量一定要留够。WF24这类ESP8266模块在WiFi发包瞬间电流尖峰能到300mA以上,如果供电电路压降太大,就会出现一种诡异现象:串口调试时手动发AT一切正常,但MQTT一连接,模块就重启。排查方法很简单,用示波器或者万用表观察模块VCC引脚在发包瞬间的电压有没有跌落。这个问题在USB转TTL供电的场景下特别常见,我现在的做法是直接在模块旁边加一个100uF钽电容,效果立竿见影。
MCU串口收数据要用中断或DMA。AT指令看似简单,但+MQTTSUBRECV这种异步消息是随时可能到达的,如果MCU主循环忙于其他任务,就会丢数据。我在这套方案里用的是串口DMA加空闲中断,把收到的完整帧丢给解析队列,实测在115200波特率下非常稳定。
订阅命令下发时,request_id是动态变化的。我在第一个版本里把request_id写死成测试值,结果平台一直报命令响应异常,排查了很久才发现问题。正确的做法是从下发Topic字符串里把request_id=后面的部分提取出来,再拼到响应Topic里,这个字符串处理逻辑一定要在MCU端做对。