拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FreeRTOS内核(二)任务的核心机制

一、任务的本质函数 独立栈函数比较好理解// 任务函数示例无限循环 void Task1(void *pvParameters) { while(1) { // 任务业务逻辑比如打印、控制LED printf(Task1 Running\n); vTaskDelay(1000); // 主动放弃CPU } }但是栈是用来干嘛的(注意是独立栈)保存局部变量函数内的局部变量如数组、临时变量存在栈里保存调用关系函数嵌套调用时返回地址LR存在栈里保存任务现场任务被切换中断时CPU 寄存器的值存在栈里恢复时从栈里读回。二、重新理解任务创建1、任务创建函数与TCBBaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // 1. 任务函数指针必传 const char * const pcTaskName, // 2. 任务名可选 uint16_t usStackDepth, // 3. 栈大小字节如 100*4400 void *pvParameters, // 4. 函数参数如 int arg10 UBaseType_t uxPriority, // 5. 任务优先级0~configMAX_PRIORITIES-1 TaskHandle_t *pvCreatedTask // 6. 任务句柄输出用于操作任务 );结合我们之前学的汇编语言参数作用代码示例pvTaskCode任务函数地址PC寄存器指向它Task1函数地址 →xTaskCreate(Task1, ...)usStackDepth栈大小字节→ 由vPortMalloc()分配usStackDepth 100→ 100*4400字节栈pvParameters传递给任务函数的参数存入R0pvParameters arg→ 任务函数Task1(int *arg)uxPriority任务优先级决定调度顺序uxPriority 1优先级 1 比 0 高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 1→ 优先级 1xStateListItem链表节点连接到就绪列表/阻塞列表vListInsertEnd(xReadyTasksLists[uxPriority], pxNewTCB-xStateListItem);pcTaskName任务名调试用pcTaskName Task1TCB 里怎么没看到 “函数指针”函数指针和函数参数不是直接存在 TCB 里而是作为 “初始现场” 存在任务栈里任务刚创建时会在栈里预先填充PC 寄存器的值 任务函数的地址让任务第一次运行时从函数入口开始R0 寄存器的值 任务函数的参数pvParameters任务第一次被调度时从栈里恢复这些寄存器就自动跳转到任务函数执行了。2、任务栈FreeRTOS 任务栈大小的单位xTaskCreate的uxStackDepth参数是栈项数不是字节数针对 Cortex-M 内核STM32栈按4 字节1 个字对齐因此栈项数 ×4 实际栈字节数例uxStackDepth128→ 实际栈大小 128×4512 字节uxStackDepth256→ 1024 字节。栈增长方向Cortex-M 内核栈从高地址向低地址增长栈溢出会覆盖低地址的内存触发内存踩踏。关于这个内存分配可以参考一下单片机内存分配管理笔记-CSDN博客2.1栈的大小怎么确定栈的大小取决于两点void SubFunction(void) { // 延时循环 int i; // Declare at function beginning (C90 requirement) for(i 0; i 100; i) { // Empty loop body for delay } } void vTask2( void *pvParameters ) { const char *pcTaskName T2 run\r\n; // 定义100个int的数组占100*4400字节 volatile int array[100]; // 防止编译器优化故意使用数组 array[0] 123; /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务1的信息 */ printf( pcTaskName ); /* 延迟一会(比较简单粗暴) */ SubFunction(); } }1局部变量的大小反汇编验证编译后会看到0x190 是 400 字节加 4 字节临时变量共 0x190。2函数调用的深度如果任务里嵌套调用子函数LR返回地址会被覆盖必须把 LR 压入栈后面才可POP出全流程编译器自动执行我们只需要注意会不会栈溢出即可2.2实际开发中栈大小的设置原则估算局部变量大小 × 2保险起见留余量之后使用 FreeRTOS 自带工具验证核心方法FreeRTOS 提供uxTaskGetStackHighWaterMark()函数栈高水位标记能返回任务栈的剩余最小空间栈项数是判断栈是否够用的核心工具。//开启宏定义注意这个宏定义只适合调试阶段发布后就不要使用了 #ifndef INCLUDE_uxTaskGetStackHighWaterMark #define INCLUDE_uxTaskGetStackHighWaterMark 1 #endif // 获取栈剩余最小空间栈项数 UBaseType_t uxHighWaterMark uxTaskGetStackHighWaterMark(xLedTaskHandle); // 打印比如返回20表示任务运行以来栈最少剩余20个栈项80字节 printf(LED Task Stack High Water Mark: %u\n, uxHighWaterMark);关键判断规则若uxHighWaterMark返回值大于 0 且大于栈大小的 20%栈大小足够比如栈大小 128剩余≥25说明有足够余量若返回值接近 0比如≤5栈大小不足需增大比如从 128→256若返回值等于栈大小任务几乎没使用栈可减小比如从 256→128。底层实现逻辑学习后可以自己手搓先把整个任务栈空间填充一个固定的 “魔术字”默认是tskSTACK_FILL_BYTE 为0x5A可在源码中修改这个操作是栈高水位检测的基础//位于prvInitialiseNewTask函数中可看到初始化任务TCB堆栈 /* Avoid dependency on memset() if it is not required. */ #if ( tskSET_NEW_STACKS_TO_KNOWN_VALUE 1 ) { /* Fill the stack with a known value to assist debugging. */ ( void ) memset( pxNewTCB-pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) ); } #endif /* tskSET_NEW_STACKS_TO_KNOWN_VALUE */ //其中宏定义由INCLUDE_uxTaskGetStackHighWaterMark 开启 #if ( ( configCHECK_FOR_STACK_OVERFLOW 1 ) || ( configUSE_TRACE_FACILITY 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark2 1 ) ) #define tskSET_NEW_STACKS_TO_KNOWN_VALUE 1 #else #define tskSET_NEW_STACKS_TO_KNOWN_VALUE 0 #endif当任务运行后堆栈会被使用此时从栈底开始魔术字会被替代读取剩余魔术字的个数就可知剩余堆栈大小从栈底读会更方便因为是从栈顶开始覆盖的UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ) { TCB_t * pxTCB; // 定义任务控制块指针 uint8_t * pucEndOfStack;// 栈遍历的起始地址 UBaseType_t uxReturn; // 返回值剩余栈项数 // 步骤1获取任务TCBxTaskNULL时返回当前运行任务的TCB // prvGetTCBFromHandle是内部函数逻辑xTask为NULL→取当前任务TCB否则直接用传入的TCB pxTCB prvGetTCBFromHandle( xTask ); // 步骤2根据栈增长方向确定遍历的起始地址跨架构兼容的核心 #if portSTACK_GROWTH 0 // 栈从高地址→低地址增长Cortex-M、ARM { // 起始地址 栈底地址pxStack // Cortex-M中pxStack是栈的最低地址栈底栈顶是pxEndOfStack最高地址 pucEndOfStack ( uint8_t * ) pxTCB-pxStack; } #else // 栈从低地址→高地址增长如x86 { // 起始地址 栈顶地址pxEndOfStack pucEndOfStack ( uint8_t * ) pxTCB-pxEndOfStack; } #endif // 步骤3调用底层函数统计从起始地址开始的连续魔术字数量剩余栈空间 uxReturn ( UBaseType_t ) prvTaskCheckFreeStackSpace( pucEndOfStack ); // 步骤4返回剩余栈项数 return uxReturn; } //起始地址开始的连续魔术字数量 static configSTACK_DEPTH_TYPE prvTaskCheckFreeStackSpace( const uint8_t * pucStackByte ) { uint32_t ulCount 0U; // 统计连续魔术字的字节数 // 步骤1循环遍历栈字节直到找到第一个不是魔术字的字节 // 核心逻辑只要当前字节等于tskSTACK_FILL_BYTE0x5A就继续遍历 while( *pucStackByte ( uint8_t ) tskSTACK_FILL_BYTE ) { // 步骤2根据栈增长方向移动遍历指针跨架构兼容的关键 // Cortex-M下portSTACK_GROWTH-1 → pucStackByte - (-1) → pucStackByte指针1 // 也就是从栈底0x20001000向栈顶0x20001400遍历 pucStackByte - portSTACK_GROWTH; // 步骤3统计连续魔术字的字节数 ulCount; } // 步骤4将字节数转换为栈项数Cortex-M下1栈项4字节 // 比如ulCount200字节 → 200/450栈项 ulCount / ( uint32_t ) sizeof( StackType_t ); // 步骤5返回栈项数剩余栈空间 return ( configSTACK_DEPTH_TYPE ) ulCount; }其实FreeRTOS提供了两种方法对比维度uxTaskGetStackHighWaterMarkv1uxTaskGetStackHighWaterMark2v2遍历范围从起始地址遍历遇到第一个非魔术字就停止仅统计 “连续未被覆盖的魔术字”遍历整个栈空间统计所有未被覆盖的魔术字全程遍历检测精度低可能误判无法反映真实最小剩余空间高精准统计运行以来栈剩余的最小值无漏判CPU 开销小遍历到第一个非魔术字即终止稍大遍历整个栈空间但实际影响可忽略设计定位早期版本性能优先升级版精度优先FreeRTOS 推荐使用适用场景性能极度敏感、栈使用简单的场景调试 / 排查栈溢出、需要精准检测的场景以 Cortex-M 为例模拟遍历过程假设任务栈总大小 1024 字节256 栈项栈底0x20001000栈顶0x20001400任务运行后使用了 224 字节56 栈项栈指针PSP指向0x200010E0栈填充的魔术字是0x5A。遍历过程起始地址pucStackByte 0x20001000栈底*pucStackByte0x5A→ 进入循环pucStackByte - (-1)→ 指针 1 →0x20001001ulCount1重复步骤直到指针到0x200010E0栈使用的边界此时*pucStackByte≠0x5A被任务运行改写→ 退出循环此时ulCount224字节因为 0x20001000 到 0x200010DF 共 224 字节都是 0x5A转换为栈项数224/456 栈项 → 返回 56即 “任务运行以来最少剩余 56 栈项224 字节”。2.3发生内存踩踏怎么办关于这个内存分配可以参考一下单片机内存分配管理笔记-CSDN博客1内存踩踏的核心定义内存踩踏也叫内存越界 / 缓冲区溢出是指程序访问了超出分配给它的内存范围的地址导致覆盖了其他内存区域的数据比如任务栈溢出覆盖相邻任务的栈、数组越界写覆盖全局变量。嵌入式中内存踩踏分两类栈踩踏最常见任务栈溢出、数组越界写栈内存堆踩踏堆内存越界写、free 后重复使用野指针、内存碎片导致的越界。成因类型具体例子任务栈过小导致栈溢出任务栈大小 128 栈项512 字节但局部数组占 600 字节栈溢出覆盖相邻内存数组 / 缓冲区越界读写char buf[10]; buf[10] 0;下标 10 超出 buf 的 0~9 范围指针操作不当野指针指向已释放内存的指针、空指针解引用、指针偏移越界p p 100中断嵌套过深多级中断嵌套消耗任务栈超出预期递归调用无终止递归函数无退出条件不断压栈导致溢出堆内存管理不当pvPortMalloc(10)后写入 15 字节数据free 后未置 NULL重复使用2预防方法核心从源头避免① 合理设置任务栈大小最关键结合前面的 “栈高水位” 方法确保每个任务的栈有足够余量≥20%。② 开启 FreeRTOS 栈溢出检测FreeRTOS 提供两种栈溢出检测机制在FreeRTOSConfig.h中配置configCHECK_FOR_STACK_OVERFLOW1检测栈指针是否超出栈范围简单开销小configCHECK_FOR_STACK_OVERFLOW2检测栈末尾的 “魔术字” 是否被覆盖更精准开销稍大。//开启栈溢出钩子函数宏定义 #ifndef configCHECK_FOR_STACK_OVERFLOW #define configCHECK_FOR_STACK_OVERFLOW 1 #endif //钩子函数Hook Function是一种在程序运行过程中由系统在特定时 //刻或事件发生时自动调用的函数。它们通常用于拦截系统级事件或消息允 //许程序在这些事件发生时执行预定义的操作。钩子函数不是由用户直接触发 //而是由系统在满足特定条件时自动执行的。 // 栈溢出钩子函数检测到溢出时调用需要自己写函数内容 void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { // 打印溢出的任务名或触发硬件报警如LED常亮 printf(Stack Overflow: %s\n, pcTaskName); // 紧急处理停止任务/重启系统 vTaskDelete(xTask); }③ 禁止数组越界规范指针使用数组操作前做边界检查if(i sizeof(buf)/sizeof(buf[0])) { buf[i] 0; }指针初始化所有指针定义后立即置 NULL使用前检查是否为 NULL禁止直接操作裸指针封装指针操作函数增加边界检查。④ 使用 MPU内存保护单元针对 Cortex-M3/M4/M7 内核FreeRTOS 支持 MPU可给每个任务分配独立的内存区域一旦访问越界立即触发硬件异常HardFault精准定位问题。//这个我不太懂等以后有事件仔细研究研究⑤ 避免大局部变量禁止无终止递归大局部变量如 256 字节改用全局 / 静态变量或动态分配递归函数必须有明确的退出条件且限制递归深度。3排查方法定位已发生的内存踩踏① 内存填充法在栈 / 内存区域填充 “魔术字”运行后检查是否被覆盖② 硬件调试精准定位使用 J-Link/ST-Link 连接 MCU通过调试器Keil/IAR/VSCode查看内存布局Map 文件确定任务栈、全局变量的地址范围给可疑内存地址设置 “写断点”Write Breakpoint当该地址被改写时程序暂停直接定位到改写的代码行查看调用栈Call Stack分析函数嵌套和栈使用情况。内存断点中断产生查看调用深度使用情况③ 用 FreeRTOS 工具监控vTaskList()打印所有任务的状态、栈剩余空间快速找到栈不足的任务Segger SystemView可视化监控任务运行、栈使用、中断触发定位栈溢出时机xPortGetHeapStats()查看堆内存使用情况排查堆踩踏。④ 编译器开启严格警告2.4FreeRTOS栈的内存从哪里来FreeRTOS 的 “取巧” 方法定义一个巨大的全局数组作为 “内存池”创建任务时从这个数组里划分一块内存作为任务栈。创建任务时用pvPortMalloc从ucHeap里分配栈空间栈的起始地址和大小保存在 TCB 里三、任务状态与链表1.任务状态状态说明例子运行态 (Running)当前正在 CPU 上执行任务 A 正在运行就绪态 (Ready)已准备好运行等待调度任务 A 优先级高排队等待阻塞态 (Blocked)等待事件如延时、队列、信号量vTaskDelay(10)→ 等待 10ms挂起态 (Suspended)被显式暂停如vTaskSuspend()任务 B 被暂停不参与调度运行态 → 就绪态时间片到同优先级任务切换或被高优先级任务抢占运行态 → 阻塞态调用vTaskDelay等待时间、读队列没数据等待事件阻塞态 → 就绪态等待的时间到了或等待的事件发生了运行态 → 挂起态调用vTaskSuspend挂起态 → 就绪态调用vTaskResume。2.三大链表链表类型作用结构就绪链表Ready List管理所有处于“就绪态”的任务调度器从中选择最高优先级的任务运行。通常是一个数组数组的每个索引对应一个优先级每个元素是一个双向链表存储该优先级下所有就绪的任务。延迟链表Delay List管理所有处于“阻塞态等待时间”的任务用于处理任务延时或超时唤醒。通常是一个或两个按超时时间唤醒时间排序的链表。系统时钟节拍中断会检查链表头部将已超时的任务移回就绪链表。挂起链表Suspended List管理所有处于“挂起态”的任务这些任务被显式暂停不参与任何调度直到被恢复。通常是一个简单的双向链表将所有被挂起的任务链接在一起不区分优先级或时间顺序。1创建任务时加入就绪链表2任务调用 vTaskDelay 时从就绪链表移到延迟链表3Tick 中断时执行xTaskIncrementTick检查延迟链表时间到了移回就绪链表3.任务切换四、调度器的核心1. 调度的核心规则优先找最高优先级从就绪链表的 “最高优先级” 开始往下找比如先找优先级 4再找 3…最后找 0同优先级轮流执行如果最高优先级的链表有多个任务取出第一个执行执行完一个 tick 后放到链表尾部再取下一个。A.补充说明PendSVPendableRequest forSystemService可挂起的服务请求重要在 ARM Cortex-M 内核中PendSV 是NVIC (嵌套向量中断控制器)管理的一个异常源 (Exception)。其核心特点如下可手动触发无硬件自动触发源必须通过写 NVIC 的寄存器手动触发可挂起触发后若当前有更高优先级的中断 / 异常在执行会被 NVIC 标记为 “挂起状态”直到高优先级处理完才执行优先级可配置通过 NVIC 的优先级寄存器 配置优先级FreeRTOS 中会将其设为最低优先级。// 这行代码不会立即跳转 // 它只是把 NVIC 内部的一个小旗帜 (Pending 位) 竖起来了 SCB-ICSR | SCB_ICSR_PENDSVSET_Msk;FreeRTOS利用这个机制把PendSV用于任务切换。FreeRTOS有两个接口可挂起PendSVxPortSysTickHandlerSysTick 中断服务函数FreeRTOS 的 “心跳”定时触发调度延时到期 / 时间片用完最终通过置位 PendSV 触发调度portYIELD宏定义主动触发调度任务主动让权直接置位 PendSV 触发调度2. 调度的触发者Tick 中断Tick 中断是 “定时器中断”每隔固定时间默认 1ms 可设置产生一次硬件触发SysTick 定时器溢出。入口SysTick_Handler(启动文件中) - 映射到xPortSysTickHandler(port.c)。逻辑判断xTaskIncrementTick(tasks.c) - 发现任务 A 延时结束/时间片用完任务 B 优先级更高。请求切换xPortSysTickHandler设置SCB-ICSR | SCB_ICSR_PENDSVSET。退出 SysTick回到主程序或被打断的低优先级中断。进入 PendSVxPortPendSVHandler(port.c汇编部分)。SysTick中断优先级比PendSV高执行切换保存任务 A 现场 - 调度器选任务 B - 恢复任务 B 现场。运行新任务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; /* 宏定义展开通常是 * #define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) ) * #define portNVIC_PENDSVSET_BIT ( 1UL 28UL ) */ } } /* 5. 恢复中断优先级屏蔽 */ /* 将 BASEPRI 清零或恢复到允许所有用户中断的状态 * 注意这里不是恢复“旧值”而是直接清除因为我们在入口处就知道旧值是全开状态。 * 这使得中断嵌套可以立即恢复高优先级中断可以再次响应。 */ vPortClearBASEPRIFromISR(); /* 函数结束返回中断向量表指定的返回地址。 * 如果刚才触发了 PendSV且没有更高优先级中断插入 * CPU 将在返回后立即进入 PendSV_Handler。 */ }3. 任务切换的本质保存当前任务现场 恢复新任务现场进入 PendSV在 ARM Cortex-M 中异常发生时硬件会自动保存一部分寄存器r0~r3, r12, LR, PC, xPSR到当前栈中。因此PendSV 只需保存剩余的 callee-saved 寄存器r4~r11。/*-----------------------------------------------------------*/ /* 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) */ /* r0 现在指向当前任务栈顶 (即自动压栈后的位置) */ isb /* [同步] 指令同步屏障确保上面的 MSR 操作完成后再执行后续 */ ldr r3, pxCurrentTCB /* [读] 将全局变量 pxCurrentTCB 的地址加载到 r3 */ ldr r2, [ r3 ] /* [读] 解引用r2 pxCurrentTCB 的值 (即当前任务 TCB 结构体的地址) */ /* 此时 r2 指向当前正在运行的任务控制块 */ stmdb r0 !, { r4 - r11 } /* [写/核心] 保存剩余寄存器 */ /* 含义将 r4-r11 的值压入 r0 指向的栈内存中 */ /* db Decrement Before (先减地址再存向下生长栈) */ /* ! 写回 (Write-back)更新 r0 为新的栈顶地址 */ /* 此时当前任务的 R4-R11 已安全存入其私有栈中 */ str r0, [ r2 ] /* [写/核心] 更新 TCB 中的栈指针记录 */ /* 含义将新的栈顶地址 (r0) 写入 TCB 的第一个成员 (pxTopOfStack) */ /* 这样下次恢复该任务时就知道从哪里开始弹栈了 */ /* 此时当前任务现场保存完毕可以安全挂起 */ /* */ /* 阶段 2: 调用调度器 (Scheduler) 选择新任务 */ /* */ /* 在调用 C 函数前必须保护可能被 C 函数破坏的寄存器 (r3, r14) */ stmdb sp !, { r3, r14 } /* [写] 保护现场 */ /* 将 r3 (pxCurrentTCB 的地址) 和 r14 (返回地址) 压入中断栈 (MSP) */ /* 防止 vTaskSwitchContext 修改它们 */ mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* [配置] 设置中断屏蔽阈值 */ msr basepri, r0 /* [写] 屏蔽所有优先级 configMAX... 的中断 */ /* 确保调度过程不被低优先级中断打断 (临界区) */ dsb /* [同步] 数据同步屏障确保写操作完成 */ isb /* [同步] 指令同步屏障 */ bl vTaskSwitchContext /* [调用] 跳转到 C 语言调度器 */ /* 函数内部逻辑 */ /* 1. 遍历就绪链表 (Ready List) */ /* 2. 找到优先级最高的就绪任务 */ /* 3. 更新全局变量 pxCurrentTCB 指向这个新任务的 TCB */ /* 注意此时 pxCurrentTCB 的值已经变了 */ mov r0, #0 /* [配置] 准备恢复中断 */ msr basepri, r0 /* [写] 清除 BASEPRI重新允许所有中断 */ ldmia sp !, { r3, r14 } /* [读] 恢复保护现场 */ /* 从 MSP 弹出 r3 和 r14 */ /* 关键点r3 依然持有 pxCurrentTCB 的【地址】 */ /* 但 r3 指向的那个内存里的【值】已经是新任务的 TCB 地址了 */ /* */ /* 阶段 3: 恢复新任务 (Next Task) 的上下文 */ /* */ /* 此时 pxCurrentTCB 已经指向了新选出的任务 */ ldr r1, [ r3 ] /* [读] 获取新任务的 TCB 地址 */ /* r1 *pxCurrentTCB (新任务的结构体指针) */ ldr r0, [ r1 ] /* [读] 获取新任务的栈顶指针 */ /* r0 *(新任务 TCB) 新任务上次保存的 pxTopOfStack */ /* 此时 r0 指向新任务栈中保存 R4-R11 的位置 */ ldmia r0 !, { r4 - r11 } /* [读/核心] 恢复剩余寄存器 */ /* 含义从 r0 指向的栈内存中弹出数据到 r4-r11 */ /* ! 更新 r0 为弹出后的新栈顶地址 */ /* 此时新任务的 R4-R11 已恢复到 CPU 寄存器中 */ msr psp, r0 /* [写/核心] 切换栈指针 */ /* 将 CPU 的 PSP 寄存器更新为新任务的栈顶地址 */ /* 此时CPU 的栈环境已经完全切换到了新任务 */ isb /* [同步] 确保 PSP 更新生效 */ bx r14 /* [跳转] 中断返回 */ /* 跳转到 r14 (中断入口时的返回地址) */ /* 【硬件魔术时刻】 */ /* CPU 检测到这是异常返回会自动执行以下操作 */ /* 1. 使用当前的 PSP (新任务的栈) 进行出栈 */ /* 2. 自动弹出 R0-R3, R12, LR, PC, xPSR */ /* 3. PC 被更新为新任务的入口地址 */ /* 4. CPU 模式切回 Thread 模式开始执行新任务 */ nop /* [填充] 空操作通常用于对齐或调试断点 */ /* *INDENT-ON* */ } /*-----------------------------------------------------------*/疑问为什么 PendSV 中需要手动保存r4~r111在正常中断函数中如果中断函数内部用到了 r4~r11被调用者保存寄存器编译器会自动生成代码先压栈保存r4~r11函数返回前自动弹出恢复注意此时压栈的指针是MSP整个过程中r4~r11 只是临时借用中断栈存放用完就丢弃不触碰任务自己的 PSP 栈2在FreeRTOS-PendSV切换任务中与普通中断返回地址不一样任务切换的 PendSV中我们故意不返回原来的任务而是要跳到新任务去执行。所以任务上下文必须完整存在任务自己的 PSP 栈里不能存在 MSP需要手动换栈指针。另外任务栈必须有固定统一的布局编译器生成的是 “按需保存”、布局不固定4.特殊任务空闲任务优先级最低优先级 0调度器启动时自动创建核心工作做 “清理工作”—— 比如删除任务后释放任务的栈和 TCB。5.关键配置配置项configUSE_PREEMPTION抢占式调度设为 1开启抢占式默认高优先级任务抢占低优先级设为 0关闭抢占式任务只能主动放弃 CPU高优先级任务不会抢占。配置项configUSE_TIME_SLICING时间片轮转设为 1开启时间片轮转默认同优先级任务轮流执行一个 tick设为 0关闭时间片轮转同优先级任务中第一个任务会一直运行直到主动放弃。五、总结任务 函数 独立栈函数是业务逻辑栈是保存局部变量、调用关系、现场的空间TCB 是任务的 “身份证”保存栈顶指针、优先级、链表节点用来管理任务链表用于任务管理就绪链表、延迟链表、挂起链表分别管理不同状态的任务调度器靠 Tick 中断驱动每隔一段时间检查延迟链表、切换任务核心调度规则高优先级抢占同优先级时间片轮转空闲任务是 “兜底”优先级最低做清理工作还会礼让同优先级任务中断优先级分层硬件中断(包括Tick) PendSV 中断 任务。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门