C语言FreeRTOS怎么入门?

99ANYc3cd6
预计阅读时长 24 分钟
位置: 首页 C语言 正文

FreeRTOS 是一个轻量级、实时性强的操作系统内核,专门为嵌入式系统设计,它本身是用 C 语言编写的,并提供 C 语言 API 供开发者使用,掌握 FreeRTOS 是嵌入式开发工程师的一项重要技能。

c语言freertos
(图片来源网络,侵删)

FreeRTOS 的核心概念(为什么需要它?)

在没有操作系统的单片机中,程序通常是“前后台系统”:

  • 后台: 一个 while(1) 的主循环,顺序执行各种任务。
  • 前台: 中断服务程序,可以打断主循环。

这种模式简单,但在处理多个需要“运行的复杂任务时,会变得非常困难,代码会变得臃肿、难以维护,且无法保证任务的实时性。

FreeRTOS 通过引入多任务调度器来解决这个问题。

任务

任务可以看作是一个独立的、拥有自己栈空间的、无限循环的 C 函数,每个任务都有其优先级,看起来像是在“运行。

c语言freertos
(图片来源网络,侵删)

关键特性:

  • 独立性: 每个任务有自己的栈空间,一个任务的崩溃通常不会直接导致整个系统崩溃。
  • 优先级: FreeRTOS 是一个抢占式内核,高优先级的任务可以随时抢占低优先级任务的 CPU 使用权。
  • 状态: 任务有几种状态:Running (运行态)、Ready (就绪态)、Blocked (阻塞态)、Suspended (挂起态)。

调度器

调度器是 FreeRTOS 的“大脑”,它决定在任何一个时刻,哪个处于就绪态的任务最高优先级的任务应该获得 CPU 的控制权。

  • 抢占式调度: 当一个高优先级的任务从阻塞态变为就绪态时,它会立即抢占当前正在运行的低优先级任务。
  • 时间片轮转: 对于相同优先级的多个任务,调度器会以时间片(由 configTICK_RATE_HZ 决定)为单位,轮流让它们运行。

队列

队列是任务间、任务与中断间进行通信和同步的主要方式,它像一个 FIFO(先进先出)的管道,可以发送和接收数据。

  • 用途:
    • 传递数据(如传感器读数、控制命令)。
    • 信号量(一种特殊的队列,用于同步)。
    • 通知(一种更轻量级的同步机制)。

信号量

信号量主要用于任务间的同步和资源管理,本质上是计数值大于等于 0 的队列。

c语言freertos
(图片来源网络,侵删)
  • 二值信号量: 计数只能是 0 或 1,常用于任务同步,例如一个任务等待某个事件发生(中断触发)。
  • 计数信号量: 计数可以大于 1,常用于管理多个相同的资源,例如有 3 个空闲的 UART 缓冲区。
  • 互斥量: 一种特殊的二值信号量,带有优先级继承机制,用于保护共享资源(如全局变量、硬件外设),防止优先级反转。

互斥量

当多个任务访问同一个共享资源时,可能会发生“优先级反转”(低优先级任务持有资源,高优先级任务被阻塞,导致中优先级任务先执行),互斥量通过“优先级继承”机制来解决此问题:当一个高优先级任务被一个低优先级任务阻塞时,低优先级任务的临时优先级会提升到与高优先级任务相同,直到它释放互斥量。


FreeRTOS 的关键 API 函数(C 语言接口)

以下是使用最频繁的一些 API 函数。

任务管理

函数 描述
xTaskCreate() 创建一个新任务,这是最常用的创建任务函数。
vTaskDelete() 删除一个任务,释放其占用的资源。
vTaskDelay() 让当前任务进入阻塞态,指定的时间后自动进入就绪态。
vTaskSuspend() / vTaskResume() 挂起和恢复任务,挂起的任务不会被调度器执行。

队列管理

函数 描述
xQueueCreate() 创建一个队列。
xQueueSend() / xQueueReceive() 向队列发送数据和从队列接收数据(阻塞式)。
xQueueSendFromISR() / xQueueReceiveFromISR() 在中断服务程序中向队列发送/接收数据。

信号量管理

函数 描述
xSemaphoreCreateBinary() 创建一个二值信号量。
xSemaphoreCreateMutex() 创建一个互斥量。
xSemaphoreGive() / xSemaphoreTake() 释放和获取信号量/互斥量(阻塞式)。
xSemaphoreGiveFromISR() / xSemaphoreTakeFromISR() 在中断服务程序中释放/获取信号量。

一个完整的 FreeRTOS 项目示例(C 语言)

下面是一个基于 STM32 HAL 库和 FreeRTOS 的简单示例,创建两个任务:Task1 每 1 秒打印一次 "Hello from Task 1",Task2 每 2 秒打印一次 "Hello from Task 2"。

包含必要的头文件

/* 包含 FreeRTOS 的头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/* 包含标准库和芯片特定的头文件 */
#include <stdio.h>
#include "main.h" // STM32 HAL 生成的头文件
/* 定义任务句柄,用于后续任务管理 */
TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;

定义任务函数

每个任务函数都必须是一个无限循环,并且通常返回 void

/**
 * @brief 任务1:每1秒打印一次信息
 * @param pvParameters 任务创建时传入的参数(本例中未使用)
 */
void Task1Function(void *pvParameters)
{
    for (;;)
    {
        printf("Hello from Task 1\n");
        vTaskDelay(pdMS_TO_TICKS(1000)); // 阻塞1秒 (1000ms)
    }
}
/**
 * @brief 任务2:每2秒打印一次信息
 * @param pvParameters 任务创建时传入的参数(本例中未使用)
 */
void Task2Function(void *pvParameters)
{
    for (;;)
    {
        printf("Hello from Task 2\n");
        vTaskDelay(pdMS_TO_TICKS(2000)); // 阻塞2秒 (2000ms)
    }
}

main 函数中初始化和启动任务

main 函数是整个程序的入口,你需要初始化硬件,然后创建任务,最后启动调度器。

int main(void)
{
    /* 1. 初始化硬件(如HAL库、时钟、串口等) */
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init(); // 假设你使用串口1打印
    /* 2. 创建任务 */
    // 参数1: 任务函数名
    // 参数2: 任务名称(用于调试)
    // 参数3: 任务栈大小(字节数)
    // 参数4: 任务参数(本例为NULL)
    // 参数5: 任务优先级(数字越大,优先级越高)
    // 参数6: 任务句柄(用于后续管理)
    // 参数7: 任务创建在哪个核心(对于多核MCU,如ESP32)
    xTaskCreate(
        Task1Function,    /* 任务函数 */
        "Task 1",        /* 任务名称 */
        128,             /* 栈大小 */
        NULL,            /* 任务参数 */
        1,               /* 任务优先级 */
        &Task1Handle     /* 任务句柄 */
    );
    xTaskCreate(
        Task2Function,
        "Task 2",
        128,
        NULL,
        1, // 注意:两个任务优先级相同,调度器会进行时间片轮转
        &Task2Handle
    );
    /* 3. 启动调度器 */
    // 这行代码之后,FreeRTOS 将接管CPU控制权,开始调度任务。
    // vTaskStartScheduler() 不会返回。
    vTaskStartScheduler();
    // 正常情况下,代码不会执行到这里。
    // 如果调度器启动失败(比如内存不足),会进入一个错误处理函数。
    for (;;);
}

FreeRTOSConfig.h 配置文件

这是 FreeRTOS 的“心脏”,它定义了内核的各种行为,你需要根据你的硬件和应用需求来修改它。

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
#include "stm32f4xx.h" // 根据你的MCU型号修改
/* 基础配置 */
#define configUSE_PREEMPTION                    1       // 使用抢占式调度
#define configUSE_PORT_OPTIMISED_TASK_SELECTION  1       // 使用优化的任务选择
#define configUSE_TICK_HOOK                      0       // 不使用Tick Hook函数
#define configCPU_CLOCK_HZ                      (SystemCoreClock) // CPU主频
#define configTICK_RATE_HZ                       1000    // 1KHz的节拍率,即1ms一个tick
/* 内存分配 */
#define configSUPPORT_STATIC_ALLOCATION         1       // 支持静态内存分配
#define configSUPPORT_DYNAMIC_ALLOCATION        1       // 支持动态内存分配
#define configTOTAL_HEAP_SIZE                   (15 * 1024) // 总堆大小15KB
#define configAPPLICATION_ALLOCATED_HEAP        0       // 不由应用程序分配堆
/* 任务相关 */
#define configMAX_PRIORITIES                    5       // 最大任务优先级数(0-4)
#define configMINIMAL_STACK_SIZE                128     // 最小任务栈大小(字节数)
/* IDLE任务和钩子函数 */
#define configIDLE_SHOULD_YIELD                 1       // IDLE任务可以主动让出CPU
#define configUSE_IDLE_HOOK                      0       // 不使用IDLE Hook
#define configUSE_TICK_HOOK                      0       // 不使用Tick Hook
/* 栈溢出检测 */
#define configCHECK_FOR_STACK_OVERFLOW          2       // 栈溢出检测级别
/* 断言定义 */
#define configASSERT( x ) if ((x) == 0) { taskDISABLE_INTERRUPTS(); for( ;; ); }
/* 中断配置 */
#define configKERNEL_INTERRUPT_PRIORITY         255     // 最低优先级
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    191     // 最高可屏蔽中断优先级
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15      // FreeRTOS内核中断优先级
#endif /* FREERTOS_CONFIG_H */

最佳实践和注意事项

  1. 栈大小设置: 栈大小是任务能使用的内存上限,设置太小会导致栈溢出,程序崩溃;设置太大会浪费内存,可以通过 uxTaskGetStackHighWaterMark() 函数来监控任务的最大使用量,从而调整栈大小。
  2. 中断处理:
    • 中断要尽可能短! 在中断中只做最紧急的事,如读取硬件状态、清除中断标志。
    • 不要在中断中调用会阻塞的 API,如 vTaskDelay()xQueueSend()(除非是 FromISR 版本)。
    • 使用 FromISR 版本的 API 与任务通信,并注意处理“发送完成”的返回值,确保任务能被正确唤醒。
  3. 优先级反转: 在访问共享资源时,始终使用互斥量来保护,而不是简单地用 taskENTER_CRITICAL()/taskEXIT_CRITICAL()(关中断)。
  4. 资源竞争: 避免在多个任务中直接修改全局变量,通过队列或信号量来传递数据,确保数据的一致性。
  5. 调试: 使用 RTOS 提供的调试工具,如任务状态查看器、栈溢出检测等,IAR Keil、Segger Ozone 等调试器都支持 FreeRTOS 的可视化调试。

在 C 语言中使用 FreeRTOS,核心就是围绕 “任务”“同步/通信机制”(队列、信号量)来构建你的应用。

  • 任务是你的“工人”,负责完成具体的工作。
  • 调度器是“项目经理”,决定哪个工人该在什么时候工作。
  • 队列/信号量是“工人们之间的沟通工具”或“工作许可证”。

通过合理地组合使用这些组件,你可以将复杂的嵌入式应用分解为多个清晰、独立、易于维护的任务,从而构建出稳定、高效、可靠的嵌入式系统。

-- 展开阅读全文 --
头像
dede上下篇定义规则是什么?
« 上一篇 04-13
c语言 getRoot
下一篇 » 04-13

相关文章

取消
微信二维码
支付宝二维码

目录[+]