Chapter 09 · ARM 嵌入式 C 开发

FreeRTOS 实时操作系统

从裸机到 RTOS 的思维转变,掌握任务状态机、调度算法、IPC 原语和内存管理,构建健壮的多任务系统。

课程进度90%

关键概念

裸机 vs RTOS
裸机(Bare-Metal):main() 一个大循环,所有逻辑串行执行,用标志位和状态机模拟并发,代码耦合度高,实时性差。RTOS:多个任务各自独立运行(并发),由调度器管理 CPU 时间片,任务之间通过 IPC(队列、信号量等)通信,结构清晰,实时性有保证。
任务(Task)
FreeRTOS 中的基本执行单元。每个任务是一个无限循环的 C 函数,有自己的栈和优先级。任务不会自己返回,要用 vTaskDelete() 删除或在函数末尾死循环。STM32 上通常创建 4-8 个任务,每个任务栈 128-512 字(512-2048 字节)。
调度算法(Scheduler)
FreeRTOS 使用抢占式优先级调度:优先级最高的就绪任务立即获得 CPU;同优先级任务轮转(Round-Robin)调度,每个 SysTick 心跳(默认 1ms)切换一次。任务调用 vTaskDelay() 等阻塞 API 时主动让出 CPU,调度器立即运行下一个就绪任务。
任务四种状态
就绪(Ready):等待 CPU,已具备运行条件。运行(Running):当前占用 CPU。阻塞(Blocked):等待事件(延时/信号量/队列),不占 CPU。挂起(Suspended):被 vTaskSuspend() 暂停,不参与调度,需 vTaskResume() 唤醒。
队列(Queue)
FreeRTOS 的 FIFO 消息传递机制,支持多生产者多消费者,线程安全。发送方调用 xQueueSend()(阻塞直到有空间),接收方调用 xQueueReceive()(阻塞直到有数据)。ISR 中必须使用 xQueueSendFromISR() 版本。传递的是数据副本(按值传递),不是指针(避免所有权问题)。
信号量(Semaphore)
计数信号量:用于资源计数和任务同步,ISR 中 give、任务中 take。二值信号量:特殊的计数信号量(值只有 0/1),用于 ISR 通知任务。互斥量(Mutex):带优先级继承的二值信号量,保护共享资源(临界区),防止优先级反转。
优先级反转
低优先级任务持有互斥锁,中优先级任务持续运行,高优先级任务等不到锁。FreeRTOS 互斥量(Mutex)通过"优先级继承"解决:持锁的低优先级任务临时继承等待者的高优先级,直到释放锁。因此保护共享资源必须用 Mutex 而非普通二值信号量。
堆内存管理
FreeRTOS 提供 5 种堆实现(heap_1 ~ heap_5)。heap_4(最常用):支持碎片合并,适合动态创建/删除任务和队列。heap_5:支持多个不连续内存区域(如 SRAM + CCM RAM)。嵌入式中尽量在启动时一次性创建所有任务和队列,避免运行期动态分配导致堆碎片。

任务状态机图解

FreeRTOS 任务状态转换 ═══════════════════════════════════════════════════════════════ xTaskCreate() │ ▼ ┌─────────┐ 调度器选中 ┌─────────┐ vTaskSuspend() ┌──────────┐ │ 就绪 │◄────────────│ 运行 │ ─────────────────► │ 挂起 │ │ Ready │─────────────► │ ◄───────────────── Suspended│ └────┬────┘ 被高优先级 └────┬────┘ vTaskResume() └──────────┘ │ 任务抢占 │ │ 事件 等待资源/ │ 发生 延时到期 ▼ │ ┌─────────┐ │ │ 阻塞 │ ◄────────────────┘ │ Blocked │ vTaskDelay() / xQueueReceive() / xSemaphoreTake() └─────────┘ 阻塞原因: ① vTaskDelay(pdMS_TO_TICKS(100)) — 延时100ms ② xQueueReceive(q, &data, portMAX_DELAY) — 等待队列数据 ③ xSemaphoreTake(sem, portMAX_DELAY) — 等待信号量 ④ ulTaskNotifyTake(pdTRUE, portMAX_DELAY) — 等待任务通知 调度器运行时机: 每个 SysTick(1ms) 检查是否有更高优先级就绪任务(抢占) 或当前任务主动调用阻塞 API(协作式切换)

创建任务和队列

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "timers.h"

/* ─── 任务句柄和队列句柄 ──────────────────────────── */
static TaskHandle_t  hTask_Sensor  = NULL;
static TaskHandle_t  hTask_Display = NULL;
static QueueHandle_t qSensorData   = NULL;
static SemaphoreHandle_t xMutex_OLED = NULL;

/* ─── 传输数据结构 ───────────────────────────────── */
typedef struct {
  float    temperature;
  float    humidity;
  uint32_t timestamp;
} SensorData_t;

/* ─── 传感器采集任务(生产者)───────────────────── */
void Task_Sensor(void *pvParams) {
  (void)pvParams;
  SensorData_t data;

  for (;;) {
    /* 采集传感器数据(假设已实现)*/
    data.temperature = DHT22_ReadTemp();
    data.humidity    = DHT22_ReadHumi();
    data.timestamp   = HAL_GetTick();

    /* 发送到队列(阻塞最多 100ms,若队列满则丢弃)*/
    if (xQueueSend(qSensorData, &data, pdMS_TO_TICKS(100)) != pdPASS) {
      /* 队列满,可记录日志或统计丢帧 */
    }

    vTaskDelay(pdMS_TO_TICKS(2000));  /* 每2秒采集一次 */
  }
}

/* ─── 显示任务(消费者)────────────────────────── */
void Task_Display(void *pvParams) {
  (void)pvParams;
  SensorData_t data;
  char buf[32];

  for (;;) {
    /* 等待队列数据(永久阻塞,不占 CPU)*/
    if (xQueueReceive(qSensorData, &data, portMAX_DELAY) == pdPASS) {
      /* 获取 OLED 互斥锁(防止多任务同时刷屏)*/
      if (xSemaphoreTake(xMutex_OLED, pdMS_TO_TICKS(50)) == pdPASS) {
        SSD1306_Clear();
        snprintf(buf, sizeof(buf), "T:%.1fC H:%.1f%%",
                 data.temperature, data.humidity);
        SSD1306_PrintString(0, 0, buf);
        SSD1306_Refresh();
        xSemaphoreGive(xMutex_OLED);
      }
    }
  }
}

/* ─── 主函数:创建所有任务后启动调度器 ─────────── */
int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C1_Init();

  /* 创建队列(深度=5,每条消息 sizeof(SensorData_t) 字节)*/
  qSensorData   = xQueueCreate(5, sizeof(SensorData_t));
  xMutex_OLED   = xSemaphoreCreateMutex();

  /* 创建任务(名称、栈字数、参数、优先级、句柄)*/
  xTaskCreate(Task_Sensor,  "Sensor",  256, NULL, 3, &hTask_Sensor);
  xTaskCreate(Task_Display, "Display", 256, NULL, 2, &hTask_Display);

  /* 启动调度器(永不返回)*/
  vTaskStartScheduler();
  while (1);  /* 永远不会到这里 */
}

任务通知(Task Notification)

任务通知是 FreeRTOS v8.2+ 提供的轻量级替代方案,比信号量快 45%,无需额外内核对象:

/* 从 ISR 通知任务(最高效的 ISR→Task 同步方式)*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  if (GPIO_Pin == GPIO_PIN_0) {
    vTaskNotifyGiveFromISR(hTask_Sensor, &xHigherPriorityTaskWoken);
    /* 若被唤醒的任务优先级更高,请求上下文切换 */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

/* 任务中等待通知 */
void Task_WaitForButton(void *pvParams) {
  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  /* 清零并等待 */
    /* 按键事件处理 */
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
  }
}

看门狗(IWDG / WWDG)

/**
 * IWDG(独立看门狗):由 LSI(32kHz) 时钟驱动,即使主时钟故障也能工作
 * 超时后强制系统复位,用于检测程序跑飞
 * 喂狗操作:定期调用 HAL_IWDG_Refresh()
 */
IWDG_HandleTypeDef hiwdg;

void IWDG_Init(void) {
  hiwdg.Instance       = IWDG;
  hiwdg.Init.Prescaler = IWDG_PRESCALER_64;  /* 32kHz/64 = 500Hz */
  hiwdg.Init.Reload    = 500;               /* 500/500Hz = 1秒超时 */
  HAL_IWDG_Init(&hiwdg);
}

/* FreeRTOS 喂狗任务:最低优先级,正常运行时能定期执行 */
void Task_WDG(void *pvParams) {
  for (;;) {
    HAL_IWDG_Refresh(&hiwdg);
    vTaskDelay(pdMS_TO_TICKS(500));  /* 每500ms喂一次,超时1秒 */
  }
}

FreeRTOS IPC 速查

  • xQueueCreate(len, size) — 创建队列
  • xQueueSend(q, &data, timeout) — 发送
  • xQueueReceive(q, &data, timeout) — 接收
  • xSemaphoreCreateBinary() — 二值信号量
  • xSemaphoreCreateMutex() — 互斥量
  • xSemaphoreGive/Take(sem) — 释放/获取
  • xEventGroupCreate() — 事件组
  • xEventGroupSetBits(eg, bits) — 设置事件

任务管理常用 API

  • xTaskCreate(func, name, stack, param, pri, &hdl)
  • vTaskDelete(handle) — 删除任务
  • vTaskDelay(ticks) — 相对延时
  • vTaskDelayUntil(&prev, period) — 绝对延时
  • uxTaskGetStackHighWaterMark(h) — 栈余量
  • vTaskSuspend/Resume(handle) — 挂起/恢复
  • vTaskPrioritySet(h, priority) — 改优先级
  • xTaskGetTickCount() — 获取系统节拍
FreeRTOS 调试技巧

在 FreeRTOSConfig.h 中开启 configUSE_TRACE_FACILITY=1configGENERATE_RUN_TIME_STATS=1,然后调用 vTaskGetRunTimeStats() 可以打印每个任务的 CPU 占用百分比。这是定位某任务占用 CPU 过多(导致其他任务饥饿)的最直接工具。另外,uxTaskGetStackHighWaterMark() 返回任务栈的最小剩余量,若为 0 则表示曾经溢出。

FreeRTOS 常见错误

① 栈太小:任务函数调用层次深或有大型局部变量,导致栈溢出 → HardFault。建议先开启 configCHECK_FOR_STACK_OVERFLOW=2 捕获溢出。
② ISR 中调用非 FromISR 版本 API:如 xQueueSend() 而非 xQueueSendFromISR(),会导致调度器死锁。
③ Mutex 在 ISR 中使用:互斥量不能在 ISR 中 Take/Give,ISR 应使用二值信号量。
④ configMAX_SYSCALL_INTERRUPT_PRIORITY 设置错误:中断优先级高于此值的 ISR 不能调用 FreeRTOS API,否则破坏内核状态。