FreeRTOS 在 STM32 上的移植与任务调度机制详解
👁 10 阅读 · 2026-08-14 · RTOS RTOS
本文以STM32F103为例,系统讲解FreeRTOS的移植流程与任务调度原理。从下载源码、配置内核、修改中断到编写多任务Demo,逐步演示如何让RTOS在Cortex-M3上高效运行,并深入剖析基于优先级抢占式调度与时间片轮转的协同机制,帮助嵌入式开发者快速上手FreeRTOS。
## 一、为什么要用 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,你的嵌入式开发思维将提升一个层次。