You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cortex-M4自制OS进入PendSV中断时current_task被覆盖致上下文切换异常

我来帮你排查这个上下文切换的异常问题——核心症状是进入PendSV中断时current_task被意外覆盖,导致切换后两个任务指向同一块栈空间。咱们从最可能的原因开始分析,一步步解决:

1. PendSV中断的上下文保存汇编逻辑错误(最可能的根因)

你的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
2. 调度器中的delay处理逻辑错误

你的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;
}
3. 全局变量访问的一致性验证

虽然你已经给current_task和next_task加了volatile修饰,但要确保:

  • 所有访问这两个全局变量的地方(线程上下文、中断上下文)都直接从内存读取,没有被编译器优化;
  • 任务结构体的sp成员也必须是volatile的,避免编译器优化掉对它的读写(你已经做到了)。
4. 任务栈的边界检查

最后,检查你的任务栈初始化是否正确,确保每个任务的栈有足够的空间(至少要能容纳上下文切换时的R4-R11寄存器,加上任务本身的局部变量和函数调用栈)。如果栈空间不足,会导致栈溢出,覆盖相邻的全局变量(比如current_task),这也会出现你描述的现象。

可以通过调整链接脚本,把任务栈放在单独的内存区域,或者添加栈溢出检测(比如在栈底放一个魔术字,定期检查是否被覆盖)。

内容的提问来源于stack exchange,提问作者R.S.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:32:59