一、为什么要用 RTOS?

裸机开发中,我们常依赖中断+主循环(Super Loop)来实现并发,但这种方式在任务增多时会出现响应延迟、代码耦合度高、维护困难等问题。引入 RTOS(实时操作系统)后,我们可以将复杂的业务拆分为多个独立任务,由调度器统一管理。FreeRTOS 是一个开源的、小型化的 RTOS,支持抢占式、协作式和时间片调度,非常适合在 STM32 这类资源有限的 MCU 上运行。

二、FreeRTOS 在 STM32 上的移植准备

1. 获取源码

  • 从 FreeRTOS 官网(或 GitHub)下载最新版本,例如 V10.4.6。
  • 核心源码位于 FreeRTOS/Source 目录,包括 tasks.cqueue.clist.ctimers.c 等。
  • 与移植相关的代码在 FreeRTOS/Source/portable 中,STM32 使用 RVDS/ARM_CM3GCC/ARM_CM3,根据编译器选择。
  • 还需要 FreeRTOS/Source/include 下的头文件,以及 Demo 目录中可选的 FreeRTOSConfig.h

2. 工程文件组织

Project/
├── BSP/                   // 板级驱动
├── FreeRTOS/
│   ├── include/           // 头文件
│   ├── portable/
│   │   ├── GCC/ARM_CM3/   // 移植层(Cortex-M3)
│   │   └── MemMang/       // 内存管理(heap_4.c)
│   ├── tasks.c
│   ├── queue.c
│   └── list.c
└── User/
    └── FreeRTOSConfig.h   // FreeRTOS 配置文件

三、关键配置 FreeRTOSConfig.h

FreeRTOSConfig.h 是 FreeRTOS 的灵魂,下面是一个适配 STM32F103 的典型配置:

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

#include "stm32f1xx.h"

#define configUSE_PREEMPTION                    1   // 使能抢占式调度
#define configUSE_TIME_SLICING                  1   // 使能时间片轮转
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1   // 使用硬件指令优化任务选择
#define configUSE_TICKLESS_IDLE                 0   // 不使用低功耗 tickless
#define configCPU_CLOCK_HZ                      72000000UL  // CPU 主频 72MHz
#define configTICK_RATE_HZ                      1000        // 系统节拍 1000Hz
#define configMAX_PRIORITIES                    5           // 最大优先级数
#define configMINIMAL_STACK_SIZE                128         // 最小任务栈大小(字)
#define configTOTAL_HEAP_SIZE                   (10 * 1024) // 堆大小 10KB
#define configMAX_TASK_NAME_LEN                 16          // 任务名最大长度

#define configUSE_16_BIT_TICKS                  0           // 32 位 tick
#define configIDLE_SHOULD_YIELD                 1           // IDLE 任务让出 CPU

#define configQUEUE_REGISTRY_SIZE               8
#define configUSE_MUTEXES                       1
#define configUSE_COUNTING_SEMAPHORES           1
#define configUSE_RECURSIVE_MUTEXES             1

/* 中断优先级设置 */
#define configPRIO_BITS                         4           // STM32 使用 4 位优先级
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY     15      // 最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5       // 可调用 FreeRTOS API 的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY \
    ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
    ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

#define configUSE_TIMERS                         1
#define configTIMER_TASK_PRIORITY                2
#define configTIMER_QUEUE_LENGTH                 10
#define configTIMER_TASK_STACK_DEPTH             256

#endif
#endif

注意:STM32 中断优先级分组必须设置为 NVIC_PriorityGroup_4(即全为抢占优先级),否则 FreeRTOS 临界区保护可能失效。在 main 函数开头调用:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

四、中断与时钟配置

FreeRTOS 依赖 SysTick 作为系统节拍。在 STM32 上,我们一般不让 FreeRTOS 接管 SysTick,而是使用一个专用的硬件定时器(如 TIM7)来提供时基,避免影响 HAL 库的 HAL_Delay。但更简单的方式是让 FreeRTOS 使用 SysTick,并在 SysTick_Handler 中调用 xPortSysTickHandler()

extern void xPortSysTickHandler(void);

void SysTick_Handler(void)
{
    xPortSysTickHandler();
}

此外,需要实现 vApplicationTickHook(如果配置了),通常留空即可。

五、编写任务并启动调度器

下面是一个双任务示例,一个任务 LED 闪烁,另一个任务串口打印:

#include "FreeRTOS.h"
#include "task.h"

void vLEDTask(void *pvParameters)
{
    for(;;)
    {
        GPIO_ToggleBits(GPIOC, GPIO_Pin_13);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vPrintTask(void *pvParameters)
{
    for(;;)
    {
        printf("FreeRTOS running, tick=%d\r\n", xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    SystemInit();
    // 初始化 GPIO、串口等

    xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);
    xTaskCreate(vPrintTask, "Print", 128, NULL, 2, NULL);

    vTaskStartScheduler();  // 启动调度器,不会返回

    while(1);   // 不会执行到这里
}

六、任务调度原理详解

1. 任务状态与切换

FreeRTOS 中每个任务有四种状态:运行(Running)、就绪(Ready)、阻塞(Blocked)、挂起(Suspended)。调度器通过任务控制块(TCB)和就绪列表来管理任务。

  • 抢占式调度:当更高优先级的任务进入就绪态时,立即抢占当前任务。这由 vTaskDelay、信号量、队列等 API 触发上下文切换。
  • 时间片轮转:当多个任务具有相同优先级时,每个任务运行一个时间片(通常为 1 个 SysTick)后切换给下一个同优先级任务。
  • 空闲任务:如果没有就绪任务,运行优先级最低的空闲任务,可在此执行低功耗等操作。

2. 上下文切换细节

在 Cortex-M3 上,FreeRTOS 使用 PendSV 异常来完成上下文切换。其核心原理是:

  • 每次 SysTick 中断(或更高优先级任务就绪)时,触发 PendSV。
  • 在 PendSV_Handler 中,先保存当前任务的寄存器(R4-R11、PSP、LR 等)到任务自己的栈中。
  • 然后选择下一个就绪任务,恢复其寄存器上下文。
  • 最后通过 bx lr 触发异常返回,硬件自动恢复 R0-R3、R12、LR、PC、xPSR。

PendSV 优先级被设置为最低,因此不会干扰其他中断。

void PendSV_Handler(void)
{
    __asm volatile (
        " mrs r0, psp                         \n"
        " isb                                \n"
        " ldr r1, =__get_PSP\n"
        // 完整汇编实现由 FreeRTOS 提供的 port.c 处理
    );
    // 实际项目直接使用 FreeRTOS 自带的 vPortPendSVHandler,切勿重复定义
}

实际移植时,不要自行写 PendSV_Handler,而应使用 portable/GCC/ARM_CM3/port.c 中定义的 xPortPendSVHandler,在启动文件中替换或重映射中断向量。

七、内存管理

FreeRTOS 提供 5 种 heap 方案,常用的 heap_4.c 支持合并碎片,适合大多数场景。确保 configTOTAL_HEAP_SIZE 足够所有任务栈 + 内核对象分配。

八、注意事项

  • 中断优先级:绝不能使用优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断来调用 FreeRTOS API,否则会破坏临界区。
  • 任务栈大小xTaskCreate 的栈大小以“字”为单位,在 STM32 上 1 字 = 4 字节。128 表示 512 字节,不要搞混。
  • 钩子函数vApplicationStackOverflowHookvApplicationMallocFailedHook 建议实现,便于调试。
  • SysTick 频率configTICK_RATE_HZ 不宜过高,1000Hz 已足够,过高会增加调度开销。
  • 临界区taskENTER_CRITICAL() 会关闭中断,注意代码执行时间不能太长。

九、总结

FreeRTOS 在 STM32 上的移植并不复杂,核心是配置 FreeRTOSConfig.h、选择正确的移植层、处理好中断。理解任务调度原理——优先级抢占 + 时间片轮转,以及基于 PendSV 的上下文切换,能帮助你在多任务设计中更游刃有余。熟练掌握 RTOS,你的嵌入式开发思维将提升一个层次。