2. 嵌入式系统架构基础

好,咱们直接进入正题。RTOS 的核心是什么?说白了就是「任务调度」。你想想看,一个单片机只有一个 CPU 核心,却要同时处理按键扫描、LCD 刷新、传感器读取、通信协议栈……怎么做到的?就是靠调度器在任务之间快速切换,让你感觉它们在「同时运行」。

我刚开始用裸机写项目时,主循环里塞了一堆函数,后来加个功能就得重构。用了 RTOS 之后,真有种「解放了」的感觉。但调度机制要是没搞懂,踩坑是迟早的事。

2.1 抢占式调度 vs 协作式调度

这两种调度模式,是 RTOS 世界的「两条路」。FreeRTOS 默认用抢占式,但也支持协作式。咱们先看看区别。

特性 抢占式调度 协作式调度
切换时机 任何时刻,只要高优先级任务就绪 只有当前任务主动让出 CPU
实时性 高,响应快 低,依赖任务自觉
代码复杂度 需要处理临界区保护 相对简单,但容易死锁
典型场景 工业控制、通信协议 简单轮询、低功耗应用

关键点:抢占式调度中,高优先级任务可以「打断」低优先级任务的执行。这意味着你必须在共享数据访问时加临界区保护,否则会出现数据竞争。

我在一个电机控制项目里遇到过这种情况:两个任务共享一个速度变量,一个任务写,一个任务读。没加临界区保护,结果读到的数据有时是半个字节——电机直接抖了一下。嗯,从那以后我再也不敢轻视临界区了。

2.2 任务状态机

每个任务在 RTOS 里都有生命周期。FreeRTOS 定义了四种状态:运行态、就绪态、阻塞态、挂起态。我习惯用一张图来理解它们的关系。

FreeRTOS 任务状态机 运行态 就绪态 阻塞态 挂起态 被抢占/时间片用完 调度器选中 等待事件/延时 事件到达/超时 vTaskSuspend() vTaskResume()

个人经验:调试任务卡死时,先查任务状态。如果任务一直处于就绪态但没运行,说明有更高优先级的任务占着 CPU。如果任务卡在阻塞态,检查事件或超时设置。

2.3 临界区与中断管理

临界区,就是一段「不允许被打断」的代码。在 FreeRTOS 里,临界区通过关中断来实现。为什么?因为中断是最高优先级的「任务」,它随时可能发生,如果中断里访问了共享数据,而主循环也在访问,数据就乱了。

FreeRTOS 提供了两套临界区保护机制:

  • taskENTER_CRITICAL() / taskEXIT_CRITICAL():用于任务级临界区,会关掉所有可屏蔽中断。
  • 中断级临界区:用于中断服务函数中,使用 portSET_INTERRUPT_MASK() 等宏。
// 任务级临界区示例
void vTaskFunction(void *pvParameters) {
    for(;;) {
        taskENTER_CRITICAL();
        // 访问共享资源,比如全局变量
        g_shared_counter++;
        taskEXIT_CRITICAL();
        // 其他代码
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

注意:临界区里不要做耗时操作!关中断时间过长,会导致系统无法响应外部事件。我曾经在一个项目中把 SPI 通信放在临界区里,结果中断延迟从 5us 飙升到 2ms——整个系统直接「聋了」。

中断管理方面,FreeRTOS 有一个重要的概念:中断优先级分组。ARM Cortex-M 内核支持 8~256 级中断优先级,FreeRTOS 通过 configMAX_SYSCALL_INTERRUPT_PRIORITY 来划分「安全中断」和「不安全中断」。优先级高于这个值的中断,FreeRTOS 不会关掉它们——这是为了保证硬实时中断的响应。

中断优先级 能否调用 FreeRTOS API 典型用途
高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 不能 硬实时中断(如电机霍尔传感器)
低于或等于 configMAX_SYSCALL_INTERRUPT_PRIORITY 能(使用 FromISR 后缀 API) 普通外设中断(如 UART、定时器)

2.4 FreeRTOS 源码移植实战

移植 FreeRTOS 到新平台,说白了就是让内核知道「怎么切换任务」、「怎么管理中断」。我以 STM32F4 为例,说说移植的关键步骤。

  1. 准备源码:从官网下载 FreeRTOS 源码,只需要 Source 目录下的 .c 和 .h 文件,以及 portable 目录下对应编译器(如 GCC/Keil/IAR)的移植文件。
  2. 配置 FreeRTOSConfig.h:这是移植的核心。需要定义 configCPU_CLOCK_HZ、configTICK_RATE_HZ、configMAX_PRIORITIES 等宏。
  3. 实现 PendSV_Handler 和 SysTick_Handler:PendSV 是任务切换的「触发器」,SysTick 提供系统心跳。
  4. 修改启动文件:确保中断向量表中 PendSV 和 SysTick 的入口指向 FreeRTOS 的实现。
// FreeRTOSConfig.h 关键配置示例
#define configCPU_CLOCK_HZ                ((unsigned long)168000000)
#define configTICK_RATE_HZ                ((TickType_t)1000)
#define configMAX_PRIORITIES              (5)
#define configMINIMAL_STACK_SIZE          ((unsigned short)128)
#define configTOTAL_HEAP_SIZE             ((size_t)(40 * 1024))
#define configUSE_PREEMPTION              1
#define configUSE_IDLE_HOOK               0
#define configUSE_TICK_HOOK               0

避坑指南:我曾经在移植时忘了改 configCPU_CLOCK_HZ,结果系统时钟跑得飞快,任务调度完全乱套。调试了整整一天才发现是时钟配置不对。所以移植第一步,先确认时钟频率。

移植完成后,写一个简单的测试任务:

void vTestTask(void *pvParameters) {
    for(;;) {
        // 翻转一个 GPIO,用示波器看频率
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

如果示波器上看到 1Hz 的方波,恭喜你,移植成功了。

小技巧:调试移植问题时,先把 configUSE_PREEMPTION 设为 0(协作式调度),这样任务切换只发生在 vTaskDelay() 或 taskYIELD() 时,更容易定位问题。