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 的核心难点不是代码量,而是对固件行为的理解

  1. RTU 固件必须先 AT+DTUTASKS="ONENET"——这是最隐蔽的坑,TCP 通了 MQTT 不通,容易误导你怀疑参数配错
  2. MQTT_CONNECT 是异步的——收到后要等 5 秒再确认,刚连上就被踢掉的情况很常见
  3. 属性标识符要和平台严格对齐——包括名字和数据类型,float 传整数、int 传浮点都可能被静默丢弃
  4. 重连机制要有冷却期——不要断线就立刻重试,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 实测验证

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐