Appearance
《FreeRTOS实战快速入门》课程讲义
零、前言
FreeRTOS 实战快速入门(纯手撸版)
此教程不会讲过多理论,以动手实操为主,解决大伙学了半天 FreeRTOS 操作系统不知道干什么的问题。
为什么要学 FreeRTOS ?
更有钱途!!
只会祼机开发的单片机工程师,薪资注定不会高于会 FreeRTOS 的工程师;
有了 FreeRTOS 基础,对于将来学习 Linux 操作系统会更加有帮助;
如何学好 FreeRTOS ?
无它,多写代码,多做项目!
一定要把本课程里所有项目全部自己动手做一遍,加深理解。光看不练假把式!!
误区:不建议一上来就啃源码!!
学习本课程前置要求
- C 语言熟练;
- STM32 要熟练,没学过 STM32 的同学建议学一下《STM32实战快速入门》课程
本课程做了哪些升级?
- 所有代码全程手敲,没有使用任何 CubeMX
- 加入三个实战项目:排队控制系统、智能门禁系统、智能台灯
- 项目代码以大厂要求开发,包括开发过程、流程图、代码风格等
准备好了吗?快乐启程~

本课程用到的硬件
- 主要以 STM32F103C8T6 开发板为主

- 做项目需要用到的硬件
- ST-Link
- USB转TTL
- 杜邦线
- 继电器
- 蜂鸣器
- 红外传感器
- LCD1602
- 矩阵键盘
- OLED 屏幕
- W25Q128
- 蓝牙模块
- 超声波传感器
- 光敏电阻传感器
- 高功率LED灯
- KEY × 4
一、FreeRTOS简介
1. 什么是 FreeRTOS ?
Free即免费的,RTOS的全称是Real time operating system,中文就是实时操作系统。
注意:RTOS不是指某一个确定的系统,而是指一类操作系统。比如:uc/OS,FreeRTOS,RTX,RT-Thread等这些都是RTOS类操作系统。
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。 由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为10.4.4版。
(以上来自百度百科)
2. 为什么选择 FreeRTOS ?
FreeRTOS 是免费的,无潜在商业风险;
很多半导体厂商产品的SDK(Software Development Kit)软件开发工具包,就使用FreeRTOS作为其操作系统,尤其是WIFI、蓝牙这些带有协议栈的芯片或模块。
简单,因为FreeRTOS的文件数量很少。
3. 祼机开发与FreeRTOS

祼机开发:
while(1)
{
太监();
杨贵妃();
张贵妃();
if(time_flag == 1)
{
开会();
}
皇太后();
}裸机又称为前后台系统,前台系统指的中断服务函数,后台系统指的大循环,即应用程序。

FreeRTOS:
void main(void)
{
xTaskCreate(太监);
xTaskCreate(杨贵妃);
xTaskCreate(张贵妃);
xTaskCreate(皇太后);
}void 太监(void)
{
while(1)
{
汇报工作();
}
}
void 杨贵妃(void)
{
while(1);
{
唱歌();
}
}
void 张贵妃(void)
{
while(1);
{
跳舞();
}
}
void 皇太后(void)
{
while(1);
{
教训();
}
}RTOS全称为:Real Time OS,就是实时操作系统,强调的是:实时性

FreeRTOS 实现多任务的原理
系统将时间分割成很多时间片,然后轮流执行各个任务——分时复用。
每个任务都是独立运行的,互不影响,由于切换的频率很快,就感觉像是同时运行的一样。

4. FreeRTOS相比裸机开发的优势
4.1 多任务实时调度
- 裸机局限:裸机开发通常采用轮询或前后台系统,任务需顺序执行,紧急事件易被延迟处理(如任务1执行时无法响应任务2)。
- FreeRTOS优势:支持抢占式调度和时间片轮转,高优先级任务可立即抢占低优先级任务,确保实时性;延时函数(如
vTaskDelay())释放 CPU 给其他任务,避免空等待。
4.2 资源管理高效化
裸机局限:需手动处理中断堆栈、数据同步等问题,中断嵌套易导致堆栈溢出或数据混乱。
FreeRTOS优势:提供信号量、互斥锁、队列等同步机制,避免资源冲突;优先级继承机制防止死锁,提升系统稳定性。
4.3 代码结构与可维护性
- 裸机局限:功能代码集中在
while(1)循环中,结构臃肿,维护困难。 - FreeRTOS优势:任务模块化设计,功能解耦,代码清晰易扩展;开发者可专注于业务逻辑,减少底层调度代码编写。
4.4 低资源占用与可移植性
裸机局限:需针对特定硬件定制开发,移植成本高。
FreeRTOS优势:内核仅需 4–9KB RAM,支持多种处理器(如 ARM、RISC-V);源码采用 C 语言编写,移植层代码少,适配不同硬件便捷。
二、FreeRTOS移植
1. 移植前准备
- 基础工程
- FreeRTOS源码

2. 手把手教你移植FreeRTOS
2.1 添加FreeRTOS源码
- 创建FreeRTOS子文件夹

- 拷备FreeRTOS源码

| 名称 | 描述 |
|---|---|
| include | 内包含了FreeRTOS的头文件 |
| portable | 内包含了FreeRTOS的移植文件 |
| croutine.c | 协程相关文件 |
| event_groups.c | 事件相关文件 |
| list.c | 列表相关文件 |
| queue.c | 队列相关文件 |
| stream_buffer.c | 流式缓冲区相关文件 |
| tasks.c | 任务相关文件 |
| timers.c | 软件定时器相关文件 |
- 精简portable文件夹

| 名称 | 描述 |
|---|---|
| Keil | 指向RVDS文件夹 |
| RVDS | 不同内核芯片的移植文件 |
| MemMang | 内存管理文件 |
2.2 将FreeRTOS源码添加到基础工程
- 创建两个文件分组

- 将FreeRTOS源码添加到文件分组里

- 添加头文件路径

2.3 添加FreeRTOSConfig.h文件

2.4. 修改工程代码
2.4.1 注释回调函数
stm32f1xx_it.c文件
c
//void SVC_Handler(void)
//{
//}
//void PendSV_Handler(void)
//{
//}
//void SysTick_Handler(void)
//{
// HAL_IncTick();
//}2.4.2 修改delay模块
使用「带操作系统的延时函数」里的delay模块。
c
//以下添加到delay.c
#include "FreeRTOS.h"
#include "task.h"
extern void xPortSysTickHandler(void);
void SysTick_Handler(void)
{
HAL_IncTick();
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) /* OS开始跑了,才执行正常的调度处理 */
{
xPortSysTickHandler();
}
}
void delay_init(void)
{
SysTick_Config(HAL_RCC_GetHCLKFreq()/configTICK_RATE_HZ);
}
//以下添加到delay.h
void delay_init(void);2.4.3 修改main模块
c
#include "FreeRTOS.h"
#include "freertos_test.h"
#include "task.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init();
led_init(); /* 初始化LED灯 */
uart1_init(115200);
printf("hello world!\r\n");
freertos_test();
vTaskStartScheduler();
}2.4.4 修改宏值
c
#define __NVIC_PRIO_BITS 4 //去掉尾巴的U2.5 添加测试代码
3. 测试
硬件接线:
| STM32 | ST-Link | USB转TTL |
|---|---|---|
| CLK | CLK | |
| DIO | DIO | |
| TX1 | RX | |
| RX1 | TX | |
| 3V3 | 3V3 | |
| GND | GND | GND |
实验现象:
led1以1秒的频率闪烁,led2以500ms频率闪烁。
三、任务
1. 任务的基本特性
1.1 任务定义与结构
每个任务是一个独立的函数,遵循 void TaskFunction(void *pvParameters) 的原型,通常以无限循环形式运行,通过 vTaskDelay() 等函数主动释放 CPU 资源。任务由**任务控制块(TCB)**描述,包含栈指针、优先级、状态等信息。
• 堆栈分配:每个任务需预分配独立栈空间(静态或动态),例如 STM32 中 configMINIMAL_STACK_SIZE 设置为 128 字(即 512 字节)。 • 优先级范围:0(最低)至 configMAX_PRIORITIES-1(最高),优先级越高越易抢占 CPU(数值越大优先级越高)。
1.2 任务类型
• 动态任务:通过 xTaskCreate() 创建,由内核自动分配内存,适用于灵活性要求高的场景。 • 静态任务:使用 xTaskCreateStatic() 创建,需手动预分配 TCB 和栈内存,适合资源受限系统。
2. 任务状态与转换
FreeRTOS 的任务包含五种状态,通过调度器动态切换:
1. 运行态(Running):当前占用 CPU 的任务,单核系统中同一时刻只能有一个任务处于运行态。 2. 就绪态(Ready):已准备就绪,等待调度器分配 CPU 时间。 3. 阻塞态(Blocked):等待事件(如信号量、队列消息或延时)触发,例如调用 vTaskDelay(1000) 进入 1 秒阻塞。 4. 挂起态(Suspended):通过 vTaskSuspend() 显式挂起,需调用 vTaskResume() 恢复,期间不参与调度。 5. 终止态/僵尸态(Deleted):任务被删除后进入终止态,由空闲任务清理资源。

1、仅就绪态可转变成运行态
2、其他状态的任务想运行,必须先转变成就绪态
3. 任务调度机制
3.1 调度概述
1. 抢占式调度 • 高优先级抢占:当高优先级任务就绪时,立即抢占当前任务(如优先级 2 的任务抢占优先级 1 的任务)。 • 优先级继承:低优先级任务持有互斥锁时,临时继承高优先级任务的优先级,避免死锁。
2. 时间片轮转调度 • 同优先级任务按时间片轮流执行,每个时间片长度由 configTICK_RATE_HZ 定义(如 1ms Tick)。 • 示例:任务 A 和 B 优先级相同,各自运行 1 个时间片后切换。
3. 协程式调度 • 当前执行任务将会一直运行,同时高优先级的任务不会抢占低优先级任务 • FreeRTOS现在虽然还支持,但是官方已经表示不再更新协程式调度
3.2 抢占式调度详解

运行条件:
依次创建三个任务:task1、task2、task3,优先级分别为1、2、3。
运行过程如下:
- task1优先被创建,优先进入运行态;当task2被创建并进入就绪态时,由于优先级比task1高,则抢占task1进入运行态;
- task3被创建并进入就绪态时,由于优先级比task2高,则抢占task2进入运行态;
- task3在运行过程中被阻塞了(如调用了vTaskDelay()函数),进入阻塞态,此时task2由就绪态转为运行态;
- task3解除了阻塞(如延时时间到),由阻塞态转为就绪态,由于优先级比task2高,由抢占task2转为运行态。
3.3 时间片轮转详解
任务运行固定时间片(由configTICK_RATE_HZ定义,如1ms)后自动切换至同优先级的下一个任务。

运行条件:
依次创建三个任务:task1、task2、task3,优先级均为1。
运行过程如下:
- task1优先被创建,优先进入运行态;进行完一个时间片后,转为就绪态,此时task2进入运行态;
- task2运行完一个时间片后,转为就绪态,此时task3进入运行态;
- task3在运行过程中被阻塞了(如调用了vTaskDelay()函数),进入阻塞态,此时未用完的时间片被丢弃;
- 调度器直接调度task1进入运行态,task1运行完一个时间片后,切换至task2运行。
4. 任务创建函数
任务创建函数主要有两种:动态创建与静态创建。
二者有何区别?
动态创建: 通过 xTaskCreate() 函数实现,任务控制块(TCB)和任务堆栈内存由 FreeRTOS 从系统堆(Heap)中自动分配,需依赖内存管理算法(如 heap_4.c)。 • 优点:开发者无需手动管理内存,适合频繁创建/删除任务的场景。 • 缺点:可能因内存碎片导致长期运行后分配失败。
静态创建: 通过 xTaskCreateStatic() 函数实现,开发者需预先分配 TCB 和堆栈内存(如全局数组),并通过参数传递给函数。 • 优点:内存分配在编译时完成,无动态内存开销,适合对内存确定性要求高的系统(如汽车电子)。 • 缺点:需手动管理内存生命周期,灵活性较低。
4.1 动态(内存分配)创建任务:xTaskCreate()
函数原型:
c
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针(函数原型:void task(void *pvParams))
const char *const pcName, // 任务名称(调试用,长度≤configMAX_TASK_NAME_LEN)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位:字,如1024字=4KB for ARM)
void *const pvParameters, // 任务参数指针(传递给任务函数)
UBaseType_t uxPriority, // 优先级(0最低,configMAX_PRIORITIES-1最高)
TaskHandle_t *const pxCreatedTask // 任务句柄(用于后续操作如删除、挂起)
);返回值: • pdPASS:任务创建成功。 • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:堆内存不足,创建失败。
注意:
需要将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1
4.2 静态(内存分配)创建任务:xTaskCreateStatic()
函数原型:
c
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pvTaskCode,
const char *const pcName,
uint32_t ulStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
StackType_t *const puxStackBuffer, // 用户预分配的堆栈内存
StaticTask_t *const pxTaskBuffer // 用户预分配的TCB内存
);返回值: • 成功:返回任务句柄。 • 失败:返回 NULL(参数错误或内存未对齐)。
使用流程:
1. 配置宏与预分配内存
• 在 FreeRTOSConfig.h 中启用静态内存分配:
c
#define configSUPPORT_STATIC_ALLOCATION 1 // 启用静态内存分配• 预分配 TCB 和堆栈内存:
c
StaticTask_t xTaskTCB; // 任务TCB
StackType_t xTaskStack[128]; // 任务堆栈2. 实现空闲/定时器任务接口(必选)
• 定义空闲任务内存(否则编译报错):
c
//空闲任务配置
StaticTask_t idle_task_tcb;
StackType_t idle_task_stack[configMINIMAL_STACK_SIZE];
//软件定时器任务配置
StaticTask_t timer_task_tcb;
StackType_t timer_task_stack[configTIMER_TASK_STACK_DEPTH];
//空闲任务内存分配
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
configSTACK_DEPTH_TYPE * pulIdleTaskStackSize )
{
* ppxIdleTaskTCBBuffer = &idle_task_tcb;
* ppxIdleTaskStackBuffer = idle_task_stack;
* pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
//软件定时器内存分配
void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer,
StackType_t ** ppxTimerTaskStackBuffer,
configSTACK_DEPTH_TYPE * pulTimerTaskStackSize )
{
* ppxTimerTaskTCBBuffer = &timer_task_tcb;
* ppxTimerTaskStackBuffer = timer_task_stack;
* pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}3. 调用 xTaskCreateStatic 创建任务
c
TaskHandle_t xStaticTaskHandle = xTaskCreateStatic(
vTaskDemo, // 任务函数指针
"StaticTask", // 任务名称
1024, // 堆栈深度
NULL, // 任务参数
3, // 优先级
xTaskStack, // 预分配的堆栈地址
&xTaskTCB // 预分配的 TCB 地址
);• 返回值检查:若 xStaticTaskHandle != NULL 表示成功。
5. 任务删除函数
函数原型:
c
void vTaskDelete(TaskHandle_t xTaskToDelete);参数: • xTaskToDelete:任务句柄。若为 NULL,删除当前任务自身。
行为: • 删除任务后,任务进入 僵尸态,其TCB和堆栈由 空闲任务(Idle Task)自动回收。 • 若任务持有资源(如互斥锁、队列),需先手动释放,否则可能引发内存泄漏。
什么是空闲任务?
空闲任务的概述
空闲任务(Idle Task)是 FreeRTOS 调度器启动时自动创建的后台任务,具有以下特性:
- 最低优先级:固定为 0 优先级,仅在所有其他任务阻塞或挂起时运行。
- 必要性:确保系统始终有任务可运行,防止处理器“空转”。
- 资源占用:默认堆栈大小为
configMINIMAL_STACK_SIZE(通常较小,如 128 字)。
空闲任务的核心功能
• 自删除任务的内存释放: 当任务调用 vTaskDelete(NULL) 删除自身时,其任务控制块(TCB)和堆栈内存由空闲任务自动回收。 • 直接删除任务的资源处理: 若任务 A 删除任务 B,则任务 B 的资源立即释放,无需空闲任务介入。
小实验1
- 动态创建三个任务:task1、task2、task3,三个任务作用如下:
- task1:以1000ms频率闪烁LED1;
- task2:以500ms频率闪烁LED2;
- task3:检测按键
- task2创建后即自我删除;
- 检测到按键1按下,则删除task1任务。
小实验2
使用静态任务创建方法完成小实验1。
6. 任务挂起与恢复函数
6.1 函数介绍
6.1.1 vTaskSuspend() - 任务挂起函数
• 功能:将指定任务置为挂起态,该任务将不再参与调度,直到被显式恢复。 • 参数: • xTaskToSuspend:目标任务的句柄(传 NULL 表示挂起自身)。 • 源码行为: • 从就绪/阻塞列表中移除任务,并插入挂起列表 xSuspendedTaskList。 • 若挂起自身,立即触发上下文切换 portYIELD_WITHIN_API()。 • 配置依赖:需在 FreeRTOSConfig.h 中启用宏 INCLUDE_vTaskSuspend=1。
6.1.2 vTaskResume() - 任务恢复函数
• 功能:将挂起的任务恢复到就绪态。 • 参数: • xTaskToResume:目标任务的句柄。 • 限制:仅能恢复通过 vTaskSuspend() 挂起的任务,不支持恢复因事件阻塞的任务。 • 无返回值:直接修改任务状态至就绪列表。
6.1.3 xTaskResumeFromISR() - 中断中任务恢复函数
• 功能:在中断服务程序(ISR)中恢复挂起的任务。 • 参数: • xTaskToResume:目标任务的句柄。 • 返回值: • pdTRUE:恢复的任务优先级≥当前任务,需调用 portYIELD_FROM_ISR() 触发上下文切换。 • pdFALSE:恢复的任务优先级较低,无需立即切换。 • 配置依赖:需启用宏 INCLUDE_xTaskResumeFromISR=1。 • 注意: • NVIC中断优先级分组需设置为分组4; • 中断服务程序中要调用freeRTOS的API函数则中断优先级不能高于FreeRTOS所管理的最高优先级。
6.2 函数特性与设计规则
6.2.1 非嵌套性
• 无论调用多少次 vTaskSuspend() 挂起任务,只需调用一次 vTaskResume() 即可恢复。
6.2.2 中断安全性
• 挂起限制:禁止在 ISR 中调用 vTaskSuspend(),否则会导致程序异常。 • 恢复操作:在 ISR 中恢复任务时,必须处理返回值并决定是否触发上下文切换。
小实验3
基于小实验1,实现以下功能:
- 按下按键1,挂起task1;
- 按下按键2,在任务中恢复task1;
小实验4
基于小实验1,实现以下功能:
- 按下按键1,挂起task1;
- 按下按键2,在中断中恢复task1;
四、中断管理
1. 中断优先级管理
1.1 优先级分组与范围
• 分组方式:FreeRTOS 强制使用 NVIC_PriorityGroup_4,即所有中断仅通过抢占优先级区分(0-15级,数值越小优先级越高)。 • 管理范围: • 可管理中断:优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY(默认5)的中断(即5-15)。在 ISR 中必须使用带 FromISR 的 API(如 xTaskResumeFromISR()),避免调用阻塞函数。 • 不可管理中断:优先级 <5 的中断(0-4),属于高优先级中断,不可调用 FreeRTOS API,即使系统处于临界区也不受影响。
中断优先级寄存器(IPR):

1.2 BASEPRI 寄存器机制
• 作用:通过设置 BASEPRI 寄存器的阈值(如0x50对应优先级5),屏蔽优先级低于该阈值的中断。 • 临界区操作: • portDISABLE_INTERRUPTS():写 BASEPRI 为 configMAX_SYSCALL_INTERRUPT_PRIORITY,屏蔽优先级5-15的中断。 • portENABLE_INTERRUPTS():写 BASEPRI 为0,解除屏蔽。
BASEPRI寄存器:
作用:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断。 比如: BASEPRI设置为0x50,代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行。

2. 临界段代码保护
2.1 基本原理
临界段代码指必须完整执行且不可被打断的代码段(如硬件初始化、共享资源访问)。FreeRTOS 通过操作 BASEPRI 寄存器屏蔽中断,确保临界区的原子性。

什么可以打断当前程序的运行?
- 中断
- 任务调度
2.2 任务级临界区保护
• 函数: • taskENTER_CRITICAL():进入临界区,关闭优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断(默认优先级≥5)。 • taskEXIT_CRITICAL():退出临界区,恢复中断。 • 嵌套机制: 使用全局变量 uxCriticalNesting 记录嵌套次数,仅在最外层退出时重新使能中断。 • 代码示例:
c
taskENTER_CRITICAL();
// 临界区操作(如修改全局变量)
taskEXIT_CRITICAL();2.3 中断级临界区保护
• 函数: • taskENTER_CRITICAL_FROM_ISR():在 ISR 中进入临界区,保存当前 BASEPRI 值并屏蔽中断。 • taskEXIT_CRITICAL_FROM_ISR(status):退出时恢复原 BASEPRI 值。 • 使用场景: 用于中断服务程序中需要保护共享资源的场景,如 UART 接收中断中操作队列。
3. 调度器挂起与恢复
3.1 调度器挂起
• 函数:vTaskSuspendAll() • 作用:挂起调度器,禁止任务切换(但不关闭中断)。 • 适用场景: 长耗时操作(如大块内存拷贝)需避免任务切换,但需允许中断响应。 • 代码示例:
c
vTaskSuspendAll();
memcpy(buffer, source, LARGE_SIZE); // 耗时操作
xTaskResumeAll();3.2 调度器恢复
• 函数:xTaskResumeAll() • 作用:恢复调度器,返回 pdTRUE 表示恢复期间有更高优先级任务就绪。 • 注意事项: 若挂起期间有任务解除阻塞,恢复时会立即触发任务切换。
3.3 临界区保护与调度器挂起的对比
| 特性 | 临界区保护 | 调度器挂起 |
|---|---|---|
| 中断状态 | 关闭部分中断 | 保持中断开启 |
| 任务切换 | 禁止 | 禁止 |
| 适用场景 | 短耗时操作(<几微秒) | 长耗时操作(如文件系统访问) |
| 资源消耗 | 低(仅寄存器操作) | 低(无上下文切换) |
| 嵌套支持 | 支持 | 支持 |
小实验5
在FreeRTOS中,多任务使用printf时,极易产生乱码的现象,请解决此问题。
五、延时函数
1. 延时函数概述
1.1 基本概念
延时函数是实时操作系统中用于控制任务执行时序的重要机制,使任务能够在指定时间内暂停执行,让处理器执行其他任务。在FreeRTOS中,延时函数是任务管理和时间管理的重要组成部分。
1.2 延时函数的作用
- 任务调度控制:允许低优先级任务获得CPU执行时间
- 时序控制:确保操作按照预定时间间隔执行
- 降低功耗:在无需执行时进入低功耗状态
- 软件定时:实现周期性操作和超时检测
- 防止任务占用CPU:避免高优先级任务长时间占用处理器
2. FreeRTOS中的延时函数
2.1 相对延时函数
2.1.1 vTaskDelay
c
void vTaskDelay(TickType_t xTicksToDelay);- 功能:使当前任务延时指定的时钟节拍数
- 参数:
xTicksToDelay- 延时的时钟节拍数 - 特点:
- 相对延时(从调用时开始计时)
- 不保证精确的时间间隔
- 延时期间任务处于阻塞状态
- 调用后立即让出CPU
2.1.2 宏定义pdMS_TO_TICKS
c
#define pdMS_TO_TICKS(xTimeInMs) ((TickType_t)(((uint32_t)(xTimeInMs) * (uint32_t)configTICK_RATE_HZ) / (uint32_t)1000))- 功能:将毫秒转换为系统时钟节拍数
- 用法:
vTaskDelay(pdMS_TO_TICKS(100)); // 延时100毫秒
2.2 绝对延时函数
2.2.1 vTaskDelayUntil
c
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);- 功能:使任务延时到指定的绝对时间点
- 参数:
pxPreviousWakeTime- 上次唤醒时间的指针xTimeIncrement- 周期间隔的时钟节拍数
- 特点:
- 绝对延时(从指定时间点计时)
- 适合周期性任务
- 能保证固定执行频率
小实验6
请编程验证vTaskDelay与vTaskDelayUntil的区别。
六、队列
1. 队列简介
队列(Queue)是FreeRTOS中实现任务间通信的核心机制,用于在任务与任务、中断与任务之间传递数据。

• 本质:队列是一个先进先出(FIFO)的环形缓冲区,支持数据项的存储与按顺序读取。 • 数据传递方式:采用值传递(复制数据本身),而非指针传递,确保数据安全性和独立性。 • 应用场景: • 传感器数据采集与处理任务间的数据传输 • 按键事件通知 • 中断服务程序(ISR)向任务传递事件标志
关于队列的几个名词:
队列项目:队列中的每一个数据;
队列长度:队列能够存储队列项目的最大数量;
创建队列时,需要指定队列长度及队列项目大小。
2. 队列的核心特点
2.1 线程安全
• 通过临界区保护(关闭中断)实现多任务并发访问的原子性操作。
写队列:
xQueueSend( )
{
// 进入临界区(关中断)
写队列实际操作
// 退出临界区(开中断)
}
读队列:
xQueueReceive( )
{
// 进入临界区(关中断)
读队列实际操作
// 退出临界区(开中断)
}• 支持任务在队列满/空时阻塞等待,并可通过优先级唤醒机制自动调度任务。
2.2 灵活的数据存储
• 支持FIFO(默认)和LIFO(通过xQueueSendToFront()实现)两种模式。 • 每个数据项大小固定,队列长度在创建时指定(例如:存储10个int型数据)。
2.3 动态与静态内存分配
• 动态创建:xQueueCreate()从FreeRTOS堆分配内存,适合运行时灵活调整。 • 静态创建:xQueueCreateStatic()需预分配内存,适用于资源受限或确定性要求高的场景。
2.4 中断安全
• 提供FromISR后缀的API(如xQueueSendFromISR()),确保中断服务程序安全操作队列。
3. 队列API详解
3.1 队列创建与删除
创建队列
c
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);uxQueueLength:队列可容纳的最大消息数uxItemSize:每个消息的大小(字节)- 返回:成功返回队列句柄,失败返回NULL
静态创建方式(无需动态内存分配):
c
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);删除队列
c
void vQueueDelete(QueueHandle_t xQueue);xQueue:要删除的队列句柄
3.2 发送数据到队列
基本发送
c
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);xQueue:队列句柄pvItemToQueue:指向要发送数据的指针xTicksToWait:阻塞等待时间(如队列已满)- 返回:
pdPASS表示成功,errQUEUE_FULL表示失败
xQueueSendToBack()与xQueueSend()功能相同。
发送到队列前端
c
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);- 发送数据到队列前端,使其成为下一个被读取的项目
强制发送(覆盖)
c
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void *pvItemToQueue
);- 如果队列已满,覆盖最旧的数据
- 主要用于长度为1的队列(邮箱)
3.3 从队列接收数据
基本接收
c
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);xQueue:队列句柄pvBuffer:接收数据的缓冲区指针xTicksToWait:阻塞等待时间(如队列为空)- 返回:
pdPASS表示成功,errQUEUE_EMPTY表示失败 - 从队列头部读取消息,并删除消息
查看但不取出
c
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);- 查看队列中的数据但不从队列中移除
3.4 中断中使用队列
从中断发送
c
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);c
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);c
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);pxHigherPriorityTaskWoken:如设为pdTRUE,表示此操作唤醒了更高优先级任务,需要进行任务切换
从中断接收
c
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken
);中断中覆盖写入
c
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);3.5 队列信息查询
c
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue); // 获取队列中消息数
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue); // 获取队列中空闲空间数
BaseType_t xQueueIsQueueEmptyFromISR(QueueHandle_t xQueue); // 中断中判断队列是否为空
BaseType_t xQueueIsQueueFullFromISR(QueueHandle_t xQueue); // 中断中判断队列是否已满3.6 队列重置
c
BaseType_t xQueueReset(QueueHandle_t xQueue); // 清空队列内的所有消息小实验7
- 创建队列queue;
- 动态创建三个任务:task1、task2、task3,三个任务作用如下:
- task1:以1000ms频率闪烁LED1,表示系统正常运行
- task2:接收队列queue的数据;
- task3:检测按键
- 检测到按键按下,则往队列queue发送当前按键值;
小实验8
与小实验7类似,不同之处在于:
检测到按键1按下,则往queue传递大数据。
七、信号量概述
在多任务嵌入式系统中,随着任务数量的增加,各个任务之间必然会存在着协作与竞争的关系。例如,多个任务可能需要访问同一个硬件资源,或者一个任务产生的数据需要被另一个任务消费。这就需要引入信号量这一重要的同步机制。
信号量是操作系统中一种经典的同步控制机制,它的本质是一种特殊的变量,用于在多任务环境中实现任务之间的同步和互斥。在FreeRTOS中,信号量是基于队列实现的一种特殊数据结构。与普通队列不同,信号量的队列项长度为零,也就是说,信号量不传递实际的数据,而只是用计数值表示资源的可用性或事件的发生次数。


1. 信号量的基本操作
信号量的基本操作包括"获取"(take)和"释放"(give)两种。当任务获取信号量时,信号量的计数值减1;当任务释放信号量时,计数值加1。如果计数值为0,任何试图获取信号量的任务都将被阻塞,直到其他任务或中断释放信号量,使计数值增加。这种简单而强大的机制可以解决多种多样的同步问题。

2. 信号量的应用场景
在现代嵌入式系统设计中,信号量的应用十分广泛。当多个任务需要共享访问某个硬件资源时,如串口或SPI总线,可以使用信号量确保同一时刻只有一个任务能够访问该资源,避免数据混乱。另一方面,当系统中的一个任务需要等待特定事件发生后才能继续执行时,可以利用信号量实现这种条件等待的逻辑。
FreeRTOS提供了多种类型的信号量,包括二值信号量、互斥量和计数信号量,以满足不同场景下的需求。二值信号量主要用于任务间的同步;互斥量带有优先级继承机制,主要用于防止优先级反转问题;而计数信号量则允许多个资源的管理或多个事件的计数。
八、二值信号量
1. 二值信号量概念
1.1 什么是二值信号量?
二值信号量是一种特殊的同步机制,只有 0(无效) 和 1(有效) 两种状态。它类似于现实生活中的“通行证”,任务需要持有该“通行证”才能执行特定操作。 • 本质:长度为1、消息大小为0的特殊消息队列。 • 特点:不关心队列中的具体数据,只关注队列是否为空(0)或满(1)。
1.2 核心作用
实现 任务与任务 或 任务与中断 之间的 同步(如事件触发、数据就绪通知)。 示例:串口接收数据时,任务无需轮询,通过信号量等待中断通知数据到达。
while(1)
{
if(flag == 1)
//do something
else if(flag == 0)
//do something else
}2. 工作机制
2.1 同步流程(以串口接收为例)
任务A(接收任务) 中断(数据到达)
├─ 等待信号量(阻塞) ──→ 释放信号量
└─ 收到信号量→处理数据 ←─ 触发中断(任务切换)2.2 关键特性
• 优先级继承机制缺失:与互斥信号量不同,可能导致优先级翻转问题(需用户自行处理)。 • 非阻塞与阻塞: • 任务可设置阻塞时间(如xTicksToWait),超时后自动恢复。 • 中断中释放信号量时需使用xSemaphoreGiveFromISR,避免阻塞中断。
3. API函数详解
3.1 创建二值信号量
c
SemaphoreHandle_t xSemaphoreCreateBinary(void);• 返回值:成功返回句柄,失败返回NULL(内存不足)。 • 初始化状态:默认无效(0),需手动释放。
3.2 释放信号量
c
// 任务级释放
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
// 中断级释放
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);• 参数:pxHigherPriorityTaskWoken标记是否需要任务切换。 • 返回值:pdPASS(成功)或errQUEUE_FULL(失败)。
3.3 获取信号量
c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);• 阻塞时间:xBlockTime可设为portMAX_DELAY(无限等待)或具体节拍数。 • 返回值:pdTRUE(成功获取)或pdFALSE(超时)。
小实验9
- 创建二值信号量semphore;
- 动态创建二个任务:task1、task2,两个任务作用如下:
- task1:以1000ms频率闪烁LED1,表示系统正常运行
- task2:检测按键
- 检测到按键1按下,则释放二值信号量;检测到按键2按下,则获取二值信号量。
九、计数型信号量
1. 计数型信号量概念
1.1 什么是计数型信号量?
计数型信号量是一种允许多任务共享资源的同步机制,其计数值范围为 0 到设定的最大值。 • 本质:长度为N(N≥1)、消息大小为0的特殊消息队列。 • 特点:计数值表示可用资源数量或事件积累次数,不同于二值信号量的0/1二元状态。
1.2 核心作用
• 资源管理:跟踪有限资源的可用数量(如停车场车位、内存块)。 • 事件计数:记录事件发生的次数,供任务批量处理(如多次按键触发)。
2. 工作机制
2.1 资源管理场景(以停车场为例)
• 初始化:创建最大计数值为100的计数信号量,初始值设为100(总车位数量)。 • 车辆进入:任务调用xSemaphoreTake获取信号量,计数值减1;若计数值为0,任务阻塞。 • 车辆离开:任务调用xSemaphoreGive释放信号量,计数值加1,唤醒等待任务。
2.2 事件计数场景(如按键触发)
• 中断触发:每次按键中断调用xSemaphoreGiveFromISR释放信号量,计数值加1。 • 任务处理:任务循环调用xSemaphoreTake获取信号量,批量处理累计事件(如处理10次按键事件)。
2.3 底层实现原理
• 基于消息队列结构,队列长度对应最大计数值,成员变量uxMessagesWaiting存储当前计数值。 • 获取信号量即“消费队列项”,释放信号量即“生产队列项”,但无需传递实际数据。
3. API函数详解
3.1 创建计数型信号量
c
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);• 参数: • uxMaxCount:最大计数值(如停车场总车位)。 • uxInitialCount:初始值(资源管理设为总资源数,事件计数设为0)。 • 返回值:成功返回句柄,失败返回NULL(内存不足)。
3.2 获取信号量
c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);• 行为:计数值减1,若为0则任务阻塞。 • 超时:xBlockTime设为portMAX_DELAY表示无限等待。
3.3 释放信号量
c
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore); // 任务级
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken); // 中断级• 限制:若计数值已达最大值,释放失败返回errQUEUE_FULL。 • 中断安全:使用xSemaphoreGiveFromISR避免阻塞中断,并通过portYIELD_FROM_ISR触发任务切换。
小实验10
- 创建二值信号量count_semphore_handle,最大计数值为5,初始值为0。
- 动态创建二个任务:task1、task2,两个任务作用如下:
- task1:以1000ms频率闪烁LED1,表示系统正常运行
- task2:检测按键
- 检测到按键1按下,则释放计数型信号量;检测到按键2按下,则获取计数型信号量。
十、互斥信号量
1. 优先级翻转
1.1 优先级翻转的定义
优先级翻转(Priority Inversion)是多任务实时操作系统中一种典型的资源竞争问题,表现为高优先级任务因等待低优先级任务占用的资源而被阻塞,而中等优先级任务在此期间抢占执行,导致系统任务调度顺序违背预设的优先级规则。这种现象可能严重破坏系统的实时性,尤其是在关键任务场景中(如工业控制、航空航天)。
1.2 典型场景与发生条件

假设存在三个任务,优先级排序为 H > M > L(H最高,L最低):
- 初始状态:任务L占用共享资源(如互斥锁、信号量),任务H和M处于挂起状态等待事件触发。
- 事件触发:任务H的事件发生,进入就绪态并抢占CPU。当任务H尝试获取资源时,因资源被任务L占用而阻塞。
- 中等优先级任务介入:此时任务M的事件触发,因其优先级高于任务L,开始执行。任务M无需访问共享资源,持续占用CPU,导致任务H被迫等待任务M和任务L执行完毕。
最终结果:高优先级任务H的响应时间被中优先级任务M和低优先级任务L共同拉长,形成优先级翻转。
1.3 优先级翻转的危害
• 实时性破坏:高优先级任务无法及时响应,可能导致系统超时或功能失效(如传感器数据丢失)。 • 资源浪费:中等优先级任务无意义抢占CPU,降低系统效率。 • 死锁风险:若多个任务形成循环等待资源,可能引发死锁。
1.4 FreeRTOS的解决方案:优先级继承
FreeRTOS通过互斥信号量(Mutex)的优先级继承机制缓解优先级翻转问题: • 核心原理:当高优先级任务因资源被低优先级任务占用而阻塞时,低优先级任务的优先级被临时提升至高优先级任务的级别,使其尽快释放资源,避免被中等优先级任务抢占。

• 具体流程:
- 任务L占用互斥锁。
- 任务H请求互斥锁时阻塞,触发优先级继承机制,任务L的优先级提升至任务H的级别。
- 任务L执行期间,任务M无法抢占,任务L释放锁后优先级恢复。
- 任务H获得锁并执行,任务M恢复原有调度顺序。
1.5 优先级继承的局限性
• 不完全解决翻转:仅缩短高优先级任务的阻塞时间,无法完全消除(如任务L本身执行时间过长)。 • 中断中不可用:互斥信号量不能在中断服务程序(ISR)中使用,需配合二值信号量或直接处理。
小实验11
编程演示优先级翻转现象。
2. 互斥信号量概念
2.1 什么是互斥信号量?
互斥信号量是一种特殊的二值信号量,用于保护共享资源,确保同一时刻仅有一个任务能访问临界资源。其核心特性包括: • 所有权机制:只有获取信号量的任务才能释放它,避免资源被错误释放。 • 优先级继承:当高优先级任务因资源被低优先级任务占用而阻塞时,系统会临时提升低优先级任务的优先级,缓解优先级翻转问题。
互斥量的核心设计原则是“谁获取,谁释放”。当一个任务获取互斥量后,它成为该互斥量的“持有者”(holder),理论上只有持有者能释放它。这种机制旨在保护临界资源,避免数据竞争或逻辑错误。FreeRTOS的互斥量在代码实现上未强制要求必须由持有者释放。即使任务A获取了互斥量,任务B仍可调用xSemaphoreGive()释放该互斥量,且操作会成功。
2.2 与二值信号量的区别
| 特性 | 互斥信号量 | 二值信号量 |
|---|---|---|
| 用途 | 资源保护 | 任务/中断同步 |
| 所有权 | (理认上)必须由获取者释放 | 任何任务均可释放 |
| 优先级继承 | 支持,防止优先级翻转 | 不支持 |
| 适用场景 | 硬件资源(如串口)、共享数据 | 事件通知(如中断触发任务) |
3. 工作机制
3.1 优先级翻转问题
• 场景:低优先级任务(L)占用资源 → 中优先级任务(M)抢占执行 → 高优先级任务(H)因资源被L占用而阻塞。 • 结果:H被迫等待L和M执行完成,违背实时性要求。
3.2 优先级继承机制
• 流程:
- 当H请求被L占用的资源时,L的优先级临时提升至与H相同。
- L释放资源后,优先级恢复原状,H立即执行。 • 意义:将H的阻塞时间从“L+M执行时间”缩短为“L执行时间”,大幅降低优先级翻转的影响。
4. API函数详解
4.1 创建互斥信号量
c
SemaphoreHandle_t xSemaphoreCreateMutex(void);• 要求:将宏configUSE_MUTEXES置1。 • 返回值:成功返回句柄,失败返回NULL(内存不足)。 • 初始化状态:默认有效(计数值1),无需手动释放。
4.2 获取信号量
c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);• 阻塞时间:若资源被占用,任务进入阻塞态,最长等待xBlockTime(portMAX_DELAY表示无限等待)。 • 返回值:pdTRUE(成功获取)或pdFALSE(超时)。
4.3 释放信号量
c
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);• 限制:必须由获取信号量的任务释放,否则导致未定义行为。
4.4 删除信号量
c
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);• 适用场景:动态创建的信号量在不再需要时需手动删除,释放内存。
5. 典型应用场景
5.1 硬件资源保护
• 案例:多个任务共享串口发送数据。
c
void UART_SendTask(void *pvParameters) {
while (1) {
if (xSemaphoreTake(uartMutex, portMAX_DELAY) == pdTRUE) {
UART_SendData(buffer); // 发送数据
xSemaphoreGive(uartMutex);
}
vTaskDelay(100);
}
}5.2 共享数据结构访问
• 案例:多任务操作全局链表或计数器,防止数据竞争。
5.3 文件系统操作
• 案例:嵌入式文件系统中,确保同一时刻仅一个任务写入Flash。
小实验12
使用互斥量缓解优先级翻转现象。
十一、队列集
1. 队列集介绍
1.1 核心功能
队列集(Queue Set)是FreeRTOS中用于统一管理多个队列或信号量的机制,允许任务同时监听多个事件源(如队列数据到达、信号量触发等),任一事件触发即可唤醒任务。其核心作用包括:
• 减少任务轮询开销:任务无需逐个检查队列/信号量,通过事件驱动机制提升效率。 • 简化多事件处理逻辑:适用于需要响应多种异步事件的场景(如多设备输入、混合数据源同步)。 • 支持动态管理:可在运行时动态添加或移除队列/信号量。
1.2 与传统轮询的对比
| 对比维度 | 传统轮询方式 | 队列集方式 |
|---|---|---|
| CPU资源消耗 | 高(需循环检查每个队列) | 低(仅在事件触发时唤醒任务) |
| 实时性 | 延迟高(需遍历所有队列) | 延迟低(事件即时触发) |
| 代码复杂度 | 高(需手动管理多个队列的监听逻辑) | 低(统一监听接口) |
1.3 适用场景
• 多设备输入整合:如同时监听触摸屏、按键、传感器数据。 • 混合事件处理:需同时处理异步消息(网络数据)和同步信号(中断触发)。 • 复杂同步需求:多个任务需协作时,通过队列集实现高效同步。
2. 队列集使用流程
2.1 启用队列集功能
在FreeRTOSConfig.h中启用队列集:
c
#define configUSE_QUEUE_SETS 1 // 必须设置为12.2 创建队列集
c
QueueSetHandle_t xQueueSet = xQueueCreateSet(uxTotalLength);• 参数规则:uxTotalLength = 所有被监听队列/信号量的最大容量之和。 • 示例:若监听2个队列(长度分别为3、2)和1个信号量(长度1),则uxTotalLength = 3+2+1=6。 • 错误后果:长度不足会导致事件丢失或任务无法唤醒。
2.3 添加队列/信号量到队列集
c
xQueueAddToSet(xQueueHandle, xQueueSet); // 添加队列
xQueueAddToSet(xSemaphoreHandle, xQueueSet); // 添加信号量• 关键限制:
- 队列必须为空:若队列已有数据,添加会失败(返回
pdFAIL)。 - 仅属于一个队列集:同一队列/信号量不可同时加入多个队列集。
2.4 任务监听与处理
c
QueueSetMemberHandle_t xActivated = xQueueSelectFromSet(xQueueSet, xBlockTime);• 阻塞机制:若队列集无事件,任务进入阻塞状态,最长等待xBlockTime(单位:Tick)。 • 返回值处理: • 返回NULL:超时或无事件。 • 返回有效句柄:需根据句柄类型(队列或信号量)执行对应操作。
3. 队列集API函数详解
3.1 创建队列集
函数原型:
c
QueueSetHandle_t xQueueCreateSet(UBaseType_t uxEventQueueLength);功能: 创建一个新的队列集,用于统一监听多个队列或信号量的事件。
参数: • uxEventQueueLength:队列集的总容量,必须等于所有被监听队列/信号量的最大长度之和。 示例:若监听2个队列(长度分别为3和5)和1个信号量(长度1),则 uxEventQueueLength = 3+5+1=9。
返回值: • 成功:返回队列集句柄(QueueSetHandle_t)。 • 失败:返回 NULL(通常因内存不足或配置未启用队列集功能)。
注意事项:
- 严格计算容量:容量不足会导致事件丢失,任务无法及时唤醒。
- 必须启用配置:在
FreeRTOSConfig.h中设置#define configUSE_QUEUE_SETS 1。 - 内存分配:队列集占用内存由FreeRTOS自动分配,需确保系统有足够堆空间。
3.2 添加队列/信号量到队列集
函数原型:
c
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);功能: 将队列或信号量添加到队列集中,使其成为被监听的对象。
参数: • xQueueOrSemaphore:待添加的队列或信号量句柄。 • xQueueSet:目标队列集的句柄。
返回值: • pdPASS:添加成功。 • pdFAIL:添加失败(队列非空、已被其他队列集占用或句柄无效)。
注意事项:
- 队列必须为空:若队列已有数据,需先调用
xQueueReceive()清空才能添加。 - 独占性:同一队列/信号量不可同时加入多个队列集。
- 动态添加:可在运行时动态添加,但需注意实时性影响。
3.3 从队列集移除成员
函数原型:
c
BaseType_t xQueueRemoveFromSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);功能: 将队列或信号量从队列集中移除,停止对其监听。
参数: • xQueueOrSemaphore:待移除的队列或信号量句柄。 • xQueueSet:队列集句柄。
返回值: • pdPASS:移除成功。 • pdFAIL:移除失败(队列仍有未处理数据或句柄无效)。
注意事项:
- 清空数据:移除前需确保队列/信号量无未处理事件。
- 中断安全:禁止在中断服务例程(ISR)中直接调用此函数。
3.4 监听队列集事件
函数原型:
c
QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, TickType_t xTicksToWait);功能: 阻塞任务,直到队列集中任一成员触发事件(如队列收到数据或信号量被释放)。
参数: • xQueueSet:队列集句柄。 • xTicksToWait:最大阻塞时间(单位:Tick),设为 portMAX_DELAY 表示永久阻塞。
返回值: • 有效句柄:触发事件的队列或信号量句柄。 • NULL:超时或无事件发生。
注意事项:
- 事件处理:返回句柄后需立即读取队列数据或获取信号量,否则可能被其他任务抢占。
- 超时处理:需检查返回值是否为
NULL,避免空指针操作。 - 任务优先级:高优先级任务可能抢占事件处理,需合理设计任务优先级。
示例:
c
QueueSetMemberHandle_t xActivated = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if (xActivated == xQueueA) {
int data;
xQueueReceive(xQueueA, &data, 0); // 立即读取数据
} else if (xActivated == xSemaphore) {
xSemaphoreTake(xSemaphore, 0); // 立即获取信号量
}3.5 中断安全版监听函数
函数原型:
c
QueueSetMemberHandle_t xQueueSelectFromSetFromISR(QueueSetHandle_t xQueueSet);功能: 在中断服务例程(ISR)中检查队列集是否有事件触发。
参数: • xQueueSet:队列集句柄。
返回值: • 有效句柄:触发事件的队列或信号量句柄。 • NULL:无事件发生。
注意事项:
- 不可阻塞:ISR中禁止使用阻塞操作,此函数仅检查当前状态。
- 任务切换:若需要唤醒任务处理事件,需手动调用
portYIELD_FROM_ISR()。 - 数据读取:ISR中无法直接处理队列数据,通常仅标记事件并唤醒任务处理。
小实验13
- 创建一个队列集queueset,一个队列queue,一个二值信号量semphr,并且将queue和semphr添加到queueset里;
- 动态创建两个任务:task1、task2,两个任务作用如下:
- task1:检测按键,当按键1按下时,往queue写入数据;当按键2按下时,释放semphr。
- task2:监听queueset。
十二、事件标志组
1. 事件标志组介绍
1.1 核心概念
事件标志组(Event Group)是FreeRTOS中用于多任务同步与通信的机制,通过位掩码(Bitmask)管理多个独立事件的状态: • 事件位(Event Bit):每个位(bit)表示一个事件是否发生,例如:
• BIT0:按键K1按下
• BIT1:传感器数据就绪
• BIT2:网络连接成功
• 事件组(Event Group):由多个事件位组成的整数变量,FreeRTOS中默认支持24个事件位(低24位有效,高8位保留用于控制)。
1.2 核心作用
- 多事件同步:支持“与”(所有指定事件位触发)或“或”(任一事件位触发)逻辑。
- 高效资源利用:相比信号量或队列,事件标志组仅需少量内存(一个
EventBits_t变量)。 - 中断与任务协作:可在中断中设置事件位,任务异步响应。
1.3 与信号量的对比
| 特性 | 事件标志组 | 信号量 |
|---|---|---|
| 同步逻辑 | 支持多事件位逻辑组合 | 单一计数器增减 |
| 资源占用 | 低(仅需1个变量) | 较高(需独立数据结构) |
| 适用场景 | 多事件协同、复杂条件触发 | 单一资源管理或简单同步 |
适用场景举例: • 多传感器数据就绪检测:需同时等待温度、湿度传感器数据。
• 任务链同步:任务A完成后触发任务B,任务B完成后触发任务C。
2. 事件标志组常用API详解
2.1 创建事件标志组
函数原型:
c
EventGroupHandle_t xEventGroupCreate(void);功能:动态创建事件标志组,返回其句柄。 返回值: • 成功:事件标志组句柄(EventGroupHandle_t)。
• 失败:NULL(内存不足或未启用事件组功能)。
注意事项: • 需在FreeRTOSConfig.h中启用configUSE_EVENT_GROUPS = 1。
2.2 设置事件位
函数原型:
c
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet
);功能:在任务中设置指定事件位(置1)。 参数: • xEventGroup:事件标志组句柄。
• uxBitsToSet:需设置的事件位掩码(如BIT0 | BIT2)。
返回值:设置后的事件组值(可用于调试)。 注意事项: • 若需在中断中设置事件位,需使用xEventGroupSetBitsFromISR()。
2.3 等待事件位
函数原型:
c
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait
);功能:阻塞任务,直到指定事件位触发或超时。 参数: • uxBitsToWaitFor:等待的事件位掩码。
• xClearOnExit:pdTRUE表示触发后自动清零事件位。
• xWaitForAllBits:pdTRUE表示需所有位触发(“与”逻辑),pdFALSE表示任一触发(“或”逻辑)。
返回值:触发的事件位掩码(若超时返回当前事件组值)。 示例:
c
// 等待BIT0和BIT1均触发(“与”逻辑)
EventBits_t bits = xEventGroupWaitBits(group, BIT0 | BIT1, pdTRUE, pdTRUE, portMAX_DELAY);2.4 清除事件位
函数原型:
c
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear
);功能:在任务中清除指定事件位(置0)。 参数: • uxBitsToClear:需清除的事件位掩码。
返回值:清除前的事件组值。 注意事项: • 中断中需使用xEventGroupClearBitsFromISR()。
2.5 中断安全API
2.5.1 设置事件位:xEventGroupSetBitsFromISR() 函数原型:
c
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken
);功能: 在中断中设置指定事件位为1,通过延迟处理机制将操作转发给RTOS守护任务(Daemon Task),避免在ISR中直接操作事件组。
参数: • xEventGroup:事件组句柄。
• uxBitsToSet:待设置的事件位掩码(如0x03表示设置bit0和bit1)。
• pxHigherPriorityTaskWoken:记录是否需要触发任务切换(若守护任务优先级高于当前任务,则设为pdTRUE)。
返回值: • pdPASS:命令成功发送至守护任务队列。
• pdFAIL:队列已满,操作失败。
注意事项:
- 非阻塞操作:不直接在ISR中修改事件组,而是通过定时器命令队列异步处理。
- 上下文切换:若
pxHigherPriorityTaskWoken返回pdTRUE,需在中断退出前调用portYIELD_FROM_ISR()触发任务切换。 - 队列容量:确保定时器命令队列有足够空间,否则可能丢失事件。
2.5.2 清除事件位:xEventGroupClearBitsFromISR() 函数原型:
c
BaseType_t xEventGroupClearBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear
);功能: 在中断中清除指定事件位(置0),操作同样通过守护任务异步处理。
参数: • xEventGroup:事件组句柄。
• uxBitsToClear:待清除的事件位掩码。
返回值: • pdPASS:命令成功发送至队列。
• pdFAIL:队列已满或参数无效。
注意事项:
- 内存安全:与
xEventGroupSetBitsFromISR类似,需依赖守护任务处理实际清除操作。 - 避免竞态条件:清除操作可能被其他任务或中断打断,需通过返回值判断操作结果。
2.5.3 获取事件位:xEventGroupGetBitsFromISR() 函数原型:
c
EventBits_t xEventGroupGetBitsFromISR(
EventGroupHandle_t xEventGroup
);功能: 在中断中直接读取当前事件组的值(无需通过守护任务)。
参数: • xEventGroup:事件组句柄。
返回值: 当前事件组的位掩码值(仅低24位有效)。
注意事项:
- 无阻塞风险:此函数仅读取当前状态,不修改事件组,可直接在ISR中使用。
- 实时性:返回值为调用时刻的瞬时值,可能因并发操作而后续变化。
小实验14
- 创建一个事件标志组eventgroup;
- 动态创建两个任务:task1、task2,两个任务作用如下:
- task1:检测按键,当按键1按下时,将事件标志组的bit0位置1;当按键2按下时,将事件标志组的bit1位置1。
- task2:监听事件标志组,当bit0和bit1同时为1时,打印log。
十三、任务通知
1. 任务通知简介
1.1 核心概念
任务通知是FreeRTOS中一种轻量级任务间通信机制,允许一个任务或中断直接向另一个任务发送事件或数据,无需创建队列、信号量等中间对象。每个任务内部自带一个32位的“通知值”(ulNotifiedValue)和一个“通知状态”(ucNotifyState),通过操作这两个变量实现快速同步。

通俗比喻: 任务通知就像直接给同事发微信消息,而传统队列/信号量则像通过公共邮箱传递信件。前者直接、快速,后者需要中间环节。

1.2 优势与劣势
| 优势 | 劣势 |
|---|---|
| 速度快:比队列快45%,比信号量快15倍。 | 单接收方:只能一对一通信,无法广播给多个任务。 |
| 省内存:每个任务仅需8字节额外内存。 | 无数据缓冲:只能保存一个32位值,新数据可能覆盖旧值。 |
| 多功能:可模拟信号量、事件组、队列。 | 中断限制:任务无法向中断发送通知。 |
| 直接操作:无需创建中间对象,代码更简洁。 | 发送方无法阻塞:若接收方未处理通知,发送方只能立即返回失败。 |
适用场景举例: • 中断响应:ISR快速通知任务处理数据。
• 轻量同步:替代二值信号量(如按键检测)。
• 单次数据传递:传递32位整数或指针(如传感器读数)。
2. 任务通知值与通知状态
2.1 通知值(Notification Value)
• 定义:每个任务内部的32位无符号整数,用于存储通知内容。
• 操作方式:
• 按位操作:类似事件组(如设置bit0表示按键按下)。
• 数值操作:覆盖、递增或条件更新(如模拟计数器)。
• 示例:
c
#define DATA_READY_BIT (1 << 0)
xTaskNotify(xTaskHandle, DATA_READY_BIT, eSetBits); // 设置bit02.2 通知状态(Notification State)
• 三种状态:
- 未等待通知(
taskNOT_WAITING_NOTIFICATION):初始状态,任务未等待通知。 - 等待通知(
taskWAITING_NOTIFICATION):任务调用等待函数(如xTaskNotifyWait)进入阻塞。 - 已接收通知(
taskNOTIFICATION_RECEIVED):通知到达但未被处理(类似“待处理邮件”)。
• 状态转换:
• 任务调用xTaskNotifyWait() → 进入“等待通知”状态。
• 其他任务发送通知 → 状态变为“已接收通知”,任务解除阻塞。
3. 任务通知常用API函数
3.1 发送通知函数
(1) 基础发送:xTaskNotify()
函数原型:
c
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify, // 目标任务句柄
uint32_t ulValue, // 通知值
eNotifyAction eAction // 更新方式
);参数详解: • xTaskToNotify:目标任务的句柄,可通过xTaskCreate()或xTaskGetHandle()获取。
• ulValue:根据eAction决定用途的32位值:
• 当eAction = eSetBits时,按位设置(如0x03表示设置bit0和bit1)。
• 当eAction = eSetValueWithOverwrite时,直接覆盖通知值为ulValue。
• 其他模式下可能被忽略。
• eAction:枚举类型,定义通知值的更新逻辑:
• eNoAction:仅触发通知,不修改通知值。
• eSetBits:按位或操作(类似事件组)。
• eIncrement:通知值自增1(模拟计数信号量)。
• eSetValueWithOverwrite:强制覆盖通知值。
• eSetValueWithoutOverwrite:仅在通知未被处理时覆盖。
返回值: • pdPASS:通知成功发送(默认返回值)。
• pdFAIL:仅当eAction = eSetValueWithoutOverwrite且目标任务未处理上一次通知时返回。
(2) 简化发送:xTaskNotifyGive()
函数原型:
c
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);参数: • xTaskToNotify:目标任务的句柄。
返回值: • pdPASS:始终返回成功(本质是xTaskNotify(..., eIncrement)的简化版)。
说明:专用于模拟计数信号量,通知值自增1,无数据传递需求时更高效。
(3) 中断安全版本:
xTaskNotifyFromISR() 函数原型:
c
BaseType_t xTaskNotifyFromISR(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken
);新增参数: • pxHigherPriorityTaskWoken:
• 输入前需初始化为pdFALSE。
• 若发送通知导致目标任务解除阻塞且优先级更高,则设为pdTRUE,需在中断退出前调用portYIELD_FROM_ISR()触发上下文切换。
返回值:与xTaskNotify()一致。
vTaskNotifyGiveFromISR() 函数原型:
c
void vTaskNotifyGiveFromISR(
TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken
);参数与返回值: • 参数与xTaskNotifyFromISR()一致,但无返回值(专用于中断中递增通知值)。
3.2 接收通知函数
(1) 信号量模式:ulTaskNotifyTake()
函数原型:
c
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, // 退出时清零或减1
TickType_t xTicksToWait // 阻塞超时时间
);参数详解: • xClearCountOnExit:
• pdTRUE:清零通知值(模拟二值信号量)。
• pdFALSE:通知值减1(模拟计数信号量)。
• xTicksToWait:最大阻塞时间,portMAX_DELAY表示无限等待。
返回值: • 非零值:接收成功,返回操作前的通知值。
• 0:超时或通知未到达。
(2) 通用模式:xTaskNotifyWait()
函数原型:
c
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 等待前清除的位
uint32_t ulBitsToClearOnExit, // 退出前清除的位
uint32_t *pulNotificationValue, // 接收到的通知值
TickType_t xTicksToWait
);参数详解: • ulBitsToClearOnEntry:进入等待前对通知值按位清零(如0xFF清空低8位)。
• ulBitsToClearOnExit:退出前对通知值按位清零(如0x01清空bit0)。
• pulNotificationValue:存储接收到的原始通知值(可传NULL忽略)。
返回值: • pdTRUE:成功接收到通知。
• pdFALSE:超时或等待失败。
小实验15
使用任务通知模拟信号量
小实验16
使用任务通知模拟邮箱
小实验17
使用任务通知模拟事件标志组
十四、定时器
从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可自定义定时器的周期。

1. 软件定时器介绍
1.1 硬件定时器与软件定时器的核心差异
| 对比维度 | 硬件定时器 | 软件定时器 |
|---|---|---|
| 实现原理 | 依赖芯片硬件模块(如STM32的TIMx) | 基于系统滴答中断(SysTick)和任务调度机制 |
| 精度 | 高(可达纳秒级) | 较低(依赖系统节拍,通常毫秒级) |
| 资源占用 | 占用硬件外设,数量固定 | 仅占用内存,可动态创建多个 |
| 触发方式 | 中断触发,实时性强 | 任务上下文触发(回调函数在守护任务中执行) |
| 适用场景 | 高精度控制(PWM、输入捕获) | 周期性任务、超时检测、事件通知等非实时场景 |
通俗理解:
硬件定时器类似机械闹钟,精准但数量有限;软件定时器像手机里的虚拟闹钟,灵活但精度稍低。
1.2 软件定时器的优缺点
优点:
• 省硬件资源:不占用硬件外设,仅需内存即可创建;
• 灵活扩展:数量仅受内存限制,可动态管理;
• 多功能性:支持单次、周期模式等。
缺点:
• 精度受限:依赖系统节拍(Tick),可能被高优先级任务打断;
• 回调限制:回调函数中不可调用阻塞API(如vTaskDelay());
• 无硬件中断:无法处理高实时性任务。
1.3 FreeRTOS软件定时器的核心特点

守护任务(Daemon Task):
系统自动创建的管理任务,优先级由configTIMER_TASK_PRIORITY配置,负责处理定时器命令队列和执行回调函数。守护任务类似“闹钟管理员”,统一管理所有定时器的触发逻辑。
• 守护任务优先级:需设为系统最高优先级之一(如configMAX_PRIORITIES-1),否则可能因任务调度延迟导致定时器超时回调延迟。
• 堆栈深度:通过configTIMER_TASK_STACK_DEPTH配置,需足够大以处理回调函数逻辑。
命令队列:
所有API操作(如启动、停止)通过队列发送命令,由守护任务统一处理,确保线程安全。
• 队列长度:由configTIMER_QUEUE_LENGTH定义,若队列满则新命令可能被阻塞或丢弃。
轻量化设计:
每个定时器仅需约40字节内存,适合资源受限的嵌入式场景。
1.4 软件定时器的配置流程与注意事项
配置步骤:
- 启用定时器功能:在
FreeRTOSConfig.h中设置:c#define configUSE_TIMERS 1 // 启用软件定时器 #define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) // 守护任务优先级 #define configTIMER_QUEUE_LENGTH 10 // 命令队列长度 #define configTIMER_TASK_STACK_DEPTH 1024 // 守护任务堆栈大小(单位:字) - 创建定时器:通过
xTimerCreate()指定周期、模式、回调函数等参数。
注意事项:
• 回调函数:必须短小精悍,避免阻塞操作;若需复杂操作,应通过任务通知或队列委托给其他任务。
• 中断安全:中断中操作定时器需使用FromISR版本API,并处理pxHigherPriorityTaskWoken标志触发任务切换。
1.5 软件定时器的状态与转换
休眠态(Dormant):
• 定时器创建后默认处于休眠态,需调用xTimerStart()激活。
• 单次定时器超时后自动进入休眠态,需手动重启。
运行态(Active):
• 定时器启动后开始计时,超时后根据模式决定是否自动重启。
• 状态转换示例:
◦ 单次定时器:`休眠态 → 启动 → 运行态 → 超时回调 → 休眠态`
◦ 周期定时器:`休眠态 → 启动 → 运行态 → 超时回调 → 自动重启运行态`。

1.6 单次定时器与周期定时器的对比
| 类型 | 触发逻辑 | 典型场景 |
|---|---|---|
| 单次定时器 | 触发一次后停止,需手动重启 | 设备超时检测(如按键无操作自动休眠) |
| 周期定时器 | 自动重启计时,周期性触发回调函数 | 心跳检测、LED闪烁、数据采集 |
示例:
• 单次定时器:用户设置10秒无操作进入休眠,超时后触发一次回调;
• 周期定时器:每1秒采集一次传感器数据并发送到云端。
2. 软件定时器常用API函数
2.1 创建定时器
核心作用:
动态分配内存并初始化定时器。
函数原型:
c
TimerHandle_t xTimerCreate(
const char *pcTimerName, // 定时器名称(调试用)
TickType_t xTimerPeriod, // 周期(单位:Tick)
UBaseType_t uxAutoReload, // 模式:pdTRUE(周期)/pdFALSE(单次)
void *pvTimerID, // 用户自定义ID(区分多个定时器)
TimerCallbackFunction_t pxCallbackFunction // 回调函数
);参数详解:
• pcTimerName:名称仅用于调试,不影响功能。
• xTimerPeriod:可用pdMS_TO_TICKS(毫秒值)转换时间(如pdMS_TO_TICKS(1000)表示1秒)。
• pvTimerID:用于多定时器共用回调函数时区分来源(如设置ID为1表示温度传感器,ID为2表示湿度传感器)。
返回值:
成功返回句柄,失败返回NULL(内存不足时可能发生)。
示例:
c
TimerHandle_t xTempTimer = xTimerCreate(
"TempSensor",
pdMS_TO_TICKS(5000),
pdTRUE,
(void*)1,
vSensorCallback
);2.2 启动定时器
核心作用:
启动或重启定时器。
函数原型:
c
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);参数说明:
• xTicksToWait:命令队列满时的最大阻塞时间(通常设为0,非阻塞模式)。
• pxHigherPriorityTaskWoken:中断版本需处理任务切换标志,若为pdTRUE需调用portYIELD_FROM_ISR()触发任务切换。
返回值:
pdPASS(成功)或pdFAIL(失败,如队列满且超时未发送)。
注意事项:
• 若定时器已在运行,xTimerStart()等效于xTimerReset(),即重新开始计时。
• 在启动调度器前调用xTimerStart(),定时器会在调度器启动后立即开始计时。
2.3 停止定时器
核心作用:
停止定时器运行。
函数原型:
c
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);适用场景:
手动终止定时任务(如设备进入休眠模式)。
示例:
c
// 停止温度传感器定时器
if (xTimerStop(xTempTimer, 0) == pdPASS) {
printf("定时器已停止\n");
}2.4 修改定时周期
核心作用:
动态调整定时器周期。
函数原型:
c
BaseType_t xTimerChangePeriod(
TimerHandle_t xTimer, // 定时器句柄
TickType_t xNewPeriod, // 新周期(Tick)
TickType_t xTicksToWait // 阻塞时间
);注意事项:
• 修改后立即生效,下一次计时从当前时刻开始。
• 若定时器正在运行,修改周期会重置当前计时。
示例:
将LED闪烁周期从1秒改为500ms:
c
xTimerChangePeriod(xLedTimer, pdMS_TO_TICKS(500), 0);2.5 复位定时器
核心作用:
重新开始计时。
函数原型:
c
BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);与xTimerStart()的区别: • xTimerReset()强制从调用时刻开始重新计时,而xTimerStart()若定时器未运行则启动,若已运行则等效于复位。
适用场景:
按键操作后重置设备超时检测。
2.6 删除定时器
核心作用:
释放定时器内存。
函数原型:
c
void xTimerDelete(TimerHandle_t xTimer, TickType_t xTicksToWait);注意事项:
删除后句柄失效,不可再操作。
2.7 查询定时器状态
核心作用:
查询软件定时器是否处于活动或休眠状态。
函数原型:
c
BaseType_t xTimerIsTimerActive(TimerHandle_t xTimer);参数:
xTimer
被查询的定时器。
返回值:
- 如果定时器处于休眠状态,将返回 pdFALSE。
- 如果定时器处于活动状态,将返回 pdFALSE 以外的值。
3. 实战技巧与调试建议
3.1 回调函数设计原则
• 禁止阻塞:不可调用vTaskDelay()或访问带阻塞的队列/信号量。
• 快速执行:若需复杂操作(如发送大量数据),应通过任务通知或队列委托给其他任务。
错误示例:
c
void vBadCallback(TimerHandle_t xTimer) {
vTaskDelay(100); // 错误!阻塞操作会导致守护任务卡死
}3.2 中断中操作定时器
示例:在按键中断中复位定时器:
c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (xTimerResetFromISR(xLedTimer, &xHigherPriorityTaskWoken) == pdPASS) {
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 触发任务切换
}
}3.3 多定时器共享回调函数

c
// 创建两个定时器,共用回调函数
TimerHandle_t xTimer1 = xTimerCreate("Timer1", ..., (void*)1, vSharedCallback);
TimerHandle_t xTimer2 = xTimerCreate("Timer2", ..., (void*)2, vSharedCallback);
// 回调函数内区分ID
void vSharedCallback(TimerHandle_t xTimer) {
uint32_t id = (uint32_t)pvTimerGetTimerID(xTimer);
if (id == 1) {
// 处理Timer1逻辑
} else if (id == 2) {
// 处理Timer2逻辑
}
}小实验18
- 创建两个定时器:timer1 -> 单次定时器;timer2 -> 周期定时器
- 创建一个任务task1,用于检测按键:
- 按键1按下:如果timer1正在运行则停止timer1;若timer1停止则启动timer1;
- 按键2按下:如果timer2正在运行则停止timer2;若timer2停止则启动timer2;
十五、低功耗
1. 低功耗模式简介
1.1 应用场景与硬件基础
低功耗设计是嵌入式系统的核心能力,直接影响设备续航与部署灵活性。以下场景需重点关注:
• 移动医疗设备(如血糖仪、心电图仪):需在待机时维持μA级电流,突发任务时快速响应。
• 工业无线传感器(如振动监测节点):部署在高温、密闭环境中,电池更换困难,需5年以上续航。
• 智能农业设备(如土壤墒情监测器):依赖太阳能+电池混合供电,需动态调整功耗策略。
1.2 STM32低功耗模式对比(以F4系列为例)
| 模式 | 功耗范围 | 唤醒时间 | 数据保持 | 适用场景 |
|---|---|---|---|---|
| Sleep Mode | 1-3 mA | <10 μs | 完整 | 实时控制间隙(如PID调节) |
| Stop Mode | 10-50 μA | 200 μs | SRAM保持 | 周期性数据采集(如每5分钟) |
| Standby Mode | 0.5-2 μA | 5 ms | 仅备份域 | 紧急事件监测(如烟雾报警) |
硬件协同设计要点:
• 时钟树优化:进入低功耗前切换至HSI(内部RC时钟),关闭PLL可降低30%动态功耗
• 外设电源管理:通过RCC_AHB1PeriphClockCmd关闭未用外设时钟(如ADC/DMA)
• 引脚状态冻结:配置GPIO为模拟输入模式避免漏电流,STM32F4每个浮空引脚可能产生0.1μA漏电流
1.3 FreeRTOS适配策略
• 动态电压调节:配合STM32动态电压缩放(DVS)技术,在空闲时降低核心电压至1.2V
• 混合模式管理:将Sleep Mode与Tickless结合,实现多级功耗阶梯(如:短时空闲用Sleep,长时用Stop)
2. Tickless模式介绍
2.1 运行机制全解析
Tickless低功耗模式的本质是通过调用指令 WFI 实现睡眠模式!

任务运行时间统计实验中,可以看出,在整个系统的运行过程中,其实大部分时间是在执行空闲任务的。
空闲任务:是在系统中的所有其它任务都阻塞或被挂起时才运行的。
为了可以降低功耗,又不影响系统运行,该如何做?
可以在本该空闲任务执行的期间,让MCU 进入相应的低功耗模式;当其他任务准备运行的时候,唤醒MCU退出低功耗模式
空闲窗口预测: • 通过
prvGetExpectedIdleTime()计算下一个任务唤醒时间xExpectedIdleTime• 算法核心:遍历所有阻塞任务的
xTaskDelayUntil参数,找出最小剩余时间SysTick动态调整:
c/* 计算重装载值 */ ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) ); portNVIC_SYSTICK_LOAD_REG = ulReloadValue;• 启用LPTIM(低功耗定时器)突破SysTick 24位限制,支持长达36小时的休眠
中断管理策略: • 唤醒源分级:将RTC/Wakeup-Pin设为最高优先级(如抢占优先级0)
• 虚假唤醒处理:通过
eTaskConfirmSleepModeStatus()检测任务队列变化
2.2 时钟补偿黑科技
• 动态偏差修正:
c
if( ulCompleteTickPeriods > 0 ) {
vTaskStepTick( ulCompleteTickPeriods );
}• 采用RTC校准补偿晶振温漂(误差<±500ppm)
• 当实际休眠时间偏离预期10%时,触发自适应算法调整预测模型
2.3 性能实测数据
| 场景 | 常规模式电流 | Tickless电流 | 降幅 |
|---|---|---|---|
| 1秒周期任务 | 15 mA | 3.2 mA | 78.6% |
| 10分钟数据上报 | 8.7 mA | 42 μA | 99.5% |
| (数据来源:STM32F429+FreeRTOS实测) |
三、Tickless配置实战手册
3.1 FreeRTOSConfig.h关键配置
c
#define configUSE_TICKLESS_IDLE 1 // 启用低功耗处理
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2 // 最小休眠2个tick3.2 预处理/后处理函数范例
c
#define configPRE_SLEEP_PROCESSING( x ) PreSleepProcessing()
#define configPOST_SLEEP_PROCESSING( x ) PostSleepProcessing()
// 进入低功耗前操作
void PreSleepProcessing(void) {
HAL_RTCEx_DeactivateWakeUpTimer(&hrtc); // 关闭原有RTC唤醒
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, ulExpectedIdleTime, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
__HAL_RCC_GPIOA_CLK_DISABLE(); // 关闭GPIOA时钟
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI); // 进入Stop2模式
}
// 退出低功耗后恢复
void PostSleepProcessing(void) {
SystemClock_Config(); // 重新配置主时钟
HAL_RTCEx_ActivateWakeUpTimer(&hrtc);
__HAL_RCC_GPIOA_CLK_ENABLE();
}小实验19
低功耗实验
十六、内存管理
1. FreeRTOS内存管理简介
1.1 核心功能与设计目标
FreeRTOS的内存管理模块是操作系统的核心组件,专为嵌入式实时系统优化,提供以下核心能力:
• 动态内存分配:允许任务在运行时按需申请内存(如创建任务、队列、信号量等)
• 静态内存分配:编译时预分配固定内存区域,避免运行时开销(适用于资源受限设备)
• 碎片控制:通过合并相邻空闲内存块降低碎片风险
1.2 为何不使用C库的malloc/free?
尽管C库函数通用,但在嵌入式场景中存在致命缺陷:
- 线程不安全:标准库未考虑多任务竞争,可能引发数据错乱(如两个任务同时申请内存)
- 确定性差:执行时间不可预测,影响实时性(例如某些实现可能因内存不足触发磁盘交换)
- 内存碎片:频繁分配释放导致内存被分割成小块,最终无法分配大块内存(例如连续申请不同大小的任务栈)
- 代码臃肿:标准库实现复杂,占用过多Flash空间(例如某些C库malloc代码量超过10KB)
- 硬件适配差:无法针对特定硬件优化(如STM32的CCM高速内存区)
2. FreeRTOS内存管理算法
2.1 五大算法对比
| 算法 | 优点 | 缺点 |
|---|---|---|
| heap_1 | 分配简单,时间确定 | 只允许申请内存,不允许释放内存 |
| heap_2 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 |
| heap_3 | 直接调用C库函数malloc()和 free() ,简单 | 速度慢、时间不定 |
| heap_4 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 |
| heap_5 | 能够管理多个非连续内存区域的 heap_4 | 时间不定 |
2.2 FreeRTOS内存管理算法深度解析
1. Heap_1:单次分配,永不回收

核心机制
Heap_1是FreeRTOS最简单的内存管理算法,通过预定义的静态数组ucHeap[configTOTAL_HEAP_SIZE]实现线性分配。所有内存请求按顺序从数组起始地址递增分配,不支持内存释放,仅适用于初始化阶段创建对象后永不删除的场景。
实现细节
• 内存池初始化:在首次调用pvPortMalloc()时,根据portBYTE_ALIGNMENT对齐数组首地址,确保分配的内存满足处理器对齐要求(如STM32需8字节对齐)。
• 分配过程:记录当前分配位置xNextFreeByte,每次分配后直接递增指针。若剩余空间不足,返回NULL并触发vApplicationMallocFailedHook()钩子函数。
适用场景
• 工业控制器等静态系统,所有任务、队列、信号量在启动时创建且永不删除。
• 确定性要求高的场景,分配时间固定(无内存碎片影响)。
局限性
• 内存利用率低,无法回收已分配内存,长期运行可能导致内存耗尽。
2. Heap_2:最佳匹配算法,碎片化严重

核心机制
Heap_2在Heap_1基础上增加了内存释放功能,采用最佳匹配算法(Best Fit)。空闲块通过链表管理,分配时选择最小且足够的空闲块,但不合并相邻空闲块,导致外部碎片积累。
实现细节
• 空闲块链表:使用BlockLink_t结构维护空闲块,包含指向下一块的指针和块大小。
• 分配策略:遍历链表找到最接近请求大小的块,分割剩余空间为新空闲块(若剩余空间足够)。
• 释放策略:仅将释放块标记为空闲,不进行合并。
适用场景
• 固定大小内存请求(如频繁创建/删除相同栈大小的任务)。
• 短期运行的嵌入式系统,对碎片容忍度较高。
局限性
• 长期运行后碎片化严重,无法分配连续大内存。
3. Heap_3:封装C库,兼容但低效
核心机制
Heap_3直接调用标准C库的malloc()和free(),通过挂起调度器实现线程安全。内存堆大小由链接器配置(如STM32的启动文件定义),与configTOTAL_HEAP_SIZE无关。
实现细节
• 线程安全:在pvPortMalloc()和vPortFree()中调用vTaskSuspendAll()暂停任务调度。
• 内存来源:依赖编译器的堆空间,可能分散在多个内存区域(如内部SRAM和外部SDRAM)。
适用场景
• 需要快速移植现有代码到FreeRTOS的复杂系统。
• 与标准库兼容性要求高的场景(如混合Linux和RTOS环境)。
局限性
• 非确定性,分配/释放时间不可预测。
• 标准库的碎片问题未解决,且代码体积大(如某些C库实现占用超10KB Flash)。
4. Heap_4:首次适应算法,碎片优化

核心机制
Heap_4采用首次适应算法(First Fit)和空闲块合并机制。分配时从链表头部查找第一个足够大的块,释放时检查相邻块是否空闲并合并,显著减少碎片。
实现细节
• 空闲块管理:链表按地址升序排列,释放时触发prvInsertBlockIntoFreeList()合并相邻块。
• 内存标记:通过块头部的xBlockSize最高位标记是否空闲(0为空闲,1为已分配)。
• 性能优化:支持动态调整空闲链表,减少遍历时间。
适用场景
• 通用嵌入式系统(STM32默认推荐),频繁分配/释放不同大小内存。
• 需要长期稳定运行的低碎片场景(如物联网网关)。
局限性
• 内存必须连续,无法管理非连续物理内存(如多块SRAM)。
5. Heap_5:多区域管理的Heap_4增强版
核心机制
Heap_5在Heap_4基础上扩展支持多块非连续内存区域。用户需预先定义HeapRegion_t数组描述各内存块起始地址和大小,初始化时通过vPortDefineHeapRegions()将其串联为逻辑连续空间。
实现细节
• 多区域配置:
c
const HeapRegion_t xHeapRegions[] = {
{ (uint8_t*)0x20000000, 0x10000 }, // 内部RAM 64KB
{ (uint8_t*)0x60000000, 0x80000 }, // 外部SDRAM 512KB
{ NULL, 0 } // 结束标记
};
vPortDefineHeapRegions(xHeapRegions);• 分配策略:与Heap_4相同,但支持跨区域分配。
适用场景
• 异构存储系统(如STM32H7+外部SDRAM)。
• 需要灵活管理分散内存的复杂应用(如视频处理设备)。
局限性
• 初始化复杂,需手动定义内存区域。
• 合并算法跨区域效率较低。
总结与选型建议
• 确定性优先:选择Heap_1(医疗设备、航天控制)。
• 低碎片需求:首选Heap_4(通用IoT设备)。
• 非连续内存:必须使用Heap_5(多核/大内存系统)。
• 兼容性要求:考虑Heap_3(混合开发环境)。
3. FreeRTOS内存管理API函数
3.1 内存申请
c
void *pvPortMalloc(size_t xWantedSize);• 参数:xWantedSize为请求字节数(自动对齐,如8字节对齐)
• 返回值:成功返回内存首地址,失败返回NULL(可挂钩configUSE_MALLOC_FAILED_HOOK处理)
• 示例:创建任务时自动调用此函数分配TCB和栈空间
3.2 内存释放
c
void vPortFree(void *pv);• 参数:pv为pvPortMalloc返回的地址
• 注意:未释放内存会导致泄漏,需配合xPortGetFreeHeapSize监控
3.3 堆状态查询
c
size_t xPortGetFreeHeapSize(void); // 当前空闲内存
size_t xPortGetMinimumEverFreeHeapSize(void); // 历史最小空闲值小实验20
- 动态创建两个任务:task1、task2,两个任务作用如下:
- task1:检测按键;
- task2:打印剩余空闲内存大小;
- 检测到按键1按下,申请内存;检测到按键2按下,释放内存。
十七、其它常用API
1. 任务管理相关API
1.1 uxTaskPriorityGet
**作用:**获取指定任务的当前优先级。
原型:UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask);
参数:
• xTask:任务句柄,传入NULL表示获取当前任务优先级。
**返回值:**任务优先级(0到configMAX_PRIORITIES-1)。
注意事项:
• 需在FreeRTOSConfig.h中启用INCLUDE_uxTaskPriorityGet宏。
1.2 vTaskPrioritySet
**作用:**动态修改任务的优先级。
原型:void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
参数:
• xTask:任务句柄,NULL表示修改当前任务。
• uxNewPriority:新优先级(自动限制在configMAX_PRIORITIES-1)。
注意事项:
• 若新优先级高于当前任务,会触发上下文切换。
• 需启用INCLUDE_vTaskPrioritySet宏。
1.3 xTaskGetCurrentTaskHandle
**作用:**获取当前任务的句柄。
原型:TaskHandle_t xTaskGetCurrentTaskHandle(void);
**返回值:**当前任务句柄。
注意事项:
• 需启用INCLUDE_xTaskGetCurrentTaskHandle宏。
1.4 xTaskGetHandle
**作用:**根据任务名称获取任务句柄。
原型:TaskHandle_t xTaskGetHandle(const char *pcNameToQuery);
参数:
• pcNameToQuery:任务名称字符串。
返回值:任务句柄,NULL表示未找到。
1.5 vTaskList
**作用:**以字符串形式输出所有任务状态信息(调试用)。
原型:void vTaskList(char *pcWriteBuffer);
参数:
• pcWriteBuffer:字符缓冲区(建议长度≥40×任务数)。
注意事项:
• 需启用configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS宏。
1.6 uxTaskGetSystemState
**作用:**获取系统中所有任务的详细状态信息。
原型:
c
UBaseType_t uxTaskGetSystemState(TaskStatus_t *pxTaskStatusArray,
UBaseType_t uxArraySize,
uint32_t *pulTotalRunTime);参数:
• pxTaskStatusArray:TaskStatus_t结构体数组。
• uxArraySize:数组大小(建议通过uxTaskGetNumberOfTasks()获取)。
• pulTotalRunTime:总运行时间(需启用configGENERATE_RUN_TIME_STATS)。
**返回值:**实际任务数量。
1.7 vTaskGetInfo
**作用:**获取单个任务的详细信息(栈使用、状态等)。
原型:
c
void vTaskGetInfo(TaskHandle_t xTask,
TaskStatus_t *pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState);参数:
• xTask:任务句柄。
• pxTaskStatus:存储信息的结构体。
• xGetFreeStackSpace:是否计算栈剩余空间(pdTRUE/pdFALSE)。
• eState:直接指定任务状态或eInvalid自动获取。
2. 队列管理相关API
2.1 uxQueueSpacesAvailable
**作用:**获取队列剩余可存放数据项的数量。
原型:UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);
**返回值:**剩余空间数(队列满时返回0)。
2.2 uxQueueMessagesWaiting
**作用:**获取队列中当前存储的数据项数量。
原型:UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
2.3 xQueueReset
**作用:**清空队列。
原型:BaseType_t xQueueReset(QueueHandle_t xQueue);
返回值:pdPASS表示成功。
3. 信号量相关API
xSemaphoreGetMutexHolder
**作用:**获取当前持有互斥量的任务句柄。
原型:TaskHandle_t xSemaphoreGetMutexHolder(SemaphoreHandle_t xMutex);
**注意事项:**仅适用于互斥量,需启用INCLUDE_xSemaphoreGetMutexHolder宏。
4. 定时器相关API

4.1 xTaskGetTickCount
**作用:**获取系统启动以来的时钟节拍数。
原型:TickType_t xTaskGetTickCount(void);
返回值:当前系统节拍计数值(TickType_t 类型,通常为 uint32_t)
4.2 xTimerGetExpiryTime
**作用:**获取定时器下次到期时间(tick单位)。
原型:TickType_t xTimerGetExpiryTime(TimerHandle_t xTimer);
4.3 xTimerGetPeriod
**作用:**获取定时器的周期。
原型:TickType_t xTimerGetPeriod(TimerHandle_t xTimer);
4.4 pvTimerGetTimerID
**作用:**获取定时器的用户自定义ID。
原型:void *pvTimerGetTimerID(TimerHandle_t xTimer);
5. 事件组相关API
5.1 xEventGroupGetBits
**作用:**获取当前事件组的值。
原型:EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);
5.2 xEventGroupSync
**作用:**同步多个任务到事件组(原子操作设置位并等待)。
原型:
c
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait);参数:
• uxBitsToSet:要设置的位掩码。
• uxBitsToWaitFor:等待的位掩码。
• xTicksToWait:超时时间。
xEventGroupSetBits 与 xEventGroupSync 的区别详解
1. 核心功能对比
| 特性 | xEventGroupSetBits | xEventGroupSync |
|---|---|---|
| 核心目的 | 设置事件位(单次操作),通知其他任务事件发生 | 原子化同步多任务(设置+等待),确保多任务同步完成 |
| 操作类型 | 非原子操作(分步执行) | 原子操作(设置位与等待位一步完成) |
| 适用场景 | 单向事件通知(如传感器数据就绪、按键触发) | 多任务协作同步(如任务A、B、C需同时到达同步点) |
2. 参数与行为差异 xEventGroupSetBits参数:
• xEventGroup:事件组句柄
• uxBitsToSet:要设置的位掩码(如 0x03 表示置位 bit0 和 bit1)
行为:
• 设置指定事件位,触发等待这些位的任务解除阻塞
• 无阻塞,立即返回当前事件组的最新值(可能因其他任务修改而不同)
xEventGroupSync参数:
• xEventGroup:事件组句柄
• uxBitsToSet:要设置的位掩码
• uxBitsToWaitFor:需等待的事件位掩码
• xTicksToWait:超时时间(0表示非阻塞)
行为:
• 原子操作:先设置 uxBitsToSet,然后阻塞等待 uxBitsToWaitFor 的所有位被置位
• 若条件满足(所有等待位被置位),自动清除 uxBitsToWaitFor 指定的位
• 若超时,返回超时时刻的事件组值
3. 典型应用场景xEventGroupSetBits 的适用场景 • 单向通知:任务A完成数据采集后,设置事件位通知任务B处理
c
// 任务A设置事件位
xEventGroupSetBits(xEventGroup, DATA_READY_BIT);• 多事件广播:中断服务例程(通过 xEventGroupSetBitsFromISR)设置多个位,唤醒多个等待任务
xEventGroupSync 的适用场景 • 多任务同步:任务A、B、C需同时完成初始化后才能执行下一步操作
c
// 每个任务调用xEventGroupSync
EventBits_t bits = xEventGroupSync(xEventGroup, TASK_A_BIT, ALL_TASKS_BITS, portMAX_DELAY);• 防止竞争条件:确保设置事件位与等待操作不可分割,避免其他任务中途修改事件组状态
4. 关键注意事项
原子性差异: •
xEventGroupSetBits仅设置位,需配合xEventGroupWaitBits使用,但两者非原子操作,可能导致同步失败•
xEventGroupSync保证设置与等待的原子性,避免中间状态干扰阻塞行为: •
xEventGroupSetBits无阻塞,适用于触发事件后无需等待响应的场景•
xEventGroupSync会阻塞任务,直到所有指定位被设置或超时返回值处理: •
xEventGroupSetBits返回操作后的即时事件组值(可能已被其他任务修改)•
xEventGroupSync返回条件满足时的事件组值(自动清除前)或超时值,需检查是否满足同步条件
5. 性能与资源影响 • 资源占用:
• xEventGroupSync 因涉及阻塞和原子操作,可能增加调度器负担,适用于高优先级同步场景
• xEventGroupSetBits 轻量,适合高频事件触发
• 中断安全:
• xEventGroupSetBitsFromISR 用于中断,但需通过守护任务间接操作
• xEventGroupSync 不可在中断中使用,仅限任务上下文
总结选择依据:
• 单向通知 ➔ xEventGroupSetBits
• 多任务同步 ➔ xEventGroupSync
设计建议:
• 优先使用 xEventGroupSync 处理复杂同步逻辑,避免竞态条件
• 简单事件触发场景可结合 xEventGroupSetBits 和 xEventGroupWaitBits,但需注意非原子操作的潜在风险
6. 内存管理相关API
vPortGetHeapStats
**作用:**获取堆详细统计信息(空闲块、最大块等)。
原型:
c
void vPortGetHeapStats(HeapStats_t *pxHeapStats);结构体:
c
typedef struct {
size_t xAvailableHeapSpaceInBytes; // 当前空闲内存
size_t xSizeOfLargestFreeBlockInBytes; // 最大连续空闲块
size_t xSizeOfSmallestFreeBlockInBytes; // 最小连续空闲块
size_t xNumberOfFreeBlocks; // 空闲块总数
size_t xMinimumEverFreeBytesRemaining; // 历史最小剩余堆空间
size_t xNumberOfSuccessfulAllocations; // 分配次数
size_t xNumberOfSuccessfulFrees; // 释放次数
} HeapStats_t;实战项目1:流量控制系统
原名:排队控制系统
项目需求
- 红外传感器检测有人通过并计数;
- 计数值显示在LCD1602
- 允许通过时,LED1闪烁,蜂鸣器不响,继电器不闭合;
- 不允许通过时,LED2闪烁,蜂鸣器响,继电器闭合;
- 每次允许通过5个人,之后转为不允许通过,3秒后再转为允许通过
硬件清单
- 继电器(模拟匣机)
- 蜂鸣器
- 红外避障模块
- LCD1602
- STM32开发板
- ST-Link
硬件接线
| STM32 | LCD1602 | 继电器 | 蜂鸣器 | 红外 |
|---|---|---|---|---|
| GND | GND | |||
| 5V | VDD | |||
| GND | V0 | |||
| B1 | RS | |||
| B2 | RW | |||
| B10 | E | |||
| A0 | D0 | |||
| A1 | D1 | |||
| A2 | D2 | |||
| A3 | D3 | |||
| A4 | D4 | |||
| A5 | D5 | |||
| A6 | D6 | |||
| A7 | D7 | |||
| 3.3 | BLA | VCC | VCC | VCC |
| GND | BLK | GND | GND | GND |
| B4 | OUT | |||
| B5 | I/O | |||
| B6 | IN |
项目框图






实战项目2:智能门禁
项目需求
- 矩阵键盘输入密码,正确则开锁,错误则提示,三次错误蜂鸣器响3秒;
- 按下#号确认输入,按下*号修改密码;
- 密码保存在 W25Q128 里;
- OLED 屏幕显示信息。
硬件清单
矩阵键盘
OLED 屏幕
蜂鸣器
W25Q128
继电器
杜邦线
STM32开发板
ST-Link
USB转TTL
硬件接线
| STM32 | 矩阵键盘 | OLED屏幕 | 蜂鸣器 | W25Q128 | 继电器 |
|---|---|---|---|---|---|
| PB0 | R1 | ||||
| PB1 | R2 | ||||
| PB2 | R3 | ||||
| PB10 | R4 | ||||
| PB11 | C1 | ||||
| PB12 | C2 | ||||
| PB13 | C3 | ||||
| PB8 | SCL | ||||
| PB9 | SDA | ||||
| PC13 | I/O | ||||
| PA4 | CS | ||||
| PA5 | CLK | ||||
| PA6 | DO | ||||
| PA7 | DI | ||||
| PB7 | I/O | ||||
| 5V | VCC | ||||
| 3.3V | VCC | VCC | VCC | ||
| GND | GND | GND | GND | GND |
项目框图





实战项目3:智能台灯
项目需求
- 红外传感器检测是否有人,有人的话实时检测距离,过近则报警;同时计时,超过固定时间则报警;
- 按键 1 切换工作模式:智能模式、按键模式、远程模式;
- 智能模式下,根据光照强度自动调整光照档位(低亮、中亮、高亮),没人则自动关灯;
- 按键模式下,按键 2 可以手动调整光照档位;
- 远程模式下,可以通过蓝牙控制光照档位、计时等;
- 按键 3 暂停/开始计时,按键 4 清零计时;
- OLED 显示各项数据/状态。
硬件清单
- 蓝牙模块
- 超声波传感器
- 红外传感器
- 光敏电阻传感器
- OLED
- 高功率LED灯
- 蜂鸣器
- KEY × 4
- 杜邦线
- STM32开发板
- ST-Link
- USB转TTL
硬件接线
| STM32 | OLED | LED灯 | 蜂鸣器 | 蓝牙 | 超声波 | 红外 | 光敏电阻 | KEY1 | KEY2 | KEY3 | KEY4 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| PB6 | SCL | ||||||||||
| PB7 | SDA | ||||||||||
| PB0 | I/O | ||||||||||
| PB13 | I/O | ||||||||||
| PA3 | TX | ||||||||||
| PA2 | RX | ||||||||||
| PA11 | Trig | ||||||||||
| PA12 | Echo | ||||||||||
| PB12 | I/O | ||||||||||
| PA1 | I/O | ||||||||||
| PA4 | I/O | ||||||||||
| PA5 | I/O | ||||||||||
| PA6 | I/O | ||||||||||
| PA7 | I/O | ||||||||||
| 3.3V | VCC | VCC | VCC | VCC | |||||||
| 5V | VCC | VCC | VCC | ||||||||
| GND | GND | GND | GND | GND | GND | GND | GND | GND | GND | GND | GND |
项目框图






