3. FreeRTOS内核移植:源码目录结构、port层文件修改、汇编启动文件适配

好,咱们进入正题。FreeRTOS移植这件事,说白了就是让这个RTOS内核能在你的目标芯片上跑起来。我刚开始做移植时也觉得挺玄乎,后来发现其实就是三件事:搞懂源码结构、改好port层、配好启动文件。嗯,咱们一个一个来。

3.1 源码目录结构——先别急着改代码

拿到FreeRTOS源码包,第一件事不是打开IDE,而是先看看它长什么样。我个人习惯先理清目录结构,不然改着改着就迷路了。

典型的FreeRTOS源码目录是这样的:

FreeRTOS/
├── Source/
│   ├── include/          # 内核头文件
│   ├── portable/         # 移植层,重点!
│   │   ├── MemMang/      # 内存管理方案
│   │   ├── RVDS/         # ARM RealView编译器
│   │   ├── GCC/          # GCC编译器
│   │   └── IAR/          # IAR编译器
│   ├── tasks.c           # 任务管理核心
│   ├── queue.c           # 队列
│   ├── timers.c          # 软件定时器
│   └── event_groups.c    # 事件组
└── Demo/                 # 示例工程

这里我要强调一下portable目录。它里面按编译器分了子目录,每个子目录下又有按架构分的文件。比如ARM Cortex-M3/M4,在GCC目录下你会看到:

  • portmacro.h —— 数据类型重定义、架构相关宏
  • port.c —— 任务切换、中断处理、临界区
  • portasm.s —— 汇编实现的上下文切换
我的经验: 别自己从头写port层。找到最接近你芯片的现成port文件,在此基础上改。我在项目中移植到国产MCU时,就是拿STM32的port文件改的,省了至少一周时间。

3.2 port层文件修改——核心中的核心

port层是FreeRTOS和硬件之间的桥梁。说白了,它要告诉内核三件事:怎么保存任务上下文、怎么切换任务、怎么处理中断。

3.2.1 portmacro.h 修改

这个文件定义了一些基础类型和宏。比如:

/* 数据类型重定义 */
#define portCHAR        char
#define portFLOAT       float
#define portDOUBLE      double
#define portLONG        long
#define portSHORT       short
#define portSTACK_TYPE  uint32_t
#define portBASE_TYPE   long

/* 架构相关宏 */
#define portMAX_DELAY           ( TickType_t ) 0xffffffffUL
#define portTICK_PERIOD_MS      ( ( TickType_t ) 1000 / configTICK_RATE_HZ )
#define portBYTE_ALIGNMENT      8

这里要注意portSTACK_TYPE。它决定了栈指针的宽度。32位MCU用uint32_t,16位用uint16_t。我曾经见过有人把32位芯片配成16位,结果任务栈一压栈就崩,查了两天才发现是这里的问题。

3.2.2 port.c 修改——临界区与中断

临界区保护是RTOS的命根子。FreeRTOS通过开关中断来实现:

/* 进入临界区 */
void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
    
    /* 如果嵌套计数为1,保存当前中断状态 */
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}

/* 退出临界区 */
void vPortExitCritical( void )
{
    configASSERT( uxCriticalNesting > 0 );
    uxCriticalNesting--;
    
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}
避坑指南: 我曾经在移植时忘了处理临界区嵌套。结果一个中断里调了API,API里又进了临界区,嵌套计数乱掉,中断再也出不来了。嗯,从那以后我每次移植都会仔细检查uxCriticalNesting的处理逻辑。

3.3 汇编启动文件适配——让芯片知道从哪开始

启动文件是芯片上电后执行的第一段代码。它要做的事包括:设置栈指针、初始化中断向量表、调用main函数。FreeRTOS移植时,我们需要在启动文件里做两件事:

  1. 设置系统时钟和中断优先级 —— 确保SysTick能正确产生心跳
  2. 分配RTOS需要的栈空间 —— 特别是中断服务程序的栈

以ARM Cortex-M为例,启动文件里通常有这样的代码:

; 栈配置
__initial_sp   EQU  0x20010000    ; 栈顶地址

; 中断向量表
                DCD   __initial_sp              ; 栈指针
                DCD   Reset_Handler             ; 复位中断
                DCD   NMI_Handler               ; NMI
                DCD   HardFault_Handler         ; 硬错误
                ; ... 其他中断向量

; 复位处理
Reset_Handler   PROC
                EXPORT  Reset_Handler
                
                ; 初始化系统时钟
                BL      SystemInit
                
                ; 跳转到main
                BL      main
                
                ENDP

这里有个关键点:中断向量表的顺序不能错。芯片上电后会从0地址读取栈指针,从4地址读取复位入口。我见过有人把这两个位置搞反了,芯片直接跑飞。

3.3.1 SysTick中断适配

FreeRTOS的心跳由SysTick提供。在port.c里,我们需要实现vPortSetupTimerInterrupt

void vPortSetupTimerInterrupt( void )
{
    /* 设置SysTick重装载值 */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    
    /* 使能SysTick中断 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
                                   portNVIC_SYSTICK_INT_BIT |
                                   portNVIC_SYSTICK_ENABLE_BIT );
}
重要提醒: configSYSTICK_CLOCK_HZ的值取决于你的时钟配置。如果系统时钟是72MHz,而SysTick用的是处理器时钟的8分频,那这里就要填9MHz。我刚开始移植时直接填了72MHz,结果心跳快了8倍,任务调度乱成一锅粥。

3.4 移植后的验证——别急着跑应用

移植完成后,我建议先跑一个最简单的测试:创建两个任务,一个闪灯,一个打印。如果这两个任务能正常切换,说明移植基本成功了。

测试代码大概长这样:

void vTask1( void *pvParameters )
{
    for( ;; )
    {
        vTaskDelay( pdMS_TO_TICKS( 1000 ) );
        printf( "Task1 running\n" );
    }
}

void vTask2( void *pvParameters )
{
    for( ;; )
    {
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
        printf( "Task2 running\n" );
    }
}

int main( void )
{
    xTaskCreate( vTask1, "Task1", 128, NULL, 1, NULL );
    xTaskCreate( vTask2, "Task2", 128, NULL, 2, NULL );
    vTaskStartScheduler();
    
    /* 正常情况下不会执行到这里 */
    for( ;; );
}

如果两个任务能交替打印,恭喜你,移植成功了。如果卡住了,别慌,先检查这几个地方:

  • 中断优先级分组是否配置正确?FreeRTOS要求使用4位抢占优先级
  • SysTick中断是否使能?优先级是否够高?
  • 临界区开关中断的指令是否正确?
我的调试技巧:vPortStartFirstTaskxPortPendSVHandler里加几个GPIO翻转。用示波器看波形,能直观看到任务切换是否正常。这比看打印信息快多了。

嗯,FreeRTOS移植其实没那么神秘。把源码结构理清楚,port层改对,启动文件配好,剩下的就是细心调试。我每次移植都会在笔记本上记下踩过的坑,下次遇到同系列芯片就能直接复用。好了,这一章就到这,下一章咱们聊聊任务栈的优化——这可是个能省出大块内存的技巧。