100 篇文章精通 STM32F103(第 24 篇):阿里云 IoT 双向通信实战 —— 云端远程控制设备

大家好!在前 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. 核心主题(以 “自定义产品” 为例)

主题类型 主题格式 作用说明
上行数据上传
/sys/${ProductKey}/${DeviceName}/thing/event/property/post
设备向云端上传数据(如温湿度、设备状态)
下行指令下发
/sys/${ProductKey}/${DeviceName}/thing/service/property/set
云端向设备下发控制指令(如 LED 开关、参数调整)
指令响应上报
/sys/${ProductKey}/${DeviceName}/thing/service/property/set_reply
设备执行指令后,向云端反馈 “执行结果”(如 “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 的
params
字段中提取指令参数,区分不同指令类型(如 “LED” 对应开关控制,“SampleInterval” 对应参数调整)。

三、实操:云端远程控制 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
,数据类型:
枚举
,枚举值:
ON
(1)、
OFF
(0);
属性 2:
属性名:
SampleInterval
,标识符:
SampleInterval
,数据类型:
数值
,单位:
分钟
,取值范围:
1~120
(1 分钟到 2 小时);

点击 “发布”,物模型配置生效(设备需按此格式解析指令)。

步骤 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

阿里云在线调试页面发送
LED=ON
指令,STM32 PA5 引脚 LED 点亮,串口输出 “执行指令:LED 开启”;云端 “设备详情→物模型数据” 实时更新 “LED” 状态为 “ON”,收到设备反馈的 “success” 响应。

调整采样间隔

发送
SampleInterval=10
指令,串口输出 “执行指令:采样间隔调整为 10 分钟”;设备后续每 10 分钟唤醒一次(而非原 1 小时),上传数据后继续休眠,验证间隔修改生效。

四、双向通信常见问题与调试技巧

设备收不到云端指令?

未订阅主题:确认
BC26_Subscribe_Cmd_Topic
函数调用成功,串口输出 “订阅指令主题成功”;主题格式错误:检查主题中的
ProductKey

DeviceName
是否与云端一致(区分大小写,无多余空格);QoS 等级不匹配:订阅时 QoS 设为 1,云端下发指令时 QoS 也需设为 1(阿里云在线调试默认 QoS=0,需手动改为 1)。

JSON 解析失败?

指令格式错误:云端下发的 JSON 必须包含
params
节点,且参数名与物模型定义一致(如 “LED” 而非 “led”);cJSON 库未正确添加:确认工程中包含
cJSON.c

cJSON.h
,且编译无报错(需关闭 “C99 模式” 兼容);缓冲区溢出:
uart_rx_buf
大小需足够存储完整指令(建议设为 512 字节以上,避免指令被截断)。

指令执行后云端收不到响应?

响应主题错误:确认
Report_Cmd_Result
函数中的主题格式正确,包含
set_reply
后缀;响应 JSON 格式错误:必须包含
code
(200 = 成功)、
msg
字段,否则阿里云视为 “未响应”;网络断开:执行指令时若 NB 网络已断开,需重新联网后再上报响应(可在解析指令前检查网络状态)。

五、第 24 篇总结与下一篇预告

总结

这一篇我们掌握了阿里云 IoT 双向通信的核心:

双向通信基于 MQTT 主题订阅机制,设备订阅 “下行指令主题” 接收云端命令,通过 “响应主题” 反馈结果;指令解析依赖 JSON 格式,需用
cJSON
库提取
params
中的参数,区分控制指令和配置指令;实操中实现了 “云端控制 LED + 动态调整采样间隔”,验证了双向通信的有效性,让设备从 “被动上报” 升级为 “主动响应”。

下一篇预告

当设备长期运行时,可能会遇到 “网络断开后无法重连”“指令执行异常” 等问题,需要一套 “故障自恢复机制”。第 25 篇我们将学习 “STM32 物联网设备的健壮性设计”,包括网络重连、指令重发、数据备份、故障报警等功能,让设备在复杂环境下也能稳定运行。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容