FreeRTOS 任务、任务列表、任务切换和任务间通信

任务

任务状态

  • 运行态(Running)
    • 任务当前正在 CPU 上执行
    • 在单核系统中,同时只有一个任务处于运行态
    • 任务只有在调度器(Scheduler)选择它执行时,才会进入运行态
  • 就绪态(Ready)
    • 任务已经具备执行条件,但由于 CPU 资源被其他更高优先级任务占用,它暂时无法运行
    • 任务处于就绪列表(Ready List),等待 FreeRTOS 调度它运行
    • 当更高优先级任务阻塞或时间片结束,调度器可能会让它进入运行态
  • 阻塞态(Blocked)
    • 任务正在等待某个事件(如 vTaskDelay、信号量、消息队列、事件组等),暂时无法运行
    • 任务在等待事件时会从就绪态转换到 阻塞态,避免占用 CPU 资源
    • 当等待的事件发生后(如 信号量释放、消息到达、延时时间结束),任务会转换回 就绪态
  • 挂起态(Suspended)
    • 任务被显式挂起(使用 vTaskSuspend()),不会被调度执行
    • 任务不会自动恢复,必须调用 vTaskResume()vTaskResumeFromISR() 才能恢复运行
    • 与阻塞态不同,挂起任务不会因为外部事件自动恢复

task状态转换图

任务优先级

FreeRTOS 任务的优先级用一个整数表示,数值越大,优先级越高。默认情况下,FreeRTOS 的最低优先级是 0,最大优先级由 configMAX_PRIORITIES 定义(通常在 FreeRTOSConfig.h 中配置)。

任务调度方式

  • 抢占式调度
  • 时间片调度
  • 协程式调度(已基本弃用)

抢占式调度

即高优先级任务抢占低优先级任务。当高优先级任务进入就绪态,调度器会立刻抢占低优先级任务,并切换到高优先级任务执行。只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。

时间片调度

相同优先级的任务采用时间片轮转。若多个任务拥有相同优先级,FreeRTOS 默认使用时间片轮转调度,每个任务轮流执行一个时间片(依赖于 configUSE_TIME_SLICING),即调度器会在每一次时间片之后切换任务,CPU轮流运行优先级相同的任务。

任务控制块(TCB,Task Control Block)

TCB为一结构体:

1
2
3
4
5
6
7
8
9
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack; // 任务栈指针
ListItem_t xStateListItem; // 任务在就绪、阻塞等列表中的位置
StackType_t *pxStack; // 任务栈的起始地址
char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名称
UBaseType_t uxPriority; // 任务优先级
...
} tskTCB;

任务栈

动态方式创建任务时,系统会自动从系统heap中分配一块内存作为任务的栈空间:

1
2
3
4
5
6
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask)

usStackDepth即为栈大小,以字(32位)为单位(非字节)。

API函数

  • xTaskCreate():动态创建任务
  • xTaskCreateStatic(): 静态创建任务
  • xTaskCreateRestricted(): 动态创建使用 MPU 限制的任务
  • xTaskCreateRestrictedStatic(): 静态创建使用 MPU 限制的任务
  • vTaskDelete(): 删除任务

xTaskCreate()

FreeRTOSConfig.h中需要将configSUPPORT_DYNAMIC_ALLOCATION配置为1

1
2
3
4
5
6
7
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask);
  • pxTaskCode 指向任务函数的指针
  • pcName 任务名,最大长度为 configMAX_TASK_NAME_LEN
  • usStackDepth 任务堆栈大小,单位:字(注意,单位不是字节)
  • pvParameters 传递给任务函数的参数
  • uxPriority 任务优先级,最大值为(configMAX_PRIORITIES-1)
  • pxCreatedTask 任务句柄,任务成功创建后,会返回任务句柄。任务句柄就
    是任务的任务控制块

返回值:

  • pdPASS 任务创建成功
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 内存不足,任务创建失败

xTaskCreateStatic()

FreeRTOSConfig.h中需要将configSUPPORT_STATIC_ALLOCATION配置为1

1
2
3
4
5
6
7
8
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer);

和动态分配基本一致,不同在于:

  • puxStackBuffer 任务栈指针,内存由用户分配提供
  • pxTaskBuffer 任务控制块指针,内存由用户分配提供

返回值:

  • NULL 用户没有提供相应的内存,任务创建失败
  • 其他值 任务句柄,任务创建成功

xTaskCreateRestricted()

用于使用动态的方式创建受 MPU 保护的任务,任务的任务控制块以及任务的栈空
间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要将宏configSUPPORT_DYNAMIC_ALLOCATION``和宏portUSING_MPU_WRAPPERS 同时配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。

1
2
3
BaseType_t xTaskCreateRestricted(
const TaskParameters_t * const pxTaskDefinition,
TaskHandle_t * pxCreatedTask);
  • pxTaskDefinition 指向任务参数结构体的指针,建结构体中包含任务函数、任
    务名、任务优先级等任务参数
  • pxCreadedTask 任务句柄,任务成功创建后,会返回任务句柄。任务句柄就
    是任务的任务控制块

vTaskDelete()

数用于删除已被创建的任务,被删除的任务将被从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除.

注意:空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。

若使用此函数,需要在FreeRTOSConfig.h文件中将宏INCLUDE_vTaskDelete配置为 1

1
void vTaskDelete(TaskHandle_t xTaskToDelete);
  • xTaskToDelete 待删除任务的任务句柄

vTaskSuspend()

挂起任务

vTaskResume()

恢复被挂起的任务
vTaskResumeFromISR()可用于在中断中恢复被挂起的任务。

注意事项

  1. FreeRTOS 任务函数不允许返回,因此void task1(void *pvParameters)task的具体实现必须包含在while(1)中,否则会触发异常(HardFault 或任务栈溢出)
  2. 任务中的延时不能用裸机时的delay_msdelay_us,要用vTaskDelay()。该函数按照给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。常量portTICK_PERIOD_MS可用于根据滴答频率计算实际时间:
1
2
3
4
5
6
7
8
9
10
11
12
void vTaskFunction( void * pvParameters )
{
/* Block for 500ms. */
const TickType_t xDelay = 500 / portTICK_PERIOD_MS;

for( ;; )
{
/* Simply toggle the LED every 500ms, blocking between each toggle. */
vToggleLED();
vTaskDelay( xDelay );
}
}

该函数通过阻塞的方式来进行延时,因此vTaskDelay()不能很好的控制周期性任务的频率,因为途径代码的路径和其他任务/中断会影响vTaskDelay()被调用的频率,久而久之会影响周期性任务的触发。请参阅 vTaskDelayUntil(),了解设计用于方便 固定频率执行的替代 API 函数。此函数指定调用任务应取消阻塞的绝对时间(而非相对时间)来实现这一点 。

列表(List)

定义

列表(List) 是一个非常核心的数据结构,主要用于管理各种内核对象的状态和优先级排序,比如就绪任务、延时任务、阻塞任务等。FreeRTOS 的调度器、延时机制、同步机制(如信号量)都依赖于列表来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 双向链表 */
typedef struct xLIST_ITEM
{
TickType_t xItemValue;
struct xLIST_ITEM *pxNext;
struct xLIST_ITEM *pxPrevious;
void *pvOwner;
void *pvContainer;
} ListItem_t;

typedef struct xLIST
{
UBaseType_t uxNumberOfItems;
ListItem_t *pxIndex;
ListItem_t xListEnd; // 哨兵节点,值最大
} List_t;

作用

  1. 任务调度:就绪列表(Ready List)
  • FreeRTOS 会根据任务优先级把就绪任务插入对应的列表(每个优先级一个 List)。
  • 调度器从最高优先级的列表中选出第一个任务来运行。
  1. 任务延时:延时列表(Delay List)
  • 当任务调用 vTaskDelay() 等函数后,会被放入延时列表,列表按照时间排序(Tick 值)。
  • 每个系统节拍(tick)检查列表,时间到了就移回就绪列表。
  1. 阻塞管理:等待列表(例如队列、信号量)
  • 当任务在等待某些资源时(比如消息队列、信号量),会被挂入等待列表中。
  • 当资源可用时,从列表中唤醒一个或多个任务。

API

  • vListInitialise(): 初始化一个 List。
  • vListInitialiseItem(): 初始化一个 ListItem。
  • vListInsert(): 插入元素到列表中(按 xItemValue 排序)。
  • vListInsertEnd(): 插入到列表末尾。
  • uxListRemove(): 从列表中删除一个项。

任务切换

基本概念

OS内核中的任务切换有两种触发方式:

  1. 应用程序通过SVC指令触发
  2. SysTick周期性中断
    但是如果有IRQ请求在SysTick中断前产生,SysTick就可能抢占IRQ,导致IRQ被延迟处理,影响RTOS的实时性并导致Usage Fault.

PendSV

PendSV 全称是 “Pendable Service Call”,是 Cortex-M 系列处理器(比如 STM32)特有的一个系统异常,用于延迟处理的一种软件中断。

  • “Pend” 表示它是可以“挂起”的。
  • 它的优先级可以被设置为最低,确保只有当别的高优先级中断都处理完之后才会执行。

PendSV 是 FreeRTOS 实现任务上下文切换(context switch)的关键机制,具体而言:

  1. 执行任务切换:当 FreeRTOS 决定要从一个任务切换到另一个任务时(比如发生了任务阻塞、任务优先级发生变化等),不会立即切换,而是设置一个“请求切换任务”的标志,让 PendSV 异常触发,延迟到安全时机再做真正的上下文切换。

PendSV中执行任务切换

  1. 保护上下文切换不被打断:PendSV 的优先级设置为最低,意味着它执行的时候不会打断别的中断,保证了上下文切换过程是原子且稳定的。

任务切换的本质

保存->切换->恢复->跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;

/* *INDENT-OFF* */
PRESERVE8

/* 从 PSP(进程堆栈指针)获取当前任务的栈顶指针到 r0,即当前任务栈的位置 */
mrs r0, psp
isb /* 清洗流水线,确保指令顺序执行完成 */

/* 加载TCB地址到R3,再取出TCB到R2 */
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]

/* 检查是否启用了浮点协处理器(FPU) */
/* 如果启用,将高位VFQ寄存器压栈,防止浮点任务之间干扰 */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}

/* 保存核心寄存器 */
stmdb r0!, {r4-r11, r14}

/* 将新栈顶保存至TCB,更新栈顶 */
str r0, [ r2 ]

/* 1. 暂存R0、R3 */
/* 2.设置basepri,屏蔽低优先级中断,防止上下文切换被打断 */
/* 3.调用vTaskSwitchContext(),即调度器,更新TCB至新任务 */
/* 4.取消中断屏蔽 */
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3}

/* 获取新任务的TCB至R1,再获取新任务的栈顶地址至R0 */
ldr r1, [ r3 ]
ldr r0, [ r1 ]

/* 恢复核心寄存器 */
ldmia r0!, {r4-r11, r14}

/* 检查是否启用了浮点协处理器(FPU) */
/* 如果启用,将高位VFQ寄存器出栈 */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}

/* 设置新的堆栈指针 */
msr psp, r0
isb

#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif

/* 返回新任务的执行地址 */
bx r14
/* *INDENT-ON* */
}

任务间通信

队列(Queue)

  • FIFO(先进先出)
  • 既能同步(阻塞),又能传数据
  • 任务和中断都可以用
  • 可设长度 N,缓存多个元素

常见用法:

  • 从中断向任务发送传感器数据
  • 一个任务给另一个任务派发命令
  • UART/ADC驱动中,任务读取缓冲区数据

即“不仅要通知,还要带点东西”:

1
2
3
4
5
6
7
typedef struct {
uint8_t cmd;
uint16_t value;
} Message_t;

xQueueSend(xQueue, &msg, portMAX_DELAY);
xQueueReceive(xQueue, &msg, portMAX_DELAY);

队列集(Queue Set)

多个队列的集合,用于处理单个队列只能传输同一类型数据的情况,可在多个队列/信号量之间选择哪个先有数据/事件发生,避免无效率的轮询。

常见用法:

  • 一个任务等待多个外设的输入(例如:UART1 / UART2 / CAN 同时接收)
  • 管理多个任务发来的指令队列,集中处理

即“有很多输入源,我想让一个任务来监听它们,并优先处理哪个先有数据”

1
xQueueSelectFromSet(xQueueSet, portMAX_DELAY); // 等哪个先来

信号量(Semaphore)

普通信号量分为Binary Semaphore和Counting Semaphore,都用于任务同步和中断通知,本身并不传输数据,类似于一个信号灯。

常见用法:

  • 中断服务程序用 xSemaphoreGiveFromISR() 通知任务去处理数据
  • 任务 A 通知任务 B 某件事完成
  • 控制允许 N 个任务并发访问某资源(计数型)

中断通知任务下,二值信号量的工作机制为:

  1. 任务A等待某件事发生,于是它调用:
1
xSemaphoreTake(xSemaphore, portMAX_DELAY);

并且任务A状态变为阻塞,直到信号量更新或超时

  1. 某个中断发生后,ISR内调用
1
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);

通过信号量发出信号,告知任务A期所期望的事件已经到达

  1. FreeRTOS检测到信号到达,任务A由阻塞变为就绪

把信号量看成门铃:

  1. 任务按下 Take() 就是“等门铃响”。
  2. 中断调用 Give() 就是“按门铃”。
  3. 门铃一响,任务就醒了,去执行它的事情。

互斥信号量(Mutex Semaphore)

保护共享资源,防止多个任务同时访问。互斥信号量基于信号量实现,但只能由拥有者释放,支持优先级继承(防止优先级反转)。

常见用法:

  • 多任务访问串口、SPI、LCD等,避免同一硬件被多任务使用
  • 避免数据竞争

即“我只想让一个任务进入临界区,并防止资源冲突”

1
2
3
xSemaphoreTake(xMutex, portMAX_DELAY);
LCD_Print("Hello"); // 临界区
xSemaphoreGive(xMutex);

对比

假设有个任务控制打印机,有多个任务要发打印请求:

  • 队列:多个任务传“打印任务结构体”给打印机任务
  • 队列集:多个来源(如 GUI、网络、中断)发任务 → 打印任务监听哪个先到
  • 信号量:中断来了发一个信号量通知任务“有数据了”
  • 互斥信号量:多个任务打印时共享串口 → 用互斥锁保护 printf()

任务与进程、线程

在 FreeRTOS 中,所谓的 “任务(Task)” 实际上是 线程(Thread) 的概念,而不是进程。FreeRTOS中不同任务共享全局变量、堆和BSS,每个任务只有自己的任务栈,并没有独立的地址空间。同时STM32系列大多没有MMU,创建的所有任务都是运行在同一个地址空间,多个任务共享堆、数据段和代码段,硬件上不支持多进程。进程级别的隔离需要 MMU(内存管理单元),这超出了大多数小 MCU 的硬件能力。

作为补充,FreeRTOS任务栈中存储的内容包括:

  • 该任务运行时的局部变量
  • 函数调用时的返回地址
  • 寄存器上下文备份(为了调用任务后返回)
  • 中断返回现场保存
  • 浮点寄存器状态(若FPU启用)

栈是私有的,作用是支撑任务自身运行,栈上不会有别的任务的东西;每次任务切换时,FreeRTOS 保存/恢复的就是当前任务的栈指针;就像 Linux 线程一样,虽然共享地址空间,但栈是各自独立的。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void TaskA(void *pvParam) {
int a = 42; // 局部变量a在 TaskA 的任务栈中
while (1) {
printf("TaskA: %d\n", a);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

void TaskB(void *pvParam) {
static int b = 100; // 静态变量b在 .data 段,是共享的
int c = 99; // 局部变量c在 TaskB 的任务栈中
while (1) {
printf("TaskB: %d\n", c);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}

FreeRTOS 任务、任务列表、任务切换和任务间通信
http://akichen891.github.io/2025/04/02/RTOS任务基础/
作者
Aki
发布于
2025年4月2日
更新于
2025年4月18日
许可协议