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—— 汇编实现的上下文切换
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();
}
}
uxCriticalNesting的处理逻辑。
3.3 汇编启动文件适配——让芯片知道从哪开始
启动文件是芯片上电后执行的第一段代码。它要做的事包括:设置栈指针、初始化中断向量表、调用main函数。FreeRTOS移植时,我们需要在启动文件里做两件事:
- 设置系统时钟和中断优先级 —— 确保SysTick能正确产生心跳
- 分配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中断是否使能?优先级是否够高?
- 临界区开关中断的指令是否正确?
vPortStartFirstTask和xPortPendSVHandler里加几个GPIO翻转。用示波器看波形,能直观看到任务切换是否正常。这比看打印信息快多了。
嗯,FreeRTOS移植其实没那么神秘。把源码结构理清楚,port层改对,启动文件配好,剩下的就是细心调试。我每次移植都会在笔记本上记下踩过的坑,下次遇到同系列芯片就能直接复用。好了,这一章就到这,下一章咱们聊聊任务栈的优化——这可是个能省出大块内存的技巧。