3、FreeRTOS移植实战(上):移植前的准备工作
说实话,很多朋友学RTOS卡就卡在移植这一步。裸机写得好好的,一上RTOS就懵了。我当年第一次移植FreeRTOS到STM32F103时,也折腾了整整两天。后来发现,其实移植没那么玄乎,关键是准备工作要做足。
这一章咱们就先把移植前的活儿干完。说白了,就是三件事:拿到源码、配好Config文件、搞定port层。这三件事做好了,启动第一个任务就是水到渠成的事。
3.1 源码获取与工程模板搭建
先说说源码怎么拿。FreeRTOS官方源码托管在GitHub上,你直接搜"FreeRTOS/FreeRTOS"就能找到。我个人习惯用LTS版本,稳定,坑少。目前最新的LTS是202210.01版。
下载下来后,你会发现源码结构是这样的:
FreeRTOS/
├── FreeRTOS/
│ ├── Source/
│ │ ├── include/ # 核心头文件
│ │ ├── portable/ # 移植层代码
│ │ ├── tasks.c # 任务管理
│ │ ├── queue.c # 队列
│ │ ├── timers.c # 软件定时器
│ │ └── event_groups.c # 事件组
│ └── Demo/ # 官方示例
└── FreeRTOS-Plus/ # 扩展组件
嗯,这里要注意:我们真正需要的只有 FreeRTOS/Source/ 目录下的东西。Demo目录是参考用的,别一股脑全拷进项目里。
工程模板怎么搭?我建议用你熟悉的IDE先建一个裸机工程,能点灯、能跑串口就行。然后在这个基础上加FreeRTOS源码。为什么?因为这样你能确认硬件本身没问题,出错了可以定位到是RTOS的问题。
具体步骤:
- 新建一个裸机工程,配置好时钟、GPIO、串口
- 在工程里新建 FreeRTOS 分组
- 添加 Source 目录下的所有 .c 文件(除了 croutine.c,协程现在基本不用)
- 添加 portable 目录下对应架构的 port.c 和 portmacro.h
- 添加 FreeRTOSConfig.h 配置文件
这里有个坑:heap_1.c 到 heap_5.c 选哪个? 初学者先用 heap_4.c,它支持合并碎片,够用。我刚开始不懂,用了 heap_2,结果跑着跑着内存分配失败,查了半天才发现是碎片问题。
3.2 FreeRTOSConfig.h 配置详解
这个文件是FreeRTOS的灵魂。配置不对,系统跑起来各种诡异。我见过有人把 configTICK_RATE_HZ 设成100000,结果系统直接卡死——中断太频繁了。
下面这张图展示了配置项的核心关系:
下面这张表是我常用的配置模板,以STM32F103 @72MHz为例:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| configCPU_CLOCK_HZ | 72000000 | CPU主频,必须和实际时钟一致 |
| configTICK_RATE_HZ | 1000 | 系统Tick频率,1000Hz=1ms中断一次 |
| configMAX_PRIORITIES | 5 | 优先级数量,够用就行,多了浪费RAM |
| configTOTAL_HEAP_SIZE | 10240 | 堆大小,单位字节,根据项目调整 |
| configMINIMAL_STACK_SIZE | 128 | 空闲任务栈大小,单位字(word) |
| configUSE_PREEMPTION | 1 | 启用抢占式调度,实时性更好 |
| configUSE_TIME_SLICING | 1 | 同优先级任务时间片轮转 |
3.3 port.c 与 portmacro.h 的移植要点
这两个文件是FreeRTOS和硬件之间的桥梁。说白了,port.c负责底层的上下文切换、中断处理;portmacro.h定义了一些宏和数据类型。
如果你用的是Cortex-M3/M4/M7内核,恭喜你,官方已经帮你写好了。在 portable/GCC/ARM_CM3/ 目录下就能找到。但如果你用的是其他内核,或者想自己优化,那就得理解里面的关键点。
port.c 里最重要的三个函数:
- vPortStartFirstTask:启动第一个任务,说白了就是触发一次PendSV中断,让调度器开始工作
- vPortSVCHandler:SVC中断处理,用于启动第一个任务时的上下文加载
- xPortPendSVHandler:PendSV中断处理,真正的上下文切换在这里完成
portmacro.h 里需要关注这几个宏:
/* 数据类型定义 */
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
/* 中断屏蔽宏 */
#define portDISABLE_INTERRUPTS() __asm volatile "cpsid i"
#define portENABLE_INTERRUPTS() __asm volatile "cpsie i"
/* 临界区保护 */
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
嗯,这里有个细节:为什么上下文切换要用PendSV而不是SVC? 因为PendSV可以被挂起,等当前中断处理完了再执行。你想想看,如果在某个中断里调用了任务切换,直接切走的话,中断现场就乱了。PendSV保证了切换的时机是安全的。
- 确认 port.c 里的中断向量名和启动文件一致(比如 PendSV_Handler 还是 xPortPendSVHandler)
- 确认 portmacro.h 里的栈增长方向(Cortex-M是向下增长,configSTACK_GROWTH 设为 -1)
- 确认 SysTick 中断优先级配置(FreeRTOS要求SysTick和PendSV优先级最低)
3.4 启动第一个任务:vTaskStartScheduler
准备工作都做完了,怎么验证移植是否成功?很简单,启动调度器,看第一个任务能不能跑起来。
vTaskStartScheduler 的内部流程是这样的:
- 创建空闲任务(Idle Task)和定时器服务任务(如果启用了)
- 初始化全局变量,设置调度器状态
- 启动SysTick定时器,开始产生Tick中断
- 调用 vPortStartFirstTask,触发SVC中断
- SVC中断里加载第一个任务的上下文,然后跳转到任务函数
写个最简单的测试代码:
void vTask1(void *pvParameters)
{
while(1)
{
// 点灯或者打印,证明任务在跑
GPIO_ToggleBits(GPIOB, GPIO_Pin_0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void)
{
// 硬件初始化
SystemClock_Config();
GPIO_Init();
// 创建任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会跑到这里
while(1);
}
如果移植正确,LED会以500ms的间隔闪烁。如果没反应,别慌,按这个顺序排查:
- SysTick中断没使能——检查NVIC配置
- PendSV优先级设置不对——FreeRTOS要求最低优先级,否则会死锁
- 堆大小不够——configTOTAL_HEAP_SIZE 设大点试试
- 中断向量表没重映射——特别是从Bootloader跳过来的情况
说实话,第一次看到LED按预期闪烁时,那种成就感还是很爽的。这意味着你的裸机程序已经成功跑在了RTOS之上,后面的事情就好办了。