概述
本文档以环境监测终端 产品为例,介绍基于 STM32 MCU + EG800Z 模组 的 AT 指令方案接入移远物联网开发者中心的完整上云流程。设备端采用 at_middleware 中间件驱动 MCU 与模组间的 AT 通信,通过 TTLV 编解码实现物模型数据上下行,最终在 PaaS 平台、SaaS 系统与移联万物 App 中形成全链路闭环。
本方案中,EG800Z 模组仅承担通信职责,STM32 MCU 作为主控负责采集 SPL06-001 气压/温度 、光敏光照 、雨量 三类传感器数据并上报云端,同时根据雨量阈值本地联动驱动 RGB 指示灯;两者通过 UART 使用 AT 指令交互。

环境监测终端 AT 方案全链路架构
图1:环境监测终端 AT 方案全链路架构图
本文档与《RGB灯蜂窝模组AT方案上云全流程实操指南》同源,设备端中间件、AT 命令表、URC 处理、初始化状态机的框架一致。差异主要在于多传感器采集、7 项物模型属性、周期上报与本地联动控制。
1. 前置条件
1.1 硬件与开发环境
|--------------|-----------------------------------------|-----|
| 项目 | 型号/说明 | 数量 |
| MCU 开发板 | STM32F103ZET6 | 1 |
| 蜂窝模组 | EG800Z(支持 QuecThing AT 指令,固件版本 ≥ 2.3.3) | 1 |
| 气压/温度传感器 | SPL06-001(I2C,高精度数字气压+温度,可解算海拔) | 1 |
| 光敏传感器 | 模拟量光敏模块(AO 输出) | 1 |
| 雨量传感器 | 模拟量雨量模块(AO 输出) | 1 |
| RGB 灯珠 | WS2811F 全彩灯珠(本地联动指示) | 1 |
| SIM 卡 | 已激活的物联网 SIM 卡(确保有流量) | 1 |
| IDE | Keil MDK-ARM v5(或 STM32CubeIDE) | --- |
| 烧录工具 | ST-Link 或 J-Link | --- |
| 串口调试工具 | QCOM / SecureCRT / SSCOM(任选其一) | --- |
| USB 转 TTL 模块 | 串口调试与固件烧录 | 1 |
| 杜邦线 | 硬件连接 | 若干 |
| 工程源码 | 08_AT_EnvironmentalMonitoring 工程源码 | --- |
模组固件版本必须 ≥ 2.3.3,否则 QuecThing AT 指令不可用。可通过 AT+QIOTSTATE? 返回 ERROR 判断版本是否支持。
本方案涉及的核心硬件实物如下。第一排为小尺寸模块(EG800Z 模组、SPL06 气压计、光敏传感器),第二排为大尺寸模块(STM32 开发板、WS2811F RGB 灯、雨量传感器):
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
|
|
|
(第一排:EG800Z 模组 / SPL06 气压计 / 光敏传感器)
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
|
|
|
图2:主要硬件实物(第二排:STM32 开发板 / WS2811F RGB 灯 / 雨量传感器)
实物图中的气压/光敏/雨量模块以正点原子 ATK 系列模块为例,实际选型只要输出接口一致(SPL06 为 I2C,光敏/雨量为模拟量 AO)即可替换。
1.2 接线说明
根据工程代码 硬件连接.md、main.h、at_uart.c、light_sensor.c、spl06_i2c.c,MCU 与模组、各传感器、RGB 灯珠的引脚连接关系如下:
|------------|--------|---------------------|-----------|-----------------------|
| MCU 端 | MCU 引脚 | 外设引脚 | 信号方向 | 说明 |
| USART1_TX | PA9 | 模组 RXD | MCU → 模组 | AT 命令发送 |
| USART1_RX | PA10 | 模组 TXD | 模组 → MCU | AT 响应接收 |
| USART2_TX | PA2 | USB-TTL RXD | MCU → PC | 调试日志发送 |
| USART2_RX | PA3 | USB-TTL TXD | PC → MCU | 调试日志接收 |
| GPIO | PA8 | WS2811F DIN | MCU → RGB | WS2811 数据时序(bit-bang) |
| ADC1_IN4 | PA4 | 光敏模块 AO | 传感器 → MCU | 光照模拟量采集 |
| ADC1_IN5 | PA5 | 雨量模块 AO | 传感器 → MCU | 雨量模拟量采集(联动) |
| 软件 I2C SCL | PB8 | SPL06 SCL | MCU → 传感器 | 气压 I2C 时钟 |
| 软件 I2C SDA | PB9 | SPL06 SDA | 双向 | 气压 I2C 数据 |
| GND | GND | 模组/各传感器/WS2811F GND | 共地 | 所有器件共地 |
与 RGB 灯方案的差异 :本方案把 AT 通信(USART1)与调试日志(USART2)分到两个串口 ,互不干扰;SPL06 走 软件模拟 I2C(PB8/PB9,非硬件 I2C 外设)。
此外还需正确接入各器件电源:
|-----------------|------------|-----------------|------------------------------------|
| 器件 | 电源引脚 | 接法 | 说明 |
| EG800Z 模组 | VCC / VBAT | 底板 3.8~4.2V 供电 | 模组瞬时电流较大,须由专用电源电路供电,不可从 MCU I/O 取电 |
| SPL06 / 光敏 / 雨量 | VCC | 3.3V | 模拟传感器与 I2C 器件均可取板载 3.3V |
| WS2811F | VCC | 开发板 5V | 可直接接开发板 5V 排针 |
| 各器件 | GND | 与 MCU 共地 | 模拟量采集、I2C 通信、WS2811 时序都要求共地 |
供电与共地提示:模拟量传感器(光敏/雨量)与 MCU 必须共地,否则 ADC 读数漂移;SPL06 的 I2C 与 WS2811 数据线同样要求与 MCU 共地,否则通信/时序异常。

MCU 与模组 / 传感器 / RGB 灯引脚连接
图3:MCU 与模组、SPL06、光敏/雨量、RGB 灯引脚连接图
1.3 串口配置
|-----|--------|-----------------------------------------------------------|
| 参数 | 值 | 代码设计 |
| 波特率 | 115200 | usart.c 中 huart1.Init.BaudRate = 115200(USART1/USART2 一致) |
| 数据位 | 8 | UART_WORDLENGTH_8B |
| 停止位 | 1 | UART_STOPBITS_1 |
| 校验位 | None | UART_PARITY_NONE |
| 流控 | None | UART_HWCONTROL_NONE |
2. 云平台配置
2.1 创建产品
登录 移远物联网开发者中心,进入 产品开发 → 产品管理,点击"创建产品"。

产品创建入口
图4:开发者中心 --- 产品创建入口
选择合适的环境监测/传感器 相关品类(若无完全匹配品类,可选"自定义品类"并自行定义功能点),连网方式选择 2G/3G/4G/5G ,数据格式选择 物模型,自定义产品名称,点击"创建"。

品类选择
图5:品类选择与查找
创建成功后,平台分配 ProductKey(PK)与 ProductSecret(PS)。本工程示例值为 PK p11zA8、PS RTRQLzBGRHlaci9Q,实际使用时必须替换为你自己产品的 PK/PS。
PK 与 PS 是设备认证的关键凭证,请妥善保管。可在产品详情页直接查看,也会通过邮件发送至注册邮箱。
除 PK/PS 外,设备连接平台还需消费激活码(License)------每台设备连接时消费一个,数量不足将无法注册上线。开发者中心为每个账号赠送免费激活码(企业账号 10 个、个人账号 5 个)供开发调试,量产或额度耗尽后需联系移远采购。
上云前请在产品管理 页面确认目标产品的剩余可用激活码 > 0 。激活码机制详见 激活码介绍。
2.2 定义物模型
在产品详情 → 功能定义 页面添加以下物模型功能点。本方案共 7 个属性,功能 ID 必须与设备端一致:

功能定义页面
图6:功能定义页面
|-------|----------------|------|------|------|-----------------------------------|
| 功能 ID | 功能名称(code) | 功能类型 | 读写类型 | 数据类型 | 说明 |
| 1 | Rain_level | 属性 | 读写 | INT | 雨量原始 ADC(PA5,0~4095) |
| 2 | Pressure_level | 属性 | 读写 | INT | 气压(hPa,SPL06 解算后截去小数) |
| 3 | Light_level | 属性 | 读写 | INT | 光照原始 ADC(PA4,0~4095) |
| 4 | control | 属性 | 读写 | BOOL | 强控开关(true=强制亮白灯,false=交回本地雨量联动) |
| 5 | rain_limit | 属性 | 读写 | INT | 雨量联动阈值(PA5 ADC,0~4095,初始化为 1000) |
| 6 | Temp | 属性 | 读写 | INT | 温度(℃,截去小数) |
| 7 | Altitude | 属性 | 读写 | INT | 海拔估算值(m,截去小数) |
功能 ID 必须与设备端 TTLV 编解码中的 Tag 保持一致。本工程 thing_model_tabel.h 的枚举 PROP_RAIN_LEVEL=1 ... PROP_ALTITUDE=7 即对应上表的功能 ID。
数据类型约束:中间件对 DTYPE_INT 固定按 4 字节(int32_t)解引用、DTYPE_BOOL 按 1 字节(uint8_t)。定义物模型与绑定本地变量时类型宽度必须对应,否则真机上行/下行会错位。
3. 设备端开发
本章节是全文重点,按操作步骤组织,介绍 MCU 与模组间如何通过 AT 中间件实现采集、上报、下行控制的完整上云链路。
中间件原理与 API 说明参见 AT命令中间件说明与移植指南.md,本章节不再重复,聚焦于"需要自己实现什么"。
关于文件名的约定 :本章以 demo 工程 08_AT_EnvironmentalMonitoring 的文件名(at_uart.c / app.c / at_cmd_table.c / at_urc_table.c / at_dev.c / thing_model_tabel.c 等)为例进行说明。 - 若你直接基于 demo 二次开发 ,文中文件一一对应,函数骨架已存在,按提示补全即可; - 若你移植到自有工程 ,文件名可自定,文中括号内的文件名仅为示例。重点是把文中列出的接口实现出来并完成挂接,而非拘泥于文件名。
获取工程源码:打开本资料库 knowledge-base/quectel-iot/辅助文档(自定义)/08_AT_EnvironmentalMonitoring/,使用完整工程,不要仅复制本文节选独立编译。先按 3.2~3.10 核对接口、产品信息与采集链路,再按 3.11 编译烧录;本文代码片段保留真实接口和执行条件。
3.1 工程结构概览
工程采用三层结构,客户移植时需要关注"应用层"和"移植层":
08_AT_EnvironmentalMonitoring/
├── APP/ 应用层(客户需要编写或修改)
│ ├── app.c 主初始化与主循环
│ ├── at_cmd_table.c AT 命令表
│ ├── at_urc_table.c URC 事件表 + 下行处理
│ ├── at_dev.c 模组初始化状态机 + 上报
│ ├── at_uart.c 串口移植层
│ └── thing_model_tabel.c 物模型属性表(本方案 7 属性)
├── Middleware/ 中间件层(核心可直接复用)
│ ├── at_core.c/h AT 中间件核心引擎
│ ├── spsc_fifo.c/h 无锁 FIFO
│ ├── ttlv.c/h TTLV 编解码
│ ├── thing_model.c/h 物模型通用编解码器
│ ├── rgb_led.c/h WS2811F RGB 驱动 + 联动状态
│ ├── light_sensor.c/h 光敏/雨量 ADC 采集 + 联动 + 周期上报
│ ├── spl06.c/h SPL06 气压/温度/海拔驱动
│ ├── spl06_i2c.c/h SPL06 软件 I2C 底层
│ └── log.c/h 日志
├── Core/ STM32 HAL 核心(CubeMX 生成)
├── Drivers/ STM32 HAL 驱动
└── MDK-ARM/ Keil 工程文件
Middleware/ 中的 at_core、spsc_fifo、ttlv、thing_model 为中间件核心,可直接复用;rgb_led、light_sensor、spl06* 为本方案的业务/传感器驱动,可按自己的硬件裁剪替换。
3.2 移植中间件到工程
- 从 08_AT_EnvironmentalMonitoring/Middleware/ 目录复制以下核心文件到工程的 Middleware/ 目录:
- at_core.c / at_core.h
- spsc_fifo.c / spsc_fifo.h
- ttlv.c / ttlv.h
- thing_model.c / thing_model.h
- 按需复制业务/传感器驱动:rgb_led.*、light_sensor.*、spl06.*、spl06_i2c.*。
- 在 Keil 工程的 Include Paths 中添加:Middleware,并将上述 .c 文件加入工程编译。
3.3 实现串口移植层
在工程的串口驱动源文件 (demo 中为 APP/at_uart.c)中实现下面 4 个函数------前 3 个是中间件要求的必填接口(发送、发结束符、启动接收),第 4 个是串口接收中断回调,负责把收到的字节推入中间件 FIFO。本工程 AT 通信走 USART1(PA9/PA10)。
#include "at_uart.h"
#include "usart.h"
uint8_t huart1_rx_data;
/* 1. 发送:阻塞式输出字节流 */
void usart1_send(const char *data, uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET)
;
huart1.Instance->DR = (uint8_t)datai;
}
}
/* 2. 发送结束符 \r\n */
void usart1_send_line_ending(void)
{
usart1_send("\r\n", 2);
}
/* 3. 启动中断接收(由 at_port_t.init 调用) */
void usart1_start_recv_data(void)
{
HAL_UART_Receive_IT(&huart1, &huart1_rx_data, 1);
}
/* 4. 串口接收完成中断回调:把字节推入 FIFO */
void usart1_recv_irq(at_channel_t *channel)
{
at_recv_push(channel, (const uint8_t *)&huart1_rx_data, 1);
usart1_start_recv_data();
}
继续核对 Core/Src/stm32f1xx_it.c 的 HAL 回调与 APP/app.c 的转发函数,两层均需保留:
/* Core/Src/stm32f1xx_it.c */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
extern void uart_irq(void);
uart_irq();
}
}
/* app.c */
void uart_irq(void)
{
usart1_recv_irq(&g_at_channel);
}
调试日志走 USART2(PA2/PA3),由 log.c 独立输出,不与 AT 通道共用串口。若 AT 命令发得出去却收不到 OK,优先检查 USART1 的接收中断是否已挂到 usart1_recv_irq。
3.4 定义物模型属性表并绑定本地变量
在物模型表文件 (demo 中为 APP/thing_model_tabel.c)中,把 7 个属性登记到 tm_property_table,并在 thing_model_init() 里把每个属性绑定到本地变量。物模型注册与绑定必须在 AT 通道装配之前完成,否则云端下行写入的是空指针。
/* APP/thing_model_tabel.c:保留原文件的头文件 */
static void prop_changed_noop(void)
{
}
extern void on_control_change(void);
/* thing_model_tabel.c:7 属性表(节选) */
tm_t tm_property_tablePROPERTY_ITEM_NUM = {
{ .func_id = PROP_RAIN_LEVEL, .func_type = FTYPE_PROPERTY, .func_name = "Rain_level",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_PRESSURE_LEVEL, .func_type = FTYPE_PROPERTY, .func_name = "Pressure_level",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_LIGHT_LEVEL, .func_type = FTYPE_PROPERTY, .func_name = "Light_level",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_CONTROL, .func_type = FTYPE_PROPERTY, .func_name = "control",
.data_type = DTYPE_BOOL, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_RAIN_LIMIT, .func_type = FTYPE_PROPERTY, .func_name = "rain_limit",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_TEMP, .func_type = FTYPE_PROPERTY, .func_name = "Temp",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
{ .func_id = PROP_ALTITUDE, .func_type = FTYPE_PROPERTY, .func_name = "Altitude",
.data_type = DTYPE_INT, .rw_type = RWTYPE_READ_WRITE },
};
void thing_model_init(void)
{
tm_table_init(FTYPE_PROPERTY, tm_property_table, PROPERTY_ITEM_NUM);
/* 把本地变量绑定到各属性:云端下行写字段、上行读字段走同一块内存 */
tm_property_bind(PROP_RAIN_LEVEL, &g_rain_raw, prop_changed_noop);
tm_property_bind(PROP_PRESSURE_LEVEL, &g_pressure_level, prop_changed_noop);
tm_property_bind(PROP_LIGHT_LEVEL, &g_light_raw, prop_changed_noop);
tm_property_bind(PROP_CONTROL, &rgb_led.control, on_control_change); /* 强控回调 */
tm_property_bind(PROP_RAIN_LIMIT, &rgb_led.rain_limit, prop_changed_noop);
tm_property_bind(PROP_TEMP, &g_temp, prop_changed_noop);
tm_property_bind(PROP_ALTITUDE, &g_altitude, prop_changed_noop);
}
绑定变量的类型必须与物模型数据类型宽度一致:INT → int32_t(如 g_rain_raw、g_temp),BOOL → uint8_t(如 rgb_led.control)。只读/仅上报的属性回调可挂空实现 prop_changed_noop;control 需要立即生效,挂 on_control_change。
3.5 初始化中间件通道与传感器(app.c)
在工程的主初始化文件 (demo 中为 APP/app.c)的 mcu_init() 中,先注册物模型,再装配 AT 通道,最后初始化各传感器;在主循环 mcu_task() 中周期调用状态机、采集任务与上报任务。
先保留同文件中的端口配置和缓冲区定义,再核对初始化调用顺序:
static at_port_t g_at_port = {
.init = usart1_start_recv_data,
.get_tick_ms = HAL_GetTick,
.lock = NULL,
.unlock = NULL,
.send = usart1_send,
.send_end_line = usart1_send_line_ending,
};
static at_callback_t g_at_cb = {
.timeout_cb = NULL,
.resp_done_cb = NULL,
};
uint8_t channel_rx_buf256;
uint8_t channel_resp_buf256;
at_channel_t g_at_channel;
void mcu_init(void)
{
/* 1. 物模型注册与绑定(必须在 AT 通道装配之前) */
thing_model_init();
/* 2. AT 通道初始化 */
at_set_port(&g_at_channel, &g_at_port);
at_set_cb(&g_at_channel, &g_at_cb);
at_cmd_set_table(&g_at_channel, g_cmd_table, APP_CMD_MAX);
at_urc_set_table(&g_at_channel, g_urc_table, g_urc_table_count);
at_set_rx_buff(&g_at_channel, channel_rx_buf, sizeof (channel_rx_buf));
at_set_resp_buff(&g_at_channel, channel_resp_buf, sizeof (channel_resp_buf));
at_init(&g_at_channel);
/* 3. RGB LED 初始化 */
rgb_led_init();
/* 4. 光敏/雨量 ADC 采集初始化(ADC 已在 MX_ADC1_Init 中初始化好) */
light_sensor_init();
/* 5. 气压计 SPL06 初始化(软件 I2C = PB8/PB9) */
if (spl06_init() != 0)
LOG("SPL06 init failed\r\n");
else
LOG("SPL06 init OK\r\n");
}
void mcu_task(void)
{
module_task(&g_at_channel); /* 模组初始化流程(3.7 节) */
at_state_machine(&g_at_channel); /* 中间件状态机推进 */
led_task(); /* 板载指示灯 */
light_sensor_task(); /* 采 PA4/PA5 + 雨量联动(3.9 节) */
rgb_led_task(); /* RGB 驱动刷新 */
light_sensor_periodic_report(); /* 周期上报 5 项传感数据(3.8 节) */
}
与 RGB 灯方案相比,主循环多了三项:light_sensor_task(采集+联动)、spl06 采集(在 light_sensor_task 内调用)、light_sensor_periodic_report(周期上报)。
入口与验证:确认 Core/Src/main.c 在 GPIO、USART1/2、ADC1 初始化后调用 mcu_init(),在 while (1) 中持续调用 mcu_task();不要用 1 秒阻塞延时调度整个主循环。复位后先核对 SPL06 init OK 与每秒 ADC 日志,再确认 USART1 接收字节进入 FIFO。
3.6 设计 AT 命令表与 URC 表
命令表(demo 中为 APP/at_cmd_table.c)把 QuecThing AT 指令登记为 at_cmd_item_t 数组。与上云直接相关的核心命令与 RGB 灯方案一致:
|---------------------|--------------|--------|----------------------|-------------------|
| 命令 ID | 命令字 | 超时 | parse 回调 | 用途 |
| APP_CMD_QURCCFG | +QURCCFG | 5000ms | parse_advance_on_ok | 将 URC 输出定向到 UART1 |
| APP_CMD_QIOTREG | +QIOTREG | 5000ms | parse_advance_on_ok | 查询/配置连接模式 |
| APP_CMD_QIOTCFG | +QIOTCFG | 5000ms | parse_advance_on_ok | 配置产品 PK/PS |
| APP_CMD_QIOTSTATE | +QIOTSTATE | 5000ms | parse_qiotstate_noop | 查询平台连接状态 |
| APP_CMD_QIOTMODELTD | +QIOTMODELTD | 5000ms | parse_advance_on_ok | 上报物模型数据 |
大部分命令共享 parse_advance_on_ok(响应结束即推进状态机);parse_qiotstate_noop 仅在检测到 +QIOTSTATE: 8(已订阅)时推进:
保留 APP/at_cmd_table.c 的完整命令数组,核对下面实际注册项(数组内节选,不要删除其他命令):
APP_CMD_QIOTCFG = { "+QIOTCFG", 5000, parse_advance_on_ok },
APP_CMD_QIOTREG = { "+QIOTREG", 5000, parse_advance_on_ok },
APP_CMD_QIOTSTATE = { "+QIOTSTATE", 5000, parse_qiotstate_noop },
APP_CMD_QIOTMODELTD = { "+QIOTMODELTD", 5000, parse_advance_on_ok },
APP_CMD_QURCCFG = { "+QURCCFG", 5000, parse_advance_on_ok },
static int parse_advance_on_ok(at_ret_t status, const char *resp, uint16_t len)
{
module_init_next_step();
return 0;
}
static int parse_qiotstate_noop(at_ret_t status, const char *resp, uint16_t len)
{
if (status != AT_OK || resp == NULL || len == 0)
return 0;
static const char needle\[\] = "+QIOTSTATE: 8";
const uint16_t nlen = sizeof (needle) - 1;
for (uint16_t i = 0; i + nlen <= len; i++)
if (memcmp(resp + i, needle, nlen) == 0) {
module_init_next_step();
return 0;
}
return 0;
}
URC 表(demo 中为 APP/at_urc_table.c)注册 +QIOTEVT: 异步事件,回调区分两种下行:
at_urc_item_t g_urc_table\[\] = {
{ "+QIOTEVT:", urc_cb_qiotevt, &g_at_channel },
};
uint8_t g_urc_table_count = sizeof (g_urc_table) / sizeof (g_urc_table0);
|-------|-------------|-------------------------------------------------------------------------|
| 事件码 | 含义 | 处理方式 |
| 10210 | 云端写属性(下发控制) | 取后续 TTLV 行 → tm_downlink_dispatch() 写入本地变量 → module_report_props() 全量回传 |
| 10211 | 云端读属性(查询请求) | 解析 PkgID + 属性 ID 列表 → 按列表编码当前值 → at_send_data 应答 |
验证点:公共回调不检查 status,因此不能把"推进一步"当作收到 OK。调试时逐条核对配置命令响应、状态查询返回 8、首报到达三项结果,不仅检查完成标志。
3.7 实现模组初始化状态机(at_dev.c)
在初始化状态机文件(demo 中为 APP/at_dev.c)中实现 module_task()------6 步顺序推进的状态机,每步发送一条 AT 命令,由该命令的 parse 回调推进到下一步。
|----|--------------------------------------------|---------------------|------------------|
| 步骤 | AT 命令 | 说明 | 推进条件 |
| 0 | AT+QURCCFG="urcport","uart1" | 将 URC 输出定向到 UART1 | 公共 parse 回调调用时推进 |
| 1 | AT+QIOTREG? | 查询当前连接模式 | 公共 parse 回调调用时推进 |
| 2 | AT+QIOTCFG="productinfo","<PK>","<PS>" | 配置产品 PK/PS | 公共 parse 回调调用时推进 |
| 3 | AT+QIOTREG=1 | 启用 QuecThing 连接平台 | 公共 parse 回调调用时推进 |
| 4 | AT+QIOTSTATE? | 轮询平台连接状态 | state=8(已订阅) |
| 5 | AT+QIOTMODELTD=0,<len> | 上电首次上报 5 项 INT 传感数据 | 公共 parse 回调调用时推进 |
| 6 | --- | 完成 | --- |
第 2 步需要替换为实际产品信息:将 <PK> 和 <PS> 替换为第 2 章创建产品分配的 ProductKey 与 ProductSecret。工程中硬编码的 p11zA8 / RTRQLzBGRHlaci9Q 为示例值,直接使用将连不上你的产品。
第 5 步说明 :上电首次只上报 5 项 INT 传感数据(Rain_level、Pressure_level、Light_level、Temp、Altitude),control、rain_limit 为 RW 控制项,不在首报中(等云端下行触发)。
操作 APP/at_dev.c:替换顶部 PRODUCT_PK / PRODUCT_PS(保留原有 #ifndef 保护),核对首报掩码及完整状态机。以下示例凭证须换成第 2 章实际产品信息。
#define PRODUCT_PK "p11zA8"
#define PRODUCT_PS "RTRQLzBGRHlaci9Q"
uint8_t is_inited = 0;
uint8_t step = 0;
uint8_t send_flag = 0;
/* 上电首报掩码:bit = 1 << func_id,只报 5 项 INT */
#define FIRST_REPORT_MASK \
((1UL << PROP_RAIN_LEVEL) | (1UL << PROP_PRESSURE_LEVEL) | (1UL << PROP_LIGHT_LEVEL) | \
(1UL << PROP_TEMP) | (1UL << PROP_ALTITUDE))
static int module_report_first(at_channel_t *channel)
{
static uint8_t buf64; /* static:payload 要活到两段式命令响应结束 */
int len = tm_property_encode_many(FIRST_REPORT_MASK, buf, sizeof (buf));
if (len <= 0)
return -1;
return at_send_data(channel, APP_CMD_QIOTMODELTD, AT_OP_WRITE,
buf, (uint16_t)len, "%d,%u", 0, (uint16_t)len);
}
static void modele_init(at_channel_t *channel)
{
switch (step)
{
case 0:
if (0 == send_flag) {
send_flag = 1;
at_send(channel, APP_CMD_QURCCFG, AT_OP_WRITE,
"\"%s\",\"%s\"", "urcport", "uart1");
}
break ;
case 1:
if (0 == send_flag) {
send_flag = 1;
at_send(channel, APP_CMD_QIOTREG, AT_OP_READ, NULL);
}
break ;
case 2: /* 配置 PK/PS ------ 替换为你自己的产品信息 */
if (0 == send_flag) {
send_flag = 1;
at_send(channel, APP_CMD_QIOTCFG, AT_OP_WRITE, "\"%s\",\"%s\",\"%s\"",
"productinfo", PRODUCT_PK, PRODUCT_PS);
}
break ;
case 3:
if (0 == send_flag) {
send_flag = 1;
at_send(channel, APP_CMD_QIOTREG, AT_OP_WRITE, "1");
}
break ;
case 4:
if (0 == send_flag) {
send_flag = 1;
at_send(channel, APP_CMD_QIOTSTATE, AT_OP_READ, NULL);
} else {
static uint32_t last_poll_ms = 0;
uint32_t now_ms = channel->port->get_tick_ms();
if (now_ms - last_poll_ms >= 5000) {
last_poll_ms = now_ms;
send_flag = 0;
}
}
break ;
case 5: /* 上电首次上报 5 项传感数据 */
if (0 == send_flag) {
send_flag = 1;
if (module_report_first(channel) < 0) {
LOG("ttlv encode all failed\r\n");
send_flag = 0;
}
}
break ;
case 6:
LOG("Module init Complete\r\n");
is_inited = 1;
break ;
}
}
void module_task(at_channel_t *channel)
{
if (is_inited == 0) {
modele_init(channel);
}
}
void module_init_next_step(void)
{
send_flag = 0;
if (0xff > step) {
step++;
}
}
验证点:观察 USART1 命令顺序及 +QIOTSTATE: 8,再确认首报仅含 ID 1、2、3、6、7。Module init Complete 表示程序进入完成分支;命令成功与首报送达仍需结合模组响应和平台通信日志确认。下行写后的全量回传使用 module_report_props(),与 module_report_first() 的五项首报不同。
3.8 实现周期上报(light_sensor.c)
在采集文件(demo 中为 Middleware/light_sensor.c)中实现 light_sensor_periodic_report(),入网后每隔固定周期把 5 项 INT 传感数据一次性编码上报。本工程周期为 1 分钟(REPORT_PERIOD_MS)。
#define REPORT_PERIOD_MS (60U * 1000U) /* 1 分钟 */
void light_sensor_periodic_report(void)
{
static uint32_t last_tick = 0;
uint32_t now = HAL_GetTick();
if ((now - last_tick) < REPORT_PERIOD_MS)
return ;
last_tick = now;
extern uint8_t is_inited;
if (is_inited == 0) /* 仅入网后才上报 */
return ;
static uint8_t buf64; /* static:跨过两段式发送周期 */
int len = tm_property_encode_many(
(1UL << PROP_RAIN_LEVEL) | (1UL << PROP_PRESSURE_LEVEL) |
(1UL << PROP_LIGHT_LEVEL) | (1UL << PROP_TEMP) | (1UL << PROP_ALTITUDE),
buf, sizeof (buf));
if (len <= 0)
return ;
at_send_data(&g_at_channel, APP_CMD_QIOTMODELTD, AT_OP_WRITE,
buf, (uint16_t)len, "%d,%u", 0, (uint16_t)len);
}
tm_property_encode_many(mask, ...) 按掩码(bit = 1 << func_id)把多个属性的当前本地值一次性编码成 TTLV 流,再由 at_send_data 两段式发送(先发命令头 AT+QIOTMODELTD=0,<len>,收到 > 后发 TTLV)。周期值可按业务需要调整。
验证点:平台在线后观察相邻定时数据约隔 60 秒,均包含五项传感数据,不包含控制与阈值项。last_tick 在检查 is_inited 之前更新,节拍按系统 tick 计算,不是上线后重新倒计时;本地灯光跳变不额外触发上报。
3.9 实现传感采集与本地联动(light_sensor.c / spl06.c)
操作 Middleware/light_sensor.c:先保留真实 ADC 通道切换与采集函数,再核对每秒任务。光照与雨量均为 0~4095 原始 ADC,不是 lux 或 mm。
static uint32_t light_sensor_read_channel(uint32_t channel)
{
ADC_ChannelConfTypeDef sCh = { 0 };
sCh.Rank = ADC_REGULAR_RANK_1;
sCh.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
sCh.Channel = channel;
if (HAL_ADC_ConfigChannel(&hadc1, &sCh) != HAL_OK) {
return 0U;
}
if (HAL_ADC_Start(&hadc1) != HAL_OK) {
return 0U;
}
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) {
return (uint32_t)HAL_ADC_GetValue(&hadc1);
}
return 0U;
}
light_sensor_task() 默认每秒采 PA4/PA5,调用 SPL06 采气压/温度/海拔,再按雨量阈值联动 RGB。沿用同文件的 clamp_raw_to_level()、clamp_limit(),任务关键代码如下:
void light_sensor_task(void)
{
static uint32_t last_tick = 0;
uint32_t now = HAL_GetTick();
if ((now - last_tick) < LIGHT_SENSOR_PERIOD_MS) /* 默认 1000ms */
return ;
last_tick = now;
/* PA4 光照(仅上报)、PA5 雨量(上报 + 联动) */
uint32_t raw_light = light_sensor_read_channel(ADC_CHANNEL_4);
uint32_t raw_rain = light_sensor_read_channel(ADC_CHANNEL_5);
g_light_raw = clamp_raw_to_level(raw_light);
g_rain_raw = clamp_raw_to_level(raw_rain);
LOG("ADC CH4=%u CH5=%u\r\n", (unsigned)raw_light, (unsigned)raw_rain);
/* SPL06 气压/温度/海拔采集(写入 g_pressure_level / g_temp / g_altitude) */
extern void spl06_print_task(void);
spl06_print_task();
/* control=1:云端强控亮白灯,本任务不覆盖 */
if (rgb_led.control == 1)
return ;
/* control=0:雨量单阈值联动。rain_raw > rain_limit → 白灯,否则黑灯 */
int32_t rain_th = clamp_limit(rgb_led.rain_limit);
if (g_rain_raw > rain_th) {
rgb_led_apply_color(255, 255, 255);
rgb_led_light();
} else {
rgb_led_apply_color(0, 0, 0);
rgb_led_light();
}
}
操作 Middleware/spl06.c:保留 spl06_get_data() 的读寄存器、符号扩展及补偿算法。其末尾实际执行以下换算与属性赋值,不要把气压当作 Pa 上报:
/* spl06_get_data() 内的连续片段 */
p_res->tcomp = spl06_calc_temperature(p_res->traw);
p_res->pcomp = spl06_calc_pressure(p_res->praw, p_res->traw) / 100.0f;
p_res->asl = spl06_pressure_to_asl(p_res->pcomp);
g_pressure_level = (int32_t)p_res->pcomp;
g_temp = (int32_t)p_res->tcomp;
g_altitude = (int32_t)p_res->asl;
同文件的任务负责调用上述接口:
void spl06_print_task(void)
{
spl06_result_t res;
spl06_get_data(&res);
LOG("SPL06 T=%.1f C P=%.1f hPa ASL=%.1f m\r\n",
(double)res.tcomp, (double)res.pcomp, (double)res.asl);
}
SPL06 走软件模拟 I2C(spl06_i2c.c,PB8 SCL / PB9 SDA),I2C 从机地址 0x76;初始化时会校验 CHIP_ID。若 spl06_init() 返回非 0,检查 I2C 接线、地址(SDO 电平决定 0x76/0x77)与供电。
验证点:按 rgb_led_init() 的实际赋值确认默认 rain_limit=1000。保持 control=false,使 PA5 原始值高于阈值时亮白灯,等于或低于阈值时灭灯;改变 PA4 只更新光照属性,不应触发雨量联动。核对 SPL06 日志单位为 hPa/℃/m,平台值截去小数;海拔按源码 1015.7 hPa 基准估算,不作测量精度承诺。
3.10 实现下行控制(at_urc_table.c)
云端对属性的写/读通过 +QIOTEVT: 下行,在 URC 回调 urc_cb_qiotevt() 中处理:
操作 APP/at_urc_table.c:保留文件内 match_event()、parse_dl_read()、fetch_follow_line() 与 reply_qiotmodel_read(),再核对下面分发回调。parse_dl_read() 解析 ASCII 字段 PkgID,id1,...,回调最多接收 8 个属性 ID,不把读请求当作 TTLV 数据行。
static void urc_cb_qiotevt(const char *line, uint16_t len, void *user)
{
if (user == NULL || len < 7)
return ;
at_channel_t *ch = (at_channel_t *)user;
switch (match_event(line, len))
{
case 10210: /* 云端写:取 TTLV → 写本地变量 → 全量回传 */
{
uint8_t bufAT_LINE_MAX;
uint16_t got = 0;
if (fetch_follow_line(ch, buf, sizeof (buf), &got) != AT_OK)
return ;
tm_downlink_dispatch(buf, got, NULL); /* 按 TTLV ID 写入并触发 on_change */
module_report_props(ch); /* 写入后全量回传,保持同步 */
break ;
}
case 10211: /* 云端读:解析 PkgID + id 列表 → 编码应答 */
{
uint16_t wanted_ids8 = {0};
uint8_t wanted_cnt = 0;
uint32_t pkg_id = parse_dl_read(line, len, wanted_ids, 8, &wanted_cnt);
if (pkg_id == 0)
return ;
reply_qiotmodel_read(ch, (uint16_t)pkg_id, wanted_ids, wanted_cnt);
break ;
}
default :
break ;
}
}
非缓存模式下,写请求 TTLV 紧随 URC 行;保留真实取数与带 PkgID 的读应答实现:
static at_ret_t fetch_follow_line(at_channel_t *ch, uint8_t *buf,
uint16_t buf_size, uint16_t *got)
{
uint32_t start_ms = ch->port->get_tick_ms();
at_ret_t r;
for (;;) {
r = at_recv_line(ch, buf, buf_size, got);
if (r == AT_OK) { return r; }
if (ch->port->get_tick_ms() - start_ms > 50) {
*got = 0;
return r;
}
for (volatile uint32_t d = 0; d < 1000; d++) { }
}
}
static uint8_t g_read_reply_buf128;
static void reply_qiotmodel_read(at_channel_t *ch, uint16_t pkg_id,
const uint16_t *wanted_ids, uint8_t wanted_cnt)
{
uint16_t len = 0;
int n;
if (wanted_cnt == 0) {
n = tm_property_encode_many(0, g_read_reply_buf, sizeof (g_read_reply_buf));
if (n <= 0) { return ; }
len = (uint16_t)n;
} else {
for (uint8_t i = 0; i < wanted_cnt; i++) {
n = tm_property_encode_one(wanted_idsi, g_read_reply_buf + len,
(uint16_t)(sizeof (g_read_reply_buf) - len));
if (n <= 0) { continue ; }
len = (uint16_t)(len + n);
}
}
if (len == 0) { return ; }
(void)at_send_data(ch, APP_CMD_QIOTMODELTD, AT_OP_WRITE,
g_read_reply_buf, len, "%d,%u,%u", 1, len, pkg_id);
}
写后回传调用 APP/at_dev.c 的以下函数,mask=0 编码全部七项;不要误用五项首报函数:
int module_report_props(at_channel_t *channel)
{
static uint8_t buf64;
int len = tm_property_encode_many(0, buf, sizeof (buf));
if (len <= 0) { return -1; }
return at_send_data(channel, APP_CMD_QIOTMODELTD, AT_OP_WRITE,
buf, (uint16_t)len, "%d,%u", 0, (uint16_t)len);
}
验证点:写属性后检查七项全量回传;再读取 ID 1、3,核对 AT+QIOTMODELTD=1,<len>,<PkgID> 中 PkgID 与请求一致、只返回所选属性。无 ID 列表时编码全部属性。保持模组非缓存配置(buffer=0);缓存模式需配套主动读取流程,不能直接套用"取下一行"。
control 值改变时触发 on_control_change();rain_limit 绑定空回调,由下一次采集任务读取。保留真实强控回调:
/* control 下行后立即生效:1=强制白灯覆盖联动;0=交回雨量联动 */
void on_control_change(void)
{
if (rgb_led.control == 1) {
rgb_led_apply_color(255, 255, 255);
rgb_led_light();
}
/* control=0 不动硬件,由 light_sensor_task 按雨量判断 */
}
云端下发 rain_limit 后,无需额外代码------light_sensor_task 下一周期会用新阈值重新判断,实现"远程调节雨量联动灵敏度"。
同时核对 Middleware/rgb_led.c 的持续强控路径:
void rgb_led_task(void)
{
if (rgb_led.control == 1) {
rgb_led_apply_color(255, 255, 255);
rgb_led_light();
}
}
验证点:将 control 从 false 改为 true,确认持续白灯;改回 false,等待下一次采集按雨量判定,false 不等于立即强制关灯。再写 rain_limit,同步核对全量回传与 PA5 越界动作,重复写相同控制值不触发变化回调。
3.11 编译与烧录
代码完成后,用 Keil MDK 打开工程编译并烧录到开发板:
- 打开工程:进入工程的 MDK-ARM/ 目录,双击 .uvprojx 工程文件用 Keil MDK 打开。
- 确认修改 PK/PS:烧录前确认已将 3.7 第 2 步的 PK/PS 替换为你在第 2 章创建产品获得的实际值。
- 编译 :点击 Build (快捷键 F7),确认输出窗口 0 Error(s)。
- 连接烧录器:用 ST-Link / J-Link 连接开发板的 SWD 接口,确保开发板已上电。
- 下载 :点击 Download(快捷键 F8)将固件烧录入 MCU。
- 复位运行:烧录完成后按开发板复位键,设备开始运行,转入第 4 章观察串口日志与上云状态。
若编译报错找不到头文件,检查 3.2 的 Include Paths 是否已添加 Middleware;若烧录时提示未识别调试器,检查 ST-Link/J-Link 驱动与 Keil 中的 Debug 设置。
4. 设备调试与验证
4.1 串口日志解读
打开 USART2(PA2/PA3,115200/8N1),按源码中的日志格式核对,不要求输出 step 序号:
|--------------------------------------|------------------------|
| 日志格式(数值为运行时填充) | 核对内容 |
| SPL06 init OK / SPL06 init failed | I2C 气压计初始化结果 |
| Module init Complete | 状态机进入完成分支;再到平台确认在线与首报 |
| ADC CH4=%u CH5=%u | 每秒光照/雨量原始 ADC |
| SPL06 T=%.1f C P=%.1f hPa ASL=%.1f m | 实际单位与浮点日志;平台 INT 值截去小数 |
4.2 设备上线确认
平台连接成功后,模组会依次上报 +QIOTEVT: 1,10200(认证成功)→ 2,10200(注册成功)→ 3,10200(订阅成功)。在 开发者中心 设备列表中可看到设备显示为"在线"。
4.3 上行验证
设备入网后,先在 step 5 上报一次 5 项传感数据,之后每 1 分钟周期上报一次。在平台设备调试页面的"物模型数据/通信日志"中可看到 Rain_level、Pressure_level、Light_level、Temp、Altitude 的实时值随环境变化。

平台设备调试界面
图7:开发者中心 --- 设备调试页面(属性读取 / 服务调用 / 通信日志)
4.4 下行验证
在平台设备调试页面选择"属性调试"→"设置":
- 写 control = true:设备 RGB 灯应立即常亮白灯(强控覆盖本地联动);写 false 则交回雨量联动。
- 写 rain_limit(如 500):改变雨量联动阈值,当 Rain_level(PA5 ADC)超过该值时 RGB 自动亮白灯。
下行写入后,设备会通过 10210 回传机制全量上报一次,平台可见属性值同步更新。
5. SaaS 数据订阅
企业系统可通过平台基于 AMQP 协议的消息订阅能力,实时获取环境监测终端上报的气压、温度、光照、雨量、海拔数据,用于看板展示或告警。

进入资产管理SaaS平台
图8:开发者中心 --- 进入资产管理 SaaS 平台
|----|--------------|--------------------|
| 步骤 | 操作 | 说明 |
| 1 | 创建 AccessKey | 在开发者中心生成订阅凭证 |
| 2 | 配置订阅规则 | 选择本产品、消息类型(属性上报) |
| 3 | 开发 AMQP 客户端 | 连接队列消费消息,需具备断线重连机制 |
在 SaaS 平台的产品管理页面启用本产品后,设备数据将同步至 SaaS。

SaaS平台启用产品
图9:资产管理 SaaS 平台 --- 启用产品
订阅概述与客户端接入参见 AMQP 消息订阅概述。
6. App 绑定与控制
模组上电并成功连接平台后,可通过移联万物 App 扫码绑定设备。绑定后进入设备面板,可实时查看各项环境数据,并远程下发 control(强控开关)、rain_limit(联动阈值)等控制属性。App 下发的控制指令经平台转发至模组,再由模组以 +QIOTEVT: 5,10210 下发给 MCU 执行。
7. 常见问题排查
|----------------|-----------------|--------------------------------------------------|
| 问题现象 | 可能原因 | 排查方法 |
| 初始化卡在 step 4 | 模组未注网或 PK/PS 错误 | 先用"模组联网自检命令"确认注网正常,再检查 PK/PS 配置 |
| +QIOTEVT 不触发 | URC 未定向到 UART | 检查 step 0 的 AT+QURCCFG 是否执行 |
| 气压/温度一直为 0 | SPL06 初始化失败 | 查日志是否 SPL06 init failed;检查 PB8/PB9 接线、地址 0x76、供电 |
| 光照/雨量读数异常 | ADC 未共地或通道错 | 确认 PA4/PA5 接线与共地,ADC1 已 MX_ADC1_Init |
| 下行 control 灯不变 | 物模型 ID 不匹配 | 核对 thing_model_tabel.h 中 ID 与平台物模型一致 |
| 物模型上报失败 | data 缓冲生命周期不足 | 确保 at_send_data 的 buf 为 static,存活到响应结束 |
其中"初始化卡在 step 4、设备不上线"最常见的根因是模组未联网(SIM、信号或注网问题)。可用串口工具手动向模组发送以下 AT 命令逐项自检(本 demo 未内置这些命令,你也可按需将其加入自有工程做开机自检):
|-----------|----------------|----------------------------------|-------------------------------|
| AT 命令 | 作用 | 正常返回 | 异常处理 |
| AT+CPIN? | 查询 SIM 卡状态 | +CPIN: READY | 非 READY:检查 SIM 卡是否插好、是否已激活 |
| AT+CSQ | 查询信号质量 | +CSQ: <rssi>,<ber>,rssi 越大越好 | rssi=99 表示无信号:检查天线连接与所在环境信号 |
| AT+CREG? | 查询 GSM/2G 注网状态 | 第二个参数为 1(已注册)或 5(漫游) | 为 0/2/3:等待注网或检查 SIM 卡流量、运营商覆盖 |
| AT+CEREG? | 查询 4G/LTE 注网状态 | 第二个参数为 1 或 5 | 同上 |
| AT+COPS? | 查询当前运营商 | 返回运营商名称 | 无返回:模组尚未驻网,结合 CSQ/CEREG 排查 |
排查顺序建议:AT+CPIN?(卡就绪)→ AT+CSQ(有信号)→ AT+CEREG?/AT+CREG?(已注网)。三者正常后模组才能连接平台;若均正常仍上不了云,再回头核对 PK/PS 与激活码余量。