100 篇文章精通 STM32F103(第 25 篇):物联网设备健壮性设计 —— 故障自恢复与稳定运行

大家好!在前 24 篇中,我们实现了物联网设备的 “数据上传 + 远程控制” 双向通信,但在复杂实际环境(如户外信号弱、电源波动、网络中断)中,设备仍会频繁出现故障:网络断开后无法重连、指令执行一半突然断电导致数据丢失、传感器异常导致采集值错误…… 这些问题会让设备 “离线” 或 “失控”,失去物联网监控的意义。这一篇我们将从 “健壮性设计的核心目标” 讲起,详解五大关键保障技术(网络重连、数据备份、故障检测、指令重试、异常报警),通过 “户外温湿度监测设备” 的实操优化,让你的设备在恶劣环境下也能稳定运行,实现 “无人值守” 的核心需求。

一、为什么需要健壮性设计?用 “户外监控摄像头” 理解痛点

物联网设备(如户外传感器、远程水表)的运行环境远比实验室复杂,常见故障场景就像 “户外摄像头突然罢工”:

网络波动:基站信号临时中断,设备与云端断开连接,数据无法上传,指令无法接收;电源不稳:锂电池电压下降或瞬间断电,未保存的采集数据丢失,设备重启后参数重置;硬件异常:传感器接触不良导致采集值跳变(如温度突然从 25℃变为 80℃),指令执行后无反馈;云端异常:云端平台临时维护,设备连接被拒绝,需等待恢复后自动重试。

健壮性设计的本质是 “让设备具备‘自愈’能力”—— 就像摄像头配备 “备用电源 + 自动重连网络” 功能:网络断了自动重试,没电了用备用电源续航,数据丢了从本地备份恢复,最终实现 “故障不影响核心功能,恢复后无缝衔接”。

健壮性设计的核心目标

降低离线率:网络 / 硬件故障时,设备能自动恢复连接,离线时间≤5 分钟;保障数据完整性:关键数据(采集值、配置参数)本地备份,断电不丢失;故障可感知:异常发生时,主动向云端上报故障信息(如 “传感器异常”),便于远程排查;指令可靠性:下发的控制指令确保 “执行一次、反馈一次”,不重复执行也不遗漏。

二、五大核心健壮性技术:从故障预防到恢复

针对物联网设备的常见故障,我们提炼出五大关键保障技术,覆盖 “连接、数据、硬件、指令、报警” 全链路:

1. 网络重连机制:断网后自动恢复连接

网络断开是最常见的故障,核心解决思路是 “定时检测 + 阶梯重试”,避免频繁重试浪费功耗:

检测方式:通过 NB-IoT 模块的 AT 指令(如
AT+CGREG?
)检测网络注册状态,或通过 “MQTT 心跳包” 检测云端连接;重试策略:采用 “阶梯间隔” 重试(首次 10 秒,第二次 30 秒,第三次 1 分钟,之后每 5 分钟重试一次),既保证快速恢复,又避免低电量时过度消耗;重连流程:检测到断网→关闭当前连接→重新初始化模块→注册网络→连接云端→恢复数据上传与指令接收。

2. 数据备份机制:关键数据本地不丢失

断电或断网时,未上传的采集数据、设备配置参数容易丢失,核心解决思路是 “分级存储 + 定期同步”:

存储分级

临时数据(未上传的采集值):存储在 RAM 缓冲区,满了后写入 SPI Flash(断电不丢失);关键配置(采样间隔、设备 ID、云端参数):存储在 STM32 内部 Flash,每次修改后立即备份;
同步策略:网络恢复后,优先上传 Flash 中缓存的历史数据,再上传新采集的数据,确保云端数据不遗漏;数据校验:存储数据时添加 “时间戳 + CRC 校验”,避免数据损坏(如 Flash 存储错误导致采集值乱码)。

3. 硬件故障检测:提前发现异常硬件

传感器、通信模块等硬件故障会导致数据错误或功能失效,核心解决思路是 “实时监测 + 阈值判断”:

传感器检测:采集数据后,判断是否超出 “合理范围”(如温度 – 40℃~85℃,湿度 0%~100%),超出则标记为 “传感器异常”;模块检测:定期向 NB-IoT/SPI Flash 发送测试指令(如
AT
指令、读 ID 指令),无响应则标记为 “模块故障”;电源检测:通过 ADC 采集锂电池电压,低于 3.0V(18650 电池放电下限)时,上报 “低电量报警”,并进入超低功耗模式。

4. 指令可靠性机制:确保指令 “执行 + 反馈” 闭环

云端下发的指令可能因网络丢失或设备故障导致 “执行失败”,核心解决思路是 “指令缓存 + 结果校验”:

指令缓存:设备收到指令后,先存储到 Flash(避免断电丢失),再执行操作;结果反馈:执行完成后,立即向云端上报 “执行结果”,若云端未收到(如网络断了),设备重启后重新上报;去重处理:指令中添加 “唯一 ID”,设备执行前检查 Flash 中是否已执行过该 ID 指令,避免重复执行(如云端重发导致 LED 反复开关)。

5. 异常报警机制:故障可远程感知

设备出现故障时,不能 “默默离线”,需主动上报故障信息,核心解决思路是 “分级报警 + 优先传输”:

报警分级

紧急故障(如低电量、网络永久断开):立即暂停普通数据上传,优先上报报警信息;一般故障(如传感器异常):随下一次数据上传一起上报;
报警内容:包含 “故障类型(如 0 = 传感器异常,1 = 网络故障)+ 故障时间 + 当前状态”,便于云端定位问题;本地指示:故障时通过 LED 闪烁频率区分故障类型(如每秒 3 次闪烁 = 传感器异常,每秒 1 次 = 网络故障),方便现场排查。

三、实操:优化户外温湿度监测设备(添加健壮性功能)

我们基于第 24 篇的 NB-IoT 设备,新增五大健壮性功能,优化为 “可户外无人值守” 的监测设备:

网络断连时,按 “10s→30s→1min→5min” 阶梯重试重连;未上传的温湿度数据缓存到 SPI Flash,网络恢复后优先上传;检测温度超范围(-40℃~85℃)或传感器无响应,上报 “传感器异常”;采集锂电池电压,低于 3.0V 时上报 “低电量报警”,进入超低功耗;云端指令添加唯一 ID,避免重复执行,执行结果缓存后上报。

1. 硬件准备与连接

沿用第 24 篇硬件,仅新增 “电池电压检测” 引脚:

锂电池电压→PA0(ADC1_IN0,需串联 10kΩ+10kΩ 分压电阻,将 3.0~4.2V 电压降至 1.5~2.1V,适配 ADC 0~3.3V 输入范围);其他硬件:STM32F103C8T6+BC26+SHT30+SPI Flash+18650 锂电池。

2. 核心优化代码实现

(1)网络重连机制(nb_iot.c 新增函数)



#include "nb_iot.h"
#include "delay.h"
 
// 网络重连次数(用于阶梯重试)
uint8_t g_NetRetryCnt = 0;
// 网络状态(0=断开,1=连接正常)
uint8_t g_NetState = 0;
 
// 检测网络与云端连接状态
uint8_t Check_Net_Connect(void)
{
  // 1. 检测NB网络注册状态(+CGREG: 0,1或0,5表示注册成功)
  if (NB_Send_AT_CMD("AT+CGREG?", "+CGREG: 0,1", 3000) != 0 && 
      NB_Send_AT_CMD("AT+CGREG?", "+CGREG: 0,5", 3000) != 0)
  {
    g_NetState = 0;
    return 0;
  }
 
  // 2. 检测MQTT连接状态(AT+QMTCONN?,+QMTCONN: 0,1表示连接正常)
  if (NB_Send_AT_CMD("AT+QMTCONN?", "+QMTCONN: 0,1", 5000) != 0)
  {
    g_NetState = 0;
    return 0;
  }
 
  g_NetState = 1;
  g_NetRetryCnt = 0; // 连接正常,重置重试次数
  return 1;
}
 
// 网络重连(阶梯重试)
uint8_t Net_Reconnect(void)
{
  uint32_t retry_delay;
 
  // 根据重试次数设置间隔(阶梯重试)
  switch(g_NetRetryCnt)
  {
    case 0: retry_delay = 10000; break;  // 10秒
    case 1: retry_delay = 30000; break;  // 30秒
    case 2: retry_delay = 60000; break;  // 1分钟
    default: retry_delay = 300000; break; // 5分钟
  }
  g_NetRetryCnt++;
 
  // 等待重试间隔
  HAL_Delay(retry_delay);
 
  // 重新初始化模块并连接
  printf("第%d次尝试重连...
", g_NetRetryCnt);
  if (BC26_Init() == 0 && BC26_Connect_AliIoT(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET) == 0)
  {
    printf("网络重连成功!
");
    // 重连成功后,优先上传Flash缓存的历史数据
    Upload_History_Data();
    return 1;
  }
  printf("网络重连失败,下次重试间隔%d秒
", retry_delay/1000);
  return 0;
}
(2)数据备份与历史上传(data_backup.c)



#include "data_backup.h"
#include "w25qxx.h"
#include "rtc.h"
 
// SPI Flash缓存区起始地址(0x001000,避开配置参数区)
#define DATA_BACKUP_ADDR 0x001000
// 单个数据长度(时间戳4字节+温度4字节+湿度4字节+CRC2字节=14字节)
#define DATA_LEN 14
// 最大缓存数据量(100条,共1400字节)
#define MAX_DATA_CNT 100
 
// 存储采集数据到Flash
void Save_Data_To_Flash(float temp, float humi)
{
  uint8_t data_buf[DATA_LEN];
  uint32_t timestamp = RTC_Get_Timestamp(); // 获取当前时间戳(秒)
  uint16_t crc;
 
  // 1. 封装数据:时间戳+温度+湿度
  memcpy(data_buf, &timestamp, 4);
  memcpy(data_buf+4, &temp, 4);
  memcpy(data_buf+8, &humi, 4);
 
  // 2. 计算CRC校验(前12字节)
  crc = CRC_Calculate(data_buf, 12);
  memcpy(data_buf+12, &crc, 2);
 
  // 3. 查找Flash中第一个空闲位置(0xFF表示空闲)
  uint32_t addr = DATA_BACKUP_ADDR;
  for (uint16_t i=0; i<MAX_DATA_CNT; i++)
  {
    uint8_t temp_byte;
    W25Q_ReadData(addr + i*DATA_LEN, &temp_byte, 1);
    if (temp_byte == 0xFF) // 找到空闲位置
    {
      W25Q_WritePage(addr + i*DATA_LEN, data_buf, DATA_LEN);
      printf("数据缓存到Flash,地址:0x%X
", addr + i*DATA_LEN);
      return;
    }
  }
 
  // 4. 缓存满了,覆盖最早的数据(循环覆盖)
  W25Q_EraseSector(DATA_BACKUP_ADDR); // 擦除整个缓存区
  W25Q_WritePage(DATA_BACKUP_ADDR, data_buf, DATA_LEN);
  printf("Flash缓存满,覆盖最早数据
");
}
 
// 上传Flash中的历史数据
void Upload_History_Data(void)
{
  uint8_t data_buf[DATA_LEN];
  uint32_t timestamp;
  float temp, humi;
  uint16_t crc, calc_crc;
 
  uint32_t addr = DATA_BACKUP_ADDR;
  for (uint16_t i=0; i<MAX_DATA_CNT; i++)
  {
    // 1. 读取1字节,判断是否为有效数据(非0xFF)
    W25Q_ReadData(addr + i*DATA_LEN, data_buf, 1);
    if (data_buf[0] == 0xFF) break;
 
    // 2. 读取完整数据
    W25Q_ReadData(addr + i*DATA_LEN, data_buf, DATA_LEN);
 
    // 3. CRC校验
    calc_crc = CRC_Calculate(data_buf, 12);
    memcpy(&crc, data_buf+12, 2);
    if (calc_crc != crc)
    {
      printf("历史数据CRC错误,跳过
");
      continue;
    }
 
    // 4. 解析数据并上传
    memcpy(&timestamp, data_buf, 4);
    memcpy(&temp, data_buf+4, 4);
    memcpy(&humi, data_buf+8, 4);
    printf("上传历史数据:%d秒, %.1f℃, %.1f%%
", timestamp, temp, humi);
    BC26_Upload_Data(PRODUCT_KEY, DEVICE_NAME, temp, humi);
 
    // 5. 上传成功后,标记该位置为空闲(写入0xFF)
    uint8_t empty_buf[DATA_LEN] = {0xFF};
    W25Q_WritePage(addr + i*DATA_LEN, empty_buf, DATA_LEN);
  }
}
(3)硬件故障检测(fault_detect.c)



#include "fault_detect.h"
#include "adc.h"
#include "sht30.h"
#include "nb_iot.h"
 
// 故障类型定义
#define FAULT_SENSOR 0  // 传感器异常
#define FAULT_BAT_LOW 1 // 低电量
#define FAULT_NET 2     // 网络故障
 
// 读取锂电池电压(分压后计算实际电压)
float Read_Battery_Volt(void)
{
  uint16_t adc_val;
  float volt;
 
  // 启动ADC采样PA0(分压后的电压)
  HAL_ADC_Start(&hadc1);
  if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
  {
    adc_val = HAL_ADC_GetValue(&hadc1);
    // 计算实际电压:分压比1:1,ADC参考电压3.3V,12位分辨率
    volt = (float)adc_val * 3.3f / 4095.0f * 2;
    return volt;
  }
  return 0.0f;
}
 
// 检测传感器异常(温度超范围或无响应)
uint8_t Detect_Sensor_Fault(float temp, float humi)
{
  // 1. 检测温度范围(-40℃~85℃)
  if (temp < -40.0f || temp > 85.0f)
  {
    return 1;
  }
 
  // 2. 检测湿度范围(0%~100%)
  if (humi < 0.0f || humi > 100.0f)
  {
    return 1;
  }
 
  // 3. 检测传感器是否响应(连续3次采集失败)
  static uint8_t sensor_err_cnt = 0;
  if (temp == 0.0f && humi == 0.0f) // 假设0.0f为无效值
  {
    sensor_err_cnt++;
    if (sensor_err_cnt >= 3)
    {
      sensor_err_cnt = 0;
      return 1;
    }
  }
  else
  {
    sensor_err_cnt = 0;
  }
 
  return 0;
}
 
// 上报故障信息
void Report_Fault(uint8_t fault_type, float param)
{
  char payload[128];
  char fault_msg[32];
 
  // 1. 构建故障描述
  switch(fault_type)
  {
    case FAULT_SENSOR:
      sprintf(fault_msg, "传感器异常,当前值:%.1f℃", param);
      break;
    case FAULT_BAT_LOW:
      sprintf(fault_msg, "低电量报警,电压:%.2fV", param);
      break;
    case FAULT_NET:
      sprintf(fault_msg, "网络故障,重试次数:%d", (int)param);
      break;
    default:
      strcpy(fault_msg, "未知故障");
  }
 
  // 2. 上报到阿里云(使用自定义故障主题)
  sprintf(payload, "{"params":{"FaultType":%d,"FaultMsg":"%s"}}", fault_type, fault_msg);
  char topic[128];
  sprintf(topic, "/sys/%s/%s/thing/event/fault/post", PRODUCT_KEY, DEVICE_NAME);
  BC26_Publish_Message(topic, payload); // 自定义发布函数,类似上传数据
 
  // 3. 本地LED指示(故障类型不同,闪烁频率不同)
  for (uint8_t i=0; i<fault_type+1; i++)
  {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(200);
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(200);
  }
  printf("上报故障:%s
", fault_msg);
}
(4)主程序整合(main.c 优化)



#include "main.h"
#include "nb_iot.h"
#include "data_backup.h"
#include "fault_detect.h"
#include "adc.h"
 
float temp, humi;
float bat_volt;
 
int main(void)
{
  HAL_Init();
  SystemClock_Config();
 
  // 初始化外设(新增ADC初始化,用于电池电压检测)
  MX_GPIO_Init();
  MX_USART1_Init();
  MX_I2C1_Init();
  MX_SPI1_Init();
  MX_RTC_Init();
  MX_ADC1_Init();
 
  // 启动USART接收中断和ADC
  HAL_UART_Receive_IT(&huart1, (uint8_t*)uart_rx_buf, 1);
  HAL_ADC_Start(&hadc1);
 
  // 初始化RTC(默认采样间隔30分钟)
  RTC_Update_Alarm(30*60);
 
  // 读取Flash中保存的配置参数(如采样间隔)
  Load_Config_From_Flash(); // 自定义函数,读取之前备份的配置
 
  while (1)
  {
    // 1. 检测网络状态,断了则重连
    if (!Check_Net_Connect())
    {
      if (!Net_Reconnect())
      {
        // 重连失败,上报网络故障
        Report_Fault(FAULT_NET, (float)g_NetRetryCnt);
        goto LOW_POWER;
      }
    }
 
    // 2. 检测电池电压
    bat_volt = Read_Battery_Volt();
    if (bat_volt < 3.0f)
    {
      Report_Fault(FAULT_BAT_LOW, bat_volt);
      // 进入超低功耗模式,仅保留RTC唤醒
      Enter_Ultra_LowPower_Mode();
      continue;
    }
 
    // 3. 采集温湿度数据
    SHT30_Read_Data(&temp, &humi);
    printf("采集数据:%.1f℃, %.1f%%, 电池:%.2fV
", temp, humi, bat_volt);
 
    // 4. 检测传感器故障
    if (Detect_Sensor_Fault(temp, humi))
    {
      Report_Fault(FAULT_SENSOR, temp);
      // 传感器异常,缓存数据后跳过上传(避免错误数据)
      Save_Data_To_Flash(temp, humi);
      goto LOW_POWER;
    }
 
    // 5. 上传当前数据,失败则缓存到Flash
    if (BC26_Upload_Data(PRODUCT_KEY, DEVICE_NAME, temp, humi) != 0)
    {
      Save_Data_To_Flash(temp, humi);
    }
 
    // 6. 处理云端下发的指令(已在中断中解析)
 
  LOW_POWER:
    // 7. 进入低功耗模式
    BC26_Enter_PSM();
    RTC_Update_Alarm(g_SampleInterval);
    Enter_Stop_Mode();
  }
}

3. 健壮性验证

网络重连验证

手动断开 NB 模块天线(模拟信号丢失),串口输出 “第 1 次尝试重连… 间隔 10 秒”,10 秒后重试;重新插上天线,模块自动重连成功,并优先上传 Flash 中缓存的历史数据。

数据备份验证

断网状态下采集 3 条数据,数据自动缓存到 Flash;恢复网络后,设备先上传这 3 条历史数据,再上传新采集的数据,云端日志无遗漏。

故障检测验证

断开 SHT30 传感器(模拟传感器异常),设备采集到温度 = 0℃,上报 “传感器异常”,LED 每秒闪烁 1 次;用可调电源降低电池电压至 2.9V,设备上报 “低电量报警”,进入超低功耗模式。

指令可靠性验证

云端下发 “SampleInterval=5” 指令(ID=123),设备执行后上报结果;重启设备,设备读取 Flash 中缓存的 “指令 ID=123”,不重复执行,仅重新上报结果。

四、第 25 篇总结与系列进阶方向

总结

这一篇我们掌握了物联网设备健壮性设计的核心:

五大关键技术覆盖 “连接、数据、硬件、指令、报警”,解决实际环境中的常见故障;核心思路是 “预防(如数据备份)+ 检测(如硬件故障判断)+ 恢复(如网络重连)+ 上报(如故障报警)”,形成完整闭环;实操中优化的户外设备能在断网、低电量、传感器异常等场景下稳定运行,满足 “无人值守” 需求。

系列进阶方向(后续学习建议)

至此,我们已完成 STM32F103 从基础到物联网实战的完整技术栈,后续可向三个方向进阶:

硬件设计:学习 STM32 最小系统板设计、电源管理(LDO/DC-DC)、PCB 布局(抗干扰),实现从 “软件开发” 到 “软硬结合”;高级协议:学习 MQTT-SN(低功耗 MQTT)、CoAP(物联网轻量级协议),适配更复杂的云端场景;行业应用:结合具体场景开发项目(如智能水表、环境监测站、智能家居控制器),将技术落地为产品。

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

请登录后发表评论

    暂无评论内容