当前位置:首页 > 技术 > 正文内容

FreeRTOS内核:任务机制深度解析

访客 技术 2026年6月19日 1

一、任务的本质:函数 + 独立栈

函数比较容易理解,它定义了任务的具体行为逻辑。

// 任务函数示例:无限循环处理
void WorkerTask(void *pvParams) {
    while(1) {
        // 任务业务逻辑:传感器数据处理、控制算法等
        ProcessSensorData();
        ToggleOutputPin();
        vTaskDelay(500); // 主动放弃CPU使用权
    }
}

但栈的作用是什么(注意是独立栈)?

  • 保存局部变量:函数内的局部变量(如数组、临时变量)存储在栈中;
  • 保存调用关系:函数嵌套调用时,返回地址(LR)保存在栈中;
  • 保存任务现场:任务被切换(中断)时,CPU寄存器的值存入栈,恢复时从栈中读回。

二、重新理解任务创建

1、任务创建函数与TCB

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskFunction,  // 1. 任务函数指针(必传!)
    const char * const pcName,      // 2. 任务名称(可选)
    uint16_t usStackDepth,         // 3. 栈深度(字节数,如 150*4=600)
    void *pvParameters,            // 4. 函数参数(如 float value=3.14)
    UBaseType_t uxPriority,        // 5. 任务优先级(0~configMAX_PRIORITIES-1)
    TaskHandle_t *pxCreatedTask    // 6. 任务句柄(输出,用于操作任务)
);

结合计算机体系结构知识

参数 作用 代码示例
pvTaskFunction 任务函数地址(PC 寄存器指向它) WorkerTask 函数地址 → xTaskCreate(WorkerTask, ...)
usStackDepth 栈大小(字节数)→ 由 pvPortMalloc() 分配 usStackDepth = 150 → 150*4=600字节栈
pvParameters 传递给任务函数的参数(存入 R0 pvParameters = &sensorValue → 任务函数 WorkerTask(float *value)
uxPriority 任务优先级(决定调度顺序) uxPriority = 2(优先级 2 比 1 高)

TCB任务控制块(精简版本)

TCB = Task Control Block,是一个C语言结构体,用于"描述和管理一个任务",相当于任务的"身份证"。

typedef struct tskTaskControlBlock {
    volatile StackType_t *pxTopOfStack; // 1. 栈顶指针(栈起始位置)
    #if (configUSE_TRACE_FACILITY == 1)
        UBaseType_t uxTCBNumber; // 任务编号
    #endif
    UBaseType_t uxPriority;          // 2. 任务优先级
    ListItem_t xStateListItem;       // 3. 用于链表(就绪/阻塞列表)
    ListItem_t xEventListItem;       // 4. 用于事件列表(如队列)
    #if (configUSE_MUTEXES == 1)
        UBaseType_t uxBasePriority;  // 优先级继承用
    #endif
    char pcTaskName[configMAX_TASK_NAME_LEN]; // 5. 任务名称
} tskTCB;
TCB 字段 作用 代码中如何体现
pxTopOfStack 栈顶地址(任务切换时恢复寄存器) pxStack = (StackType_t *)pvPortMalloc(usStackDepth * sizeof(StackType_t));
uxPriority 优先级(决定调度顺序) uxPriority = 2 → 优先级 2
xStateListItem 链表节点(连接到就绪列表/阻塞列表) vListInsertEnd(&xReadyTasksLists[uxPriority], &pxNewTCB->xStateListItem);
pcTaskName 任务名称(调试用) pcTaskName = "WorkerTask"

TCB 里为什么没有"函数指针"?

函数指针和函数参数不是直接存储在TCB中,而是作为"初始现场"存储在任务栈中!

  • 任务刚创建时,会在栈中预先填充:
  • PC寄存器的值 = 任务函数的地址(让任务第一次运行时从函数入口开始);
  • R0寄存器的值 = 任务函数的参数(pvParameters);
  • 任务第一次被调度时,从栈中恢复这些寄存器,自动跳转到任务函数执行。

2、任务栈

FreeRTOS任务栈大小的单位:xTaskCreateusStackDepth参数是栈项数(不是字节数)

  • 针对Cortex-M内核(STM32),栈按**4字节(1个字)**对齐,因此栈项数×4 = 实际栈字节数;
  • 例:usStackDepth=200 → 实际栈大小 = 200×4=800字节;usStackDepth=512 → 2048字节。
  • 栈增长方向:Cortex-M内核栈从高地址向低地址增长,栈溢出会覆盖低地址的内存(触发内存踩踏)。

2.1栈的大小如何确定?

栈的大小取决于两点:

void HelperFunction(void) {
    // 延时循环
    int counter;  // 在函数开始处声明(C90要求)
    for(counter = 0; counter < 200; counter++)
    {
        // 空循环体用于延时
    }
}

void DataProcessingTask( void *pvParams )
{
	const char *pcTaskName = "DataProc\r\n";
	// 定义150个int的数组,占150*4=600字节
    volatile int dataBuffer[150];
    // 防止编译器优化,故意使用数组
    dataBuffer[0] = 999;
	/* 任务函数的主体一般都是无限循环 */
	for( ;; )
	{
		/* 处理数据任务信息 */
		printf( pcTaskName );
		
		/* 延迟一会(比较简单粗暴) */
		HelperFunction();
	}
}
(1)局部变量的大小

反汇编验证:编译后会看到

局部变量占用栈空间的大小可以通过反汇编确认。编译器会自动为局部变量分配栈空间。

(2)函数调用的深度

如果任务中嵌套调用子函数,LR(返回地址)会被保存,必须将LR压入栈,后续才能POP出(整个流程由编译器自动执行,我们只需注意是否会栈溢出即可)

2.2实际开发中栈大小的设置原则

估算:局部变量大小 × 2(保险起见,留余量)

之后使用FreeRTOS自带工具验证(核心方法),FreeRTOS提供uxTaskGetStackHighWaterMark()函数(栈高水位标记),能返回任务栈的剩余最小空间(栈项数),是判断栈是否够用的核心工具。

//开启宏定义,注意这个宏定义只适合调试阶段,发布后就不要使用了
#ifndef INCLUDE_uxTaskGetStackHighWaterMark
    #define INCLUDE_uxTaskGetStackHighWaterMark    1
#endif
// 获取栈剩余最小空间(栈项数)
UBaseType_t uxMinSpace = uxTaskGetStackHighWaterMark(xDataTaskHandle);
// 打印:比如返回30,表示任务运行以来,栈最少剩余30个栈项(120字节)
printf("Data Task Stack High Water Mark: %u\n", uxMinSpace);

关键判断规则:

  • uxMinSpace返回值大于0且大于栈大小的20%:栈大小足够(比如栈大小200,剩余≥40,说明有足够余量);
  • 若返回值接近0(比如≤5):栈大小不足,需增大(比如从200→400);
  • 若返回值等于栈大小:任务几乎没使用栈,可减小(比如从400→200)。

底层实现逻辑(学习后可以自己实现):

先把整个任务栈空间填充一个固定的"魔术字"(默认是tskSTACK_FILL_BYTE为0x5A,可在源码中修改),这个操作是栈高水位检测的基础

当任务运行后,堆栈会被使用,此时从栈底开始魔术字会被替代,读取剩余魔术字的个数就可知剩余堆栈大小(从栈底读会更方便,因为是从栈顶开始覆盖的)

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
{
    TCB_t * pxTCB;          // 定义任务控制块指针
    uint8_t * pucStackStart;// 栈遍历的起始地址
    UBaseType_t uxResult;   // 返回值(剩余栈项数)

    // 步骤1:获取任务TCB(xTask=NULL时,返回当前运行任务的TCB)
    pxTCB = prvGetTCBFromHandle( xTask );

    // 步骤2:根据栈增长方向,确定遍历的起始地址(跨架构兼容的核心)
    #if portSTACK_GROWTH < 0  // 栈从高地址→低地址增长(Cortex-M、ARM)
        {
            // 起始地址 = 栈底地址(pxStack)
            pucStackStart = ( uint8_t * ) pxTCB->pxStack;
        }
    #else                     // 栈从低地址→高地址增长(如x86)
        {
            // 起始地址 = 栈顶地址(pxEndOfStack)
            pucStackStart = ( uint8_t * ) pxTCB->pxEndOfStack;
        }
    #endif

    // 步骤3:调用底层函数,统计从起始地址开始的连续魔术字数量(剩余栈空间)
    uxResult = ( UBaseType_t ) prvCheckFreeStackSpace( pucStackStart );

    // 步骤4:返回剩余栈项数
    return uxResult;
}

FreeRTOS提供了两种高水位检测方法

对比维度 uxTaskGetStackHighWaterMark(v1) uxTaskGetStackHighWaterMark2(v2)
遍历范围 从起始地址遍历,遇到第一个非魔术字就停止(仅统计"连续未被覆盖的魔术字") 遍历整个栈空间,统计所有未被覆盖的魔术字(全程遍历)
检测精度 低(可能误判,无法反映真实最小剩余空间) 高(精准统计运行以来栈剩余的最小值,无漏判)
CPU开销 小(遍历到第一个非魔术字即终止) 稍大(遍历整个栈空间,但实际影响可忽略)
设计定位 早期版本,性能优先 升级版,精度优先(FreeRTOS推荐使用)
适用场景 性能极度敏感、栈使用简单的场景 调试/排查栈溢出、需要精准检测的场景

2.3发生内存踩踏怎么办?

(1)内存踩踏的核心定义

内存踩踏(也叫内存越界/缓冲区溢出)是指程序访问了超出分配给它的内存范围的地址,导致覆盖了其他内存区域的数据(比如任务栈溢出覆盖相邻任务的栈、数组越界写覆盖全局变量)。

嵌入式中内存踩踏分两类:

  • 栈踩踏(最常见):任务栈溢出、数组越界写栈内存;
  • 堆踩踏:堆内存越界写、free后重复使用(野指针)、内存碎片导致的越界。
成因类型 具体例子
任务栈过小导致栈溢出 任务栈大小100栈项(400字节),但局部数组占500字节,栈溢出覆盖相邻内存
数组/缓冲区越界读写 char buffer[20]; buffer[20] = 0;(下标20超出buffer的0~19范围)
指针操作不当 野指针(指向已释放内存的指针)、空指针解引用、指针偏移越界(p = p + 150
中断嵌套过深 多级中断嵌套,消耗任务栈超出预期
递归调用无终止 递归函数无退出条件,不断压栈导致溢出
堆内存管理不当 pvPortMalloc(20)后,写入30字节数据;free后未置NULL,重复使用
(2)预防方法(核心,从源头避免)

① 合理设置任务栈大小(最关键)

结合前面的"栈高水位"方法,确保每个任务的栈有足够余量(≥20%)。

② 开启FreeRTOS栈溢出检测

FreeRTOS提供两种栈溢出检测机制,在FreeRTOSConfig.h中配置:

  • configCHECK_FOR_STACK_OVERFLOW=1:检测栈指针是否超出栈范围(简单,开销小);
  • configCHECK_FOR_STACK_OVERFLOW=2:检测栈末尾的"魔术字"是否被覆盖(更精准,开销稍大)。
//开启栈溢出钩子函数宏定义
#ifndef configCHECK_FOR_STACK_OVERFLOW
    #define configCHECK_FOR_STACK_OVERFLOW    1
#endif

// 栈溢出钩子函数(检测到溢出时调用)需要自己写函数内容
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    // 打印溢出的任务名,或触发硬件报警(如LED常亮)
    printf("Stack Overflow: %s\n", pcTaskName);
    // 紧急处理:停止任务/重启系统
    vTaskDelete(xTask);
}

③ 禁止数组越界,规范指针使用

  • 数组操作前做边界检查:if(i < sizeof(buffer)/sizeof(buffer[0])) { buffer[i] = 0; }
  • 指针初始化:所有指针定义后立即置NULL,使用前检查是否为NULL;
  • 禁止直接操作裸指针:封装指针操作函数,增加边界检查。

④ 使用MPU(内存保护单元)

针对Cortex-M3/M4/M7内核,FreeRTOS支持MPU,可给每个任务分配独立的内存区域,一旦访问越界,立即触发硬件异常(HardFault),精准定位问题。

⑤ 避免大局部变量,禁止无终止递归

  • 大局部变量(如>256字节)改用全局/静态变量,或动态分配;
  • 递归函数必须有明确的退出条件,且限制递归深度。
(3)排查方法(定位已发生的内存踩踏)

① 内存填充法

在栈/内存区域填充"魔术字",运行后检查是否被覆盖

② 硬件调试(精准定位)

使用调试器连接MCU:

  1. 查看内存布局(Map文件),确定任务栈、全局变量的地址范围;
  2. 给可疑内存地址设置"写断点"(Write Breakpoint),当该地址被改写时,程序暂停,直接定位到改写的代码行;
  3. 查看调用栈(Call Stack),分析函数嵌套和栈使用情况。

③ 用FreeRTOS工具监控

  • vTaskList():打印所有任务的状态、栈剩余空间,快速找到栈不足的任务;
  • Segger SystemView:可视化监控任务运行、栈使用、中断触发,定位栈溢出时机;
  • xPortGetHeapStats():查看堆内存使用情况,排查堆踩踏。

④ 编译器开启严格警告

2.4FreeRTOS栈的内存从哪里来?

FreeRTOS的"取巧"方法:定义一个巨大的全局数组作为"内存池",创建任务时从这个数组里划分一块内存作为任务栈。

创建任务时,用**pvPortMallocucHeap**里分配栈空间,栈的起始地址和大小保存在TCB里

三、任务状态与链表

1.任务状态

状态 说明 例子
运行态 (Running) 当前正在CPU上执行 任务A正在运行
就绪态 (Ready) 已准备好运行,等待调度 任务A优先级高,排队等待
阻塞态 (Blocked) 等待事件(如延时、队列、信号量) vTaskDelay(15) → 等待15ms
挂起态 (Suspended) 被显式暂停(如vTaskSuspend() 任务B被暂停,不参与调度
  • 运行态→就绪态:时间片到(同优先级任务切换),或被高优先级任务抢占;
  • 运行态→阻塞态:调用vTaskDelay(等待时间)、读队列没数据(等待事件);
  • 阻塞态→就绪态:等待的时间到了,或等待的事件发生了;
  • 运行态→挂起态:调用vTaskSuspend
  • 挂起态→就绪态:调用vTaskResume

2.三大链表

链表类型 作用 结构
就绪链表(Ready List) 管理所有处于"就绪态"的任务,调度器从中选择最高优先级的任务运行。 通常是一个数组,数组的每个索引对应一个优先级,每个元素是一个双向链表,存储该优先级下所有就绪的任务。
延迟链表(Delay List) 管理所有处于"阻塞态(等待时间)"的任务,用于处理任务延时或超时唤醒。 通常是一个或两个按超时时间(唤醒时间)排序的链表。系统时钟节拍中断会检查链表头部,将已超时的任务移回就绪链表。
挂起链表(Suspended List) 管理所有处于"挂起态"的任务,这些任务被显式暂停,不参与任何调度直到被恢复。 通常是一个简单的双向链表,将所有被挂起的任务链接在一起,不区分优先级或时间顺序。

(1)创建任务时:加入就绪链表

(2)任务调用vTaskDelay时:从就绪链表移到延迟链表

(3)Tick中断时执行xTaskIncrementTick:检查延迟链表,时间到了移回就绪链表

3.任务切换

四、调度器的核心

1. 调度的核心规则

优先找最高优先级:从就绪链表的"最高优先级"开始往下找(比如先找优先级5,再找4,…,最后找0);

同优先级轮流执行:如果最高优先级的链表有多个任务,取出第一个执行,执行完一个tick后放到链表尾部,再取下一个。

A.补充说明PendSV(Pendable Request for System Service可挂起的服务请求)(重要)

在ARM Cortex-M内核中,PendSV是NVIC(嵌套向量中断控制器)管理的一个异常源(Exception)。其核心特点如下:

  • 可手动触发:无硬件自动触发源,必须通过写NVIC的寄存器手动触发;
  • 可挂起:触发后若当前有更高优先级的中断/异常在执行,会被NVIC标记为"挂起状态",直到高优先级处理完才执行;
  • 优先级可配置:通过NVIC的优先级寄存器配置优先级,FreeRTOS中会将其设为最低优先级
// 这行代码不会立即跳转!
// 它只是把NVIC内部的一个小旗帜(Pending位)竖起来了
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; 

FreeRTOS利用这个机制,把PendSV用于任务切换。FreeRTOS有两个接口可挂起PendSV:

  • xPortSysTickHandler:SysTick中断服务函数,FreeRTOS的"心跳",定时触发调度(延时到期/时间片用完),最终通过置位PendSV触发调度;
  • portYIELD:宏定义,主动触发调度(任务主动让权),直接置位PendSV触发调度;

2. 调度的触发者:Tick中断

  • Tick中断是"定时器中断",每隔固定时间(默认1ms可设置)产生一次;
  1. 硬件触发:SysTick定时器溢出。
  2. 入口SysTick_Handler(启动文件中) -> 映射到xPortSysTickHandler(port.c)。
  3. 逻辑判断xTaskIncrementTick(tasks.c) -> 发现任务A延时结束/时间片用完,任务B优先级更高。
  4. 请求切换xPortSysTickHandler设置SCB->ICSR |= SCB_ICSR_PENDSVSET
  5. 退出SysTick:回到主程序或被打断的低优先级中断。
  6. 进入PendSVxPortPendSVHandler(port.c汇编部分)。(SysTick中断优先级比PendSV高
  7. 执行切换:保存任务A现场 -> 调度器选任务B -> 恢复任务B现场。
  8. 运行新任务:CPU开始执行任务B。
/*-----------------------------------------------------------*/
/* FreeRTOS SysTick 中断处理程序 (ARM Cortex-M)              */
/* 文件位置:FreeRTOS/Source/portable/[编译器]/[架构]/port.c */
/* 核心功能:更新系统节拍 + 决策是否需要任务切换             */
/*-----------------------------------------------------------*/

void xPortSysTickHandler( void )
{
    /* 
     * 【关键背景】
     * 在标准的 FreeRTOS Cortex-M 端口中,SysTick 中断的优先级被配置为
     * configKERNEL_INTERRUPT_PRIORITY (通常是最低优先级,如 0xFF)。
     * 
     * 这意味着:
     * 1. 当 SysTick 运行时,所有比它优先级高的中断(用户中断)本来就可以打断它。
     * 2. 但是,我们需要防止比它优先级【低】或【相等】的中断(实际上没有更低的了,主要是防止逻辑冲突)
     *    干扰 FreeRTOS 的内部临界区操作(如链表修改)。
     * 
     * 【优化策略】
     * 因为已知当前中断优先级是最低的,所以不需要保存之前的中断屏蔽状态(因为之前肯定是全开的)。
     * 直接使用 vPortRaiseBASEPRI() 快速提升优先级屏蔽阈值,比通用的 portSET_INTERRUPT_MASK_FROM_ISR() 更快。
     */
    
    /* 1. 提升 BASEPRI,屏蔽所有 <= configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断 */
    /* 这创建了一个短暂的临界区,保护下面的 xTaskIncrementTick 操作 */
    vPortRaiseBASEPRI();   
    
    {
        /* 2. 【核心逻辑】更新 RTOS 节拍计数 */
        /* 此函数位于 tasks.c,执行以下操作:
         *   a) xTickCount++ (时间加 1)
         *   b) 检查延迟链表 (Delay List),将超时任务移至就绪链表 (Ready List)
         *   c) 检查时间片轮转 (如果开启)
         * 
         * 返回值含义:
         *   pdFALSE: 没有更高优先级任务就绪,且不需要时间片轮转 -> 无需切换
         *   pdTRUE : 有更高优先级任务就绪,或时间片到期 -> 需要切换
         */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* 3. 【决策】需要上下文切换 */
            
            /* 重要设计:为什么不直接在这里切换?
             * 因为 SysTick 优先级较低(或者说为了保持中断响应快),
             * 真正的"保存/恢复寄存器"这种耗时操作应该交给优先级【最低】的 PendSV 中断去做。
             * 这样可以让高优先级的硬件中断(如电机保护、通信)随时打断任务切换过程。
             */
            
            /* 4. 【触发 PendSV】
             * 向 NVIC 的中断控制状态寄存器 (ICSR) 写入 PENDSVSET 位。
             * 这不会立即执行 PendSV,而是将其标记为"待决 (Pending)"。
             * 
             * 硬件行为:
             * 当当前中断 (SysTick) 退出后,CPU 会检查是否有 pending 的中断。
             * 由于 PendSV 优先级最低,它会等所有其他高优先级中断都处理完后,
             * 最后才执行 xPortPendSVHandler 进行真正的任务切换。
             */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }

    /* 5. 恢复中断优先级屏蔽 */
    vPortClearBASEPRIFromISR();
}

3. 任务切换的本质:保存当前任务现场 + 恢复新任务现场(进入 PendSV

在ARM Cortex-M中,异常发生时硬件会自动保存一部分寄存器(r0r3, r12, LR, PC, xPSR)到当前栈中。因此,PendSV只需保存剩余的callee-saved寄存器(r4r11)。

/*-----------------------------------------------------------*/
/* FreeRTOS PendSV 中断处理程序 (ARM Cortex-M)               */
/* 功能:实现任务切换的核心逻辑 (保存旧任务 -> 调度 -> 恢复新任务) */
/*-----------------------------------------------------------*/

__asm void xPortPendSVHandler( void )
{
    /* 声明外部变量和函数,链接器会在链接阶段填入实际地址 */
    extern uxCriticalNesting;      /* 临界区嵌套计数 */
    extern pxCurrentTCB;           /* 全局指针:指向当前运行任务的 TCB */
    extern vTaskSwitchContext;     /* C 语言函数:调度器核心,负责选择下一个最高优先级的任务 */

/* *INDENT-OFF* */
    PRESERVE8                      /* 指示编译器保持堆栈 8 字节对齐 (ARM 架构要求) */

    /* ======================================================== */
    /* 阶段 1: 保存当前任务 (Current Task) 的上下文             */
    /* ======================================================== */
    /* 此时 CPU 已经自动将 R0-R3, R12, LR, PC, xPSR 压入栈中。 */
    /* 我们需要手动保存剩下的 R4-R11。                          */

    mrs r0, psp                    /* [读] 获取当前任务的栈指针 (Process Stack Pointer) */
    isb                            /* [同步] 指令同步屏障,确保上面的 MSR 操作完成后再执行后续 */

    ldr r3, =pxCurrentTCB          /* [读] 将全局变量 pxCurrentTCB 的地址加载到 r3 */
    ldr r2, [ r3 ]                 /* [读] 解引用:r2 = pxCurrentTCB 的值 (即当前任务 TCB 结构体的地址) */

    stmdb r0 !, { r4 - r11 }       /* [写/核心] 保存剩余寄存器 */
                                   /* 含义:将 r4-r11 的值压入 r0 指向的栈内存中 */
                                   /* "db" = Decrement Before (先减地址再存,向下生长栈) */
                                   /* "!" = 写回 (Write-back):更新 r0 为新的栈顶地址 */

    str r0, [ r2 ]                 /* [写/核心] 更新 TCB 中的栈指针记录 */
                                   /* 含义:将新的栈顶地址 (r0) 写入 TCB 的第一个成员 (pxTopOfStack) */

    /* ======================================================== */
    /* 阶段 2: 调用调度器 (Scheduler) 选择新任务                */
    /* ======================================================== */
    stmdb sp !, { r3, r14 }        /* [写] 保护现场 */
                                   /* 将 r3 (pxCurrentTCB 的地址) 和 r14 (返回地址) 压入中断栈 (MSP) */

    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 
                                   /* [配置] 设置中断屏蔽阈值 */
    msr basepri, r0                /* [写] 屏蔽所有优先级 <= configMAX... 的中断 */
    dsb                            /* [同步] 数据同步屏障,确保写操作完成 */
    isb                            /* [同步] 指令同步屏障 */

    bl vTaskSwitchContext          /* [调用] 跳转到 C 语言调度器 */
                                   /* 函数内部逻辑: */
                                   /* 1. 遍历就绪链表 (Ready List) */
                                   /* 2. 找到优先级最高的就绪任务 */
                                   /* 3. 更新全局变量 pxCurrentTCB 指向这个新任务的 TCB */

    mov r0, #0                     /* [配置] 准备恢复中断 */
    msr basepri, r0                /* [写] 清除 BASEPRI,重新允许所有中断 */

    ldmia sp !, { r3, r14 }        /* [读] 恢复保护现场 */

    /* ======================================================== */
    /* 阶段 3: 恢复新任务 (Next Task) 的上下文                  */
    /* ======================================================== */
    ldr r1, [ r3 ]                 /* [读] 获取新任务的 TCB 地址 */
    ldr r0, [ r1 ]                 /* [读] 获取新任务的栈顶指针 */
    ldmia r0 !, { r4 - r11 }       /* [读/核心] 恢复剩余寄存器 */
    msr psp, r0                    /* [写/核心] 切换栈指针 */
    isb                            /* [同步] 确保 PSP 更新生效 */
    bx r14                         /* [跳转] 中断返回 */
    
    nop                            /* [填充] 空操作,通常用于对齐或调试断点 */
/* *INDENT-ON* */
}

4.特殊任务:空闲任务

  • 优先级最低(优先级0);
  • 调度器启动时自动创建;
  • 核心工作:做"清理工作"—— 比如删除任务后,释放任务的栈和TCB。

5.关键配置

配置项:configUSE_PREEMPTION(抢占式调度)

  • 设为1:开启抢占式(默认),高优先级任务抢占低优先级;
  • 设为0:关闭抢占式,任务只能主动放弃CPU,高优先级任务不会抢占。

配置项:configUSE_TIME_SLICING(时间片轮转)

  • 设为1:开启时间片轮转(默认),同优先级任务轮流执行一个tick;
  • 设为0:关闭时间片轮转,同优先级任务中,第一个任务会一直运行,直到主动放弃。

五、总结

任务 = 函数 + 独立栈:函数是业务逻辑,栈是保存局部变量、调用关系、现场的空间;

TCB是任务的"身份证":保存栈顶指针、优先级、链表节点,用来管理任务;

链表用于任务管理:就绪链表、延迟链表、挂起链表,分别管理不同状态的任务;

调度器靠Tick中断驱动:每隔一段时间检查延迟链表、切换任务;

核心调度规则:高优先级抢占,同优先级时间片轮转;

空闲任务是"兜底":优先级最低,做清理工作,还会礼让同优先级任务;

**中断优先级分层:**硬件中断(包括Tick) > PendSV中断 > 任务。

相关文章

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

PHPStan 有什么用?怎么用?

PHPStan 是一个 PHP 的静态分析工具,在不运行代码的情况下就能帮你发现潜在问题,比如:传错类型(把 string 传给接受 int 的函数)访问不存在的属性 / 方法null 没处理好永远不会执行到的代码数组 key/值类型不一致返回值不符合声明注释和真实类型不匹配它非常适合:想提升代码质量、减少线上 bug、统一团队风格的人(尤其是中大型项目)。一、PHPStan 有什么用(通俗点说)...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。