第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.c 和 portmacro.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,主要做三件事:
- 设置重装载值:决定中断频率
- 使能中断:让SysTick能触发异常
- 设置中断优先级:这个最关键,优先级必须设为最低
代码示例:
/* 在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_HZ 和 configTICK_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测试。移植没那么玄乎,但细节决定成败。