3、FreeRTOS入门:FreeRTOS简介、源码结构、移植方法、任务创建与删除、任务优先级

各位好,欢迎来到《运动控制多任务调度设计》的第三讲。今天咱们聊聊FreeRTOS。说实话,在运动控制领域,裸机跑循环的时代已经过去了。你想想看,一个电机要转,一个编码器要读,一个通信要处理,还要响应急停——单靠一个while(1)大循环,迟早要出问题。

FreeRTOS是什么?说白了,就是一个轻量级的实时操作系统内核。它帮你管理CPU的时间,让多个任务看起来像是“同时”在运行。我在做多轴联动控制时,就靠它来分配每个轴的计算时间片。嗯,这里要注意,FreeRTOS不是万能的,但在资源受限的MCU上,它绝对是最靠谱的选择之一。

FreeRTOS简介

FreeRTOS的全称是“Free Real-Time Operating System”,免费、开源、小巧。它的核心功能就三样:任务调度、任务间通信、内存管理。你不需要关心底层怎么切换上下文,只需要把精力放在业务逻辑上。

我个人习惯把FreeRTOS看作一个“任务管家”。你创建几个任务,告诉它哪个任务优先级高,哪个任务需要多少栈空间,剩下的它帮你搞定。比如在运动控制中,我通常会创建三个任务:一个负责轨迹规划(高优先级),一个负责IO监控(中优先级),一个负责日志输出(低优先级)。

核心特点:

  • 抢占式调度:高优先级任务随时可以打断低优先级任务
  • 协作式调度:任务主动让出CPU
  • 时间片轮转:同优先级任务轮流执行
  • 支持任务、队列、信号量、互斥量、软件定时器

我曾经在一个项目中,因为不了解抢占式调度的特性,导致一个低优先级的电机控制任务被高优先级的通信任务频繁打断,结果电机出现了抖动。后来我调整了优先级,才解决了问题。所以,优先级设计真的不是随便填个数字就完事的。

源码结构

FreeRTOS的源码结构非常清晰。你下载下来后,主要关注这几个目录:

目录/文件 说明
Source/ 核心源码,包括tasks.c、queue.c、timers.c等
Source/include/ 头文件,FreeRTOS.h、task.h、queue.h等
Source/portable/ 移植层,针对不同MCU的适配代码
Demo/ 官方示例工程,可以直接参考

你可能会问:“这么多文件,我该看哪个?”我的建议是,先看tasks.c和queue.c。这两个文件是FreeRTOS的灵魂。tasks.c负责任务创建、删除、调度;queue.c负责任务间通信。其他的比如timers.c、event_groups.c,用到的时候再看。

我记得刚开始学FreeRTOS时,总想一口气看完所有源码。结果越看越晕。后来我师傅告诉我:“别贪多,先跑起来一个任务,再慢慢加。”嗯,这个建议我一直用到现在。

移植方法

移植FreeRTOS到你的板子上,其实就三步:

  1. 选择移植层:在Source/portable/目录下找到你的MCU对应的文件夹。比如STM32就用GCC/ARM_CM4F。
  2. 配置FreeRTOSConfig.h:这个文件是FreeRTOS的“开关”。你需要配置CPU频率、堆栈大小、最大优先级数等。
  3. 实现滴答定时器:FreeRTOS需要一个1ms的定时器中断来驱动调度。通常用SysTick来实现。

小技巧: 移植时,先不要急着改配置。直接用官方Demo里的FreeRTOSConfig.h,然后根据你的MCU调整一下CPU频率和堆栈大小就行。我刚开始移植时,因为把configMINIMAL_STACK_SIZE设得太小,结果任务一运行就崩溃。后来改成128个字(512字节),才稳定下来。

下面是一个典型的FreeRTOSConfig.h片段:

#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 ) )

这里要注意,configTICK_RATE_HZ设为1000,表示每个tick是1ms。如果你需要更精细的时间控制,可以设成10000(0.1ms),但这样会增加CPU开销。我在运动控制中通常用1000Hz,因为电机控制的周期一般是1ms,刚好匹配。

任务创建与删除

任务创建是FreeRTOS最基础的操作。用xTaskCreate()函数就行。它的原型是这样的:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位:字)
    void *pvParameters,             // 任务参数
    UBaseType_t uxPriority,         // 任务优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄(输出)
);

举个例子,创建一个电机控制任务:

void vMotorControlTask(void *pvParameters)
{
    while(1)
    {
        // 读取编码器
        // 计算PID
        // 输出PWM
        vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
    }
}

void main(void)
{
    TaskHandle_t xMotorHandle = NULL;
    xTaskCreate(vMotorControlTask, "Motor", 256, NULL, 3, &xMotorHandle);
    vTaskStartScheduler(); // 启动调度器
}

删除任务用vTaskDelete()。如果你在任务内部删除自己,可以传NULL:

void vSomeTask(void *pvParameters)
{
    // 做一些初始化工作
    // 如果初始化失败,删除自己
    vTaskDelete(NULL);
}

警告: 千万不要在中断服务函数中调用vTaskDelete()!我曾经犯过这个错误,结果系统直接卡死。正确的做法是在中断中发送一个信号量,然后在任务中处理删除操作。

任务优先级

优先级是FreeRTOS调度的心脏。数值越大,优先级越高。比如优先级3的任务,会打断优先级2的任务。同优先级的任务,则按时间片轮转。

在运动控制中,我一般这样分配优先级:

  • 最高优先级(4-5):紧急停止、故障处理。这些任务必须立即响应,不能有任何延迟。
  • 高优先级(3):电机控制、轨迹插补。这些任务对实时性要求高,周期固定。
  • 中优先级(2):通信处理(CAN、EtherCAT)、IO监控。
  • 低优先级(1):日志输出、状态显示、参数配置。

你可能会问:“为什么紧急停止要单独一个任务?”嗯,这是因为在运动控制中,急停信号可能来自硬件中断,但处理逻辑需要在一个任务中完成。如果把这个任务放在低优先级,万一CPU正忙着做轨迹计算,急停信号就被忽略了。这可不是闹着玩的。

我曾经在一个项目中,把通信任务设成了最高优先级。结果通信数据量一大,电机控制任务就被饿死了。后来我调整了优先级,让电机控制任务比通信任务高一级,问题就解决了。所以,优先级设计一定要结合你的实际场景来定。

知识体系结构图

下面我用一张SVG图来总结本章的核心内容。这张图展示了FreeRTOS在运动控制中的角色,以及任务创建、优先级、移植之间的关系。

FreeRTOS在运动控制中的知识体系 FreeRTOS内核 任务创建与删除 任务优先级 移植方法 源码结构 xTaskCreate vTaskDelete 抢占式 时间片轮转 选择移植层 配置Config tasks.c queue.c 运动控制应用:电机控制 + 通信 + 日志

这张图把FreeRTOS的核心知识点串起来了。你从内核出发,左边是任务创建与删除,右边是优先级,下面是移植方法和源码结构。最底部是运动控制中的典型应用场景。嗯,这样一看,是不是清晰多了?

本章小结:

  • FreeRTOS是一个轻量级实时内核,适合资源受限的MCU
  • 源码结构清晰,重点看tasks.c和queue.c
  • 移植只需三步:选移植层、配Config、实现滴答定时器
  • 任务创建用xTaskCreate,删除用vTaskDelete
  • 优先级设计要结合实际场景,避免任务饿死

好了,这一讲就到这里。FreeRTOS入门其实不难,关键是动手跑起来。下一讲我们会深入任务调度机制,看看FreeRTOS到底是怎么切换任务的。到时候见。


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