🎓 总站 🏠 本课目录 01 概述 02 ARM与STM32 03 开发环境 04 最小系统与C 05 GPIO 06 中断 07 串口通信 08 DMA 09 定时器 10 ADC 11 从模块到项目 12 嵌入式操作系统
智能嵌入式系统设计 · 第12章

嵌入式操作系统

基于 STM32F103 · 零基础讲解嵌入式操作系统概念、FreeRTOS 任务管理、任务间通信、时间管理和内存管理。

📚 学习进度
0%

🎯学习目标

  • 了解嵌入式操作系统的基本概念、分类和特点;
  • 理解实时操作系统的概念,掌握硬实时和软实时的区别;
  • 了解 FreeRTOS 的特点、内核功能和应用场景;
  • 掌握 FreeRTOS 的任务管理,包括任务状态、创建、调度策略;
  • 掌握任务间通信机制,包括队列、信号量、互斥锁、事件标志组;
  • 掌握时间管理功能,包括延时函数和软件定时器;
  • 了解 FreeRTOS 的内存管理策略;
  • 熟悉 FreeRTOS 标准库函数和 HAL 库函数的使用;
  • 能够使用 FreeRTOS 开发多任务应用程序。

1操作系统概述

嵌入式操作系统(Embedded Operating System,Embedded OS)是管理嵌入式系统硬件资源、为应用程序提供公共服务的系统软件。它负责管理 CPU、内存、外设等硬件资源,并提供任务调度、同步通信、内存管理等基本服务。

💡 一句话理解操作系统就像管家,管理家里所有资源(CPU、内存、设备),让多个任务有序使用这些资源。
💡 通俗理解:为什么需要操作系统?

没有操作系统:你必须自己管理所有硬件,处理多个任务时代码复杂,容易出错。就像一个人要同时做饭、洗衣服、接电话,手忙脚乱。

有操作系统:操作系统帮你管理硬件,你只需要写应用代码,多个任务可以"同时"运行。就像有一个管家帮你安排好一切,你只需要告诉管家做什么。

好处:① 简化开发——不用直接操作硬件;② 多任务管理——多个任务"同时"运行;③ 资源管理——合理分配 CPU、内存;④ 实时性保证——保证重要任务及时响应;⑤ 可移植性——代码可在不同平台运行。

嵌入式操作系统的分类

分类方式类型特点代表
是否实时实时操作系统(RTOS)响应时间确定,可预测FreeRTOS、RT-Thread、μC/OS
非实时操作系统响应时间不确定Linux、Windows CE
是否可抢占可抢占式高优先级任务可抢占低优先级FreeRTOS、RT-Thread
非抢占式任务主动让出 CPU简单协作式系统
应用场景通用嵌入式 OS功能丰富,适合复杂应用嵌入式 Linux
专用 RTOS轻量、高效,适合资源受限FreeRTOS、μC/OS

嵌入式操作系统的核心功能

⚙️

任务调度

Task Scheduling
决定哪个任务使用 CPU,支持优先级抢占、时间片轮转等策略。
💬

任务通信

IPC
任务间传递数据和同步,包括队列、信号量、互斥锁等机制。
🧠

内存管理

Memory
动态分配和释放内存,避免内存碎片,支持多种分配策略。

时间管理

Timer
提供延时、定时器、时钟节拍等时间相关服务。
💡 通俗理解:操作系统 vs 裸机编程

裸机编程:就像自己盖房子,从打地基到装修全自己干。代码直接操作硬件,while(1) 循环里一个一个处理任务。缺点是任务多了就忙不过来,响应慢。

操作系统:就像请了施工队,你只需要画图纸(写应用代码)。操作系统帮你安排工人(CPU)、分配材料(内存)、协调进度(调度)。多个任务可以"同时"进行,重要任务优先处理。

2实时操作系统(RTOS)⭐

实时操作系统(Real-Time Operating System,RTOS)是能在确定时间内处理事件的操作系统。"实时"不是"快",而是"准时"——能在规定的时间内完成任务。

💡 通俗理解:什么是"实时"?

实时 ≠ 快。实时是指"在规定时间内完成"。

比如:硬实时——汽车安全气囊必须在碰撞后 50ms 内弹出,晚了人就受伤了,这是"必须准时"。软实时——视频播放要流畅,偶尔卡一下(延迟 100ms)可以接受,这是"尽量准时"。

关键:实时操作系统保证"可预测性"——你可以说"这个任务一定能在 10ms 内完成",而普通操作系统只能说"一般能在 10ms 内完成,但偶尔可能要 100ms"。

硬实时 vs 软实时(必考)

🔴

硬实时

Hard Real-Time
必须在截止时间内完成,否则系统失败甚至造成灾难。如:航空航天、医疗设备、汽车ABS、核电站控制
🟡

软实时

Soft Real-Time
尽量在截止时间内完成,偶尔超时可接受,不会造成严重后果。如:消费电子、多媒体播放、网络通信、游戏
对比项硬实时软实时
截止时间必须满足,不可违反尽量满足,偶尔可违反
超时后果系统失败、灾难性后果性能下降、用户体验变差
典型延迟微秒~毫秒级毫秒~秒级
应用场景航空航天、医疗、汽车消费电子、多媒体、网络
设计难度高,需要严格保证中,尽力而为

RTOS 的核心特点

⭐ 实时性特点(必考)
  • 确定性(Determinism):响应时间可预测,最坏情况下的响应时间是确定的;
  • 可抢占性(Preemptive):高优先级任务可以立即抢占低优先级任务;
  • 快速响应(Fast Response):中断响应时间短,通常在微秒级;
  • 可靠性(Reliability):系统稳定,不易崩溃,适合关键任务;
  • 低抖动(Low Jitter):任务执行时间波动小,行为一致。

常见嵌入式 RTOS 对比

RTOS开源特点适用场景
FreeRTOS✅ MIT轻量、生态好、AWS支持IoT、消费电子、工业控制
RT-Thread✅ Apache国产、组件丰富、中文文档好IoT、智能家居、工业
μC/OS部分开源教科书经典、认证完善工业、医疗、航空
Zephyr✅ ApacheLinux基金会、蓝牙/WiFi支持好IoT、可穿戴设备
embOS商业高性能、认证完善工业、医疗

3FreeRTOS 简介

FreeRTOS 是一个开源、轻量级的实时操作系统内核,由 Richard Barry 于 2003 年创建,现由 Amazon Web Services(AWS)维护。它是世界上使用最广泛的嵌入式 RTOS 之一。

💡 为什么 FreeRTOS 这么流行?

开源免费:MIT 许可证,商业产品免费使用;② 轻量级:内核只有几千行代码,ROM 占用 6~10KB,RAM 占用几百字节;③ 生态好:AWS IoT 支持,大量教程和例程;④ 社区活跃:全球开发者贡献,bug 修复快。

FreeRTOS 主要特点

💡 FreeRTOS 特点
  • 开源免费:MIT 许可证,商业友好,无授权费用;
  • 轻量级:内核小(6~10KB ROM),适合资源受限的 MCU;
  • 可裁剪:通过宏定义选择需要的功能,减少代码体积;
  • 可移植:支持 ARM Cortex-M、RISC-V、ESP32 等 40+ 处理器架构;
  • 丰富功能:任务管理、队列、信号量、互斥锁、事件标志组、软件定时器、内存管理等。

FreeRTOS 内核功能全景

FreeRTOS 内核功能
功能模块说明关键函数
任务管理创建、删除、挂起、恢复任务xTaskCreate, vTaskDelete, vTaskSuspend
调度器优先级抢占式调度 + 时间片轮转vTaskStartScheduler
队列任务间传递数据xQueueCreate, xQueueSend, xQueueReceive
信号量任务同步和资源计数xSemaphoreCreateBinary, xSemaphoreTake
互斥锁保护共享资源,支持优先级继承xSemaphoreCreateMutex
事件标志组多事件组合同步xEventGroupCreate, xEventGroupWaitBits
软件定时器定时回调函数xTimerCreate, xTimerStart
内存管理动态内存分配(5种策略)pvPortMalloc, vPortFree
任务通知轻量级任务间通信xTaskNotify, xTaskNotifyWait
FreeRTOS 内核 任务管理 队列 信号量 互斥锁 定时器 硬件抽象层(HAL)ARM Cortex-M3 · STM32F103
图1 · FreeRTOS 内核功能架构

4任务管理 ⭐(核心考点)

任务(Task)是 FreeRTOS 中的基本执行单元,类似于线程。每个任务都是一个独立的函数,有自己的堆栈空间,看起来像是"独占"一个 CPU。

💡 通俗理解:什么是任务?

想象一个餐厅有多个厨师(任务)共享一个灶台(CPU):

任务就像一个厨师,有自己的菜谱(代码)和工作台(堆栈)。

调度器就像厨师长,决定谁现在用灶台。优先级高的厨师(紧急订单)可以插队。

任务切换就像厨师换人:A 厨师暂停,记住做到哪一步(保存现场),B 厨师上灶(恢复现场)继续做。

时间片就像给每个厨师限时 1 分钟,超时就换人,大家轮流用灶台。

任务状态(必考)

⭐ 任务有四种状态
  • 运行态(Running):任务正在使用 CPU,正在执行代码;
  • 就绪态(Ready):任务准备好了,等待 CPU 分配;
  • 阻塞态(Blocked):任务在等待某个事件(如延时到期、队列有数据、信号量可用);
  • 挂起态(Suspended):任务被挂起,不参与调度,需要手动恢复。
运行态 Running 就绪态 Ready 阻塞态 Blocked 挂起态 Suspended 被更高优先级抢占 被调度 时间片到 挂起 恢复 等待事件 高优先级抢占
图2 · FreeRTOS 任务状态转换图

创建任务

使用 xTaskCreate() 函数创建任务,任务函数必须是无限循环

任务函数原型
// 任务函数原型(必须是无限循环)
void vTaskFunction(void *pvParameters)
{
    // 初始化代码(只执行一次)
    
    for(;;)
    {
        // 任务主循环代码
        LED_Toggle();
        vTaskDelay(500); // 延时 500ms,让出 CPU
    }
}
xTaskCreate() 函数原型
BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,    // 任务函数指针
    const char * const pcName,    // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 堆栈大小(字)
    void *pvParameters,           // 传给任务的参数
    UBaseType_t uxPriority,       // 任务优先级
    TaskHandle_t *pxCreatedTask   // 任务句柄(可为NULL)
);
创建任务完整示例
// 任务 1:LED 闪烁
void vTaskLED(void *pvParameters)
{
    for(;;)
    {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
        vTaskDelay(500); // 延时 500ms
    }
}

// 任务 2:串口打印
void vTaskUART(void *pvParameters)
{
    for(;;)
    {
        printf("Hello FreeRTOS!\r\n");
        vTaskDelay(1000); // 延时 1000ms
    }
}

// 主函数
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LED_Init();
    UART_Init();
    
    // 创建任务 1(优先级 2)
    xTaskCreate(vTaskLED, "LED", 128, NULL, 2, NULL);
    // 创建任务 2(优先级 1)
    xTaskCreate(vTaskUART, "UART", 128, NULL, 1, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 不应该运行到这里
    for(;;);
}

任务优先级与调度策略

⭐ 调度规则(必考)
  • 优先级数值越大,优先级越高(0 是最低优先级);
  • 高优先级任务可以抢占低优先级任务(抢占式调度);
  • 相同优先级的任务轮流执行(时间片轮转,需要开启 configUSE_TIME_SLICING);
  • 阻塞/挂起的任务不占用 CPU,让出给其他任务;
  • 调度器始终选择最高优先级的就绪任务运行
💡 通俗理解:优先级抢占

就像医院急诊:普通门诊(低优先级)正在看病,突然来了急诊病人(高优先级),医生立刻放下普通病人去处理急诊。

在 FreeRTOS 中:低优先级任务正在运行,高优先级任务从阻塞态变为就绪态(如延时到期),调度器立刻切换到高优先级任务。

任务控制函数

函数功能说明
vTaskDelete()删除任务传入 NULL 删除自身
vTaskSuspend()挂起任务任务进入挂起态,不参与调度
vTaskResume()恢复任务任务从挂起态恢复到就绪态
vTaskPrioritySet()设置优先级运行时动态修改任务优先级
uxTaskPriorityGet()获取优先级查询任务当前优先级
vTaskDelay()相对延时任务进入阻塞态指定时间
vTaskDelayUntil()绝对延时周期性任务使用,保证周期准确
💡 任务堆栈大小怎么选?

堆栈大小单位是"字"(4 字节),不是字节。128 表示 512 字节。

• 简单任务(LED 闪烁):128 字(512B)

• 中等任务(串口通信):256 字(1KB)

• 复杂任务(浮点运算):512 字(2KB)

• 可用 uxTaskGetStackHighWaterMark() 查询堆栈使用情况

5任务间通信 ⭐(核心考点)

多个任务需要协同工作时,就需要任务间通信(Inter-Process Communication,IPC)。FreeRTOS 提供多种通信机制:

📦

队列

Queue
任务间传递数据的 FIFO 缓冲区,支持多个生产者和消费者。最常用的通信方式
🚦

信号量

Semaphore
用于任务同步和资源计数,包括二值信号量和计数信号量。同步用二值,计数用计数
🔒

互斥锁

Mutex
用于保护共享资源,防止多个任务同时访问。支持优先级继承
🎌

事件标志组

Event Group
多个事件的组合同步,支持"与"和"或"条件。复杂同步场景

5.1 队列(Queue)

队列是 FreeRTOS 中最常用的任务间通信方式。它是一个 FIFO(先进先出)缓冲区,一个任务往里写数据,另一个任务从里面读数据。

💡 通俗理解:队列

队列就像一条传送带:生产者(任务A)把物品放上传送带,消费者(任务B)从传送带另一端取走物品。

• 传送带满了,生产者等待(阻塞);传送带空了,消费者等待(阻塞)。

• 可以设置等待时间:等多久、不等直接返回、一直等到有数据。

队列操作函数
// 创建队列
QueueHandle_t xQueueCreate(
    UBaseType_t uxQueueLength,   // 队列长度(能存几个元素)
    UBaseType_t uxItemSize       // 每个元素大小(字节)
);

// 发送到队列(任务中使用)
BaseType_t xQueueSend(
    QueueHandle_t xQueue,        // 队列句柄
    const void *pvItemToQueue,   // 要发送的数据指针
    TickType_t xTicksToWait      // 等待时间(portMAX_DELAY=永久等待)
);

// 从队列接收(任务中使用)
BaseType_t xQueueReceive(
    QueueHandle_t xQueue,        // 队列句柄
    void *pvBuffer,              // 接收缓冲区
    TickType_t xTicksToWait      // 等待时间
);

// 从中断发送到队列
BaseType_t xQueueSendFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);
队列使用完整示例
// 定义队列句柄
QueueHandle_t xQueue;

// 任务 1:发送数据
void vTaskSender(void *pvParameters)
{
    uint32_t count = 0;
    for(;;)
    {
        count++;
        xQueueSend(xQueue, &count, portMAX_DELAY);
        printf("发送: %d\r\n", count);
        vTaskDelay(1000);
    }
}

// 任务 2:接收数据
void vTaskReceiver(void *pvParameters)
{
    uint32_t received;
    for(;;)
    {
        if(xQueueReceive(xQueue, &received, portMAX_DELAY) == pdPASS)
        {
            printf("接收: %d\r\n", received);
        }
    }
}

// 主函数
int main(void)
{
    // 创建队列(能存 10 个 uint32_t 数据)
    xQueue = xQueueCreate(10, sizeof(uint32_t));
    
    xTaskCreate(vTaskSender, "Sender", 128, NULL, 2, NULL);
    xTaskCreate(vTaskReceiver, "Receiver", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

5.2 信号量(Semaphore)

信号量用于任务同步资源计数。分为二值信号量(0/1)和计数信号量(0~N)。

💡 通俗理解:信号量

二值信号量像一扇门:门关着(0),任务等待;门开了(1),任务通过。用于"事件通知"——中断来了开门,任务通过后关门。

计数信号量像停车场计数器:每进一辆车计数+1,每出一辆车计数-1。计数为 0 时,新车等待。用于"资源管理"——管理多个相同资源。

信号量操作函数
// 创建二值信号量
SemaphoreHandle_t xSemaphoreCreateBinary(void);

// 创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(
    UBaseType_t uxMaxCount,      // 最大计数值
    UBaseType_t uxInitialCount   // 初始计数值
);

// 获取信号量(等待/减 1)
BaseType_t xSemaphoreTake(
    SemaphoreHandle_t xSemaphore,
    TickType_t xTicksToWait
);

// 释放信号量(通知/加 1)
BaseType_t xSemaphoreGive(
    SemaphoreHandle_t xSemaphore
);

// 从中断释放信号量
BaseType_t xSemaphoreGiveFromISR(
    SemaphoreHandle_t xSemaphore,
    BaseType_t *pxHigherPriorityTaskWoken
);
二值信号量示例:中断通知任务
SemaphoreHandle_t xSemaphore;

// 中断服务程序
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务:等待中断通知
void vTaskButton(void *pvParameters)
{
    for(;;)
    {
        // 等待信号量(中断给的)
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdPASS)
        {
            printf("按键按下!\r\n");
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
        }
    }
}

int main(void)
{
    xSemaphore = xSemaphoreCreateBinary(); // 创建二值信号量
    xTaskCreate(vTaskButton, "Btn", 128, NULL, 2, NULL);
    vTaskStartScheduler();
}

5.3 互斥锁(Mutex)

互斥锁用于保护共享资源,确保同一时刻只有一个任务访问资源。它和二值信号量类似,但有优先级继承机制,能防止优先级反转问题。

💡 通俗理解:互斥锁

互斥锁就像卫生间门锁:一个人进去后锁上门(获取锁),其他人只能在门口等。里面的人出来后开锁(释放锁),下一个人才能进去。

优先级继承:如果低优先级任务持有锁,高优先级任务在等锁,系统临时提升低优先级任务的优先级,让它快点做完释放锁。防止中等优先级任务插队(优先级反转)。

互斥锁使用示例:保护共享资源
SemaphoreHandle_t xMutex;

// 任务 A:使用串口
void vTaskA(void *pvParameters)
{
    for(;;)
    {
        xSemaphoreTake(xMutex, portMAX_DELAY); // 获取锁
        printf("TaskA: 正在使用串口...\r\n");
        vTaskDelay(100);
        xSemaphoreGive(xMutex);                 // 释放锁
        vTaskDelay(500);
    }
}

// 任务 B:使用串口
void vTaskB(void *pvParameters)
{
    for(;;)
    {
        xSemaphoreTake(xMutex, portMAX_DELAY); // 获取锁
        printf("TaskB: 正在使用串口...\r\n");
        vTaskDelay(100);
        xSemaphoreGive(xMutex);                 // 释放锁
        vTaskDelay(500);
    }
}

int main(void)
{
    xMutex = xSemaphoreCreateMutex(); // 创建互斥锁
    xTaskCreate(vTaskA, "A", 128, NULL, 2, NULL);
    xTaskCreate(vTaskB, "B", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

5.4 事件标志组(Event Group)

事件标志组可以同时等待多个事件的组合,支持"与"(所有事件都发生)和"或"(任一事件发生)条件。

💡 通俗理解:事件标志组

就像一个任务清单:有多个 checkbox,每个 checkbox 代表一个事件。

"与"等待:所有 checkbox 都勾上才能继续(如:WiFi 连接 AND 蓝牙配对 AND 传感器就绪)。

"或"等待:任意一个 checkbox 勾上就能继续(如:按键按下 OR 定时器到期 OR 收到数据)。

事件标志组操作函数
// 创建事件标志组
EventGroupHandle_t xEventGroupCreate(void);

// 设置事件标志
EventBits_t xEventGroupSetBits(
    EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToSet
);

// 等待事件标志
EventBits_t xEventGroupWaitBits(
    EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToWaitFor,  // 要等待的位
    const BaseType_t xClearOnExit,      // 退出时清除
    const BaseType_t xWaitForAllBits,   // TRUE=与, FALSE=或
    TickType_t xTicksToWait
);

5.5 任务通知(Task Notification)⭐ 轻量级

任务通知是 FreeRTOS v10 新增的轻量级通信方式,比队列和信号量更快,但只能用于一对一直接通信。

任务通知操作函数
// 发送通知(可以携带数据)
BaseType_t xTaskNotify(
    TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction
);

// 等待通知
BaseType_t xTaskNotifyWait(
    uint32_t ulBitsToClearOnEntry,
    uint32_t ulBitsToClearOnExit,
    uint32_t *pulNotificationValue,
    TickType_t xTicksToWait
);

// 从中断发送通知
BaseType_t xTaskNotifyFromISR(
    TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction,
    BaseType_t *pxHigherPriorityTaskWoken
);

通信机制对比

机制数据传递同步速度适用场景
队列✅ 复制数据✅ 阻塞等待中等传递结构化数据
二值信号量❌ 无数据✅ 事件通知中断通知任务
计数信号量❌ 无数据✅ 资源计数管理多个资源
互斥锁❌ 无数据✅ 资源保护保护共享资源
事件标志组❌ 无数据✅ 多事件组合复杂同步
任务通知✅ 32位值✅ 直接通知最快简单一对一通信

6时间管理

FreeRTOS 提供完善的时间管理功能,包括延时函数软件定时器

6.1 延时函数

函数功能说明
vTaskDelay()相对延时从调用开始延时指定时间(会受任务执行时间影响)
vTaskDelayUntil()绝对延时周期性执行任务,周期更准确
xTaskGetTickCount()获取时钟节拍数返回系统启动以来的节拍数
xTaskGetTickCountFromISR()中断中获取节拍数中断安全版本
pdMS_TO_TICKS()毫秒转节拍将毫秒转换为时钟节拍数
💡 vTaskDelay vs vTaskDelayUntil

vTaskDelay(100):从"调用这一刻"开始等 100 个节拍。如果任务执行花了 20 个节拍,实际周期是 120 个节拍。

vTaskDelayUntil(&时间, 100):从"上一次唤醒时间"开始等 100 个节拍。即使任务执行花了 20 个节拍,周期仍然是 100 个节拍。

结论:周期性任务用 vTaskDelayUntil,普通延时用 vTaskDelay

周期性任务示例
// 方式 1:vTaskDelay(周期不准确)
void vTask1(void *pvParameters)
{
    for(;;)
    {
        LED_Toggle();
        vTaskDelay(pdMS_TO_TICKS(100)); // 延时 100ms
        // 实际周期 = 100ms + 任务执行时间
    }
}

// 方式 2:vTaskDelayUntil(周期准确)
void vTask2(void *pvParameters)
{
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(100); // 100ms 周期
    
    for(;;)
    {
        LED_Toggle();
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
        // 实际周期 = 100ms(准确)
    }
}

6.2 软件定时器(Software Timer)

软件定时器在定时器服务任务中执行回调函数,适合周期性操作。注意:回调函数中不能使用阻塞 API!

软件定时器操作函数
// 创建定时器
TimerHandle_t xTimerCreate(
    const char * const pcTimerName,  // 定时器名称
    const TickType_t xTimerPeriod,   // 周期(节拍数)
    const UBaseType_t uxAutoReload,  // TRUE=自动重载, FALSE=单次
    void * const pvTimerID,          // 定时器 ID
    TimerCallbackFunction_t pxCallbackFunction // 回调函数
);

// 启动定时器
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait);

// 停止定时器
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);

// 修改周期
BaseType_t xTimerChangePeriod(TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xTicksToWait);
软件定时器示例
// 定时器回调函数(不能阻塞!)
void vTimerCallback(TimerHandle_t xTimer)
{
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 翻转 LED
}

int main(void)
{
    // 创建定时器,500ms 周期,自动重载
    TimerHandle_t xTimer = xTimerCreate(
        "LED_Timer",
        pdMS_TO_TICKS(500),
        pdTRUE,        // 自动重载
        NULL,
        vTimerCallback
    );
    
    // 启动定时器
    xTimerStart(xTimer, 0);
    
    vTaskStartScheduler();
}

7内存管理

FreeRTOS 提供 5 种内存分配策略(heap_1 ~ heap_5),根据应用需求选择:

策略特点适用场景
heap_1只分配不释放,最简单任务创建后永不删除
heap_2支持分配和释放,不合并碎片分配大小固定
heap_3封装标准 malloc/free使用编译器提供的内存管理
heap_4支持分配和释放,合并相邻空闲块通用场景(推荐)
heap_5支持非连续内存区域内存不连续的系统
💡 选择建议

大多数情况下使用 heap_4(推荐):支持碎片合并,适合动态创建/删除任务的场景。

如果任务创建后永不删除,使用 heap_1(最简单、最小)。

内存管理函数
// 动态分配内存
void *pvPortMalloc(size_t xSize);

// 释放内存
void vPortFree(void *pv);

// 获取剩余空闲内存
size_t xPortGetFreeHeapSize(void);

// 获取历史最小空闲内存(水位线)
size_t xPortGetMinimumEverFreeHeapSize(void);

8标准库函数

FreeRTOS 标准 API 提供丰富的任务管理和通信函数。以下是常用函数分类汇总:

8.1 任务管理函数

函数原型功能
创建任务xTaskCreate(pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask)动态创建任务
删除任务vTaskDelete(xTask)删除任务(传 NULL 删除自身)
挂起任务vTaskSuspend(xTaskToSuspend)挂起任务
恢复任务vTaskResume(xTaskToResume)恢复被挂起的任务
启动调度器vTaskStartScheduler()启动 FreeRTOS 调度器
停止调度器vTaskEndScheduler()停止调度器
任务延时vTaskDelay(xTicksToDelay)相对延时
绝对延时vTaskDelayUntil(pxPreviousWakeTime, xTimeIncrement)周期性延时

8.2 队列函数

函数原型功能
创建队列xQueueCreate(uxQueueLength, uxItemSize)创建队列
发送数据xQueueSend(xQueue, pvItemToQueue, xTicksToWait)发送到队列尾部
接收数据xQueueReceive(xQueue, pvBuffer, xTicksToWait)从队列头部接收
查看数据xQueuePeek(xQueue, pvBuffer, xTicksToWait)查看但不取出
队列长度uxQueueMessagesWaiting(xQueue)获取队列中数据个数

8.3 信号量/互斥锁函数

函数原型功能
创建二值信号量xSemaphoreCreateBinary()创建二值信号量
创建计数信号量xSemaphoreCreateCounting(uxMaxCount, uxInitialCount)创建计数信号量
创建互斥锁xSemaphoreCreateMutex()创建互斥锁
获取信号量xSemaphoreTake(xSemaphore, xTicksToWait)等待/获取信号量
释放信号量xSemaphoreGive(xSemaphore)释放/给出信号量

8.4 事件标志组函数

函数原型功能
创建事件组xEventGroupCreate()创建事件标志组
设置标志xEventGroupSetBits(xEventGroup, uxBitsToSet)设置事件标志
等待标志xEventGroupWaitBits(xEventGroup, uxBitsToWaitFor, xClearOnExit, xWaitForAllBits, xTicksToWait)等待事件标志
清除标志xEventGroupClearBits(xEventGroup, uxBitsToClear)清除事件标志

9HAL 库与 FreeRTOS

STM32 HAL 库与 FreeRTOS 配合使用时,需要注意一些关键点:

9.1 HAL 库中的 FreeRTOS 配置

FreeRTOSConfig.h 关键配置
// FreeRTOSConfig.h 关键配置
#define configUSE_PREEMPTION        1    // 1=抢占式调度
#define configCPU_CLOCK_HZ          72000000  // CPU 主频
#define configTICK_RATE_HZ          1000  // 时钟节拍 1kHz(1ms)
#define configMAX_PRIORITIES        5    // 最大优先级数
#define configMINIMAL_STACK_SIZE    128  // 最小堆栈大小
#define configTOTAL_HEAP_SIZE       10240 // 堆大小(10KB)
#define configUSE_MUTEXES           1    // 启用互斥锁
#define configUSE_COUNTING_SEMAPHORES 1  // 启用计数信号量
#define configUSE_TIMERS            1    // 启用软件定时器
#define configUSE_TASK_NOTIFICATIONS 1   // 启用任务通知

9.2 HAL 库函数与 FreeRTOS 配合

HAL 函数功能FreeRTOS 中的使用
HAL_Delay()毫秒延时会阻塞整个任务,建议用 vTaskDelay()
HAL_GPIO_TogglePin()翻转 GPIO可在任务中直接使用
HAL_UART_Transmit()串口发送阻塞调用,需注意任务堆栈
HAL_UART_Receive_IT()串口中断接收配合信号量通知任务
💡 HAL_Delay vs vTaskDelay

HAL_Delay():基于 SysTick 的忙等待,会阻塞整个 CPU,其他任务无法运行。

vTaskDelay():任务进入阻塞态,CPU 可以运行其他任务。在 FreeRTOS 中应该使用 vTaskDelay()

9.3 标准库 vs HAL 库对比

对比标准外设库 (StdPeriph)HAL 库
抽象程度贴近寄存器,效率高高度抽象,跨系列可移植
FreeRTOS 集成手动配置CubeMX 可图形化配置
中断处理手动编写HAL 有统一的中断处理框架
延时函数自己实现 delay_msHAL_Delay() 或 vTaskDelay()
推荐学习理解底层实际项目开发

重点例题

例题 1:多任务 LED 闪烁

例题:创建两个任务,分别以不同频率闪烁 LED 要求:任务 1 每 500ms 翻转 LED1,任务 2 每 1000ms 翻转 LED2。
// 任务 1
void vTaskLED1(void *pvParameters)
{
    for(;;)
    {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 任务 2
void vTaskLED2(void *pvParameters)
{
    for(;;)
    {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 主函数
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LED_Init();
    
    xTaskCreate(vTaskLED1, "LED1", 128, NULL, 2, NULL);
    xTaskCreate(vTaskLED2, "LED2", 128, NULL, 1, NULL);
    
    vTaskStartScheduler();
    for(;;);
}
关键点:① 任务函数必须是无限循环;② 必须有延时或阻塞,让出 CPU;③ 优先级高的任务先执行。

例题 2:队列通信

例题:按键任务通过队列通知 LED 任务切换状态 要求:按键按下时发送消息到队列,LED 任务接收消息后翻转 LED。
QueueHandle_t xQueue;

// 按键任务
void vTaskButton(void *pvParameters)
{
    uint8_t msg = 1;
    for(;;)
    {
        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
        {
            vTaskDelay(20); // 消抖
            if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
            {
                xQueueSend(xQueue, &msg, portMAX_DELAY);
                while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET);
            }
        }
        vTaskDelay(10);
    }
}

// LED 任务
void vTaskLED(void *pvParameters)
{
    uint8_t received;
    for(;;)
    {
        if(xQueueReceive(xQueue, &received, portMAX_DELAY) == pdPASS)
        {
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
        }
    }
}

int main(void)
{
    xQueue = xQueueCreate(5, sizeof(uint8_t));
    xTaskCreate(vTaskButton, "Btn", 128, NULL, 2, NULL);
    xTaskCreate(vTaskLED, "LED", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

例题 3:信号量同步

例题:外部中断通知任务处理事件 要求:按键触发外部中断,中断释放信号量,任务获取信号量后处理。
SemaphoreHandle_t xSemaphore;

// 中断服务程序
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 处理任务
void vTaskHandler(void *pvParameters)
{
    for(;;)
    {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdPASS)
        {
            printf("中断触发,处理事件!\r\n");
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
        }
    }
}

int main(void)
{
    xSemaphore = xSemaphoreCreateBinary();
    xTaskCreate(vTaskHandler, "Handler", 256, NULL, 2, NULL);
    vTaskStartScheduler();
}

例题 4:互斥锁保护共享资源

例题:两个任务交替使用串口打印 要求:任务 A 和任务 B 都要使用串口,用互斥锁保护,避免打印混乱。
SemaphoreHandle_t xMutex;

void vTaskA(void *pvParameters)
{
    for(;;)
    {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        printf("[TaskA] Hello from A\r\n");
        vTaskDelay(100);
        xSemaphoreGive(xMutex);
        vTaskDelay(500);
    }
}

void vTaskB(void *pvParameters)
{
    for(;;)
    {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        printf("[TaskB] Hello from B\r\n");
        vTaskDelay(100);
        xSemaphoreGive(xMutex);
        vTaskDelay(700);
    }
}

int main(void)
{
    xMutex = xSemaphoreCreateMutex();
    xTaskCreate(vTaskA, "A", 128, NULL, 2, NULL);
    xTaskCreate(vTaskB, "B", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

🎯自测(点击展开)

FreeRTOS 的任务有哪几种状态?
运行态(Running)、就绪态(Ready)、阻塞态(Blocked)、挂起态(Suspended)四种状态。
硬实时和软实时有什么区别?
硬实时必须在截止时间内完成,否则系统失败(如航空航天);软实时尽量在截止时间内完成,偶尔超时可接受(如多媒体播放)。
任务间通信有哪些方式?
队列(Queue)、信号量(Semaphore)、互斥锁(Mutex)、事件标志组(Event Group)、任务通知(Task Notification)。
如何创建一个 FreeRTOS 任务?
使用 xTaskCreate() 函数,参数包括:任务函数、任务名称、堆栈大小、参数、优先级、任务句柄。
实时操作系统的特点是什么?
确定性、可抢占性、快速响应、可靠性、低抖动。
vTaskDelay 和 vTaskDelayUntil 有什么区别?
vTaskDelay 是相对延时,从调用开始计时,周期不准确;vTaskDelayUntil 是绝对延时,从上次唤醒时间计时,周期准确。
互斥锁和信号量有什么区别?
互斥锁有优先级继承机制,适合保护共享资源;信号量没有优先级继承,适合事件通知和资源计数。
FreeRTOS 的调度方式是什么?
FreeRTOS 使用优先级抢占式调度,相同优先级的任务使用时间片轮转。
软件定时器的回调函数有什么限制?
回调函数中不能使用阻塞 API(如 vTaskDelay、xQueueReceive 等),因为定时器在定时器服务任务中执行。
FreeRTOS 提供几种内存分配策略?推荐使用哪种?
提供 5 种(heap_1 ~ heap_5)。推荐使用 heap_4,支持碎片合并,适合动态创建/删除任务的场景。

📝强化题库

选择题点选即时判分;填空题输入后"检查"或"显示答案"。

已答 0/0答对 0正确率
已答 0/0答对 0