3. FreeRTOS内核移植详解:修改portmacro.h、port.c、portasm.s,适配Cortex-M4/M7架构
好,咱们进入正题。这一章是移植工作的核心,说白了就是让FreeRTOS能在Cortex-M4或M7上跑起来。你想想看,一个操作系统内核,它得知道怎么保存任务现场、怎么切换任务、怎么处理中断。这些底层操作,都藏在这三个文件里:portmacro.h、port.c、portasm.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异常正好提供了这个机制。
个人经验:在调试任务切换时,我习惯在vPortSVCHandler和xPortPendSVHandler里加断点。如果这两个函数没有被触发,说明任务调度根本没跑起来。先检查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的实现细节。到时候见。