Cortex-M4自制OS进入PendSV中断时current_task被覆盖致上下文切换异常
我来帮你排查这个上下文切换的异常问题——核心症状是进入PendSV中断时current_task被意外覆盖,导致切换后两个任务指向同一块栈空间。咱们从最可能的原因开始分析,一步步解决:
你的PendSV汇编代码里的栈操作逻辑混乱,导致保存的任务栈指针(sp)完全错误,甚至可能溢出覆盖全局变量current_task的内存区域。
看你这段栈保存的代码:
mrs r0, psp subs r0, #16 stmia r0!,{r4-r7} mov r4, r8 mov r5, r9 mov r6, r10 mov r7, r11 subs r0, #32 stmia r0!,{r4-r7} subs r0, #16
Cortex-M的栈是向下生长的(地址值减小的方向),而stmia是递增存储(地址值增大),这两个特性结合起来,你的栈偏移计算完全错误:
- 第一步
subs r0, #16后,r0是psp-16,执行stmia r0!,{r4-r7}会把R4-R7存到psp-16到psp-1的地址,然后r0回到psp。 - 接着
subs r0, #32让r0变成psp-32,执行stmia r0!,{r4-r7}(这里的R4-R7其实是R8-R11的值)存到psp-32到psp-17的地址,r0变成psp-16。 - 最后
subs r0, #16让r0变成psp-32,然后把这个值保存到current_task->sp。
这完全不符合Cortex-M上下文切换的栈帧规范,正确的做法应该用stmdb(递减存储)来匹配向下生长的栈,一次性保存R4-R11:
修正后的完整PendSV_Handler:
.syntax unified .thumb .global PendSV_Handler .type PendSV_Handler, %function PendSV_Handler: /* Disable interrupts: */ cpsid i /* Save R4-R11 to current task's stack */ mrs r0, psp ; 获取当前任务栈指针 stmdb r0!, {r4-r11} ; 把R4-R11压栈(stmdb先减地址再存储,适配向下生长的栈) /* Save current task's SP: */ ldr r2, =current_task ldr r1, [r2] str r0, [r1] ; 此时r0是压栈后的栈顶指针,正确保存 /* Load next task's SP: */ ldr r2, =next_task ldr r1, [r2] ldr r0, [r1] /* Restore R4-R11 from next task's stack */ ldmia r0!, {r4-r11} ; ldmia递增加载,对应压栈的顺序 msr psp, r0 ; 更新PSP为恢复后的栈顶指针 /* EXC_RETURN - Thread mode with PSP: */ ldr r0, =0xFFFFFFFD /* Enable interrupts: */ cpsie i bx r0 .size PendSV_Handler, .-PendSV_Handler
你的OS_Scheduler函数里的while循环会遍历所有delay不为0的任务,并将它们的delay减1,这逻辑是错误的:
while (1) { if (OS_tasktable.task_list[next].delay == 0) break; OS_tasktable.task_list[next].delay--; next = OS_GetNextTask(next); }
这个逻辑会导致每次调用调度器(不管是SysTick还是wait函数)都会把所有等待中的任务delay减1,这会让任务的实际等待时间远短于预期,甚至可能导致调度器选中当前任务(如果所有任务都被减到delay=0),从而出现current_task和next_task指向同一个任务的情况。
正确的做法是:
- 只在SysTick中断里统一处理所有处于等待状态的任务的delay减1;
OS_Scheduler的职责只负责从就绪任务(delay=0)中选择下一个要执行的任务,不要修改任务的delay值。
修改后的OS_Scheduler:
void OS_Scheduler(void) { current_task = &OS_tasktable.task_list[OS_tasktable.current_task]; current_task->state = OS_TASK_STATE_IDLE; int next = OS_GetNextTask(OS_tasktable.current_task); // 只找第一个就绪(delay=0)的任务 while (OS_tasktable.task_list[next].delay != 0) { next = OS_GetNextTask(next); } OS_tasktable.current_task = next; next_task = &OS_tasktable.task_list[OS_tasktable.current_task]; next_task->state = OS_TASK_STATE_ACTIVE; S32_SCB->ICSR |= S32_SCB_ICSR_PENDSVSET_MASK; }
然后在SysTick中断处理函数里添加delay减1的逻辑:
void SysTick_Handler(void) { // 遍历所有任务,将处于等待状态的任务delay减1 for (int i = 0; i < OS_TASK_COUNT; i++) { if (OS_tasktable.task_list[i].state == OS_TASK_STATE_WAITING && OS_tasktable.task_list[i].delay > 0) { OS_tasktable.task_list[i].delay--; } } OS_Scheduler(); }
同时,__os_wait_ms函数里要把任务状态设为WAITING:
void __os_wait_ms(unsigned int ms) { // 关中断,避免被SysTick打断 cpsid i; struct OS_task * current = &OS_tasktable.task_list[OS_tasktable.current_task]; current->delay = ms * OS_tasktable.delay_factor; current->state = OS_TASK_STATE_WAITING; // 标记为等待状态 OS_Scheduler(); cpsie i; return; }
虽然你已经给current_task和next_task加了volatile修饰,但要确保:
- 所有访问这两个全局变量的地方(线程上下文、中断上下文)都直接从内存读取,没有被编译器优化;
- 任务结构体的
sp成员也必须是volatile的,避免编译器优化掉对它的读写(你已经做到了)。
最后,检查你的任务栈初始化是否正确,确保每个任务的栈有足够的空间(至少要能容纳上下文切换时的R4-R11寄存器,加上任务本身的局部变量和函数调用栈)。如果栈空间不足,会导致栈溢出,覆盖相邻的全局变量(比如current_task),这也会出现你描述的现象。
可以通过调整链接脚本,把任务栈放在单独的内存区域,或者添加栈溢出检测(比如在栈底放一个魔术字,定期检查是否被覆盖)。
内容的提问来源于stack exchange,提问作者R.S.

