4、FreeRTOS移植:源码获取、移植步骤、任务创建与调度机制

好,咱们进入第四讲。这一章要聊的是FreeRTOS移植。说实话,很多初学者一听到「移植」两个字就头大,觉得那是大神才干的事。其实没那么玄乎。我当年第一次移植FreeRTOS到STM32上,也就花了一个下午。关键是你要知道每一步在干什么,而不是机械地复制粘贴。

4.1 FreeRTOS源码获取

先说说源码从哪来。我个人习惯直接从FreeRTOS官网下载,或者去GitHub上拉官方仓库。网址我就不写了,你搜一下「FreeRTOS official」就能找到。

下载下来后,你会看到一个叫 FreeRTOSv202212.01 这样的文件夹(版本号会变)。里面主要有两部分:

  • FreeRTOS:核心源码,包括 tasks.cqueue.clist.c 这些文件
  • FreeRTOS-Plus:一些扩展组件,比如TCP/IP协议栈、FAT文件系统等

我们做Modbus网关,只需要核心部分就够了。别贪多,先把基础跑起来。

我的小建议: 别用最新版。选一个经过大量验证的稳定版本,比如v10.4.x系列。我在项目中吃过最新版的亏——有个bug社区还没修完,折腾了我两天。

4.2 移植步骤:从零到跑起来

移植说白了就是让FreeRTOS认识你的硬件。我把它拆成四个步骤,你照着做就行。

4.2.1 准备一个裸机工程

你得先有一个能正常跑起来的裸机工程。比如用STM32CubeMX生成一个,点个LED、串口打印个「Hello World」。这一步是为了确认你的硬件和编译器没问题。

嗯,这里要注意:时钟配置一定要对。FreeRTOS的心跳(SysTick)依赖系统时钟,时钟错了,任务调度就全乱套了。

4.2.2 添加FreeRTOS源码

把下载好的源码复制到你的工程里。通常需要这几个文件:

  • tasks.cqueue.clist.ctimers.cevent_groups.c
  • portable 文件夹下的移植层代码,比如 port.cportmacro.h
  • FreeRTOSConfig.h 配置文件

我习惯把源码放在一个叫 FreeRTOS 的文件夹里,和用户代码分开。这样看着清爽,也好维护。

4.2.3 配置FreeRTOSConfig.h

这个文件是FreeRTOS的灵魂。里面定义了系统能用多少任务、堆栈多大、时钟频率多少等等。我贴一个我常用的配置模板:

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/* 基础配置 */
#define configUSE_PREEMPTION          1
#define configUSE_IDLE_HOOK           0
#define configUSE_TICK_HOOK           0
#define configCPU_CLOCK_HZ            ( ( unsigned long ) 72000000 )
#define configTICK_RATE_HZ            ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES          ( 5 )
#define configMINIMAL_STACK_SIZE      ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE         ( ( size_t ) ( 20 * 1024 ) )
#define configMAX_TASK_NAME_LEN       ( 16 )

/* 启用功能 */
#define configUSE_16_BIT_TICKS        0
#define configIDLE_SHOULD_YIELD       1
#define configUSE_MUTEXES             1
#define configUSE_RECURSIVE_MUTEXES   1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_QUEUE_SETS          0

/* 钩子函数 */
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK   1

#endif /* FREERTOS_CONFIG_H */

重点: configTOTAL_HEAP_SIZE 决定了系统可用内存。Modbus网关通常需要处理多个串口和网络连接,我建议至少给20KB。太小了任务一多就崩。

4.2.4 实现移植接口

FreeRTOS需要几个底层接口才能跑起来。主要是这三个:

  • vPortSetupTimerInterrupt():配置系统心跳定时器。通常用SysTick,也可以用一个通用定时器。
  • vPortYield():触发任务切换。一般通过PendSV中断实现。
  • prvStartFirstTask():启动第一个任务。这个函数会初始化栈指针,然后跳转到第一个任务。

如果你用的是Cortex-M3/M4内核,FreeRTOS的portable层已经帮你写好了大部分代码。你只需要在启动文件里把 PendSV_HandlerSysTick_Handler 的中断处理函数替换成FreeRTOS的版本就行。

我曾经踩过的坑: 中断优先级配置。FreeRTOS要求所有中断的优先级不能高于 configMAX_SYSCALL_INTERRUPT_PRIORITY。我一开始没注意,结果串口中断老是抢任务切换的优先级,导致系统卡死。查了整整一天才找到原因。

4.3 任务创建:让代码「活」起来

移植完成后,第一件事就是创建任务。任务说白了就是一个无限循环的函数。FreeRTOS会帮你管理这些函数的执行顺序。

创建任务用 xTaskCreate() 函数。原型是这样的:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,    // 任务函数指针
    const char * const pcName,    // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 堆栈大小(单位是字,不是字节)
    void *pvParameters,           // 任务参数
    UBaseType_t uxPriority,       // 优先级(0最低,configMAX_PRIORITIES-1最高)
    TaskHandle_t *pxCreatedTask   // 任务句柄(可以传NULL)
);

举个例子,创建一个LED闪烁任务:

void vLEDTask(void *pvParameters)
{
    (void)pvParameters;  // 防止编译器警告

    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500));  // 延时500ms
    }
}

// 在main函数中创建任务
int main(void)
{
    // 硬件初始化
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    // 创建任务
    xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);

    // 启动调度器
    vTaskStartScheduler();

    // 正常情况下不会执行到这里
    while(1);
}

你看,就这么简单。一个任务就创建好了。你想想看,如果不用RTOS,你得自己写一个延时循环,还得处理各种中断嵌套。有了FreeRTOS,你只需要关心每个任务自己的逻辑就行。

4.4 调度机制:任务是怎么切换的

任务创建好了,那它们是怎么轮流执行的?这就涉及到FreeRTOS的调度机制了。

FreeRTOS用的是抢占式调度。什么意思?就是高优先级的任务可以随时打断低优先级的任务。系统每过一个tick(比如1ms),就会检查一次有没有更高优先级的任务需要运行。

调度流程大致是这样的:

  1. SysTick中断触发
  2. 中断服务函数调用 xTaskIncrementTick()
  3. 这个函数检查所有任务的时间片是否用完
  4. 如果当前任务的时间片到了,就触发PendSV中断
  5. PendSV中断里执行 vTaskSwitchContext(),选出下一个要运行的任务
  6. 恢复新任务的上下文,继续执行

整个过程对用户来说是透明的。你只需要设置好优先级,剩下的交给FreeRTOS。

我的经验: 优先级不是越高越好。Modbus网关里,我通常把串口接收任务的优先级设得比较高(因为数据不能丢),而LED闪烁这种任务优先级最低。别把所有任务都设成同一个优先级,那样就退化成时间片轮转了,实时性反而不好。

4.5 任务状态与状态切换

任务在运行过程中,会在几个状态之间切换。理解这个对调试很有帮助。

状态 说明 如何进入
运行态 正在使用CPU 被调度器选中
就绪态 可以运行,但CPU被别的任务占着 创建任务、从阻塞态恢复
阻塞态 等待某个事件(延时、信号量、队列等) 调用vTaskDelay()、xQueueReceive()等
挂起态 不参与调度 调用vTaskSuspend()

我刚开始学的时候,总搞不清阻塞态和挂起态的区别。后来在项目里调试一个串口接收超时的问题,才真正理解了:阻塞态是「我暂时不跑,但随时准备回来」,挂起态是「别叫我,我自己会醒」。

4.6 常见问题与调试技巧

移植过程中难免遇到问题。我总结几个最常见的:

  • 任务不运行:检查 vTaskStartScheduler() 是否调用了,以及SysTick中断是否使能。
  • 堆栈溢出:在 FreeRTOSConfig.h 里把 configCHECK_FOR_STACK_OVERFLOW 设为2,然后实现 vApplicationStackOverflowHook() 函数。一旦溢出,这个函数会被调用。
  • 内存不足:检查 configTOTAL_HEAP_SIZE 是否够用。可以用 xPortGetFreeHeapSize() 查看剩余堆内存。

调试利器: FreeRTOS提供了 vTaskList()vTaskGetRunTimeStats() 两个函数,可以打印所有任务的状态和CPU占用率。我每次调试都会在串口终端里跑一下这两个函数,问题一目了然。

好了,这一章的内容就到这。移植FreeRTOS其实没那么难,关键是动手做一遍。下一章我们会讲Modbus协议栈的移植,到时候这些任务就会真正派上用场了。