自定义RTOS内核:从零实现上下文切换与任务调度——汇编、PendSV 文章目录每日一句正能量摘要一、引言为什么要从零实现RTOS二、ARM Cortex-M 上下文切换原理2.1 Cortex-M异常模型与双栈设计2.2 异常返回机制EXC_RETURN三、PendSV专为上下文切换设计的异常3.1 为什么需要PendSV3.2 PendSV的触发与执行四、任务控制块TCB与任务栈4.1 TCB数据结构4.2 任务栈初始化五、上下文切换汇编实现5.1 完整的PendSV_Handler5.2 关键汇编指令解析六、任务调度器实现6.1 任务状态机6.2 调度器核心代码七、SysTick与调度触发7.1 SysTick配置八、完整的最小RTOS示例8.1 项目结构8.2 应用代码示例8.3 调试输出九、常见问题与调试技巧9.1 上下文切换后HardFault9.2 任务不切换9.3 栈溢出检测十、总结每日一句正能量真正的善是内心有尺眼中有度是知进退、懂取舍的清醒。有尺→知道自己底线在哪有度→知道对什么人、什么事该给几分。知进退→不盲目冲锋也不冷漠旁观懂取舍→不贪求“全善”只做有效的好。善不再是柔软而是精准的平衡。摘要摘要理解RTOS内核的最佳方式是亲手实现一个。本文从零开始基于ARM Cortex-M架构深入讲解上下文切换的汇编实现、PendSV异常的设计原理、任务控制块TCB的数据结构以及抢占式调度器的完整实现。通过详细的代码注释和架构图解帮助读者真正理解RTOS内核的工作机制。一、引言为什么要从零实现RTOS在使用FreeRTOS、RT-Thread等成熟RTOS时开发者往往将其视为黑盒——知道如何调用API却不理解底层实现。当遇到以下问题时这种知其然不知其所以然的状态就会成为瓶颈上下文切换异常任务切换后HardFault无法定位问题栈溢出调试不知道栈边界在哪里如何检测中断延迟优化不理解中断嵌套与调度延迟的关系移植到新架构需要为新的CPU架构编写移植层从零实现一个最小RTOS内核是打通这些瓶颈的最佳途径。本文将基于ARM Cortex-M3/M4架构实现一个支持抢占式调度的微型RTOS。二、ARM Cortex-M 上下文切换原理2.1 Cortex-M异常模型与双栈设计ARM Cortex-M处理器有两个栈指针MSPMain Stack Pointer用于中断和异常处理PSPProcess Stack Pointer用于任务Thread模式当发生异常时处理器自动从PSP切换到MSP这为多任务操作系统提供了天然支持。上图展示了上下文切换的完整过程任务A运行时其寄存器上下文保存在自己的栈中切换时硬件自动保存xPSR、PC、LR、R12、R3-R0软件手动保存R11-R4然后从任务B的TCB中恢复栈指针逆向操作恢复所有寄存器。2.2 异常返回机制EXC_RETURNCortex-M使用特殊的LR值EXC_RETURN控制异常返回行为LR值含义0xFFFFFFF1返回Handler模式使用MSP0xFFFFFFF9返回Thread模式使用MSP0xFFFFFFFD返回Thread模式使用PSPRTOS任务使用0xFFFFFFFD返回Thread模式并使用PSP这是任务切换的关键。三、PendSV专为上下文切换设计的异常3.1 为什么需要PendSVPendSVPendable Service Call是Cortex-M中一个特殊的可挂起异常其设计目标就是上下文切换。上图解释了PendSV的设计原理它是优先级最低的可配置异常可以被所有其他中断延迟执行。这确保了上下文切换不会打断关键的中断服务程序。关键设计SysTick是周期性中断不能用于异步任务切换SVC是同步异常优先级固定不够灵活PendSV是可挂起中断可被更高优先级中断延迟在所有其他中断完成后执行避免上下文切换打断关键ISR3.2 PendSV的触发与执行/* 1. 配置PendSV为最低优先级 */NVIC_SetPriority(PendSV_IRQn,0xFF);/* 最低优先级 *//* 2. 在SysTick或任务yield中挂起PendSV */SCB-ICSR|SCB_ICSR_PENDSVSET_Msk;/* 写PENDSVSET位 *//* 3. PendSV Handler执行上下文切换 */voidPendSV_Handler(void);四、任务控制块TCB与任务栈4.1 TCB数据结构上图展示了TCB结构和任务栈布局。TCB是调度器管理任务的核心数据结构包含栈指针、优先级、状态列表项等关键字段。/** * file tcb.h * brief 任务控制块定义 */#ifndefTCB_H#defineTCB_H#includestdint.h/* 任务状态 */typedefenum{TASK_READY0,/* 就绪态 */TASK_RUNNING,/* 运行态 */TASK_BLOCKED,/* 阻塞态 */TASK_SUSPENDED,/* 挂起态 */TASK_DELETED/* 删除态 */}TaskState_t;/* 列表项用于就绪队列和阻塞队列 */typedefstructxLIST_ITEM{volatileuint32_txItemValue;/* 排序值优先级或超时时间 */structxLIST_ITEM*pxNext;/* 下一个 */structxLIST_ITEM*pxPrevious;/* 上一个 */void*pvOwner;/* 所属TCB */void*pvContainer;/* 所在列表 */}ListItem_t;/* 任务控制块 */typedefstructtskTaskControlBlock{volatileuint32_t*pxTopOfStack;/* 当前栈顶指针 */uint32_t*pxStack;/* 栈底地址 */uint32_tuxPriority;/* 当前优先级 */uint32_tuxBasePriority;/* 基础优先级用于优先级继承 */uint32_tuxStackDepth;/* 栈深度字 */charpcTaskName[16];/* 任务名称 *//* 列表项 */ListItem_t xStateListItem;/* 状态列表项 */ListItem_t xEventListItem;/* 事件列表项 *//* 阻塞相关 */uint32_txTicksToDelay;/* 延时计数 */}TCB_t;/* 就绪列表 */typedefstructxLIST{volatileuint32_tuxNumberOfItems;ListItem_t*pxIndex;ListItem_t xListEnd;/* 哨兵节点 */}List_t;#endif/* TCB_H */4.2 任务栈初始化/** * brief 初始化任务栈模拟异常返回帧 * * Cortex-M进入异常时硬件自动保存的寄存器从高到低 * xPSR, PC, LR, R12, R3, R2, R1, R0 * * 软件需要手动保存的寄存器 * R11, R10, R9, R8, R7, R6, R5, R4 */uint32_t*pxPortInitialiseStack(uint32_t*pxTopOfStack,TaskFunction_t pxCode,void*pvParameters){/* 栈必须8字节对齐 */pxTopOfStack--;/* 模拟xPSR寄存器Thumb模式位必须置1 */*pxTopOfStack0x01000000UL;/* xPSR: T-bit set */pxTopOfStack--;/* 任务入口地址 */*pxTopOfStack(uint32_t)pxCode;/* PC */pxTopOfStack--;/* 链接寄存器任务不应返回但设置异常返回值 */*pxTopOfStack0xFFFFFFFDUL;/* LR: EXC_RETURN using PSP */pxTopOfStack--;/* R12 */*pxTopOfStack0x00000000UL;pxTopOfStack--;/* R3, R2, R1 */*pxTopOfStack0x00000000UL;/* R3 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R2 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R1 */pxTopOfStack--;/* R0 任务参数 */*pxTopOfStack(uint32_t)pvParameters;/* R0 */pxTopOfStack--;/* R11-R4 初始化为0 */*pxTopOfStack0x00000000UL;/* R11 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R10 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R9 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R8 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R7 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R6 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R5 */pxTopOfStack--;*pxTopOfStack0x00000000UL;/* R4 */returnpxTopOfStack;}五、上下文切换汇编实现5.1 完整的PendSV_Handler上图展示了PendSV_Handler的完整汇编实现流程分为三个步骤保存当前任务上下文、选择新任务、恢复新任务上下文。; file port_asm.s ; brief ARM Cortex-M3/M4 上下文切换汇编实现 AREA |.text|, CODE, READONLY THUMB ; 外部符号 EXTERN pxCurrentTCB EXTERN vTaskSwitchContext ; PendSV Handler - 上下文切换核心 PendSV_Handler PROC EXPORT PendSV_Handler ; Step 1: 保存当前任务的上下文 ; --------------------------------------------- ; 获取当前任务的PSP进程栈指针 MRS R0, PSP ; 保存R4-R11到任务栈软件保存 ; STMDB Store Multiple, Decrement Before ; R0! 先递减R0再存储更新R0 STMDB R0!, {R4-R11} ; 禁用中断防止调度器状态不一致 CPSID I ; 将新的栈顶保存到当前TCB ; pxCurrentTCB-pxTopOfStack R0 LDR R3, pxCurrentTCB ; R3 pxCurrentTCB LDR R2, [R3] ; R2 pxCurrentTCB STR R0, [R2, #0] ; TCB偏移0 pxTopOfStack ; 保存LR包含EXC_RETURN值 MOV R4, LR ; Step 2: 调用调度器选择新任务 ; --------------------------------------------- BL vTaskSwitchContext ; 恢复LR MOV LR, R4 ; Step 3: 恢复新任务的上下文 ; --------------------------------------------- ; 从新TCB获取栈顶 LDR R3, pxCurrentTCB LDR R2, [R3] LDR R0, [R2, #0] ; R0 pxTopOfStack ; 恢复R4-R11软件恢复 ; LDMIA Load Multiple, Increment After ; R0! 先加载再递增R0更新R0 LDMIA R0!, {R4-R11} ; 更新PSP为新任务的栈顶 MSR PSP, R0 ; 启用中断 CPSIE I ; 异常返回 ; LR包含0xFFFFFFFD表示使用PSP返回Thread模式 BX LR ENDP ; SVC Handler - 用于启动第一个任务 SVC_Handler PROC EXPORT SVC_Handler ; 获取当前任务的栈顶 LDR R3, pxCurrentTCB LDR R1, [R3] LDR R0, [R1, #0] ; R0 pxTopOfStack ; 恢复R4-R11 LDMIA R0!, {R4-R11} ; 更新PSP MSR PSP, R0 ; 启用中断 CPSIE I ; 异常返回使用PSP BX LR ENDP ; 启动第一个任务 vPortStartFirstTask PROC EXPORT vPortStartFirstTask ; 使用SVC启动第一个任务 ; SVC 0 会触发SVC_Handler SVC 0 ENDP ; 触发PendSV进行上下文切换 vPortYield PROC EXPORT vPortYield ; 设置PendSV挂起位 LDR R0, 0xE000ED04 ; SCB-ICSR地址 LDR R1, 0x10000000 ; PENDSVSET位 STR R1, [R0] ; 数据同步屏障确保写入完成 DSB ; 指令同步屏障确保后续指令使用新状态 ISB BX LR ENDP ALIGN END5.2 关键汇编指令解析指令作用说明MRS R0, PSP读取PSP到R0获取当前任务栈指针STMDB R0!, {R4-R11}存储多个寄存器先递减R0再存储更新R0LDMIA R0!, {R4-R11}加载多个寄存器先加载再递增R0更新R0MSR PSP, R0写入PSP设置新任务的栈指针CPSID I关闭中断临界区保护CPSIE I开启中断退出临界区DSB数据同步屏障确保内存写入完成ISB指令同步屏障刷新指令流水线六、任务调度器实现6.1 任务状态机上图展示了RTOS的任务状态机。任务可在就绪、运行、阻塞、挂起、删除五种状态间转换调度器负责选择最高优先级的就绪任务执行。6.2 调度器核心代码/** * file scheduler.c * brief 抢占式优先级调度器 */#include\tcb.h\/* 全局变量 */TCB_t*volatilepxCurrentTCBNULL;staticList_t pxReadyTasksLists[configMAX_PRIORITIES];staticvolatileuint32_tuxTopReadyPriority0;staticvolatileuint32_txTickCount0;/* 阻塞任务列表按超时时间排序 */staticList_t xDelayedTaskList1;staticList_t xDelayedTaskList2;staticList_t*volatilepxDelayedTaskList;staticList_t*volatilepxOverflowDelayedTaskList;/** * brief 初始化调度器 */voidvTaskStartScheduler(void){/* 创建空闲任务 */xReturnxTaskCreate(prvIdleTask,\IDLE\,configMINIMAL_STACK_SIZE,NULL,tskIDLE_PRIORITY,xIdleTaskHandle);/* 初始化Tick计数 */xTickCount0;/* 配置SysTick */portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();/* 启动第一个任务触发SVC异常 */vPortStartFirstScheduler();/* 不应该到达这里 */for(;;);}/** * brief 调度器核心选择最高优先级的就绪任务 */voidvTaskSwitchContext(void){/* 检查是否有任务需要解除阻塞 */prvCheckDelayedTasks();/* 找到最高优先级的就绪任务 */while(listLIST_IS_EMPTY((pxReadyTasksLists[uxTopReadyPriority]))){configASSERT(uxTopReadyPriority);uxTopReadyPriority--;}/* 获取列表中的第一个任务 */pxCurrentTCB(TCB_t*)listGET_OWNER_OF_HEAD_ENTRY((pxReadyTasksLists[uxTopReadyPriority]));}/** * brief 检查阻塞任务是否超时 */staticvoidprvCheckDelayedTasks(void){TCB_t*pxTCB;/* 检查Tick是否溢出 */if(xTickCount0){/* 交换延迟列表 */List_t*pxTemppxDelayedTaskList;pxDelayedTaskListpxOverflowDelayedTaskList;pxOverflowDelayedTaskListpxTemp;}/* 检查阻塞列表头部的任务 */while(listLIST_IS_EMPTY(pxDelayedTaskList)pdFALSE){pxTCB(TCB_t*)listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList);if(xTickCountpxTCB-xTicksToDelay){break;/* 还未超时 */}/* 将任务从阻塞列表移到就绪列表 */uxListRemove((pxTCB-xStateListItem));prvAddTaskToReadyList(pxTCB);}}/** * brief 将任务添加到就绪列表 */#defineprvAddTaskToReadyList(pxTCB)\do{\vListInsertEnd((pxReadyTasksLists[(pxTCB)-uxPriority]),\((pxTCB)-xStateListItem));\if((pxTCB)-uxPriorityuxTopReadyPriority){\uxTopReadyPriority(pxTCB)-uxPriority;\}\}while(0)/** * brief 任务延时 */voidvTaskDelay(constuint32_txTicksToDelay){uint32_txAlreadyYieldedpdFALSE;if(xTicksToDelay0){/* 禁用中断 */uint32_tuxSavedInterruptStatusportSET_INTERRUPT_MASK_FROM_ISR();/* 将任务从就绪列表移除 */if(uxListRemove((pxCurrentTCB-xStateListItem))0){/* 如果就绪列表为空更新最高优先级 */portRESET_READY_PRIORITY(pxCurrentTCB-uxPriority,uxTopReadyPriority);}/* 设置延时时间 */pxCurrentTCB-xTicksToDelayxTickCountxTicksToDelay;/* 添加到阻塞列表 */vListInsert(pxDelayedTaskList,(pxCurrentTCB-xStateListItem));/* 恢复中断 */portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);}/* 如果调度器已启动进行上下文切换 */if(xSchedulerRunning!pdFALSE){if(xAlreadyYieldedpdFALSE){portYIELD();}}}/** * brief SysTick中断处理 */voidSysTick_Handler(void){uint32_tuxSavedInterruptStatus;/* 递增Tick计数 */uxSavedInterruptStatusportSET_INTERRUPT_MASK_FROM_ISR();xTickCount;portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);/* 检查是否需要上下文切换 */if(xTaskIncrementTick()!pdFALSE){/* 挂起PendSV */portNVIC_INT_CTRL_REGportNVIC_PENDSVSET_BIT;}}/** * brief 递增Tick检查是否需要调度 */BaseType_txTaskIncrementTick(void){BaseType_t xSwitchRequiredpdFALSE;TCB_t*pxTCB;TickType_t xItemValue;/* 检查阻塞任务 */if(uxTopReadyPrioritypxCurrentTCB-uxPriority){xSwitchRequiredpdTRUE;}/* 检查时间片轮转 */#if(configUSE_TIME_SLICING1){if(listCURRENT_LIST_LENGTH((pxReadyTasksLists[pxCurrentTCB-uxPriority]))1){xSwitchRequiredpdTRUE;}}#endifreturnxSwitchRequired;}七、SysTick与调度触发7.1 SysTick配置上图展示了SysTick配置和调度触发机制。SysTick作为系统心跳定期触发调度检查实际上下文切换延迟到PendSV执行确保所有中断处理完毕。/** * brief 配置SysTick定时器 * param ticks 两次中断间的Tick数 */uint32_tSysTick_Config(uint32_tticks){/* 检查是否溢出 */if((ticks-1)SysTick_LOAD_RELOAD_Msk){return1;/* 重装载值无效 */}/* 设置重装载值 */SysTick-LOADticks-1;/* 设置中断优先级为最低 */NVIC_SetPriority(SysTick_IRQn,(1__NVIC_PRIO_BITS)-1);/* 设置当前计数值为0 */SysTick-VAL0;/* 使能SysTick 使能中断 使用处理器时钟 */SysTick-CTRLSysTick_CTRL_CLKSOURCE_Msk|SysTick_CTRL_TICKINT_Msk|SysTick_CTRL_ENABLE_Msk;return0;}/** * brief 启动调度器时配置SysTick */voidvPortSetupTimerInterrupt(void){/* 计算1ms需要的Tick数 */uint32_tulReloadValueconfigCPU_CLOCK_HZ/configTICK_RATE_HZ;/* 配置SysTick */SysTick_Config(ulReloadValue);/* 配置PendSV为最低优先级 */NVIC_SetPriority(PendSV_IRQn,configKERNEL_INTERRUPT_PRIORITY);}八、完整的最小RTOS示例8.1 项目结构mini_rtos/ ├── include/ │ ├── tcb.h # 任务控制块 │ ├── list.h # 列表管理 │ ├── portmacro.h # 移植层宏 │ └── mini_rtos.h # 主头文件 ├── port/ │ ├── port_asm.s # 汇编上下文切换 │ └── port.c # C移植层 ├── kernel/ │ ├── scheduler.c # 调度器 │ ├── tasks.c # 任务管理 │ └── list.c # 列表实现 └── main.c # 应用入口8.2 应用代码示例/** * file main.c * brief 最小RTOS演示 */#include\mini_rtos.h\/* 任务栈 */staticuint32_ttask1_stack[256];staticuint32_ttask2_stack[256];staticuint32_ttask3_stack[256];/* 任务句柄 */staticTaskHandle_t xTask1Handle;staticTaskHandle_t xTask2Handle;staticTaskHandle_t xTask3Handle;/** * brief 任务1高优先级LED闪烁 */voidvTask1(void*pvParameters){(void)pvParameters;while(1){/* LED ON */GPIO_SetBits(GPIOC,GPIO_Pin_13);vTaskDelay(pdMS_TO_TICKS(500));/* LED OFF */GPIO_ResetBits(GPIOC,GPIO_Pin_13);vTaskDelay(pdMS_TO_TICKS(500));}}/** * brief 任务2中优先级串口输出 */voidvTask2(void*pvParameters){(void)pvParameters;uint32_tcount0;while(1){printf(\Task2:count%lu\\r\\n\,count);vTaskDelay(pdMS_TO_TICKS(1000));}}/** * brief 任务3低优先级后台计算 */voidvTask3(void*pvParameters){(void)pvParameters;uint32_tsum0;while(1){/* 模拟耗时计算 */for(inti0;i1000000;i){sumi;}printf(\Task3:sum%lu\\r\\n\,sum);/* 主动让出CPU */taskYIELD();}}intmain(void){/* 硬件初始化 */SystemInit();GPIO_Config();UART_Config();printf(\MiniRTOS Starting...\\r\\n\);/* 创建任务 */xTaskCreate(vTask1,\LED\,256,NULL,3,xTask1Handle);xTaskCreate(vTask2,\UART\,256,NULL,2,xTask2Handle);xTaskCreate(vTask3,\CALC\,256,NULL,1,xTask3Handle);printf(\Tasks created.Starting scheduler...\\r\\n\);/* 启动调度器 */vTaskStartScheduler();/* 不应该到达这里 */for(;;);}8.3 调试输出MiniRTOS Starting... Tasks created. Starting scheduler... Task2: count 0 Task3: sum 499999500000 Task2: count 1 Task3: sum 499999500000 Task2: count 2 ...九、常见问题与调试技巧9.1 上下文切换后HardFault原因1栈未对齐/* 确保栈8字节对齐 */pxTopOfStack(uint32_t*)(((uint32_t)pxTopOfStack)~0x7UL);原因2EXC_RETURN值错误/* 必须使用0xFFFFFFFD返回Thread模式PSP */*pxTopOfStack0xFFFFFFFDUL;原因3PSP未正确初始化/* 首次启动任务时PSP必须指向正确的栈位置 */MSR PSP,R09.2 任务不切换原因1PendSV优先级设置错误/* PendSV必须是最低优先级 */NVIC_SetPriority(PendSV_IRQn,0xFF);原因2SysTick未配置/* 确保SysTick中断使能 */SysTick-CTRL|SysTick_CTRL_TICKINT_Msk;原因3中断被全局禁用/* 检查是否有__disable_irq()未配对 */9.3 栈溢出检测/* 在栈底填充水印值 */#defineSTACK_FILL_VALUE0xA5A5A5A5voidvInitStackWatermark(uint32_t*pxStack,uint32_tulStackDepth){for(uint32_ti0;iulStackDepth;i){pxStack[i]STACK_FILL_VALUE;}}/* 检查水印是否被破坏 */uint32_tulGetStackHighWaterMark(uint32_t*pxStack,uint32_tulStackDepth){uint32_ti0;while(iulStackDepthpxStack[i]STACK_FILL_VALUE){i;}returni;/* 返回未使用的字数 */}十、总结从零实现RTOS内核核心在于理解三个机制上下文切换通过PendSV异常保存当前任务寄存器R4-R11 PSP恢复新任务寄存器双栈设计MSP用于中断PSP用于任务通过EXC_RETURN0xFFFFFFFD自动切换调度触发SysTick定期检查是否需要切换实际切换延迟到PendSV执行掌握这些原理后阅读FreeRTOS、RT-Thread等成熟RTOS的源码将变得清晰明了。更重要的是当遇到移植问题或性能优化需求时你将具备深入分析和解决的能力。转载自https://blog.csdn.net/u014727709/article/details/162496451欢迎 点赞✍评论⭐收藏欢迎指正