RTOS 基础

本文介绍实时操作系统(RTOS, Real-Time Operating System)的基本概念与典型任务调度模式。内容涵盖 RTOS 的作用、常见架构、任务优先级与时间管理机制,帮助你建立对嵌入式实时系统的完整认知,并为实际工程开发和面试打好理论基础。


8.1 内存管理

文件名 优点 缺点 说明
heap_1.c 分配简单,时间确定 只分配,不回收 最基础的策略。只支持线性分配,比如像栈一样一直向后分配,没有释放机制。优点是极快且稳定,适合初始化阶段或一次性分配内存的场景(如游戏关卡加载)。
heap_2.c 动态分配,最佳匹配 有碎片,时间不定 引入了空闲块管理机制,会尝试“最优匹配”(如 best-fit),更智能,但缺点是可能产生碎片,比如有很多小块被占用却无法合并,且查找耗时不确定。
heap_3.c 调用标准库函数(如 malloc 速度慢,时间不定 直接使用 malloc/free 等系统库函数,优点是通用性强,缺点是系统开销大、不适用于硬实时系统。适合通用 C 程序或操作系统级分配。
heap_4.c 相邻空闲内存可合并 可解决碎片问题,但时间不定 heap_2 的基础上新增了空闲块合并功能(coalescing),可以合并相邻的空闲块来减少碎片,但合并过程需要遍历,仍然耗时不确定
heap_5.c 支持分隔的内存块(多区域) 可解决碎片问题,但时间不定 heap_4 上扩展,支持多内存区域/分区分配(如小对象池、大块池)。适合复杂系统如 RTOS,效率和内存利用率进一步提高。

这些 heap_*.c 文件是实现不同策略的堆内存管理器,类似于我们平时使用的 malloc/free,但它们更接近底层,可以用于嵌入式或自定义内存池等场景。

8.1.1 关于heap_1.c

heap_1.c 是 FreeRTOS 提供的最简单的内存管理器实现。它只支持线性分配内存,不能释放已分配的内存块。适用于那些只需要一次性分配内存的场景,比如游戏关卡加载等。由于没有释放机制,它的内存使用效率较低,但速度非常快且稳定。

它只实现了 pvPortMalloc,没有实现 vPortFree。 如果你的程序不需要删除内核对象,那么可以使用 heap_1。FreeRTOS 在创建任务时,需要 2 个内核对象:task controlblock(TCB)、stack。(TCB任务控制块和栈)。

在 FreeRTOS 中,每当你调用 xTaskCreate()(或类似的 API)来创建一个新任务时,内核会为该任务分配两块关键资源:

  1. TCB(Task Control Block,任务控制块) TCB 是一个结构体,保存了任务的所有管理和调度信息,包括:

    • 任务状态(就绪/运行/阻塞/挂起)
    • 优先级(uxPriority
    • 任务名(pcTaskName
    • 指向任务栈顶和栈底的指针
    • 上下文寄存器保存区(用于切换时保存 CPU 寄存器)
    • 任务句柄(TaskHandle_t
    • 任务通知/事件/消息队列/信号量等同步对象的引用

    内核通过 TCB 来跟踪每个任务,决定哪个任务该运行、该阻塞,以及保存/恢复任务的上下文。

  2. Stack(任务栈) 每个任务都有自己独立的栈空间,用于保存:

    • 函数调用时的返回地址
    • 局部变量
    • 临时寄存器数据
    • 中断/异常发生时的寄存器上下文

    当任务被上下文切换(context switch)出去时,FreeRTOS 会把 CPU 寄存器的值压入这块栈里;当任务重新切回时,再从栈里恢复寄存器。这个栈空间通常由你在创建任务时传入的 usStackDepth(以 StackType_t 为单位)决定,或由你传入的指针指定。

为什么 heap_1 只实现 pvPortMalloc 而没有 vPortFree

FreeRTOS 提供了多种内存管理方案(heap_1、heap_2、heap_3、heap_4、heap_5),它们都用来给内核对象(如 TCB、stack)、队列、定时器等分配内存。

如果你的程序在运行过程中从不删除任务、队列或其他对象,就可以用 heap_1,省去内存管理的复杂性;但如果需要动态删除,就得选用支持释放的方案,如 heap_2(简单的 malloc/free)、heap_4(更高效的碎片整理)等。


8.2 RTOS移植与中断管理 (因地制宜)

关于移植的过程是有官方样例的。

英飞凌 AURIX™ TC3xx 系列单片机上移植 FreeRTOS 实时操作系统


8.3 中断机制

中断是嵌入式系统中非常重要的概念,RTOS 中的中断处理需要特别注意。FreeRTOS 提供了一些机制来安全地处理中断和任务切换。

在RTOS中,需要应对各类事件。这些事件很多时候是通过硬件中断产生。假设当前系统正在运行Task1任务,用户按下了按键,触发了按键中断。


8.4 消息队列

消息队列是 FreeRTOS 中用于任务间通信的重要机制。它允许一个任务将数据发送到另一个任务,或者从另一个任务接收数据。消息队列具有先进先出(FIFO)的特性,确保数据的顺序性。

在裸机系统中,两个程序间需要共享某个资源通常使用全局变量来实现;但在含操作系统(下文就拿FreeRTOS举例)的开发中,则使用消息队列完成。那么这两者有啥区别呢?

  其实在FreeRTOS系统中也能够像裸机似的,使用全局变量实现多程序共享某个资源(这里资源就可称为临界资源),则多个程序都能随时访问同一个临界资源,这时若两个程序同时访问同一个临界资源来完成两次资源读写操作,假如两个程序读取操作是同时完成,但是写入操作有先后之别,那么最后实际完成的操作就会是一个。例如下图:

Queue

看完上图后,大家可能会想:两者结果相同,无所谓了。但是呢,如果此时再来个C程序恰好读取到的值为456,那么是不是跟最终结果789存在偏差呢!!!

因此,在FreeRTOS系统中,引入了消息队列来实现某个资源共享,其不仅仅实现临界资源共享,也给临界资源提供保护,使得程序更加稳定。

有多个消息发送到消息队列时,通常将先进入队列的消息先传给任务,也就是说,任务一般读取到的消息是最先进入消息队列的消息,即先进先出原则(FIFO),但也支持后进先出原则(LIFO)


面试常见问题

  1. RTOS 的作用是什么?为什么要使用 RTOS?

    RTOS 主要用于管理多任务,提供任务调度、时间管理、资源共享等功能。它使得嵌入式系统能够同时处理多个任务,提高系统响应速度和资源利用率。RTOS 特别适合实时性要求高的应用,如工业控制、汽车电子等。

  2. FreeRTOS中的调度策略是什么?

    FreeRTOS使用抢占式调度策略。这意味着如果有更高优先级的任务变得可运行(例如,它刚刚完成了一个等待操作),当前运行的任务将被中断,调度器将切换到更高优先级的任务。这确保了关键任务能够及时响应。

  3. 如何在FreeRTOS中创建任务?

    在 FreeRTOS 中,可以使用 xTaskCreate() 函数来创建任务。该函数需要传入任务函数指针、任务名称、栈大小、任务参数、优先级和任务句柄等参数。创建成功后,任务将被添加到就绪队列中等待调度执行。

#include "FreeRTOS.h"
#include "task.h"

void vBlinkTask(void *pvParameters)
{
    const TickType_t xDelay = pdMS_TO_TICKS(500);
    for (;;)
    {
        // 翻转 LED
        ToggleLED();
        // 延时 500 ms(让出 CPU)
        vTaskDelay(xDelay);
    }
}

int main(void)
{
    // 硬件初始化
    SystemInit();
    LED_Init();

    // 创建 Blink 任务
    BaseType_t xResult;
    TaskHandle_t xBlinkHandle = NULL;
    xResult = xTaskCreate(
        vBlinkTask,          // 任务函数
        "Blink",             // 任务名
        128,                 // 栈深度:128 words
        NULL,                // 入口参数
        tskIDLE_PRIORITY+1,  // 优先级:比空闲任务高一级
        &xBlinkHandle        // 任务句柄
    );

    if (xResult == pdPASS)
    {
        // 启动调度器,开始多任务
        vTaskStartScheduler();
    }
    else
    {
        // 创建失败:可能内存不足
        ErrorHandler();
    }

    // 不会走到这里
    for (;;);
}
  1. FreeRTOS如何管理内存?

    FreeRTOS提供了几种内存管理方案。默认情况下,它使用一个单一的固定大小的堆,但你也可以实现自己的内存管理策略,比如使用动态内存分配。此外,FreeRTOS还提供了4种不同的内存分配方法,包括使用静态分配和动态分配的块。

    内存管理

  2. FreeRTOS如何处理中断?

    在FreeRTOS中,中断处理程序应该尽可能快速地执行并返回,以最小化中断服务例程(ISR)的执行时间。在ISR中,应避免执行复杂的操作或阻塞调用。如果需要执行长时间运行的任务,应将任务推迟到普通的任务上下文中执行。此外,FreeRTOS提供了一种机制,允许从中断上下文中安全地调用任务。

    中断机制

  3. FreeRTOS中的队列是什么?它们有什么用途?

    队列是FreeRTOS中的一种中间件,它允许任务之间通过发送和接收消息来通信。队列是一种先进先出(FIFO)的数据结构,任务可以向队列发送数据,其他任务则可以从队列中取出数据。队列对于任务解耦和同步非常有用,因为它们允许任务以异步的方式交换信息。

    消息队列

  4. FreeRTOS中的信号量有哪些类型?

    FreeRTOS中有三种主要类型的信号量:二值信号量、计数信号量和递归信号量。二值信号量用于锁定资源,类似于互斥锁,它们只能有两个状态:获取或释放。计数信号量可以用于管理多个相同的资源,或者用来同步多个任务。递归信号量是计数信号量的一种特殊形式,允许同一个任务多次获取同一个资源。

    • Binary Semaphore:异步事件通知,常在 ISR 与任务间使用。
    • Counting Semaphore:管理 N 份相同资源,或允许累积多个事件。
    • Recursive Mutex:同一任务可重复加锁,解决层层调用时的自锁问题,并具备优先级继承。
  5. 如何配置FreeRTOS的内核? (一般有官方参考) 配置FreeRTOS内核通常包括定义堆栈大小、设置时钟滴答率、定义任务优先级、选择调度策略等。这通常在系统的初始化代码中完成,使用vPortDefineObjects、vPortSetupTimerInterrupts、vPortStartScheduler等函数。此外,还需要根据硬件和应用需求配置中断和外设。

    FreeRTOS 源码包里的 Demo 目录或者官方文档通常提供了详细的配置示例。

  6. FreeRTOS中的软件定时器是什么?它们如何工作?

    软件定时器是FreeRTOS提供的一种机制,允许任务在将来的某个时间点被唤醒。软件定时器不是真正的硬件定时器,而是使用FreeRTOS的调度器和 时钟滴答(SysTick) 来模拟的。任务可以创建一个软件定时器,并将其与任务函数关联。当定时器到期时,关联的任务将被添加到就绪列表中,以便在下一个调度点执行。

References