第1章:FreeRTOS移植与工程搭建

各位同学,咱们今天正式开始搞FreeRTOS。说实话,我最早接触RTOS的时候,也觉得这东西挺玄乎的。后来在几个项目里摔过跟头,才慢慢摸到门道。今天这一章,咱们就把FreeRTOS的移植和第一个任务跑起来,这是后面所有实战的基础。

1.1 FreeRTOS源码结构

先看看FreeRTOS的源码长什么样。你从官网或者GitHub上下载下来,解压后会发现这么几个核心目录:

  • Source/ — 核心源码,所有RTOS的调度、队列、信号量都在这里
  • Source/include/ — 头文件,你写应用时#include的那些
  • Source/portable/ — 移植层,不同MCU的适配代码
  • Demo/ — 官方示例工程,参考价值很大

我个人习惯,拿到源码后先看FreeRTOSConfig.h这个文件。它就像RTOS的开关面板,配置不对,后面全白搭。我在一个项目中就吃过这个亏——忘了配置堆大小,任务创建直接返回NULL,查了半天。

核心文件速查表:

文件作用
tasks.c任务创建、删除、调度核心
queue.c队列与消息传递
timers.c软件定时器
event_groups.c事件组
port.c / portmacro.h移植层,与硬件相关

1.2 移植步骤

移植说白了,就是把FreeRTOS嫁接到你的芯片上。不同MCU的移植细节不一样,但核心步骤是通用的。我以STM32为例,给你拆解一下:

  1. 准备基础工程 — 先有一个能点灯的裸机工程,时钟、GPIO都调通
  2. 复制源码 — 把Source/目录下的文件加到工程里
  3. 选择移植层 — 在portable/里找到对应MCU的文件夹,比如ARM_CM3、ARM_CM4F
  4. 配置FreeRTOSConfig.h — 设置堆大小、任务优先级数量、时钟节拍频率
  5. 实现滴答定时器 — 提供一个1ms中断,调用xPortSysTickHandler()
  6. 实现SVC和PendSV中断 — 这两个是RTOS切换任务的关键
  7. 编译调试 — 先编译通过,再跑个空任务验证

注意:我曾经在移植时忘了配置中断优先级分组。FreeRTOS要求中断优先级分组为4(即4位抢占优先级,0位子优先级)。如果设置不对,调度器会跑飞。这个坑我踩过,你千万别踩。

嗯,这里要特别说一下FreeRTOSConfig.h里的几个关键配置:

#define configUSE_PREEMPTION          1   // 抢占式调度
#define configCPU_CLOCK_HZ            ( ( unsigned long ) 72000000 )  // 主频
#define configTICK_RATE_HZ            ( ( TickType_t ) 1000 )  // 1ms一个tick
#define configMAX_PRIORITIES          ( 5 )  // 优先级数量
#define configMINIMAL_STACK_SIZE      ( ( unsigned short ) 128 )  // 最小栈大小
#define configTOTAL_HEAP_SIZE         ( ( size_t ) ( 15 * 1024 ) )  // 堆大小15KB

你想想看,堆大小设得太小,任务创建会失败;设得太大,RAM不够用。我一般先给个保守值,跑起来后用uxTaskGetStackHighWaterMark()看看实际用了多少,再调整。

1.3 创建第一个任务

移植搞定后,咱们来创建第一个任务。说白了,任务就是一个永不返回的C函数,里面通常是个死循环。

void vTask1(void *pvParameters)
{
    while(1)
    {
        // 任务代码写在这里
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));  // 延时500ms
    }
}

void vTask2(void *pvParameters)
{
    while(1)
    {
        // 另一个任务
        printf("Task2 is running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

然后在main函数里创建并启动调度器:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    // 创建任务
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);

    // 启动调度器
    vTaskStartScheduler();

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

小技巧:任务栈大小单位是字(word),不是字节。在32位MCU上,128个字就是512字节。我建议每个任务至少给128个字,复杂任务给256或512。用uxTaskGetStackHighWaterMark()可以查看剩余栈空间,帮你找到最优值。

1.4 任务状态切换

任务在FreeRTOS里有四种状态:运行态、就绪态、阻塞态、挂起态。你想想看,CPU只有一个核,同一时刻只能跑一个任务。那其他任务在干嘛?它们就在这几个状态之间跳来跳去。

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

状态切换的典型场景是这样的:

  • 任务A正在运行,调用vTaskDelay(100),进入阻塞态
  • 调度器从就绪态任务中选一个优先级最高的,比如任务B,让它运行
  • 100ms后,任务A的延时结束,回到就绪态
  • 如果任务B此时也阻塞了,调度器就让任务A运行

我在一个按摩仪项目里就遇到过一个问题:两个任务优先级设反了,导致高优先级的任务一直占着CPU,低优先级的任务饿死了。后来我把按摩电机控制和按键扫描的任务优先级调了一下,问题就解决了。说白了,优先级设计是个经验活,多试几次就明白了。

记住这个原则:任务优先级不要超过5个,太多反而增加调度开销。我一般用3-4个优先级就够用了。

小结

这一章咱们把FreeRTOS的源码结构、移植步骤、第一个任务的创建、以及任务状态切换都过了一遍。你跟着做一遍,把LED闪起来,就算入门了。下一章咱们会深入任务调度机制,看看FreeRTOS到底是怎么决定「下一个该谁跑」的。

有什么问题,欢迎交流。咱们下章见。


公众号:蓝海资料掘金营,微信deep3321