大家好!在前 23 篇中,我们实现了 NB-IoT 设备向阿里云 IoT 平台的 “单向数据上传”(如温湿度采集),但实际物联网场景中,往往需要 “双向交互”—— 比如云端远程控制设备开关、调整采样间隔、下发配置参数。这一篇我们将从 “下行通信” 的核心原理讲起,详解阿里云 IoT 平台 “指令下发” 与 STM32 “指令接收解析” 的完整流程,通过 “云端远程控制 LED 开关 + 调整采样间隔” 的实操,让你掌握物联网设备 “上传数据 + 接收指令” 的双向通信能力,实现真正的远程管控。
一、双向通信:为什么需要 “下行指令”?
单向通信(仅上传)的设备就像 “只会说话不会听话的机器人”—— 只能上报状态,无法接收控制;而双向通信让设备成为 “可远程指挥的工具”,核心价值体现在三个场景:
远程控制:如云端下发 “LED 开启” 指令,设备立即执行开关操作(无需现场手动控制);参数配置:如云端下发 “采样间隔从 1 小时改为 30 分钟”,设备动态调整工作模式(无需重新烧录程序);故障排查:如设备上传 “异常报警” 后,云端下发 “日志上传” 指令,获取详细故障信息(快速定位问题)。
阿里云 IoT 的双向通信基于MQTT 协议的 “主题订阅” 机制实现,类似 “报纸订阅”:
设备端:提前 “订阅” 阿里云的 “指令下发主题”(如),相当于 “订阅报纸”;云端:向该主题 “发布” 指令(如
/sys/a1XXXX/STM32_Dev/thing/service/property/set),相当于 “投递报纸”;设备端:收到主题消息后,解析指令并执行对应操作,相当于 “读报纸并行动”。
{"LED":"ON"}
二、双向通信核心原理:MQTT 主题与指令解析
阿里云 IoT 为设备预设了标准化的 “上下行主题”,无需手动创建,只需按规则订阅和发布,核心主题与指令格式如下:
1. 核心主题(以 “自定义产品” 为例)
| 主题类型 | 主题格式 | 作用说明 |
|---|---|---|
| 上行数据上传 | |
设备向云端上传数据(如温湿度、设备状态) |
| 下行指令下发 | |
云端向设备下发控制指令(如 LED 开关、参数调整) |
| 指令响应上报 | |
设备执行指令后,向云端反馈 “执行结果”(如 “LED 已开启”) |
关键规则:
设备必须先 “订阅” 下行指令主题,才能收到云端下发的消息;设备执行指令后,需向 “指令响应主题” 上报结果,确保云端知道指令已生效(避免重复下发)。
2. 指令格式(JSON 标准化)
阿里云 IoT 的指令采用 JSON 格式封装,结构清晰且易解析,示例如下:
云端下发 “LED 开启 + 采样间隔 30 分钟” 指令:
json
{
"params": {
"LED": "ON", // 控制LED开启
"SampleInterval": 30 // 采样间隔设为30分钟
}
}
设备执行后,向云端反馈响应:
json
{
"code": 200, // 200=成功,500=失败
"msg": "success", // 提示信息
"params": {
"LED": "ON", // 反馈当前LED状态
"SampleInterval": 30 // 反馈当前采样间隔
}
}
解析关键:设备需从 JSON 的字段中提取指令参数,区分不同指令类型(如 “LED” 对应开关控制,“SampleInterval” 对应参数调整)。
params
三、实操:云端远程控制 LED + 调整采样间隔
我们基于第 23 篇的 NB-IoT 设备,新增 “指令接收解析” 功能,实现两大核心需求:
云端下发 “LED_ON”/“LED_OFF” 指令,STM32 控制 PA5 引脚 LED 开关,并反馈执行结果;云端下发 “SampleInterval=10”(单位:分钟)指令,STM32 动态调整 RTC 唤醒周期,无需重启设备;设备执行指令后,向云端上报 “执行结果”,确保云端同步状态。
1. 硬件准备与连接
沿用第 23 篇硬件,仅新增 LED 控制引脚(已在之前工程中配置):
LED(PA5):GPIO_Output(推挽输出,初始低电平);其他硬件:STM32F103C8T6+BC26 NB 模块 + SHT30 传感器 + 18650 锂电池。
2. 云端准备(阿里云 IoT 平台配置)
步骤 1:配置 “物模型”(定义指令参数)
阿里云 IoT 通过 “物模型” 标准化指令参数,需先定义设备支持的 “属性”(即指令类型):
登录阿里云 IoT 平台,进入 “产品→选中目标产品→物模型定义→编辑物模型”;点击 “添加属性”,分别创建两个 “可读写属性”(可读写 = 支持云端下发 + 设备上报):
属性 1:
属性名:,标识符:
LED,数据类型:
LED,枚举值:
枚举(1)、
ON(0);
OFF
属性 2:
属性名:,标识符:
SampleInterval,数据类型:
SampleInterval,单位:
数值,取值范围:
分钟(1 分钟到 2 小时);
1~120
点击 “发布”,物模型配置生效(设备需按此格式解析指令)。
步骤 2:准备指令下发工具
阿里云提供两种下发指令的方式,新手推荐 “在线调试” 功能:
进入 “设备→选中目标设备→在线调试”;选择 “属性设置”,在 “设置属性” 下拉框中可看到已定义的和
LED;选择
SampleInterval并设置值为
LED,或设置
ON为
SampleInterval,点击 “发送指令”,即可向设备下发控制命令。
10
3. 设备端开发:订阅主题 + 接收解析指令
步骤 1:新增指令解析依赖(JSON 解析库)
STM32 解析 JSON 需要轻量级库,此处使用(需将
cJSON和
cJSON.c添加到工程),核心用于从指令中提取
cJSON.h和
LED参数。
SampleInterval
步骤 2:设备端订阅 “下行指令主题”(nb_iot.c)
在原有函数中,新增 “订阅指令主题” 的代码,确保设备能接收云端指令:
BC26_Connect_AliIoT
c
运行
// 新增:订阅阿里云下行指令主题
uint8_t BC26_Subscribe_Cmd_Topic(char *product_key, char *device_name)
{
char cmd[128];
char topic[128];
// 构建下行指令主题:/sys/${product_key}/${device_name}/thing/service/property/set
sprintf(topic, "/sys/%s/%s/thing/service/property/set", product_key, device_name);
// 发送订阅指令(AT+QMTSUB=客户端号,消息ID,主题,QoS)
sprintf(cmd, "AT+QMTSUB=0,2,"%s",1", topic);
if (NB_Send_AT_CMD(cmd, "+QMTSUB: 0,2,0,0", 10000) != 0) // 0,2,0,0表示订阅成功
{
printf("订阅指令主题失败!
");
return 1;
}
printf("订阅指令主题成功:%s
", topic);
return 0;
}
// 修改原有BC26_Connect_AliIoT函数,添加订阅步骤
uint8_t BC26_Connect_AliIoT(char *product_key, char *device_name, char *device_secret)
{
// (原有代码:设置MQTT服务器、ClientID、连接服务器)...
// 新增:连接成功后,订阅下行指令主题
if (BC26_Subscribe_Cmd_Topic(product_key, device_name) != 0)
{
return 1;
}
return 0;
}
步骤 3:接收并解析指令(nb_iot.c 新增函数)
通过 USART 接收 BC26 转发的 MQTT 主题消息,解析 JSON 指令并执行操作:
c
运行
#include "cJSON.h"
// 全局变量:存储当前采样间隔(单位:秒),初始3600秒(1小时)
uint32_t g_SampleInterval = 3600;
// 全局变量:存储当前LED状态
uint8_t g_LED_State = 0; // 0=OFF,1=ON
// 解析阿里云下发的JSON指令
void Parse_AliCmd(char *cmd_buf)
{
cJSON *root = NULL;
cJSON *params = NULL;
cJSON *led_item = NULL;
cJSON *interval_item = NULL;
// 1. 解析JSON根节点
root = cJSON_Parse(cmd_buf);
if (root == NULL)
{
printf("JSON解析失败!
");
goto end_parse;
}
// 2. 获取params节点(指令参数都在这里)
params = cJSON_GetObjectItem(root, "params");
if (params == NULL)
{
printf("未找到params节点!
");
goto end_parse;
}
// 3. 解析LED控制指令
led_item = cJSON_GetObjectItem(params, "LED");
if (led_item != NULL)
{
if (strcmp(led_item->valuestring, "ON") == 0)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
g_LED_State = 1;
printf("执行指令:LED开启
");
}
else if (strcmp(led_item->valuestring, "OFF") == 0)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
g_LED_State = 0;
printf("执行指令:LED关闭
");
}
}
// 4. 解析采样间隔调整指令(单位:分钟→转换为秒)
interval_item = cJSON_GetObjectItem(params, "SampleInterval");
if (interval_item != NULL)
{
uint32_t new_interval = interval_item->valueint * 60;
// 限制间隔范围:1分钟(60秒)~2小时(7200秒)
if (new_interval >= 60 && new_interval <= 7200)
{
g_SampleInterval = new_interval;
// 动态更新RTC唤醒周期
RTC_Update_Alarm(g_SampleInterval);
printf("执行指令:采样间隔调整为%d分钟
", interval_item->valueint);
}
else
{
printf("采样间隔超出范围(1~120分钟)!
");
}
}
// 5. 向云端上报指令执行结果
Report_Cmd_Result();
end_parse:
// 释放cJSON内存,避免内存泄漏
if (root != NULL)
{
cJSON_Delete(root);
}
}
// 向云端上报指令执行结果
void Report_Cmd_Result(void)
{
char cmd[256];
char topic[128];
char payload[128];
// 阿里云指令响应主题
sprintf(topic, "/sys/%s/%s/thing/service/property/set_reply", PRODUCT_KEY, DEVICE_NAME);
// 构建响应JSON(200=成功)
sprintf(payload, "{"code":200,"msg":"success","params":{"LED":"%s","SampleInterval":%d}}",
g_LED_State?"ON":"OFF", g_SampleInterval/60);
// 发送MQTT消息(AT+QMTPUB)
sprintf(cmd, "AT+QMTPUB=0,3,0,0,"%s",%d,"%s"", topic, strlen(payload), payload);
NB_Send_AT_CMD(cmd, "+QMTPUB: 0,3,0", 5000);
printf("上报执行结果:%s
", payload);
}
// 修改USART接收中断回调,检测MQTT指令消息
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
uart_rx_buf[uart_rx_len++] = huart->Instance->DR;
// 检测是否收到MQTT指令消息(特征:"+QMTCONN:"后接主题消息)
if (strstr(uart_rx_buf, "+QMTPUB: 0,2,") != NULL) // 2是订阅时的消息ID
{
// 提取JSON指令(简化:假设指令在"""和"""之间)
char *json_start = strchr(uart_rx_buf, '"');
char *json_end = strrchr(uart_rx_buf, '"');
if (json_start != NULL && json_end != NULL && json_start < json_end)
{
json_start++;
*json_end = '';
Parse_AliCmd(json_start); // 解析指令
}
// 清空缓冲区,准备接收下一条消息
memset(uart_rx_buf, 0, sizeof(uart_rx_buf));
uart_rx_len = 0;
}
// 防止缓冲区溢出
if (uart_rx_len >= sizeof(uart_rx_buf)-1)
{
uart_rx_len = 0;
memset(uart_rx_buf, 0, sizeof(uart_rx_buf));
}
// 重新启动接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t*)&uart_rx_buf[uart_rx_len], 1);
}
}
步骤 4:动态更新 RTC 唤醒周期(rtc.c 新增函数)
实现函数,让设备收到 “采样间隔” 指令后,无需重启即可更新唤醒时间:
RTC_Update_Alarm
c
运行
// 动态更新RTC闹钟周期(interval:单位秒)
void RTC_Update_Alarm(uint32_t interval)
{
RTC_AlarmTypeDef sAlarm = {0};
RTC_TimeTypeDef sTime = {0};
// 1. 获取当前时间
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
// 2. 计算新闹钟时间(当前秒+interval,超过60则进位)
sTime.Seconds += interval;
if (sTime.Seconds >= 60)
{
sTime.Minutes += sTime.Seconds / 60;
sTime.Seconds %= 60;
if (sTime.Minutes >= 60)
{
sTime.Hours += sTime.Minutes / 60;
sTime.Minutes %= 60;
if (sTime.Hours >= 24) sTime.Hours %= 24;
}
}
// 3. 重新配置RTC闹钟
sAlarm.AlarmTime = sTime;
sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY; // 忽略日期,仅按时间触发
sAlarm.Alarm = RTC_ALARM_A;
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}
步骤 5:修改主程序休眠逻辑(main.c)
让 STM32 休眠时使用全局变量作为唤醒周期,而非固定值:
g_SampleInterval
c
运行
while (1)
{
// (原有代码:采集数据、上传云端)...
LOW_POWER:
// 进入低功耗:使用当前采样间隔(g_SampleInterval秒)
BC26_Enter_PSM();
// 重新配置RTC闹钟(确保下一次按新间隔唤醒)
RTC_Update_Alarm(g_SampleInterval);
Enter_Stop_Mode();
}
4. 功能验证
远程控制 LED:
阿里云在线调试页面发送指令,STM32 PA5 引脚 LED 点亮,串口输出 “执行指令:LED 开启”;云端 “设备详情→物模型数据” 实时更新 “LED” 状态为 “ON”,收到设备反馈的 “success” 响应。
LED=ON
调整采样间隔:
发送指令,串口输出 “执行指令:采样间隔调整为 10 分钟”;设备后续每 10 分钟唤醒一次(而非原 1 小时),上传数据后继续休眠,验证间隔修改生效。
SampleInterval=10
四、双向通信常见问题与调试技巧
设备收不到云端指令?
未订阅主题:确认函数调用成功,串口输出 “订阅指令主题成功”;主题格式错误:检查主题中的
BC26_Subscribe_Cmd_Topic和
ProductKey是否与云端一致(区分大小写,无多余空格);QoS 等级不匹配:订阅时 QoS 设为 1,云端下发指令时 QoS 也需设为 1(阿里云在线调试默认 QoS=0,需手动改为 1)。
DeviceName
JSON 解析失败?
指令格式错误:云端下发的 JSON 必须包含节点,且参数名与物模型定义一致(如 “LED” 而非 “led”);cJSON 库未正确添加:确认工程中包含
params和
cJSON.c,且编译无报错(需关闭 “C99 模式” 兼容);缓冲区溢出:
cJSON.h大小需足够存储完整指令(建议设为 512 字节以上,避免指令被截断)。
uart_rx_buf
指令执行后云端收不到响应?
响应主题错误:确认函数中的主题格式正确,包含
Report_Cmd_Result后缀;响应 JSON 格式错误:必须包含
set_reply(200 = 成功)、
code字段,否则阿里云视为 “未响应”;网络断开:执行指令时若 NB 网络已断开,需重新联网后再上报响应(可在解析指令前检查网络状态)。
msg
五、第 24 篇总结与下一篇预告
总结
这一篇我们掌握了阿里云 IoT 双向通信的核心:
双向通信基于 MQTT 主题订阅机制,设备订阅 “下行指令主题” 接收云端命令,通过 “响应主题” 反馈结果;指令解析依赖 JSON 格式,需用库提取
cJSON中的参数,区分控制指令和配置指令;实操中实现了 “云端控制 LED + 动态调整采样间隔”,验证了双向通信的有效性,让设备从 “被动上报” 升级为 “主动响应”。
params
下一篇预告
当设备长期运行时,可能会遇到 “网络断开后无法重连”“指令执行异常” 等问题,需要一套 “故障自恢复机制”。第 25 篇我们将学习 “STM32 物联网设备的健壮性设计”,包括网络重连、指令重发、数据备份、故障报警等功能,让设备在复杂环境下也能稳定运行。
















暂无评论内容