2. FreeRTOS内核移植(上):源码结构分析与portable层详解
好,咱们正式开始动手移植FreeRTOS。说实话,很多同学一上来就盯着汇编文件改,结果改完发现调度器跑不起来,连问题出在哪都不知道。我个人习惯是:先看懂源码结构,再动手改代码。你想想看,连FreeRTOS的文件夹里装了什么都不清楚,怎么敢说自己会移植?
2.1 FreeRTOS源码结构分析
从官网下载的FreeRTOS源码包,解压后你会看到两个核心目录:FreeRTOS 和 FreeRTOS-Plus。咱们做内核移植,只需要关注前者。
核心目录结构(以FreeRTOSv202212.01为例)
| 目录/文件 | 说明 | 移植时需要修改? |
|---|---|---|
Source/include/ |
FreeRTOS API头文件,如 task.h、queue.h | ❌ 不需要 |
Source/tasks.c |
任务调度核心实现 | ❌ 不需要 |
Source/queue.c |
队列与信号量实现 | ❌ 不需要 |
Source/timers.c |
软件定时器实现 | ❌ 不需要 |
Source/portable/ |
平台相关层,编译器与MCU适配 | ✅ 核心修改区 |
Source/FreeRTOSConfig.h |
系统配置头文件(需用户提供) | ✅ 必须自己写 |
看到了吗?真正需要你动手的,只有 portable 目录和 FreeRTOSConfig.h。其他 .c 文件你基本不用碰。我记得第一次移植时,傻乎乎地把 tasks.c 翻了个遍,结果发现根本没必要——FreeRTOS的设计哲学就是把平台相关代码隔离到 portable 层,上层逻辑完全通用。
2.2 portable层详解——移植的“心脏”
portable 层是FreeRTOS能跑在不同硬件上的关键。说白了,它就是一层硬件抽象。你想想看,ARM Cortex-M3和RISC-V的寄存器操作能一样吗?当然不能。所以FreeRTOS把底层的寄存器操作、堆栈初始化、临界区保护这些脏活累活,全部扔给了portable层。
打开 Source/portable/ 目录,你会看到两层子目录:
- 第一层:编译器 —— ARM_CM3、ARM_CM4F、GCC、IAR、RVDS 等
- 第二层:具体实现 —— port.c、portmacro.h、portasm.s
以我们最常用的 STM32F4(Cortex-M4F) + Keil(ARMCC) 为例,你需要关注的是:
移植核心文件清单
portable/RVDS/ARM_CM4F/port.c—— C语言实现的移植接口portable/RVDS/ARM_CM4F/portmacro.h—— 数据类型、宏定义、内联函数portable/RVDS/ARM_CM4F/portasm.s—— 汇编实现的 PendSV、SVC 中断处理
这三个文件,就是移植工作的全部。我曾经遇到一个项目,工程师把 portmacro.h 里的 portSTACK_TYPE 定义错了,结果任务切换时堆栈指针总是不对齐,查了整整两天。嗯,细节决定成败。
2.3 Cortex-M3/M4内核移植要点
Cortex-M3和M4的移植流程基本一致,M4多了FPU(浮点运算单元)的处理。咱们一个一个说。
2.3.1 堆栈类型定义
Cortex-M3/M4的堆栈是向下生长的,且必须8字节对齐。在 portmacro.h 中:
/* Cortex-M4F 带FPU的堆栈类型 */
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
#if __FPU_PRESENT == 1
#define portNO_FPU_SUPPORT 0
#else
#define portNO_FPU_SUPPORT 1
#endif
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
⚠️ 特别注意: 如果芯片是M4且带FPU,但你不打算使用浮点运算,可以把 portNO_FPU_SUPPORT 设为1。这样任务切换时会跳过FPU寄存器的保存/恢复,能省下不少CPU开销。我有个低功耗项目就这么干的,任务切换时间从120个周期降到了72个周期。
2.3.2 临界区保护
FreeRTOS进入临界区时,需要关中断。Cortex-M3/M4提供了 cpsid i 指令。在 portmacro.h 中:
/* 临界区宏定义 */
#define portDISABLE_INTERRUPTS() __asm volatile "cpsid i"
#define portENABLE_INTERRUPTS() __asm volatile "cpsie i"
但这里有个坑:中断嵌套。如果你在临界区里又调用了关中断,直接开中断就会破坏嵌套计数。所以FreeRTOS实际用的是 vPortRaiseBASEPRI() 这种方式,通过操作 BASEPRI 寄存器来屏蔽低于某个优先级的中断,而不是粗暴地关全局中断。
2.3.3 PendSV与SVC中断
任务切换的核心机制,就是利用 PendSV(可挂起系统异常) 和 SVC(系统服务调用)。
- SVC:用于启动第一个任务。系统初始化完成后,通过
SVC 0指令触发,在SVC中断处理函数中完成第一个任务的堆栈恢复和启动。 - PendSV:用于任务切换。每次tick中断或任务主动让出CPU时,触发PendSV,在PendSV处理函数中保存当前任务上下文,恢复下一个任务上下文。
在 portasm.s 中,这两个中断的处理函数长这样(简化版):
; SVC Handler - 启动第一个任务
SVC_Handler
; 加载第一个任务的堆栈指针
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
; 从堆栈恢复寄存器
ldmia r0!, {r4-r11}
msr psp, r0
; 返回线程模式,使用PSP
mov r0, #3
msr CONTROL, r0
bx lr
; PendSV Handler - 任务切换
PendSV_Handler
; 保存当前任务上下文
mrs r0, psp
stmdb r0!, {r4-r11}
; 更新当前TCB中的堆栈指针
ldr r3, =pxCurrentTCB
ldr r1, [r3]
str r0, [r1]
; 选择下一个任务
stmdb sp!, {r3, r14}
bl vTaskSwitchContext
ldmia sp!, {r3, r14}
; 恢复下一个任务上下文
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11}
msr psp, r0
bx r14
这段汇编代码,就是移植工作的重中之重。我建议你把它抄下来,逐行理解。我曾经在一个项目里,因为 ldmdb 和 ldmia 搞反了,导致任务切换后寄存器全部错乱,系统直接跑飞。嗯,从那以后我再也不敢凭感觉写汇编了。
2.4 启动文件修改
启动文件(startup_stm32f4xx.s)需要做两件事:
- 增加堆栈大小 —— FreeRTOS每个任务都有自己的栈,启动文件里的系统栈(Stack_Size)建议至少设为 0x400(1KB),否则初始化时可能溢出。
- 修改中断向量表 —— 将
SVC_Handler、PendSV_Handler、SysTick_Handler这三个中断向量指向FreeRTOS的实现。
具体修改如下:
; 启动文件 startup_stm32f4xx.s 中的中断向量表
; 修改前(默认弱定义)
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; 修改后(指向FreeRTOS实现)
DCD SVC_Handler ; 由 portasm.s 提供
DCD DebugMon_Handler ; 保持默认
DCD xPortPendSVHandler ; FreeRTOS的PendSV处理
DCD xPortSysTickHandler ; FreeRTOS的SysTick处理
小技巧: 如果你不想改启动文件,也可以在C代码里用 #define 做函数名重定向。比如在 FreeRTOSConfig.h 中:
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
这样启动文件不用动,链接器会自动把中断向量指向FreeRTOS的函数。我个人更喜欢这种方式,因为启动文件是厂家提供的,尽量少改。
2.5 知识体系总览
下面这张图,把FreeRTOS内核移植的核心脉络画出来了。建议你保存下来,移植过程中随时对照。
2.6 移植前的准备工作清单
动手之前,先对照这个清单检查一下,避免漏项:
| 步骤 | 检查项 | 完成状态 |
|---|---|---|
| 1 | 确认MCU型号(Cortex-M3/M4/M7?带不带FPU?) | ☐ |
| 2 | 确认编译器(Keil/IAR/GCC?) | ☐ |
| 3 | 从portable目录找到对应的编译器+架构文件夹 | ☐ |
| 4 | 复制 port.c、portmacro.h、portasm.s 到工程 | ☐ |
| 5 | 创建 FreeRTOSConfig.h(可参考官方Demo) | ☐ |
| 6 | 修改启动文件或添加函数重定向宏 | ☐ |
| 7 | 确认系统栈大小 ≥ 1KB | ☐ |
⚠️ 常见错误: 很多新手把 FreeRTOSConfig.h 放在 Source/include/ 目录下,这是不对的。这个文件必须由用户工程提供,编译器通过 -I 包含路径指向它。FreeRTOS源码里只提供了 FreeRTOSConfig.h 的模板,你需要根据自己的项目需求修改。