杰瑞科技汇

freertos中文实用教程

FreeRTOS 中文实用教程

目录

  1. 第一部分:初识 FreeRTOS

    freertos中文实用教程-图1
    (图片来源网络,侵删)
    • 1 什么是实时操作系统?
    • 2 为什么选择 FreeRTOS?(优势与应用场景)
    • 3 如何学习本教程?
  2. 第二部分:核心概念与基础

    • 1 任务 - RTOS 的核心
    • 2 队列 - 任务间的通信桥梁
    • 3 信号量 - 资源管理与任务同步
    • 4 互斥量 - 防止资源竞争的“门锁”
    • 5 事件组 - 多事件通知
    • 6 软件定时器 - 延时与周期性任务
  3. 第三部分:FreeRTOS 配置与 API 精讲

    • 1 FreeRTOSConfig.h - 定制你的内核
    • 2 核心 API 函数速查表
    • 3 关键函数详解(附代码示例)
  4. 第四部分:实战项目:多任务 LED 与串口通信

    • 1 硬件准备与软件环境
    • 2 项目目标
    • 3 代码实现与讲解
  5. 第五部分:调试与优化

    freertos中文实用教程-图2
    (图片来源网络,侵删)
    • 1 常见问题与解决方案
    • 2 使用 FreeRTOS 自带调试工具
    • 3 性能优化技巧
  6. 第六部分:进阶学习与资源

    • 1 内存管理方案
    • 2 中断管理
    • 3 推荐书籍与官方资源

第一部分:初识 FreeRTOS

1 什么是实时操作系统?

想象一下你在厨房做饭:

  • 裸机程序:你只能同时做一件事,烧水时你不能切菜,必须等水开了(一个延时函数结束)才能去切菜,程序是顺序执行的,效率低下。
  • 实时操作系统:你像一个经验丰富的厨师,可以同时管理多个任务,你把水壶放上炉(启动一个任务),设定好时间(非阻塞延时),然后立刻去切菜(执行另一个任务),当水壶的定时器到时(中断或任务通知),它会提醒你水开了,你再去处理,RTOS 就是这样一位“厨师长”,它管理着多个“任务”,确保它们能在规定的时间内得到响应。

RTOS 的核心特征

  • 多任务:看似同时执行多个任务。
  • 实时性:任务必须在可预测的、确定的时间内被响应和完成。
  • 抢占式调度:高优先级的任务可以随时“抢占”低优先级任务的 CPU 使用权。

2 为什么选择 FreeRTOS?(优势与应用场景)

FreeRTOS 是世界上最流行的实时内核之一,尤其适合嵌入式系统。

freertos中文实用教程-图3
(图片来源网络,侵删)

主要优势

  • 免费开源:遵循 MIT 许可证,商业使用无任何费用。
  • 轻量级:内核本身非常小,RAM 和 Flash 占用极低,适合资源受限的 MCU。
  • 可裁剪:通过配置文件,可以只启用你需要的功能,内核体积可以做到非常小。
  • 跨平台:支持数十种主流 MCU 架构(ARM Cortex-M/M/A, RISC-V, AVR, PIC32 等)。
  • 文档丰富,社区活跃:有大量官方文档、书籍和示例代码,遇到问题容易找到解决方案。
  • 易于集成:很多主流 IDE(如 Keil, IAR, VS Code)和 SDK(如 ESP-IDF, AWS IoT Device SDK)都内置了对 FreeRTOS 的支持。

典型应用场景

  • 物联网设备
  • 智能家居
  • 工业控制
  • 消费电子产品(智能手表、无人机)
  • 汽车电子

3 如何学习本教程?

本教程采用“理论 + 实践”的模式。

  1. 理解概念:仔细阅读第二部分,理解任务、队列、信号量等核心概念,这是基础,至关重要。
  2. 动手实践:第三和第四部分将手把手教你配置 FreeRTOS 和编写第一个多任务程序,请务必跟着敲一遍代码,并在你的硬件上运行。
  3. 解决问题:第五部分会教你如何调试和优化,这是从“会用”到“用好”的必经之路。
  4. 持续进阶:学完基础后,根据第六部分的指引,深入学习更高级的主题。

第二部分:核心概念与基础

1 任务 - RTOS 的核心

任务就是 RTOS 管理的最小执行单元,你可以把它理解为一个独立的、拥有自己栈空间的 while(1) 循环。

关键特性

  • 优先级:每个任务都有一个优先级,FreeRTOS 默认使用抢占式调度,高优先级的任务就绪后,会立即抢占低优先级任务的 CPU。
  • 状态
    • 运行态:任务正在 CPU 上运行。
    • 就绪态:任务已经准备好,等待 CPU 调度。
    • 阻塞态:任务因为等待某个事件(如延时、队列接收信号量)而暂停执行。
    • 挂起态:任务被明确暂停,不会自动恢复,需要其他任务来唤醒它。
  • :每个任务都有一块独立的内存空间(栈),用于保存函数调用、局部变量等,栈溢出是常见的错误,需要合理设置栈大小。

2 队列 - 任务间的通信桥梁

队列是一种先进先出的数据结构,用于在任务之间或任务与中断之间传递数据。

主要用途

  • 数据传递:一个生产者任务将数据(如传感器读数)放入队列,一个消费者任务从队列中取出数据进行处理。
  • 解耦:发送方和接收方不需要知道对方的存在,只需通过队列进行通信,降低了程序复杂度。

特点

  • 可以传递的数据大小和队列长度在创建时确定。
  • 当队列满时,发送数据(发送任务)可以被阻塞。
  • 当队列空时,接收数据(接收任务)可以被阻塞。

3 信号量 - 资源管理与任务同步

信号量本质上是一个计数器,用于控制对共享资源的访问或实现任务同步。

两种主要类型

  1. 二值信号量

    • 计数器值只有 0 和 1。
    • 用途:任务同步,一个中断发生时,通过释放二值信号量来唤醒一个等待该中断的任务。
  2. 计数信号量

    • 计数器值可以大于 1。
    • 用途:资源管理,系统有 3 个相同的打印机资源,就可以用一个初始值为 3 的计数信号量来表示,任务需要打印时,先获取信号量(计数器减 1),用完后释放(计数器加 1)。

工作方式

  • 获取:任务尝试获取信号量,如果计数器 > 0,则获取成功,计数器减 1,任务继续运行,如果计数器 = 0,任务则进入阻塞态,直到有其他任务或中断释放信号量。
  • 释放:释放信号量,计数器加 1,如果有任务在等待该信号量,则唤醒其中一个最高优先级的等待任务。

4 互斥量 - 防止资源竞争的“门锁”

互斥量是一种特殊的二值信号量,专门用于解决互斥访问问题,即“独占式访问”。

与二值信号量的关键区别

  • 优先级继承:这是互斥量最重要的特性,用于解决优先级反转问题。
    • 优先级反转场景:高优先级任务 H 等待被低优先级任务 L 占有的资源(互斥量),即使有中优先级的任务 M 在就绪,H 也无法执行,因为它被 L 阻塞了,L 的优先级被“反转”了,因为它阻塞了高优先级任务。
    • 优先级继承解决方案:当一个高优先级任务等待一个被低优先级任务持有的互斥量时,该低优先级任务的优先级会“临时提升”到与高优先级任务相同,直到它释放互斥量,这样,低优先级任务就能尽快执行完并释放资源,从而解决了高优先级任务被“饿死”的问题。

使用场景: 访问共享的硬件资源(如串口、I2C 总线)或全局数据结构。

5 事件组 - 多事件通知

事件组允许一个任务等待多个事件中的任意一个或全部事件发生。

特点

  • 每个事件用一位表示(事件位 0, 1, 2...)。
  • 任务可以设置等待模式:
    • WaitForAny:等待任意一个指定的事件发生。
    • WaitForAll:等待所有指定的事件都发生。
  • 适用于复杂的任务状态管理,例如一个按键任务需要同时检测“短按”、“长按”和“连续按下”等多个事件。

6 软件定时器 - 延时与周期性任务

软件定时器是由 RTOS 内核管理的定时器,它运行于独立的、高优先级的守护任务中。

工作原理

  1. 创建一个软件定时器,并设置其回调函数。
  2. 启动定时器,并设定一个周期。
  3. 当定时器到期时,RTOS 内核的守护任务会调用你预先定义好的回调函数。

用途

  • 执行周期性任务(如每 100ms 读取一次传感器)。
  • 实现超时检测。
  • 替代 vTaskDelay(),因为它不会阻塞调用任务,而是由内核在后台触发。

第三部分:FreeRTOS 配置与 API 精讲

**3.1 FreeRTOSConfig.h - 定制你的内核`

这是 FreeRTOS 的“心脏”,所有内核行为都由它控制,你需要找到这个文件(通常在项目目录的 FreeRTOSFreeRTOS-Plus 文件夹下)。

重要宏定义

// 系统时钟频率,单位 Hz
#define configCPU_CLOCK_HZ               ( ( unsigned long ) 72000000 )
// SysTick 中断的时钟源,通常选择 CPU 时钟
#define configSYSTICK_CLOCK_HZ           configCPU_CLOCK_HZ
// 定义可用的任务优先级范围,0 为最低,configMAX_PRIORITIES-1 为最高
#define configMAX_PRIORITIES             ( 5 )
// 空闲任务堆栈大小,单位为字
#define configMINIMAL_STACK_SIZE         ( ( unsigned short ) 128 )
// 定义内核是否要统计任务状态信息(如 CPU 使用率)
#define configUSE_TRACE_FACILITY         1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
// 是否使用钩子函数,如 vApplicationIdleHook
#define configUSE_IDLE_HOOK              0
#define configUSE_TICK_HOOK              0
// 是否使用协程(一般不用)
#define configUSE_CO_ROUTINES            0
// 是否要检查栈溢出
#define configCHECK_FOR_STACK_OVERFLOW   2
// 内存分配方案
#define configSUPPORT_STATIC_ALLOCATION  0
#define configSUPPORT_DYNAMIC_ALLOCATION 1

2 核心 API 函数速查表

对象 功能 创建/初始化 删除/释放 常用操作
任务 创建一个新任务 xTaskCreate() vTaskDelete() vTaskDelay(), vTaskPrioritySet()
队列 创建一个FIFO队列 xQueueCreate() vQueueDelete() xQueueSend(), xQueueReceive()
信号量 创建一个信号量 xSemaphoreCreateBinary()
xSemaphoreCreateCounting()
vSemaphoreDelete() xSemaphoreTake(), xSemaphoreGive()
互斥量 创建一个互斥量 xSemaphoreCreateMutex() vSemaphoreDelete() xSemaphoreTake(), xSemaphoreGive()
事件组 创建一个事件组 xEventGroupCreate() vEventGroupDelete() xEventGroupSetBits(), xEventGroupWaitBits()
软件定时器 创建一个定时器 xTimerCreate() xTimerDelete() xTimerStart(), xTimerStop()

3 关键函数详解(附代码示例)

任务创建 xTaskCreate()

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数的入口地址
    const char * const pcName,      // 任务的名字(用于调试)
    const uint16_t usStackDepth,    // 任务堆栈深度,单位为字
    void *pvParameters,             // 传递给任务函数的参数
    UBaseType_t uxPriority,        // 任务优先级
    TaskHandle_t *pvCreatedTask     // 用于保存任务句柄的指针
);

示例

void vLEDTask(void *pvParameters) {
    // pvParameters 可以在这里被使用
    for(;;) {
        LED_On();
        vTaskDelay(pdMS_TO_TICKS(500)); // 延时 500ms,不占用 CPU
        LED_Off();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
int main(void) {
    // ... 硬件初始化 ...
    // 创建 LED 任务,优先级为 2,堆栈深度为 128
    xTaskCreate(vLEDTask, "LED Task", 128, NULL, 2, NULL);
    // 启动调度器,这之后任务才会开始运行
    vTaskStartScheduler();
    // 正常情况下,程序不会运行到这里
    for(;;);
}

队列操作 xQueueSend() / xQueueReceive()

// 发送数据到队列(可能会阻塞)
BaseType_t xQueueSend(
    QueueHandle_t xQueue,      // 队列句柄
    const void *pvItemToQueue, // 要发送的数据指针
    TickType_t xTicksToWait    // 等待超时时间
);
// 从队列接收数据(可能会阻塞)
BaseType_t xQueueReceive(
    QueueHandle_t xQueue,      // 队列句柄
    void *pvBuffer,            // 存储接收数据的缓冲区指针
    TickType_t xTicksToWait    // 等待超时时间
);

第四部分:实战项目:多任务 LED 与串口通信

假设你的目标 MCU 是一个 STM32 开发板。

项目目标: 创建两个任务:

  1. LED_Task:以 1Hz 的频率闪烁 LED。
  2. Uart_Task:通过串口每 2 秒打印一条 "Hello FreeRTOS!" 的信息。
  3. 两个任务独立运行,互不干扰。

代码实现 (main.c)

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "stm32fxxx_hal.h" // 假设使用 STM32 HAL 库
// 任务句柄
TaskHandle_t xLEDTaskHandle = NULL;
TaskHandle_t xUartTaskHandle = NULL;
// 任务函数声明
void vLEDTask(void *pvParameters);
void vUartTask(void *pvParameters);
int main(void) {
    // HAL_Init();  // 初始化 HAL 库
    // SystemClock_Config(); // 配置系统时钟
    // MX_GPIO_Init(); // 初始化 GPIO (LED)
    // MX_USART1_UART_Init(); // 初始化 UART
    // 创建 LED 任务
    xTaskCreate(vLEDTask, "LED Task", 128, NULL, 2, &xLEDTaskHandle);
    // 创建 UART 任务
    xTaskCreate(vUartTask, "Uart Task", 128, NULL, 1, &xUartTaskHandle);
    // 启动调度器
    vTaskStartScheduler();
    // 如果调度器启动失败,会进入死循环
    for(;;);
}
// LED 任务
void vLEDTask(void *pvParameters) {
    for(;;) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 切换 LED 状态
        vTaskDelay(pdMS_TO_TICKS(500)); // 延时 500ms
    }
}
// UART 任务
void vUartTask(void *pvParameters) {
    uint8_t msg[] = "Hello FreeRTOS!\r\n";
    for(;;) {
        HAL_UART_Transmit(&huart1, msg, sizeof(msg) - 1, 100); // 通过串口发送
        vTaskDelay(pdMS_TO_TICKS(2000)); // 延时 2 秒
    }
}

代码讲解

  1. 任务创建:在 main 函数中,我们创建了两个任务。vLEDTask 优先级为 2,vUartTask 优先级为 1,这意味着 vLEDTask 的优先级更高,如果两个任务同时就绪,vLEDTask 会先运行。
  2. 无限循环:每个任务的核心都是一个 for(;;) 循环,这是 RTOS 任务的典型结构。
  3. 非阻塞延时vTaskDelay() 是 RTOS 的核心函数之一,它会让当前任务进入阻塞态,释放 CPU,500ms 后,任务会自动进入就绪态,等待调度器再次调度它,在这 500ms 内,CPU 可以去执行其他就绪的任务(如 vUartTask)。
  4. 调度器启动vTaskStartScheduler() 是最后一步,也是最重要的一步,它会创建空闲任务和定时器任务,然后开始调度,让任务“活”起来。

第五部分:调试与优化

1 常见问题与解决方案

问题现象 可能原因 解决方案
程序卡死,无法进入 main 函数或 vTaskStartScheduler() 栈溢出
中断配置错误
增大任务堆栈大小 (usStackDepth),或开启栈溢出检测 (configCHECK_FOR_STACK_OVERFLOW)。
检查中断向量表和中断处理函数。
任务不运行或运行不正常 优先级设置错误
任务意外进入阻塞态且无法唤醒
检查任务优先级是否低于空闲任务(默认优先级为 0)。
检查导致阻塞的 API(如 xQueueReceive)是否有其他任务/中断去“释放”它。
系统运行一段时间后崩溃 栈溢出
内存泄漏(动态分配)
同上,检查栈大小。
确保每次 pvPortMalloc() 都有对应的 vPortFree()
优先级反转问题 高优先级任务等待低优先级任务持有的资源 使用互斥量 (xSemaphoreCreateMutex()) 而不是二值信号量来管理共享资源。

2 使用 FreeRTOS 自带调试工具

  • vTaskGetRunTimeStats():获取每个任务的总运行时间(单位:滴答数),可以用来计算 CPU 使用率,需要在 FreeRTOSConfig.h 中启用 configGENERATE_RUN_TIME_STATS
  • uxTaskGetStackHighWaterMark():获取任务自创建以来,栈指针曾经到达过的“最低”位置,这个值越小,说明栈使用越多,调用此函数时传入任务句柄,可以检查是否存在栈溢出风险。

3 性能优化技巧

  • 合理分配优先级:将高优先级赋予对实时性要求高的任务(如硬件中断处理),低优先级赋予后台处理任务。
  • 避免在任务中使用长延时:尽量用 vTaskDelay,而不是 HAL_Delay 或类似的忙等待延时函数。
  • 使用队列长度合适的队列:队列过长会浪费内存,过短会导致数据丢失或任务频繁阻塞。
  • 善用事件组:当需要等待多个事件时,使用事件组比创建多个信号量更节省资源。

第六部分:进阶学习与资源

1 内存管理方案

FreeRTOS 提供了 5 种内存分配方案,在 FreeRTOSConfig.h 中通过 configSUPPORT_STATIC_ALLOCATIONconfigSUPPORT_DYNAMIC_ALLOCATION 来选择。

  • 方案1 & 2 (Heap_1.c / Heap_2.c):最简单,不支持释放内存,适用于任务数量固定且不删除的场景。
  • 方案3 (Heap_3.c):直接使用标准库的 malloc/free,线程不安全,不推荐在 RTOS 中使用。
  • 方案4 (Heap_4.c):最常用,支持动态分配和释放,有碎片整理机制,性能较好。
  • 方案5 (Heap_5.c):与方案4类似,但允许内存块在物理上不连续。

2 中断管理

  • 从 ISR 发送信号量/队列:在 ISR 中,不能使用带阻塞功能的 API(如 xSemaphoreGive),必须使用 xSemaphoreGiveFromISR 等后缀为 FromISR 的版本。
  • portYIELD_FROM_ISR():在 ISR 中,如果释放信号量后唤醒了一个比当前运行任务优先级更高的任务,应该调用此函数来请求一次任务切换。

3 推荐书籍与官方资源

  • 官方文档FreeRTOS.org 官方网站,包含所有 API 文档、书籍和示例,这是最权威的资料。
  • 《Mastering the FreeRTOS Real Time Kernel》:官方出品的免费电子书,是学习 FreeRTOS 的最佳读物,内容非常全面且深入。
  • 《FreeRTOS 权威指南》:国内书籍,结合了官方文档和作者的实践经验,对中文读者更友好。
  • 社区与论坛FreeRTOS 官方论坛,遇到问题可以在这里提问。

本教程为你提供了一个从零开始学习 FreeRTOS 的完整路径,RTOS 的核心思想是“分而治之”“并发”,通过将复杂的应用拆分为多个独立的任务,并使用 RTOS 提供的同步和通信机制,你可以构建出结构清晰、响应迅速、易于维护的嵌入式软件。

最重要的建议:动手实践! 理论学得再多,不如亲手写一个任务,让它跑起来,祝你学习顺利!

分享:
扫描分享到社交APP
上一篇
下一篇