100 篇文章精通 STM32F103(第 20 篇):FreeRTOS 系统监控与故障处理 —— 长期稳定运行保障实战

大家好!在前 19 篇中,我们掌握了 FreeRTOS 的任务调度、同步机制与中断交互,但当系统长期运行(如工业设备、智能家居控制器)时,隐藏的问题会逐渐暴露:任务栈溢出导致数据错乱、内存泄漏耗尽系统资源、高优先级任务饥饿导致功能失效…… 这些问题难以复现,却会直接导致系统崩溃。而 FreeRTOS 提供了完善的系统监控与故障处理工具,能实时检测异常、定位问题。这一篇我们将从 “稳定运行的核心痛点” 讲起,详解任务状态监控、栈溢出检测、钩子函数等工具,通过 “全系统状态监控” 实操,让你掌握保障设备长期稳定运行的关键技术。

一、长期运行的核心痛点:为什么需要监控与故障处理?

嵌入式设备(如户外传感器、工业控制器)往往需要 7×24 小时运行,以下隐藏问题会随时间累积,最终导致系统异常:

痛点类型 典型场景 后果
任务栈溢出 任务中定义大数组、递归调用过深,栈空间不足,覆盖其他任务的栈数据 任务状态错乱、数据校验失败,甚至触发硬件 Fault 中断
内存泄漏 动态分配内存(如
pvPortMalloc
)后未释放,长期运行后堆内存耗尽
新任务 / 队列创建失败,依赖动态内存的功能(如日志存储)瘫痪
任务饥饿 低优先级任务被高优先级任务长期占用 CPU,始终无法进入运行态 周期性任务(如设备自检、数据备份)无法执行,系统功能逐步缺失
资源死锁 多个任务交叉等待互斥锁(如 A 等 B 的锁、B 等 A 的锁),所有任务永久阻塞 系统无响应,所有依赖锁的功能(如串口、Flash)无法使用

FreeRTOS 的监控工具就像 “设备的体检仪”,能实时检测这些异常;故障处理工具则像 “急救包”,在异常发生时触发保护机制(如复位、日志上报),避免系统彻底崩溃。

二、FreeRTOS 核心监控工具:实时掌握系统状态

FreeRTOS 提供了标准化 API 与配置选项,可从 “任务、内存、资源” 三个维度监控系统状态,核心工具如下:

1. 任务状态监控:掌握每个任务的运行情况

通过 API 可实时获取任务的状态、优先级、栈使用量等关键信息,常用 API 如下:

API 函数 功能描述 实用场景

uxTaskGetSystemState
获取所有任务的状态(运行 / 就绪 / 阻塞)、优先级、栈剩余空间,存储到数组 全系统任务状态快照,排查任务饥饿

eTaskGetState
获取单个任务的状态(返回
eRunning
/
eReady
/
eBlocked
等枚举值)
定位特定任务的异常状态

uxTaskGetStackHighWaterMark
计算任务栈的 “高水位线”(栈使用的最大值,剩余空间 = 栈大小 – 高水位线) 检测栈溢出风险(剩余空间过小时预警)

vTaskList
将所有任务的状态、优先级、栈使用量格式化为字符串,输出到串口 / 终端 调试阶段实时查看任务状态

关键概念:栈高水位线
uxTaskGetStackHighWaterMark
返回的 “高水位线” 是任务运行以来的最大栈使用量。例如:任务栈大小为 512 字节,高水位线为 300 字节,说明栈剩余 212 字节 —— 剩余空间越小,栈溢出风险越高(通常建议剩余空间≥100 字节)。

2. 内存监控:防止堆内存泄漏

FreeRTOS 的动态内存管理(堆)提供了 API,可统计内存分配与释放情况,常用 API:

API 函数 功能描述 实用场景

xPortGetFreeHeapSize
获取当前堆内存的剩余大小(字节) 定期监控内存变化,检测泄漏

xPortGetMinimumEverFreeHeapSize
获取堆内存的 “最小剩余量”(运行以来的最低值) 评估内存峰值,判断堆大小是否足够

内存泄漏判断方法:定期调用
xPortGetFreeHeapSize
,若剩余内存持续减少(无明显波动),且
xPortGetMinimumEverFreeHeapSize
不断降低,说明存在内存泄漏(需排查未释放的
pvPortMalloc
调用)。

3. 资源监控:排查死锁与资源争抢

通过同步组件的 API,可监控互斥锁、信号量的使用状态,常用 API:

API 函数 功能描述 实用场景

uxSemaphoreGetCount
获取信号量的当前计数(互斥锁计数为 1 表示空闲,0 表示被占用) 排查互斥锁是否被永久占用(死锁)

xQueueGetQueueNumber
获取队列的当前数据个数、最大长度,判断队列是否溢出 检测数据是否因处理不及时丢失

三、FreeRTOS 故障处理工具:异常发生时的 “急救措施”

当监控到异常(如栈溢出、内存耗尽)时,需通过 “钩子函数(Hook)” 触发保护机制,避免系统崩溃。FreeRTOS 的钩子函数是 “用户可自定义的回调函数”,在特定异常场景下自动调用。

1. 栈溢出钩子函数(vApplicationStackOverflowHook)

触发场景:FreeRTOS 检测到任务栈溢出(如栈指针超出分配的栈空间)时调用。核心作用:记录溢出的任务名称、状态,执行紧急操作(如复位系统、上报故障日志)。配置方式:在
FreeRTOSConfig.h
中开启宏定义:


#define configCHECK_FOR_STACK_OVERFLOW 2  // 2=检测栈溢出并调用钩子函数(1=轻量级检测,不推荐)

2. 内存分配失败钩子函数(vApplicationMallocFailedHook)

触发场景:调用
pvPortMalloc
分配内存时,堆内存不足导致分配失败时调用。核心作用:释放临时资源(如关闭非关键任务),或触发系统软复位,恢复内存可用状态。配置方式:在
FreeRTOSConfig.h
中开启宏定义:


#define configUSE_MALLOC_FAILED_HOOK 1  // 启用内存分配失败钩子函数

3. 空闲任务钩子函数(vApplicationIdleHook)

触发场景:系统无就绪任务时,空闲任务(最低优先级)运行,钩子函数被周期性调用。核心作用:执行低优先级的后台操作(如内存碎片整理、设备低功耗休眠),不影响其他任务。配置方式:在
FreeRTOSConfig.h
中开启宏定义:


#define configUSE_IDLE_HOOK 1  // 启用空闲任务钩子函数

四、实操:全系统状态监控与故障处理

我们基于第 19 篇的 “电机控制系统”,新增 “系统监控任务”,实现三大功能:

每 2 秒采集一次所有任务的状态、栈高水位线、堆内存使用情况;当任务栈剩余空间 < 50 字节时,触发栈溢出预警(串口打印 + LED 闪烁);当堆内存剩余量 < 2000 字节时,触发内存不足预警(串口打印 + 尝试释放临时资源)。

1. 配置步骤(STM32CubeIDE)

步骤 1:开启 FreeRTOS 监控与钩子函数宏

在工程的
FreeRTOSConfig.h
中添加 / 修改以下宏定义:



// 栈溢出检测与钩子函数
#define configCHECK_FOR_STACK_OVERFLOW 2
// 内存分配失败钩子函数
#define configUSE_MALLOC_FAILED_HOOK 1
// 空闲任务钩子函数(可选)
#define configUSE_IDLE_HOOK 1
// 启用任务状态查询API
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
步骤 2:创建系统监控任务

在 FreeRTOS 的 “Tasks and Queues” 中添加任务:

任务名称:
Task_SysMonitor
;优先级:1(低于紧急任务、电机任务,避免占用高优先级资源);栈大小:1024(需存储任务状态数组,栈需更大);入口函数:
vTaskSysMonitor

2. 编写监控与故障处理代码

(1)系统监控任务(vTaskSysMonitor)



/* USER CODE BEGIN PV */
// 任务状态存储数组(假设系统最多8个任务,数组大小=8)
TaskStatus_t xTaskStatusArray[8];
// 任务数量
UBaseType_t uxArraySize;
// 堆内存相关变量
size_t xFreeHeapSize;
size_t xMinFreeHeapSize;
/* USER CODE END PV */
 
void vTaskSysMonitor(void *argument)
{
  char buf[128];
  UBaseType_t uxHighWaterMark;
 
  for(;;)
  {
    // 1. 采集全系统任务状态
    uxArraySize = uxTaskGetNumberOfTasks(); // 获取当前任务总数
    uxTaskGetSystemState(xTaskStatusArray, uxArraySize, NULL); // 获取任务状态
 
    // 2. 打印每个任务的状态、优先级、栈高水位线
    sprintf(buf, "===== 系统状态快照(共%d个任务)=====
", uxArraySize);
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
    for (UBaseType_t i = 0; i < uxArraySize; i++)
    {
      // 计算栈剩余空间(栈大小 - 高水位线)
      uxHighWaterMark = uxTaskGetStackHighWaterMark(xTaskStatusArray[i].xHandle);
      // 任务状态转换为字符串(eRunning→"Running",eBlocked→"Blocked")
      const char *pcTaskState;
      switch(xTaskStatusArray[i].eCurrentState)
      {
        case eRunning: pcTaskState = "Running"; break;
        case eReady: pcTaskState = "Ready"; break;
        case eBlocked: pcTaskState = "Blocked"; break;
        case eSuspended: pcTaskState = "Suspended"; break;
        default: pcTaskState = "Unknown"; break;
      }
 
      // 打印任务信息
      sprintf(buf, "任务:%s | 状态:%s | 优先级:%d | 栈剩余:%d字节
",
              xTaskStatusArray[i].pcTaskName, pcTaskState,
              xTaskStatusArray[i].uxCurrentPriority, uxHighWaterMark);
      HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
      // 3. 栈溢出预警(剩余空间<50字节)
      if (uxHighWaterMark < 50)
      {
        sprintf(buf, "【预警】任务%s栈剩余不足50字节,存在溢出风险!
",
                xTaskStatusArray[i].pcTaskName);
        HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
        // LED闪烁预警(快速翻转3次)
        for (uint8_t j=0; j<3; j++)
        {
          HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
          vTaskDelay(pdMS_TO_TICKS(100));
        }
      }
    }
 
    // 4. 堆内存监控
    xFreeHeapSize = xPortGetFreeHeapSize();
    xMinFreeHeapSize = xPortGetMinimumEverFreeHeapSize();
    sprintf(buf, "堆内存剩余:%d字节 | 堆内存最小剩余:%d字节
",
            xFreeHeapSize, xMinFreeHeapSize);
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
    // 5. 内存不足预警(剩余<2000字节)
    if (xFreeHeapSize < 2000)
    {
      sprintf(buf, "【预警】堆内存不足2000字节,可能存在内存泄漏!
");
      HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
      // 尝试释放临时资源(示例:若存在动态分配的缓存,此处释放)
      // if (pTempBuf != NULL) { vPortFree(pTempBuf); pTempBuf = NULL; }
    }
 
    sprintf(buf, "====================================

");
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
    // 每2秒监控一次
    vTaskDelay(pdMS_TO_TICKS(2000));
  }
}
(2)栈溢出钩子函数(FreeRTOSConfig.h 同级的.c 文件)



#include "FreeRTOS.h"
#include "task.h"
#include "stm32f1xx_hal.h"
 
// 栈溢出钩子函数
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
  // 1. 串口打印溢出任务信息
  char buf[64];
  sprintf(buf, "【故障】任务%s发生栈溢出!
", pcTaskName);
  HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
  // 2. 紧急处理:翻转LED报警(常亮+闪烁)
  while(1)
  {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500); // 此处用HAL_Delay,因任务已溢出,无需考虑RTOS调度
  }
}
(3)内存分配失败钩子函数



// 内存分配失败钩子函数
void vApplicationMallocFailedHook(void)
{
  char buf[64];
  sprintf(buf, "【故障】内存分配失败,堆内存耗尽!
");
  HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
 
  // 紧急处理:关闭非关键任务(如监控任务),释放内存
  vTaskSuspend(xTaskSysMonitorHandle); // 暂停监控任务
  // 若仍无法恢复,可触发系统软复位
  // NVIC_SystemReset();
}

3. 烧录验证与现象

上电后,串口每 2 秒输出一次 “系统状态快照”,包含:
所有任务(紧急停止、电机运行、系统监控)的状态(如 “Blocked”“Running”)、优先级、栈剩余空间;堆内存剩余量、最小剩余量;
若某任务栈剩余空间 <50 字节(可故意缩小栈大小模拟),串口输出 “栈溢出预警”,LED 快速闪烁 3 次;若堆内存 <2000 字节(可多次动态分配内存模拟泄漏),串口输出 “内存不足预警”,尝试释放临时资源;若故意触发栈溢出(如任务中定义超大数组),钩子函数被调用,串口输出 “栈溢出故障”,LED 进入报警闪烁模式。

五、第 20 篇总结与系列阶段性回顾

总结

这一篇我们掌握了 FreeRTOS 系统监控与故障处理的核心:

任务状态监控通过
uxTaskGetSystemState
/
uxTaskGetStackHighWaterMark
实现,可排查栈溢出、任务饥饿;内存监控通过
xPortGetFreeHeapSize
/
xPortGetMinimumEverFreeHeapSize
实现,可检测内存泄漏;故障处理依赖钩子函数,栈溢出、内存分配失败时可触发预警或紧急保护,避免系统崩溃;实操中通过 “系统监控任务” 实现全维度状态采集,结合钩子函数形成 “监控 – 预警 – 保护” 的完整闭环。

系列阶段性回顾(第 1-20 篇)

从 GPIO 控制到 FreeRTOS 系统监控,我们已覆盖 STM32F103 的核心技术栈,可支撑中小型嵌入式项目开发:

基础外设:GPIO、定时器(定时 / 中断 / PWM)、ADC(模拟采样)、DMA(高速数据传输);通信协议:UART(串口)、I2C(OLED)、SPI(SPI Flash)、FatFS(文件系统);实时系统:FreeRTOS 任务调度、同步机制(信号量 / 互斥锁 / 事件)、中断交互、系统监控;实战能力:从裸机编程到 RTOS 开发,从数据采集到长期稳定运行,形成完整技术闭环。

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

请登录后发表评论

    暂无评论内容