第4章 FreeRTOS内核移植:源码目录结构、port文件修改、系统时钟配置

好,咱们进入正题。这一章讲的是FreeRTOS移植最核心的三个动作:看懂源码怎么摆的、改好port文件、配准系统时钟。说白了,就是把FreeRTOS这个通用系统,硬塞进你的MCU里,让它能跑起来。

我刚开始做移植那会儿,也踩过不少坑。有一次在STM32F103上移植,时钟配错了,任务调度死活不切换。查了两天,最后发现是SysTick的优先级没设对。嗯,这种问题,你遇到了就知道有多头疼。

4.1 源码目录结构:先搞清楚东西放哪

拿到FreeRTOS源码包,别急着复制粘贴。先看看它的目录长什么样。我个人习惯,先打开根目录,扫一眼。

典型的FreeRTOS目录结构是这样的:

FreeRTOS/
├── Source/           ← 内核核心代码
│   ├── include/      ← 头文件
│   ├── portable/     ← 移植层(重点!)
│   └── *.c           ← tasks.c, queue.c, timers.c...
├── Demo/             ← 官方示例工程
└── License/          ← 许可证

你真正要关心的,是 Source/portable/ 这个目录。它里面按编译器、按MCU架构分了很多子目录。比如:

  • portable/GCC/ARM_CM3/ —— 针对Cortex-M3,用GCC编译器
  • portable/IAR/ARM_CM4F/ —— 针对Cortex-M4F,用IAR编译器
  • portable/MemMang/ —— 内存管理方案(heap_1.c ~ heap_5.c)

你想想看,如果你的MCU是Cortex-M4内核,编译器是Keil,那你就得找 portable/Keil/ARM_CM4F/ 这个目录下的文件。别找错了,不然编译一堆错误。

核心原则:移植工作,90%的精力都在portable目录里。其他文件基本不用动。

4.2 port文件修改:让内核认识你的硬件

port文件,全称是 port.cportmacro.h。这两个文件是FreeRTOS和硬件之间的桥梁。

portmacro.h 里定义了数据类型、临界区宏、栈增长方向等。比如:

/* 针对32位MCU,重新定义数据类型 */
#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 portSTACK_GROWTH    ( -1 )

/* 临界区宏 */
#define portDISABLE_INTERRUPTS()    __disable_irq()
#define portENABLE_INTERRUPTS()     __enable_irq()

这里有个坑,我提醒你一下。栈增长方向千万别搞反了。Cortex-M系列的栈是向下增长的,所以 portSTACK_GROWTH 要设为 -1。如果你设成 1,任务切换时栈指针会乱飞,程序直接跑飞。我曾经帮一个同事排查过这个问题,他折腾了三天,最后发现就是这里写反了。

再看 port.c,它主要实现三个东西:

  • 任务栈初始化pxPortInitialiseStack() —— 创建任务时,把CPU寄存器初始值压入栈
  • 启动第一个任务vPortStartFirstTask() —— 手动触发PendSV异常,开始调度
  • PendSV异常处理xPortPendSVHandler() —— 任务切换的核心,保存/恢复上下文

以Cortex-M3为例,PendSV处理函数的汇编代码大概长这样:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

    PRESERVE8

    mrs r0, psp         /* 获取当前任务栈指针 */
    isb

    ldr r3, =pxCurrentTCB
    ldr r2, [r3]

    stmdb r0!, {r4-r11} /* 保存r4~r11到栈 */
    str r0, [r2]        /* 更新TCB中的栈指针 */

    stmdb sp!, {r3, r14}
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp!, {r3, r14}

    ldr r1, [r3]
    ldr r0, [r1]        /* 获取新任务的栈指针 */
    ldmia r0!, {r4-r11} /* 恢复r4~r11 */

    msr psp, r0
    isb
    bx r14
}

这段代码,说白了就是:保存当前任务现场,切换到下一个任务,恢复新任务现场。你不需要背下来,但得理解它的流程。我建议你直接拿官方例程里的port.c,只改跟MCU相关的部分,比如中断优先级屏蔽寄存器(BASEPRI)的设置。

我的小技巧:移植时,先找一个官方已经支持的、跟你MCU内核相同的port文件。比如都是Cortex-M4,那就拿STM32F4的port文件改。这样能省80%的工作量。

4.3 系统时钟配置:给FreeRTOS一个心跳

FreeRTOS需要一个定时器来产生系统节拍(Tick)。通常用SysTick,也就是系统滴答定时器。这个定时器每1ms中断一次,内核就在中断里检查是否需要切换任务。

配置SysTick,主要做三件事:

  1. 设置重装载值:决定中断频率
  2. 使能中断:让SysTick能触发异常
  3. 设置中断优先级:这个最关键,优先级必须设为最低

代码示例:

/* 在port.c或单独的系统时钟配置文件中 */
void vPortSetupTimerInterrupt( void )
{
    /* 设置SysTick重装载值 */
    /* 假设系统时钟为72MHz,Tick周期为1ms */
    /* 重装载值 = 72000000 / 1000 - 1 = 71999 */
    SysTick->LOAD = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1;

    /* 使能SysTick中断,使能计数器 */
    SysTick->VAL = 0;                     /* 清空计数器 */
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk |
                    SysTick_CTRL_ENABLE_Msk;

    /* 设置SysTick中断优先级为最低 */
    /* Cortex-M中,优先级数值越大,优先级越低 */
    NVIC_SetPriority( SysTick_IRQn, (1 << __NVIC_PRIO_BITS) - 1 );
}

这里有个关键点:SysTick的中断优先级必须设为最低。为什么?因为FreeRTOS的临界区是通过屏蔽中断实现的。如果SysTick优先级太高,在临界区里它还能打断,那任务调度就会乱套。我见过有人把SysTick优先级设成0(最高),结果临界区保护完全失效,队列操作数据全乱了。

另外,configCPU_CLOCK_HZconfigTICK_RATE_HZ 这两个宏要在 FreeRTOSConfig.h 里定义好:

#define configCPU_CLOCK_HZ        ( ( unsigned long ) 72000000 )  /* 系统时钟频率 */
#define configTICK_RATE_HZ        ( ( TickType_t ) 1000 )         /* 系统节拍频率,1ms */

你想想看,如果系统时钟频率配错了,比如实际是72MHz,你配成8MHz,那SysTick的溢出时间就不是1ms了,而是8.9ms。任务调度周期全乱,延时函数也不准。嗯,这种问题排查起来特别隐蔽。

警告:SysTick中断处理函数的名字,必须跟启动文件里的中断向量表一致。Cortex-M3/M4通常叫 SysTick_Handler。FreeRTOS在port.c里会定义一个 xPortSysTickHandler(),你需要把启动文件里的 SysTick_Handler 弱定义覆盖掉,或者直接修改中断向量表指向 xPortSysTickHandler

4.4 移植后的验证:跑起来才算数

移植完别急着写应用。先跑一个最简单的测试:创建两个任务,轮流点亮LED。代码大概这样:

void vTask1( void *pvParameters )
{
    for( ;; )
    {
        LED1_ON();
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
        LED1_OFF();
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
    }
}

void vTask2( void *pvParameters )
{
    for( ;; )
    {
        LED2_ON();
        vTaskDelay( pdMS_TO_TICKS( 1000 ) );
        LED2_OFF();
        vTaskDelay( pdMS_TO_TICKS( 1000 ) );
    }
}

如果LED能按预期闪烁,说明移植基本成功了。如果灯不亮或者闪烁频率不对,优先检查:

  • SysTick配置是否正确
  • PendSV中断优先级是否设为最低
  • 栈大小是否足够(默认的configMINIMAL_STACK_SIZE可能偏小)

我记得有一次,移植到一款国产MCU上,SysTick配置完全按照手册来,但就是不进中断。后来发现是那个芯片的SysTick时钟源跟标准Cortex-M不一样,需要额外配置时钟选择位。这种坑,只能靠仔细看芯片手册来避免。

好了,这一章的内容就这些。总结一下:目录结构搞清楚,port文件改对,时钟配准,然后跑个LED测试。移植没那么玄乎,但细节决定成败。