4、FreeRTOS内核移植(上):添加FreeRTOS源码、修改启动文件、配置SysTick中断
好,咱们正式开始动手移植FreeRTOS。说实话,很多初学者一听到“移植”两个字就觉得头大,觉得这是大神才干的事。其实不然。你想想看,STM32的生态已经非常成熟了,官方和社区把大部分脏活累活都干完了。我们要做的,说白了就是把FreeRTOS的源码请进工程,然后告诉芯片:“嘿,以后这个定时器归RTOS管了。”
我个人习惯把移植分成三步走:加源码 → 改启动 → 配时钟。这一章我们先搞定前两步,顺便把SysTick中断配置讲透。嗯,这三步走顺了,你的FreeRTOS就能在STM32上跑起来了。
核心目标:让FreeRTOS的调度器能够在STM32上获得“心跳”,并且能够正确响应SysTick中断,完成任务切换的准备工作。
4.1 添加FreeRTOS源码到工程
这一步其实没什么技术含量,但容易出错。我见过不少同事直接把整个FreeRTOS文件夹拖进工程,结果编译报错一大堆——因为里面有很多用不到的demo文件和移植层代码。
正确的做法是:只拿我们需要的。FreeRTOS的源码结构其实很清晰,核心就两个目录:
| 目录/文件 | 说明 | 是否必须 |
|---|---|---|
Source/ |
内核核心代码,包括tasks.c、queue.c、list.c等 | 是 |
Source/include/ |
所有头文件,必须包含在工程头文件路径中 | 是 |
Source/portable/ |
移植层代码,按编译器+MCU架构选择 | 是 |
Source/portable/MemMang/ |
内存管理方案,heap_1.c ~ heap_5.c | 选一个 |
具体到我们的STM32F1系列(Cortex-M3内核),使用Keil MDK编译器,你需要添加的文件如下:
FreeRTOS/
├── Source/
│ ├── tasks.c
│ ├── queue.c
│ ├── list.c
│ ├── timers.c
│ ├── event_groups.c
│ └── croutine.c // 协程,一般不用,可删
│
├── Source/portable/
│ ├── RVDS/ // ARM RealView编译器
│ │ └── ARM_CM3/
│ │ ├── port.c
│ │ └── portmacro.h
│ └── MemMang/
│ └── heap_4.c // 我推荐用这个,防碎片
我的建议:内存管理选 heap_4.c。它支持合并相邻空闲块,能有效防止内存碎片。我在一个需要长期运行的设备上吃过heap_2的亏——跑了三天,任务创建失败了。换成heap_4之后,再没出过问题。
添加完文件后,别忘了在Keil的C/C++编译器选项中,把 FreeRTOS/Source/include 路径加到头文件搜索目录里。这一步漏了,编译会报“找不到头文件”,别问我怎么知道的……
4.2 修改启动文件——让FreeRTOS接管PendSV和SVC
好,源码加完了。但这时候编译,大概率会报错。为什么?因为FreeRTOS需要两个关键中断向量:SVC(系统服务调用)和 PendSV(可挂起系统调用)。
在裸机工程里,这两个中断的入口函数是弱定义的,或者干脆是空的。FreeRTOS需要把它们指向自己的处理函数。
具体来说,你要在启动文件(比如 startup_stm32f10x_hd.s)里,找到这两行:
DCD SVC_Handler ; SVC Handler
DCD PendSV_Handler ; PendSV Handler
把它们改成:
DCD vPortSVCHandler ; SVC Handler
DCD xPortPendSVHandler ; PendSV Handler
为什么要改?因为FreeRTOS的移植层代码(port.c)里,已经实现了这两个中断服务函数。如果你不改,系统启动后调用SVC指令时,会跳转到你原来的空函数,调度器根本跑不起来。
注意:有些教程会让你在工程里重新定义 SVC_Handler 和 PendSV_Handler 函数,用强符号覆盖弱符号。这种做法也能用,但我个人更推荐直接改启动文件——干净、直观,以后排查问题的时候一眼就能看出来。
改完之后,记得把启动文件里的 SVC_Handler 和 PendSV_Handler 的弱定义注释掉,避免重复定义。嗯,这一步很容易忘,我踩过这个坑。
4.3 配置SysTick中断——给RTOS一颗“心脏”
FreeRTOS需要一个周期性的时基(tick),用来做任务调度和时间管理。这个时基通常由SysTick定时器提供。SysTick是Cortex-M内核自带的24位递减计数器,简单可靠,不用额外配置外设定时器。
配置SysTick,其实就是在 FreeRTOSConfig.h 里定义好时钟频率,然后在 port.c 里调用初始化函数。但这里有个关键点:SysTick的中断优先级。
FreeRTOS要求SysTick的中断优先级必须设置为 最低优先级。为什么?因为调度器在临界区(比如操作队列、创建任务)时会关闭部分中断,如果SysTick优先级太高,它可能会打断临界区操作,导致数据不一致。
具体配置如下:
// FreeRTOSConfig.h 中配置时钟
#define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) // STM32F1主频72MHz
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 1ms一个tick
#define configSYSTICK_CLOCK_HZ ( configCPU_CLOCK_HZ / 8 ) // SysTick时钟源,通常为HCLK/8
然后在 port.c 中,FreeRTOS会调用 vPortSetupTimerInterrupt() 来初始化SysTick。这个函数内部会设置SysTick的重装载值、时钟源,以及中断优先级。
如果你用的是标准库或者HAL库,也可以手动初始化SysTick,但要注意:不要和HAL库的HAL_Init()冲突。HAL库默认也会初始化SysTick作为时基,优先级是0(最高)。如果你直接跑FreeRTOS,两个SysTick中断会打架。
避坑指南:我曾经在一个项目里,用了HAL库的 HAL_Delay(),同时又跑了FreeRTOS。结果发现任务调度偶尔会卡死。查了两天才发现,HAL库把SysTick优先级设成了0,FreeRTOS的临界区关不掉它,导致队列操作被中断破坏。解决方案:要么把HAL库的SysTick优先级也改成最低,要么干脆不用HAL_Delay,改用FreeRTOS的 vTaskDelay()。
4.4 本章知识结构图
下面这张图,帮你把整个移植流程串起来。从源码添加到启动文件修改,再到SysTick配置,每一步的输入输出都很清楚。
4.5 验证移植是否成功
配置完成后,怎么知道移植成功了?最简单的方法:写一个空任务,然后启动调度器。如果程序能跑起来,SysTick中断正常触发,说明移植基本没问题。
这里给一个最小验证代码:
#include "FreeRTOS.h"
#include "task.h"
void vTask1(void *pvParameters)
{
while(1)
{
// 任务循环,暂时什么都不做
}
}
int main(void)
{
// 硬件初始化(时钟、GPIO等)
// ...
// 创建任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
如果程序卡在 vTaskStartScheduler() 之后不动了,或者直接跑飞,别慌。先检查这几样:
- SysTick中断是否使能:在调试模式下,查看SysTick的CTRL寄存器,确认ENABLE位为1。
- 中断优先级是否正确:FreeRTOS要求SysTick优先级为最低(数值最大),检查NVIC的优先级分组是否匹配。
- PendSV和SVC向量是否指向正确函数:在启动文件中确认。
重要提醒:如果你用的是STM32CubeMX生成的工程,它默认会初始化SysTick并设置优先级为0。这时候直接跑FreeRTOS,调度器会异常。解决办法:在 HAL_Init() 之后,手动调用 HAL_SuspendTick() 暂停HAL的SysTick,然后让FreeRTOS接管。