## 一、为什么要用 RTOS? 裸机开发中,我们常依赖中断+主循环(Super Loop)来实现并发,但这种方式在任务增多时会出现响应延迟、代码耦合度高、维护困难等问题。引入 RTOS(实时操作系统)后,我们可以将复杂的业务拆分为多个独立任务,由调度器统一管理。FreeRTOS 是一个开源的、小型化的 RTOS,支持抢占式、协作式和时间片调度,非常适合在 STM32 这类资源有限的 MCU 上运行。 ## 二、FreeRTOS 在 STM32 上的移植准备 ### 1. 获取源码 - 从 FreeRTOS 官网(或 GitHub)下载最新版本,例如 V10.4.6。 - 核心源码位于 `FreeRTOS/Source` 目录,包括 `tasks.c`、`queue.c`、`list.c`、`timers.c` 等。 - 与移植相关的代码在 `FreeRTOS/Source/portable` 中,STM32 使用 `RVDS/ARM_CM3` 或 `GCC/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 的典型配置: ```c #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` 函数开头调用: ```c NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); ``` ## 四、中断与时钟配置 FreeRTOS 依赖 SysTick 作为系统节拍。在 STM32 上,我们一般不让 FreeRTOS 接管 SysTick,而是使用一个专用的硬件定时器(如 TIM7)来提供时基,避免影响 HAL 库的 `HAL_Delay`。但更简单的方式是让 FreeRTOS 使用 SysTick,并在 `SysTick_Handler` 中调用 `xPortSysTickHandler()`: ```c extern void xPortSysTickHandler(void); void SysTick_Handler(void) { xPortSysTickHandler(); } ``` 此外,需要实现 `vApplicationTickHook`(如果配置了),通常留空即可。 ## 五、编写任务并启动调度器 下面是一个双任务示例,一个任务 LED 闪烁,另一个任务串口打印: ```c #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 优先级被设置为最低,因此不会干扰其他中断。 ```c 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 字节,不要搞混。 - **钩子函数**:`vApplicationStackOverflowHook`、`vApplicationMallocFailedHook` 建议实现,便于调试。 - **SysTick 频率**:`configTICK_RATE_HZ` 不宜过高,1000Hz 已足够,过高会增加调度开销。 - **临界区**:`taskENTER_CRITICAL()` 会关闭中断,注意代码执行时间不能太长。 ## 九、总结 FreeRTOS 在 STM32 上的移植并不复杂,核心是配置 `FreeRTOSConfig.h`、选择正确的移植层、处理好中断。理解任务调度原理——优先级抢占 + 时间片轮转,以及基于 PendSV 的上下文切换,能帮助你在多任务设计中更游刃有余。熟练掌握 RTOS,你的嵌入式开发思维将提升一个层次。