任务基础
任务基础
前后台系统(单任务系统)
在使用单片机裸机开发的时候,无限循环里面的函数就相当于后台,中断就相当于前台。
前后台系统相当于所有任务优先级都是相同的,都排队轮流执行,但是简单,资源消耗少
多任务系统
freertos的任务调度器
先执行中断,再执行优先级高的任务
RTOS里面并不是并发执行任务,即同一时刻执行多个任务,每一时刻只能有一个任务执行,每个任务时间都很短,看起来像并发执行
每个任务都有自己的运行环境,不依赖于其他任务和调度器。
所以每次退出任务时候,调度器都要保证退出这个任务的时候将堆栈内容,寄存器值等上下文环境保存好,由此引出了每个任务都有的堆栈。这样子的话就从堆栈里面取出环境,恢复运行

任务状态


任务优先级
从0到configMAX_PRIORITIES-1,数字越高,优先级越高,和中断优先级相反
下面的设置为1之后configMAX_PRIORITIES不能超过32,但是最好不要设置32,会增大开销,根据需求来设置
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
//1启用特殊方法来选择下一个要运行的任务
//一般是硬件计算前导零指令,如果所使用的
//MCU没有这些硬件指令的话此宏应该设置为0!开启之后可以定义的任务,就绪态任务按照,不然的话相同优先级下的任务得到CPU时间不一样
#define configUSE_TIME_SLICING 1 //1使能时间片调度(默认式使能的)
#define configUSE_PREEMPTION 1 //1使用抢占式内核,0使用协程任务控制块
任务控制块中
每个任务都有一些属性要存储,freertos把这些属性一起用一个结构体来表示,在创建任务的时候就会分配给任务
pxTopOfStack:栈顶指针,时用于保存/恢复上下文
pxStack:栈起始地址,用于
xStateListItem:将任务连接到状态列表(就绪、阻塞、挂起)
xEventListItem:将任务连接到事件列表(如队列、信号量)
uxPriority:任务优先级,决定调度顺序
pcTaskName:任务名称,便于调试识别
/*
* 任务控制块。每个任务都会分配一个任务控制块(TCB),
* 用于存储任务状态信息,包括指向任务上下文(任务的运行时环境,包括寄存器值)的指针
*/
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /*< 指向任务栈中最后放置项的位置。这必须是TCB结构体的第一个成员。*/
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*< MPU设置由端口层定义。这必须是TCB结构体的第二个成员。*/
#endif
ListItem_t xStateListItem; /*< 状态列表项,用于表示任务的状态(就绪、阻塞、挂起)。*/
ListItem_t xEventListItem; /*< 用于从事件列表中引用任务。*/
UBaseType_t uxPriority; /*< 任务的优先级。0是最低优先级。*/
StackType_t *pxStack; /*< 指向堆栈的起始位置。*/
char pcTaskName[ configMAX_TASK_NAME_LEN ];/*< 创建任务时给出的描述性名称。仅用于调试。*/
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; /*< 在堆栈从低内存向上增长架构中,指向堆栈的末端。*/
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*< 保存关键段嵌套深度,用于不在端口层维护自己计数的端口。*/
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< 存储一个数字,每次创建TCB时递增。允许调试器确定任务何时被删除并重新创建。*/
UBaseType_t uxTaskNumber; /*< 专门供第三方跟踪代码使用的数字。*/
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< 最后分配给任务的优先级 - 用于优先级继承机制。*/
UBaseType_t uxMutexesHeld; /*< 持有的互斥锁数量。*/
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag; /*< 任务标签函数指针。*/
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ]; /*< 线程本地存储指针数组。*/
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /*< 存储任务在运行状态下花费的时间量。*/
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
/* 为此任务分配特定的Newlib可重入结构。
注意:Newlib支持因大众需求而被包含,但FreeRTOS维护者自己并不使用。
FreeRTOS不对由此产生的newlib操作负责。用户必须熟悉newlib
并提供必要存根的系统级实现。请注意(在撰写本文时)
当前的newlib设计实现了必须提供锁的系统级malloc()。*/
struct _reent xNewLib_reent; /*< Newlib可重入结构。*/
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue; /*< 任务通知值。*/
volatile uint8_t ucNotifyState; /*< 任务通知状态。*/
#endif
/* 参见tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE定义上方的注释。*/
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated; /*< 如果任务是静态分配的,则设置为pdTRUE,以确保不会尝试释放内存。*/
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted; /*< 延迟是否被中止的标志。*/
#endif
} tskTCB;
/* 旧的tskTCB名称在上面维护,然后在下面typedef为新的TCB_t名称,
以便使用较旧的内核感知调试器。*/
typedef tskTCB TCB_t;任务堆栈
freertos能就是因为有任务堆栈,无论是任务调度还是中断,恢复任务的时候从堆栈中获取保存的变量,
xTaskCreateStatic()和xTaskCreate()在创建任务时会创建堆栈,一个手动创建,一个自动创建
任务堆栈的数据类型,创建堆栈时在函数中传入的是堆栈的指针StackType_t*
#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;注意:实际分配的堆栈大小是我们手动设置数值的,
// 在 tasks.c 中的 xTaskCreate 函数内部
StackType_t *pxStack;
/* 为任务堆栈分配内存 */
pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );任务创建
动态创建
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 【任务函数指针】任务要执行的函数
const char * const pcName, // 【任务名称】用于调试识别的字符串
const uint16_t usStackDepth, // 【堆栈深度】以字(word)为单位的堆栈大小
void * const pvParameters, // 【任务参数】传递给任务函数的参数指针
UBaseType_t uxPriority, // 【任务优先级】数值越大优先级越高
TaskHandle_t * const pxCreatedTask // 【任务句柄】用于引用和管理任务的指针
)
// 创建LED闪烁任务
xTaskCreate(ledTask, // 任务函数
"LED Controller", // 任务名
128, // 堆栈大小128字 = 512字节
NULL, // 无参数
2, // 优先级2
&ledHandle); // 保存任务句柄- pxTaskCode:任务入口函数,任务实际要执行的代码,传入函数指针类型参数
- pcName:任务标识名,调试时便于区分不同任务
- usStackDepth:任务堆栈大小(单位是4字节的字)
- pvParameters:传递给任务的参数,任务函数通过此参数接收数据
- uxPriority:优先级(0 = 最低,configMAX_PRIORITIES-1 = 最高
- pxCreatedTask:输出参数,返回创建的任务句柄,用于后续操作该任务,这个,其他API可能会用到
- 创建成功返回pdpass,值是1;失败就返回其他值
参数
这里传入的是句柄的地址,是二重指针,是为了在函数中对指针的地址进行取值(*),改变这个指针的指向

问题:怎么没看见TCB?TCB和任务堆栈是什么关系
TCB和任务堆栈在函数内部自动被分配内存,只不过任务堆栈要用户显式指定大小
静态创建
使用之前要
// 必须在 FreeRTOSConfig.h 中启用:
// 场景1:无动态内存的系统(禁用heap)
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 0
// 场景2:硬实时要求,避免malloc不确定性
// 场景3:内存受限,需要精确控制内存布局
// 场景4:安全关键系统,避免内存碎片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 // 【TCB缓冲区】用户提供的TCB内存
)前5个和动态创建一样,后面两个不一样,并且返回值是任务句柄
任务删除
参数是句柄,如果的,删除后任务的堆栈和内存控制块会在,所以要给空闲任务一点时间
任务是的或者用户在任务中了内存,需要用户手动释放
静态创建的也要吗?你确定?
vTaskDelete( TaskHandle_t xTaskToDelete )空闲任务和定时器任务
这两个任务会被,用户不用创建
如果静态的,configSUPPORT_STATIC_ALLOCATION==1,那么用户需要在main.c里面实现vApplicationGetIdleTaskMemory和vApplicationGetTimerTaskMemory,最后创建这两个任务的API会调用这个函数来获取内存
//空闲任务任务堆栈
static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
//空闲任务控制块
static StaticTask_t IdleTaskTCB;
//定时器服务任务堆栈
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];
//定时器服务任务控制块
static StaticTask_t TimerTaskTCB;
//获取空闲任务地任务堆栈和任务控制块内存,因为本例程使用的
//静态内存,因此空闲任务的任务堆栈和任务控制块的内存就应该
//有用户来提供,FreeRTOS提供了接口函数vApplicationGetIdleTaskMemory()
//实现此函数即可。
//ppxIdleTaskTCBBuffer:任务控制块内存
//ppxIdleTaskStackBuffer:任务堆栈内存
//pulIdleTaskStackSize:任务堆栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
//获取定时器服务任务的任务堆栈和任务控制块内存
//ppxTimerTaskTCBBuffer:任务控制块内存
//ppxTimerTaskStackBuffer:任务堆栈内存
//pulTimerTaskStackSize:任务堆栈大小
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&TimerTaskTCB;
*ppxTimerTaskStackBuffer=TimerTaskStack;
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;
}vTaskStartScheduler() 的任务是:
初始化系统 → 创建空闲任务和定时器任务 → 设置第一个运行任务 → 启动系统时钟中断 → 进入多任务调度状态。
空闲任务(Idle Task)
- 优先级最低,用来回收已删除任务的堆栈、执行低功耗钩子函数等。
可选的定时器任务(Timer Service Task)(如果启用了软件定时器)
- 优先级比较高,负责处理延时任务、超时事件。
思考
2个简单任务都是10ms执行一次,比起裸机开发是不是太慢了?大多数时间都在执行空闲任务?
0ms: A 运行 → 延时 → 切换到 B
1ms~9ms: 其他任务就绪?没有 → Idle
10ms: Tick唤醒A(执行一次任务时间非常短,可忽略)
11ms~19ms: Idle
20ms: B唤醒...
void TaskA(void *p)
{
for(;;)
{
printf("A\r\n");
vTaskDelay(10); // 10ms
}
}
void TaskB(void *p)
{
for(;;)
{
printf("B\r\n");
vTaskDelay(10);
}
}在 FreeRTOS 里:
- 当任务都在延时 (
vTaskDelay(10)) 时,调度器不会空转; - 它会切回 Idle Task;
- Idle 任务内部可以执行:
- 系统维护(删除任务栈回收);
- 用户的
vApplicationIdleHook(); - 甚至让 MCU 进入低功耗(比如
__WFI())。
也就是说:
FreeRTOS 并不是慢,而是让 CPU 在“有事干时全速干,没事时休眠”。
while (1)
{
taskA();
taskB();
}如果主循环只花几十微秒,那它们每秒可能被执行 几万次甚至几十万次。
也就是说:
裸机循环“什么都干不了太多事”,但调用频率极高
裸机那种“疯狂轮询”的方式虽然次数多,但 CPU 一直忙、难扩展、功耗高。
任务挂起与恢复
vTaskSuspend
vTaskResume
xTaskResumeFromISR:在中断中恢复任务
//key任务函数
void key_task(void *pvParameters)
{
u8 key;
while(1)
{
key=KEY_Scan(0);
switch(key)
{
case WKUP_PRES:
vTaskSuspend(Task1Task_Handler);//挂起任务1
printf("挂起任务1的运行!\r\n");
break;
case KEY1_PRES:
vTaskResume(Task1Task_Handler); //恢复任务1
printf("恢复任务1的运行!\r\n");
break;
case KEY2_PRES:
vTaskSuspend(Task2Task_Handler);//挂起任务2
printf("挂起任务2的运行!\r\n");
break;
}
vTaskDelay(10); //延时10ms
}
}
void EXTI3_IRQHandler(void)
{
BaseType_t YieldRequired;
delay_xms(50); //消抖
if(KEY0==0)
{
YieldRequired=xTaskResumeFromISR(Task2Task_Handler);//恢复任务2
printf("恢复任务2的运行!\r\n");
if(YieldRequired==pdTRUE)
{
/*如果函数xTaskResumeFromISR()返回值为pdTRUE,那么说明要恢复的这个
任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),所以在
退出中断的时候一定要进行上下文切换!*/
portYIELD_FROM_ISR(YieldRequired);
}
}
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_3); //清除中断标志位
}#define portYIELD_FROM_ISR(x) if(x != pdFALSE) portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
最终等价于
if (x != pdFALSE)
{
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
手动触发一次 PendSV 异常(即“系统请求一次任务切换”)也就是说:
portYIELD_FROM_ISR()→ 触发 PendSV → FreeRTOS 切换任务
PendSV(Pendable Service Call)是 ARM Cortex-M 专门留给操作系统做上下文切换的中断。
FreeRTOS 的 xPortPendSVHandler() 里实现了保存当前任务栈、切换任务栈、恢复新任务的逻辑。
思考
这里为什么要手动切换任务?RTOS检测不到有高优先级的任务激活了吗?,而不是等待任务调度点位
在 任务模式 下,FreeRTOS 会自动检测并切换任务。,详见任务切换
自动调度通常发生在:
- 任务主动让出 CPU:比如调用
vTaskDelay()、vTaskSuspend(); - Tick 中断到来:FreeRTOS 的 SysTick 定时器每 1ms 触发调度检查。
✅ 在这两种情况下,内核有“控制权”,它能主动判断哪个任 务优先级高,然后切换。
中断(ISR)是 硬件打断 CPU 流程 的,
FreeRTOS 不会、也不能去自动感知中断里发生了什么。
因为中断是“裸奔”的,CPU 硬件直接跳过去执行 ISR,
RTOS 的调度器在中断期间是暂停的。
RTOS 只是“知道”Task2 应该运行,但 此时还在中断上下文中,
它不能立刻执行任务切换(因为中断还没退出)。
所以,RTOS 把决定权交给你:
你来决定是否在中断退出时立刻触发调度。
portYIELD_FROM_ISR()这个函数,就是说
“中断一退出,赶紧切换到高优先级任务!”
如果你不调用它,
那就会等到 下一个 Tick 中断 才触发任务切换。
也就是说高优先级任务要“晚一点”才运行。

