ML307R 4G Cat.1 模组接入 OneNET 新平台 MQTT 全流程
ML307R 4G Cat.1 模组接入 OneNET 新平台 MQTT 全流程
适用固件:ML307R-DC-RTU V4.34 | 板卡:ML307R-DC Mini | MCU:STM32F103RC
通信参数:115200 8N1 | 平台:OneNET Studio(新平台物模型)
一、硬件连接
ML307R-DC Mini 核心板只有 6 个引脚,和 STM32 的接线非常简单:
| STM32 | ML307R | 功能 |
|---|---|---|
| PB10 (USART3_TX) | RXD | STM32 → 模组 |
| PB11 (USART3_RX) | TXD | 模组 → STM32 |
| PB12 (GPIO) | PWR_EN | 电源使能,高电平供电 |
| 3.3V ~ 4.2V | VCC | 供电(推荐 4V) |
| GND | GND | 共地 |
⚠️ 上电顺序:先拉高 PB12 给模组供电,再等 3~5 秒让模组完成 SIM 卡初始化和驻网,然后才发 AT 指令。
STM32 端 PB12 初始化:
GPIO_InitTypeDef g;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
g.GPIO_Pin = GPIO_Pin_12;
g.GPIO_Speed = GPIO_Speed_50MHz;
g.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &g);
GPIO_SetBits(GPIOB, GPIO_Pin_12); // EN=1, 给模组供电
delay_ms(3000); // 等模组上电启动
二、固件前置知识——RTU 和 DTU 的关键区别
ML307R 市面上常见的固件有两个版本:
| DTU V3.51 | RTU V4.34 | |
|---|---|---|
| 默认运行任务 | ONENET |
TCP |
| 是否直接支持 AT+ONENET | ✅ 直接可用 | ❌ 需先切任务 |
| 额外功能 | — | MODBUS、IO 控制、GPS |
如果你买到的是 RTU 固件(出货量更大),上电后模组默认跑的是 TCP 透传任务。此时直接发 AT+ONENET=... 配置 OneNET,模组会返回 ERROR,或者 TCP 能通但 MQTT 鉴权永远失败。
★ 这是本文最重要的知识点:RTU 固件必须先执行
AT+DTUTASKS="ONENET",切换任务模式后才能使用 OneNET 系列指令。
查询当前任务:
AT+DTUTASKS?
如果返回的不是 ONENET,就必须先切:
AT+DTUTASKS="ONENET"
三、完整 AT 初始化流程
以下是实测通过的完整 AT 指令序列。双引号是命令语法的一部分,照抄即可。
3.1 阶段一:确认模组在线
AT
期待:OK
如果 3 次都无响应,说明模组没上电或串口不通,先检查 PB12 和接线。
3.2 阶段二:确认驻网
AT+CEREG?
期待:+CEREG=1 或 +CEREG=5(1=已注册归属网络,5=已注册漫游网络)
AT+CSQ?
查看信号强度,返回值在 10~31 之间表示可用。
AT+IPCALL?
期待:+IPCALL=1(PDP 拨号成功,获取到 IP)
模组上电后也会主动推送启动信息,包括:
ML307R-DC-DTU V2.7
SIM:READY
REGSTER:SUCCESS
所以你也可以不主动查询,而是等待 REGSTER:SUCCESS 关键字的出现——这是模组确认驻网成功的标志。
3.3 阶段三:切换任务 + OneNET 鉴权(★ 核心)
# 步骤 1:切任务模式(RTU 必做!)
AT+DTUTASKS="ONENET"
# 步骤 2:配置鉴权参数
# 格式:AT+ONENET=<注册模式>,"产品ID","产品AccessKey","设备名称"
# 注册模式 0=手动指定设备名,1=IMEI自动
AT+ONENET=0,"4sQh7LI8MA","1aPPH2+zJ0+NhyFTx6OXuyfnq/9JP8c5bYFdV4NP/cU=","temp"
# 步骤 3:Clean Session(OneNET 强制要求为 1)
AT+ONENETCLEAN=1
# 步骤 4:MQTT 保活心跳(单位:秒)
AT+ONENETKEEP=300
# 步骤 5:开启多通道 + 订阅系统 Topic
AT+ONENETMULT=1
AT+ONENETSUB=0,1 # 订阅通道0: thing/property/post/reply(上行响应)
AT+ONENETSUB=1,1 # 订阅通道1: thing/property/set(云端下发的属性设置指令)
3.4 阶段四:重启生效 + 等待 MQTT 连接
# 重启模组,让以上配置生效
AT+REST
# 等待约 8 秒让模组重新启动并建立 MQTT 连接,
# 直到收到模组推送:
MQTT_CONNECT:112.54.89.90:8543
⚠️ 关键细节:
MQTT_CONNECT出现后不要立即判为成功。这是异步提示——模组告诉你 TCP 建连了,但 MQTT 会话还在建立中。再等 5 秒,然后检查缓冲区里有没有MQTT_DISCONNECT。如果会话刚建好就被平台拒绝(通常是参数错误),你会收到:MQTT_CONNECT:112.54.89.90:8543 MQTT_DISCONNECT
3.5 完整初始化流程图
AT → OK
↓
AT+CEREG? → 已驻网 (1或5)
↓
AT+DTUTASKS="ONENET" → OK ★ RTU 必做
↓
AT+ONENET=0,"pid","key","dev" → OK
↓
AT+ONENETCLEAN=1 → OK
AT+ONENETKEEP=300 → OK
AT+ONENETMULT=1 → OK
AT+ONENETSUB=0,1 → OK
AT+ONENETSUB=1,1 → OK
↓
AT+REST → 等 8 秒
↓
收到 MQTT_CONNECT: ... → 再等 5 秒
↓
无 MQTT_DISCONNECT → ✅ 连接成功
3.6 STM32 发送 AT 指令的封装函数
/**
* @brief 发送 AT 指令,等待期望应答(最多重试 3 次)
* @retval 1=收到期望应答 0=超时
*/
static uint8_t SendCmd(const char *cmd, const char *expect,
uint16_t timeout_ms)
{
uint8_t i;
for (i = 0; i < 3; i++) {
clear_4g_buff(); // 清空 USART3 接收缓冲
USART3_SendString(cmd);
USART3_SendString("\r\n"); // AT 指令以 \r\n 结尾
delay_ms(timeout_ms / 3 + 1);
if (strstr(usart3_buff, expect)) // 扫描缓冲区关键字
return 1;
}
return 0;
}
核心就是发指令 → 延时 → 扫缓冲区关键字。延时时间不能太短,4G 模组响应通常需要 300~1000ms。
四、数据上传——OneJSON 物模型格式
4.1 OneJSON 格式
OneNET 物模型使用如下 JSON 格式上传属性:
{
"id": "1",
"params": {
"温度": { "value": 25.5 },
"湿度": { "value": 65.2 }
}
}
⚠️ 属性名必须和 OneNET 控制台物模型里定义的标识符严格一致。如果云端定义的是
temp_value,代码里写成temperature,OneNET 不会报错,但也不会显示数据——静默丢弃。
4.2 发布命令
ONENET 任务模式下,MQTT 的 Topic 由固件自动管理,你不需要手动指定。发布数据使用 MQPUB 指令(注意它不是 AT 指令,不要加 AT 前缀):
# MQPUB,<发布模式>,<通道ID>,<JSON数据>
# 发布模式 0 = 使用固件内置通道(推荐)
# 通道ID 0 = thing/property/post(属性上报)
MQPUB,0,0,{"id":"1","params":{"temp_value":{"value":22.5}}}
⚠️ 注意:是
MQPUB,0,0,...不是MQPUB,1,0,...。第一个参数 0 表示"使用内置发布通道",具体 Topic 由固件的AT+ONENETPUBLS?查询结果决定。
4.3 每次只发一个属性
RTU 固件对单帧 MQPUB 数据长度有限制(约 128~256 字节)。如果你把所有属性打包在一帧 JSON 里发送,偶尔会失败。推荐做法:每次只发一个属性,帧长控制在 80 字节以内。连续调用 5 次完成全部属性更新。
static uint8_t prop_index = 0; // 0=温度 1=湿度 2=土壤 3=光照 4=CO2
void PublishOneProperty(const greenhouse_data_t *data)
{
char json[128];
char pub[160];
switch (prop_index) {
case 0: // 温度 — float 类型
sprintf(json,
"{\"id\":\"%u\",\"params\":{\"temp_value\":"
"{\"value\":%s%d.%d}}}",
msg_id, data->temp < 0 ? "-" : "",
abs(data->temp) / 10, abs(data->temp) % 10);
break;
case 1: // 湿度 — int 类型,只送整数
sprintf(json,
"{\"id\":\"%u\",\"params\":{\"humidity_value\":"
"{\"value\":%d}}}",
msg_id, data->humi / 10);
break;
// ... soil / light / co2 类似 ...
}
sprintf(pub, "MQPUB,0,0,%s", json);
USART3_SendString(pub);
USART3_SendString("\r\n");
prop_index = (prop_index + 1) % 5;
msg_id++;
}
4.4 发布响应处理
MQPUB 不是 AT 指令,固件不会回复 OK。判断成功/失败的方法是扫描模组的异步响应:
| 响应内容 | 含义 |
|---|---|
"code":200 |
OneNET 平台确认收到 |
MQTT_DISCONNECT |
MQTT 连接已断开,必须重连 |
ERROR |
指令格式错误 |
"code":4 或 "code":5 |
OneNET 拒绝(参数/权限问题) |
// 发 MQPUB 后等待 3~5 秒,然后扫描响应
delay_ms(5000);
if (strstr(usart3_buff, "\"code\":200") ||
strstr(usart3_buff, "OK") ||
strstr(usart3_buff, "SUCCESS")) {
// 成功
} else if (strstr(usart3_buff, "MQTT_DISCONNECT")) {
s_mqtt_ok = 0; // ★ 立即标记断连,触发重连
} else if (strstr(usart3_buff, "ERROR")) {
// 格式错误,打印响应内容排查
}
五、下行指令接收——云端远程控制
5.1 接收格式
当云端下发属性设置指令时,模组会推送 +MQRECV URC(Unsolicited Result Code):
+MQRECV,1,$sys/xxx/thing/property/set,42,{"id":"7","params":{"fan":true}}
格式解析:
+MQRECV,<topic_id>,<topic>,<datalen>,<json_payload>
1 ... 42 {"id":"7",...}
5.2 解析代码
static void ParseDownlink(void)
{
char *p = strstr(usart3_buff, "+MQRECV,");
if (p == NULL) return;
p += 8; // 跳过 "+MQRECV,"
// 跳过 topic_id 和 topic,定位 datalen
while (*p != ',' && *p != '\0') p++;
if (*p == '\0') return;
p++; // 跳过逗号
// 找 datalen 后面的逗号
char *comma = p;
while (*comma != '\0') {
if (*comma == ',') break;
comma++;
}
if (*comma == '\0') return;
uint16_t datalen = atoi(comma + 1); // 跳过逗号读长度
// 再跳一个逗号到达 JSON payload
comma++;
while (*comma != ',' && *comma != '\0') comma++;
if (*comma == '\0') return;
comma++;
// 拷贝 JSON 到命令队列,主循环取出后解析执行
memcpy(cmd_queue[wr], comma, datalen);
cmd_queue[wr][datalen] = '\0';
wr = (wr + 1) % 4;
clear_4g_buff();
}
5.3 回复云端 ACK
收到属性设置指令后,向通道 1(set_reply)回复应答:
MQPUB,0,1,{"id":"7","code":200,"msg":"success"}
六、STM32 完整初始化代码框架
下面是一个可直接参考的 ML307R_Init() 框架:
void ML307R_Init(void)
{
printf("[4G] Waiting for module startup...\r\n");
delay_ms(5000); // 等模组上电
// ---- 1. AT 通信测试 ----
if (!SendCmd("AT", "OK", 1000)) {
// 发 AT+REST 软重启再试
USART3_SendString("AT+REST\r\n");
delay_ms(8000);
if (!SendCmd("AT", "OK", 1000)) {
printf("[4G] AT no response! Check power/wiring\r\n");
return;
}
}
// ---- 2. 等待驻网 ----
// 模组会主动推 REGSTER:SUCCESS,也可以主动查 AT+CEREG?
if (!SendCmd("AT+CEREG?", "+CEREG=\"1\"", 5000) &&
!SendCmd("AT+CEREG?", "+CEREG=\"5\"", 5000) &&
!WaitBoot(30)) // 备用:等 30 秒
{
printf("[4G] Network timeout! Check SIM/signal\r\n");
return;
}
s_cell_ok = 1;
// ---- 3. OneNET 连接 ----
s_init_done = 1; // ★ 先置位,让状态机可以后台重连
if (OneNET_Connect()) {
s_mqtt_ok = 1;
printf("[4G] === MQTT CONNECTED! ===\r\n");
}
}
static uint8_t OneNET_Connect(void)
{
char cmd[200];
if (!SendCmd("AT+DTUTASKS=\"ONENET\"", "OK", 5000))
return 0; // ★ RTU 必做
sprintf(cmd, "AT+ONENET=0,\"%s\",\"%s\",\"%s\"",
PRODUCT_ID, ACCESS_KEY, DEVICE_NAME);
if (!SendCmd(cmd, "OK", 5000)) return 0;
if (!SendCmd("AT+ONENETCLEAN=1", "OK", 1000)) return 0;
sprintf(cmd, "AT+ONENETKEEP=%s", KEEPALIVE);
if (!SendCmd(cmd, "OK", 1000)) return 0;
if (!SendCmd("AT+ONENETMULT=1", "OK", 1000) ||
!SendCmd("AT+ONENETSUB=0,1", "OK", 1000) ||
!SendCmd("AT+ONENETSUB=1,1", "OK", 1000))
return 0;
// 重启 — 模组自动用以上参数去连 MQTT
clear_4g_buff();
USART3_SendString("AT+REST\r\n");
delay_ms(8000);
// 等 MQTT_CONNECT,最多等 30 秒
for (int w = 0; w < 30; w++) {
delay_ms(1000);
if (strstr(usart3_buff, "MQTT_CONNECT:")) {
clear_4g_buff();
delay_ms(5000); // 等会话稳定
if (strstr(usart3_buff, "MQTT_DISCONNECT")) {
// 被平台拒绝
clear_4g_buff();
return 0;
}
return 1; // ✅ 成功
}
}
return 0; // 超时
}
重连状态机(精简)
void ML307R_ReconnectTask(void)
{
// 已连接:扫描下行指令
if (s_mqtt_ok) {
ParseDownlink();
return;
}
// 已断线:冷却 60 秒后重试
if (!s_init_done) return;
if (tick_now - last_reconn < 60 * 20) return; // 约60秒冷却
printf("[4G] Reconnecting...\r\n");
if (OneNET_Connect()) {
s_mqtt_ok = 1;
printf("[4G] Reconnected!\r\n");
}
last_reconn = tick_now;
}
七、常见故障速查
| 现象 | 最可能的原因 | 排查步骤 |
|---|---|---|
AT 无响应 |
模组没上电 / 接线错 | 测 PB12 是否 HIGH;RX/TX 是否交叉 |
REGSTER 超时 |
SIM 卡问题 / 信号差 | 检查 SIM 卡方向、天线是否接好、AT+CSQ 信号值 |
TCP_CONNECT OK 但无 MQTT_CONNECT |
未切 ONENET 任务 | AT+DTUTASKS? 确认当前模式 |
MQTT_CONNECT 后立即 MQTT_DISCONNECT |
鉴权参数错误 | 检查产品 ID/AccessKey/设备名是否和平台一致 |
| MQTT 连上但 OneNET 看不到数据 | 属性标识符不匹配 | 代码里的 JSON key 和平台物模型逐项比对 |
| MQTTPUB 间歇性失败 | JSON 帧太长 | 改为每次只发一个属性 |
| MQTT 断线后永远不重连 | 状态标志未清零 | 收到 DISCONNECT 必须置 s_mqtt_ok=0 |
| 串口打印乱码 | MicroLIB 不支持 %f |
所有浮点格式化改为 %d.%d 整数拆分 |
八、总结
ML307R 接入 OneNET 的核心难点不是代码量,而是对固件行为的理解:
- RTU 固件必须先
AT+DTUTASKS="ONENET"——这是最隐蔽的坑,TCP 通了 MQTT 不通,容易误导你怀疑参数配错 MQTT_CONNECT是异步的——收到后要等 5 秒再确认,刚连上就被踢掉的情况很常见- 属性标识符要和平台严格对齐——包括名字和数据类型,float 传整数、int 传浮点都可能被静默丢弃
- 重连机制要有冷却期——不要断线就立刻重试,60 秒一次避免频繁
AT+REST
掌握了以上流程,从拿到 ML307R 到数据出现在 OneNET 控制台,顺利的话 1~2 小时就能打通。
配套资料:
- 本文涉及的完整 AT 指令集: ML307R-DC-RTU AT指令集完全参考版
- STM32 端完整代码(含 CCS2530 ZigBee 采集 + OLED 显示 + 继电器控制):IoT_Agr_System 工程
- AT 串口调试工具推荐:SSCOM、QCOM(115200 8N1)
文档版本:2026-07-17 | 基于 ML307R-DC-RTU V4.34 实测验证
更多推荐
所有评论(0)