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目录是参考用的,别一股脑全拷进项目里。

我的习惯:只拷贝 Source 目录,然后在工程里建一个 FreeRTOS 分组,把源文件加进去。portable 目录下只保留你用的编译器+架构文件夹,其他的删掉,省得编译时出幺蛾子。

工程模板怎么搭?我建议用你熟悉的IDE先建一个裸机工程,能点灯、能跑串口就行。然后在这个基础上加FreeRTOS源码。为什么?因为这样你能确认硬件本身没问题,出错了可以定位到是RTOS的问题。

具体步骤:

  1. 新建一个裸机工程,配置好时钟、GPIO、串口
  2. 在工程里新建 FreeRTOS 分组
  3. 添加 Source 目录下的所有 .c 文件(除了 croutine.c,协程现在基本不用)
  4. 添加 portable 目录下对应架构的 port.c 和 portmacro.h
  5. 添加 FreeRTOSConfig.h 配置文件

这里有个坑:heap_1.c 到 heap_5.c 选哪个? 初学者先用 heap_4.c,它支持合并碎片,够用。我刚开始不懂,用了 heap_2,结果跑着跑着内存分配失败,查了半天才发现是碎片问题。

3.2 FreeRTOSConfig.h 配置详解

这个文件是FreeRTOS的灵魂。配置不对,系统跑起来各种诡异。我见过有人把 configTICK_RATE_HZ 设成100000,结果系统直接卡死——中断太频繁了。

下面这张图展示了配置项的核心关系:

FreeRTOSConfig.h 核心配置项关系图 FreeRTOSConfig.h 基础配置 configCPU_CLOCK_HZ configTICK_RATE_HZ configMAX_PRIORITIES 内存配置 configTOTAL_HEAP_SIZE configMINIMAL_STACK_SIZE configSUPPORT_DYNAMIC_ALLOCATION 功能开关 configUSE_PREEMPTION configUSE_TIME_SLICING INCLUDE_vTaskDelayUntil 配置原则 1. 基础配置:先确认CPU时钟和Tick频率,这是系统心跳 2. 内存配置:堆大小根据任务数量和栈需求估算,留余量 3. 功能开关:用不到的API关掉,节省ROM

下面这张表是我常用的配置模板,以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 同优先级任务时间片轮转
注意:configCPU_CLOCK_HZ 必须和你的系统时钟一致。我见过有人用外部8MHz晶振,但配置里写的是72MHz,结果系统时间全乱套了。delay(1000) 实际只等了111ms,排查起来非常痛苦。

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 的内部流程是这样的:

  1. 创建空闲任务(Idle Task)和定时器服务任务(如果启用了)
  2. 初始化全局变量,设置调度器状态
  3. 启动SysTick定时器,开始产生Tick中断
  4. 调用 vPortStartFirstTask,触发SVC中断
  5. 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之上,后面的事情就好办了。