3. FreeRTOS内核移植详解:修改portmacro.h、port.c、portasm.s,适配Cortex-M4/M7架构

好,咱们进入正题。这一章是移植工作的核心,说白了就是让FreeRTOS能在Cortex-M4或M7上跑起来。你想想看,一个操作系统内核,它得知道怎么保存任务现场、怎么切换任务、怎么处理中断。这些底层操作,都藏在这三个文件里:portmacro.hport.cportasm.s

我个人习惯,拿到一个新的MCU项目,第一件事就是把这几个文件从官方Demo里拷出来,然后对照着手册改。别自己从头写,那是给自己找麻烦。

3.1 portmacro.h:定义数据类型与架构特性

这个头文件,是FreeRTOS和硬件之间的“翻译官”。它告诉内核:你的int是32位的,你的栈指针是向下增长的,你的中断优先级怎么配置。

咱们先看数据类型定义。Cortex-M4/M7是32位机,所以:

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

typedef portSTACK_TYPE StackType_t;
typedef long           BaseType_t;
typedef unsigned long  UBaseType_t;

#if( configUSE_16_BIT_TICKS == 0 )
    #define portMAX_DELAY  ( TickType_t ) 0xffffffffUL
#else
    #define portMAX_DELAY  ( TickType_t ) 0xffff
#endif

嗯,这里要注意:StackType_t必须是32位的。为什么?因为Cortex-M的硬件压栈,一次压4个字节。你要是定义成16位,栈就乱套了。

接下来是中断屏蔽宏。这是FreeRTOS实现临界区的关键:

/* 中断屏蔽宏 */
#define portDISABLE_INTERRUPTS()    vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()     vPortSetBASEPRI( 0 )

这里有个坑。我曾经在M7上调试一个呼吸机项目,发现中断偶尔会丢失。查了两天才发现,是portDISABLE_INTERRUPTS把所有中断都关了,包括SysTick。正确的做法是用BASEPRI寄存器,只屏蔽优先级低于某个阈值的中断,而不是用PRIMASK全部关掉。

关键配置:在Cortex-M4/M7上,一定要使用BASEPRI方式屏蔽中断。PRIMASK方式会关掉所有中断,包括系统滴答定时器,导致任务调度停止。

3.2 port.c:实现任务切换与系统时钟

这个文件是移植工作的重头戏。它实现了任务切换的核心函数、系统时钟配置、以及中断处理。

先看系统时钟配置。FreeRTOS需要一个周期性的心跳,通常是1ms一次:

/* 系统时钟初始化 */
void vPortSetupTimerInterrupt( void )
{
    /* 配置SysTick为1ms中断 */
    configASSERT( configTICK_RATE_HZ <= 1000 );
    
    uint32_t ulReloadValue = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1;
    
    /* 设置重装载值 */
    SysTick->LOAD = ulReloadValue;
    
    /* 使能SysTick中断,使用处理器时钟 */
    SysTick->CTRL = ( 1 << 1 ) |  // 使能中断
                    ( 1 << 2 ) |  // 使用处理器时钟
                    ( 1 << 0 );   // 使能计数器
}

这里有个细节:configCPU_CLOCK_HZ一定要和实际CPU频率一致。我见过有人把168MHz写成了168kHz,结果系统时钟慢得像蜗牛,任务调度完全乱套。

接下来是任务切换函数。这是FreeRTOS最核心的部分:

/* 启动第一个任务 */
void vPortStartFirstTask( void )
{
    /* 触发SVC异常,在SVC_Handler中启动第一个任务 */
    __asm volatile (
        " ldr r0, =0xE000ED08 \n"  /* 向量表偏移寄存器 */
        " ldr r0, [r0] \n"
        " ldr r0, [r0] \n"         /* 获取栈顶指针 */
        " msr msp, r0 \n"          /* 设置主栈指针 */
        " cpsie i \n"              /* 使能中断 */
        " svc 0 \n"                /* 触发SVC */
        " nop \n"
    );
}

你可能会问:为什么要用SVC异常来启动第一个任务?因为FreeRTOS需要在一个受控的环境下,从内核模式切换到用户任务模式。SVC异常正好提供了这个机制。

个人经验:在调试任务切换时,我习惯在vPortSVCHandlerxPortPendSVHandler里加断点。如果这两个函数没有被触发,说明任务调度根本没跑起来。先检查SysTick中断是否正常,再检查中断优先级配置。

3.3 portasm.s:汇编级的任务上下文切换

这个汇编文件,是FreeRTOS移植中最“硬核”的部分。它直接操作CPU寄存器,保存和恢复任务上下文。

先看PendSV中断处理函数。这是任务切换的入口:

; PendSV中断处理
xPortPendSVHandler:
    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         ; 屏蔽高优先级中断
    dsb
    isb
    bl vTaskSwitchContext   ; 调用C函数选择下一个任务
    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             ; 更新PSP
    isb
    bx r14                  ; 异常返回

这段代码,我当年调试了整整一个通宵。为什么?因为有个细节没注意:stmdb r0!, {r4-r11}这条指令,它把寄存器压栈的顺序是R11、R10...R4。如果你恢复的时候顺序搞反了,任务栈就全乱了。

重要提醒:Cortex-M4/M7有FPU(浮点运算单元)。如果你的任务使用了浮点运算,必须在任务栈中保存FPU寄存器。否则,任务切换时浮点上下文会丢失,计算结果完全错误。

启用FPU支持需要在FreeRTOSConfig.h中定义:#define configENABLE_FPU 1

3.4 中断优先级配置的陷阱

这是移植工作中最容易出问题的地方。Cortex-M4/M7的中断优先级有4位(M4)或8位(M7),但FreeRTOS只使用高4位。

架构 优先级位数 FreeRTOS使用位数 最大优先级值
Cortex-M4 4位 高4位 15
Cortex-M7 8位 高4位 15

我曾经在一个项目里,把SysTick中断优先级设成了0(最高优先级),结果导致任务切换时无法被其他中断打断,系统实时性大打折扣。正确的做法是:SysTick和PendSV使用最低优先级(15),这样任何中断都能打断任务调度。

3.5 验证移植是否成功

改完这三个文件,怎么知道移植对了?我一般会跑一个简单的测试:

/* 移植验证测试 */
void vTestTask1( void *pvParameters )
{
    for( ;; )
    {
        /* 任务1:翻转LED1 */
        HAL_GPIO_TogglePin( LED1_GPIO_Port, LED1_Pin );
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
    }
}

void vTestTask2( void *pvParameters )
{
    for( ;; )
    {
        /* 任务2:翻转LED2 */
        HAL_GPIO_TogglePin( LED2_GPIO_Port, LED2_Pin );
        vTaskDelay( pdMS_TO_TICKS( 1000 ) );
    }
}

如果两个LED以不同的频率闪烁,说明任务调度正常。如果只有一个LED亮,或者两个都常亮,那就要回头检查了。

避坑指南:我曾经在M7上移植FreeRTOS,任务死活不切换。查了三天,最后发现是FreeRTOSConfig.h里的configCPU_CLOCK_HZ写错了。M7有动态频率调整功能,我用的时钟源是PLL,但配置里写的是HSI的16MHz。SysTick算出来的重装载值完全不对,系统时钟根本跑不起来。

嗯,这一章的内容就到这里。三个文件,每个都有它的门道。portmacro.h管数据类型和中断屏蔽,port.c管系统时钟和任务启动,portasm.s管最底层的上下文切换。改的时候,一定要对照着Cortex-M4/M7的技术参考手册,一个寄存器一个寄存器地确认。

下一章,咱们会讲FreeRTOS的内存管理,特别是heap_4.c和heap_5.c的实现细节。到时候见。