FreeRTOS 任务、任务列表、任务切换和任务间通信
任务
任务状态
- 运行态(Running)
- 任务当前正在 CPU 上执行
- 在单核系统中,同时只有一个任务处于运行态
- 任务只有在调度器(Scheduler)选择它执行时,才会进入运行态
- 就绪态(Ready)
- 任务已经具备执行条件,但由于 CPU 资源被其他更高优先级任务占用,它暂时无法运行
- 任务处于就绪列表(Ready List),等待 FreeRTOS 调度它运行
- 当更高优先级任务阻塞或时间片结束,调度器可能会让它进入运行态
- 阻塞态(Blocked)
- 任务正在等待某个事件(如
vTaskDelay
、信号量、消息队列、事件组等),暂时无法运行 - 任务在等待事件时会从就绪态转换到 阻塞态,避免占用 CPU 资源
- 当等待的事件发生后(如 信号量释放、消息到达、延时时间结束),任务会转换回 就绪态
- 任务正在等待某个事件(如
- 挂起态(Suspended)
- 任务被显式挂起(使用
vTaskSuspend()
),不会被调度执行 - 任务不会自动恢复,必须调用
vTaskResume()
或vTaskResumeFromISR()
才能恢复运行 - 与阻塞态不同,挂起任务不会因为外部事件自动恢复
- 任务被显式挂起(使用
任务优先级
FreeRTOS 任务的优先级用一个整数表示,数值越大,优先级越高。默认情况下,FreeRTOS 的最低优先级是 0,最大优先级由 configMAX_PRIORITIES
定义(通常在 FreeRTOSConfig.h
中配置)。
任务调度方式
- 抢占式调度
- 时间片调度
- 协程式调度(已基本弃用)
抢占式调度
即高优先级任务抢占低优先级任务。当高优先级任务进入就绪态,调度器会立刻抢占低优先级任务,并切换到高优先级任务执行。只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。
时间片调度
相同优先级的任务采用时间片轮转。若多个任务拥有相同优先级,FreeRTOS 默认使用时间片轮转调度,每个任务轮流执行一个时间片(依赖于 configUSE_TIME_SLICING
),即调度器会在每一次时间片之后切换任务,CPU轮流运行优先级相同的任务。
任务控制块(TCB,Task Control Block)
TCB为一结构体:
1 |
|
任务栈
动态方式创建任务时,系统会自动从系统heap中分配一块内存作为任务的栈空间:
1 |
|
usStackDepth
即为栈大小,以字(32位)为单位(非字节)。
API函数
xTaskCreate()
:动态创建任务xTaskCreateStatic()
: 静态创建任务xTaskCreateRestricted()
: 动态创建使用 MPU 限制的任务xTaskCreateRestrictedStatic()
: 静态创建使用 MPU 限制的任务vTaskDelete()
: 删除任务
xTaskCreate()
FreeRTOSConfig.h
中需要将configSUPPORT_DYNAMIC_ALLOCATION
配置为1
1 |
|
pxTaskCode
指向任务函数的指针pcName
任务名,最大长度为 configMAX_TASK_NAME_LENusStackDepth
任务堆栈大小,单位:字(注意,单位不是字节)pvParameters
传递给任务函数的参数uxPriority
任务优先级,最大值为(configMAX_PRIORITIES-1)pxCreatedTask
任务句柄,任务成功创建后,会返回任务句柄。任务句柄就
是任务的任务控制块
返回值:
pdPASS
任务创建成功errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
内存不足,任务创建失败
xTaskCreateStatic()
FreeRTOSConfig.h
中需要将configSUPPORT_STATIC_ALLOCATION
配置为1
1 |
|
和动态分配基本一致,不同在于:
puxStackBuffer
任务栈指针,内存由用户分配提供pxTaskBuffer
任务控制块指针,内存由用户分配提供
返回值:
NULL
用户没有提供相应的内存,任务创建失败其他值
任务句柄,任务创建成功
xTaskCreateRestricted()
用于使用动态的方式创建受 MPU 保护的任务,任务的任务控制块以及任务的栈空
间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要将宏configSUPPORT_DYNAMIC_ALLOCATION``和宏portUSING_MPU_WRAPPERS
同时配置为 1
。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。
1 |
|
pxTaskDefinition
指向任务参数结构体的指针,建结构体中包含任务函数、任
务名、任务优先级等任务参数pxCreadedTask
任务句柄,任务成功创建后,会返回任务句柄。任务句柄就
是任务的任务控制块
vTaskDelete()
数用于删除已被创建的任务,被删除的任务将被从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除.
注意:空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。
若使用此函数,需要在FreeRTOSConfig.h
文件中将宏INCLUDE_vTaskDelete
配置为 1
。
1 |
|
xTaskToDelete
待删除任务的任务句柄
vTaskSuspend()
挂起任务
vTaskResume()
恢复被挂起的任务
vTaskResumeFromISR()
可用于在中断中恢复被挂起的任务。
注意事项
- FreeRTOS 任务函数不允许返回,因此
void task1(void *pvParameters)
中task
的具体实现必须包含在while(1)
中,否则会触发异常(HardFault 或任务栈溢出) - 任务中的延时不能用裸机时的
delay_ms
或delay_us
,要用vTaskDelay()
。该函数按照给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。常量portTICK_PERIOD_MS
可用于根据滴答频率计算实际时间:
1 |
|
该函数通过阻塞的方式来进行延时,因此vTaskDelay()
不能很好的控制周期性任务的频率,因为途径代码的路径和其他任务/中断会影响vTaskDelay()
被调用的频率,久而久之会影响周期性任务的触发。请参阅 vTaskDelayUntil()
,了解设计用于方便 固定频率执行的替代 API 函数。此函数指定调用任务应取消阻塞的绝对时间(而非相对时间)来实现这一点 。
列表(List)
定义
列表(List) 是一个非常核心的数据结构,主要用于管理各种内核对象的状态和优先级排序,比如就绪任务、延时任务、阻塞任务等。FreeRTOS 的调度器、延时机制、同步机制(如信号量)都依赖于列表来实现。
1 |
|
作用
- 任务调度:就绪列表(Ready List)
- FreeRTOS 会根据任务优先级把就绪任务插入对应的列表(每个优先级一个 List)。
- 调度器从最高优先级的列表中选出第一个任务来运行。
- 任务延时:延时列表(Delay List)
- 当任务调用 vTaskDelay() 等函数后,会被放入延时列表,列表按照时间排序(Tick 值)。
- 每个系统节拍(tick)检查列表,时间到了就移回就绪列表。
- 阻塞管理:等待列表(例如队列、信号量)
- 当任务在等待某些资源时(比如消息队列、信号量),会被挂入等待列表中。
- 当资源可用时,从列表中唤醒一个或多个任务。
API
vListInitialise()
: 初始化一个 List。vListInitialiseItem()
: 初始化一个 ListItem。vListInsert()
: 插入元素到列表中(按 xItemValue 排序)。vListInsertEnd()
: 插入到列表末尾。uxListRemove()
: 从列表中删除一个项。
任务切换
基本概念
OS内核中的任务切换有两种触发方式:
- 应用程序通过SVC指令触发
- SysTick周期性中断
但是如果有IRQ请求在SysTick中断前产生,SysTick就可能抢占IRQ,导致IRQ被延迟处理,影响RTOS的实时性并导致Usage Fault.
PendSV
PendSV 全称是 “Pendable Service Call”,是 Cortex-M 系列处理器(比如 STM32)特有的一个系统异常,用于延迟处理的一种软件中断。
- “Pend” 表示它是可以“挂起”的。
- 它的优先级可以被设置为最低,确保只有当别的高优先级中断都处理完之后才会执行。
PendSV 是 FreeRTOS 实现任务上下文切换(context switch)的关键机制,具体而言:
- 执行任务切换:当 FreeRTOS 决定要从一个任务切换到另一个任务时(比如发生了任务阻塞、任务优先级发生变化等),不会立即切换,而是设置一个“请求切换任务”的标志,让 PendSV 异常触发,延迟到安全时机再做真正的上下文切换。
- 保护上下文切换不被打断:PendSV 的优先级设置为最低,意味着它执行的时候不会打断别的中断,保证了上下文切换过程是原子且稳定的。
任务切换的本质
保存->切换->恢复->跳转
1 |
|
任务间通信
队列(Queue)
- FIFO(先进先出)
- 既能同步(阻塞),又能传数据
- 任务和中断都可以用
- 可设长度 N,缓存多个元素
常见用法:
- 从中断向任务发送传感器数据
- 一个任务给另一个任务派发命令
- UART/ADC驱动中,任务读取缓冲区数据
即“不仅要通知,还要带点东西”:
1 |
|
队列集(Queue Set)
多个队列的集合,用于处理单个队列只能传输同一类型数据的情况,可在多个队列/信号量之间选择哪个先有数据/事件发生,避免无效率的轮询。
常见用法:
- 一个任务等待多个外设的输入(例如:UART1 / UART2 / CAN 同时接收)
- 管理多个任务发来的指令队列,集中处理
即“有很多输入源,我想让一个任务来监听它们,并优先处理哪个先有数据”
1 |
|
信号量(Semaphore)
普通信号量分为Binary Semaphore和Counting Semaphore,都用于任务同步和中断通知,本身并不传输数据,类似于一个信号灯。
常见用法:
- 中断服务程序用
xSemaphoreGiveFromISR()
通知任务去处理数据 - 任务 A 通知任务 B 某件事完成
- 控制允许 N 个任务并发访问某资源(计数型)
中断通知任务下,二值信号量的工作机制为:
- 任务A等待某件事发生,于是它调用:
1 |
|
并且任务A状态变为阻塞,直到信号量更新或超时
- 某个中断发生后,ISR内调用
1 |
|
通过信号量发出信号,告知任务A期所期望的事件已经到达
- FreeRTOS检测到信号到达,任务A由阻塞变为就绪
把信号量看成门铃:
- 任务按下
Take()
就是“等门铃响”。 - 中断调用
Give()
就是“按门铃”。 - 门铃一响,任务就醒了,去执行它的事情。
互斥信号量(Mutex Semaphore)
保护共享资源,防止多个任务同时访问。互斥信号量基于信号量实现,但只能由拥有者释放,支持优先级继承(防止优先级反转)。
常见用法:
- 多任务访问串口、SPI、LCD等,避免同一硬件被多任务使用
- 避免数据竞争
即“我只想让一个任务进入临界区,并防止资源冲突”
1 |
|
对比
假设有个任务控制打印机,有多个任务要发打印请求:
- 队列:多个任务传“打印任务结构体”给打印机任务
- 队列集:多个来源(如 GUI、网络、中断)发任务 → 打印任务监听哪个先到
- 信号量:中断来了发一个信号量通知任务“有数据了”
- 互斥信号量:多个任务打印时共享串口 → 用互斥锁保护 printf()
任务与进程、线程
在 FreeRTOS 中,所谓的 “任务(Task)” 实际上是 线程(Thread) 的概念,而不是进程。FreeRTOS中不同任务共享全局变量、堆和BSS,每个任务只有自己的任务栈,并没有独立的地址空间。同时STM32系列大多没有MMU,创建的所有任务都是运行在同一个地址空间,多个任务共享堆、数据段和代码段,硬件上不支持多进程。进程级别的隔离需要 MMU(内存管理单元),这超出了大多数小 MCU 的硬件能力。
作为补充,FreeRTOS任务栈中存储的内容包括:
- 该任务运行时的局部变量
- 函数调用时的返回地址
- 寄存器上下文备份(为了调用任务后返回)
- 中断返回现场保存
- 浮点寄存器状态(若FPU启用)
栈是私有的,作用是支撑任务自身运行,栈上不会有别的任务的东西;每次任务切换时,FreeRTOS 保存/恢复的就是当前任务的栈指针;就像 Linux 线程一样,虽然共享地址空间,但栈是各自独立的。
比如:
1 |
|