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)需要做两件事:

  1. 增加堆栈大小 —— FreeRTOS每个任务都有自己的栈,启动文件里的系统栈(Stack_Size)建议至少设为 0x400(1KB),否则初始化时可能溢出。
  2. 修改中断向量表 —— 将 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内核移植的核心脉络画出来了。建议你保存下来,移植过程中随时对照。

FreeRTOS内核移植知识体系 FreeRTOS源码结构 Source/ 通用内核代码 Source/portable/ 平台相关层 tasks.c 任务调度核心 queue.c 队列/信号量 port.c C移植接口 portasm.s 汇编切换代码 Cortex-M3/M4 移植四大要点 堆栈类型定义 8字节对齐 临界区保护 BASEPRI方式 PendSV/SVC 任务切换机制 启动文件修改 中断向量重定向 💡 移植核心:portable层 + FreeRTOSConfig.h 其他通用代码(tasks.c、queue.c)无需修改

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 的模板,你需要根据自己的项目需求修改。