网站Logo 网友马大帅的博客

riscv Freertos 调度机制

ughostx
13
2026-01-13

轮询式调度机制介绍

  1. 任务 A 调用 vTaskDelay( x ) → 内部 portYIELD()

  2. 触发 ecall → 异常向量 0 → freertos_risc_v_exception_handler

  3. SAVE ctx → vTaskSwitchContext() 选出任务 B

  4. RESTORE ctx → mret → 任务 B 继续执行

  5. 当 CPU 空闲进入 Idle → IdleHook 软算 Tick,如需切换重复 1-4。

image-cBFz.png

异常 & 中断入口(vectored)

mtvec 已设置为 freertos_vector_table|1,因此:

  • 异常(ecall、非法指令…)

cause[63]=0 ⇒ 同步,PC 跳到 BASE(表首地址,对应 vector.S:IRQ_0)→j freertos_risc_v_exception_handler.

  • 中断(Software/MachineTimer/External)

cause[63]=1 ⇒ 异步,PC = BASE + 4×IRQn,vector.S 已为每个槽写:

  j freertos_risc_v_interrupt_handler    // 一般外设
  j freertos_risc_v_mtimer_interrupt_handler // IRQ7

启动与第一条任务

  1. start.S(build/gcc/start.S)

    1. reset_vector → _start:初始化栈指针、BSS,随后 jal main。

    2. 此时 CPU 位于 M-mode,mstatus.MIE=0(关全局中断)。

  2. main.c

    1. mtvec ← freertos_vector_table | 0x1 → 进入“Machine-vectored”模式。

    2. 调用 main_blinky() 创建 2 任务 + 1 队列;随后 vTaskStartScheduler()。

  3. vTaskStartScheduler()(tasks.c)

    1. 为 Idle/Timer 任务分配(静态)TCB。

    2. 调用 xPortStartScheduler() → 进一步跳至汇编 xPortStartFirstTask。

  4. xPortStartFirstTask(portASM.S)

    1. 从 pxCurrentTCB 取首任务栈顶,执行 portcontextRESTORE_CONTEXT:

  5. 把 31 个通用寄存器、mstatus、mepc 等一并从任务栈弹出。

    1. mret → CPU 直接跳到该任务入口,开始多任务世界。

简单任务切换

  1. 软件 Tick(IdleHook)

FreeRTOSConfig.h 把 configMTIME_BASE_ADDRESS 设 0 ⇒ 禁用硬件定时器,vApplicationIdleHook() 里软件读取 mcycle,达到 25 000 周期就:

   if( xTaskIncrementTick() ) taskYIELD();

相当于 1 kHz “软时钟”。若新任务就绪则调用 taskYIELD()(宏为 ecall)。

  1. 显示让出(portYIELD / vTaskDelay 等)

  • portmacro.h:#define portYIELD() __asm volatile("ecall");

  • 任何任务调用阻塞 API 最终执行 ecall 或 portYIELD_WITHIN_API(),进入同步异常流程。

(若你启用 MTIME,中断则从 vector.S 的 IRQ_7 跳转 freertos_risc_v_mtimer_interrupt_handler,流程与下文异步中断相同,只是入口不同。)

上下文保护

  1. portContext.h 里宏族

portcontextSAVE_INTERRUPT_CONTEXT
portcontextSAVE_EXCEPTION_CONTEXT
portcontextRESTORE_CONTEXT

主要做三件事:a) 把 31 个 GPR + mstatus + mepc + 额外寄存器保存到 当前任务栈;b) 把 sp 切换到 ISR 专用栈 xISRStackTop(防止嵌套打穿任务栈);c) 退出前反向弹栈 + mret 恢复 mepc/mstatus。

  1. 差异点

  • 异常宏 会把 mepc+4 存回(确保返回到触发指令之后)。

  • 中断宏 直接保存原 mepc;软中断/硬中断都要“回到被打断的指令”。

调度路径

【同步异常:ecall / portYIELD】

vector.S → freertos_risc_v_exception_handler

① portcontextSAVE_EXCEPTION_CONTEXT

② 判断 mcause==11?(M-mode ecall)

  • 是:call vTaskSwitchContext(C 级调度算法)

  • 否:转 freertos_risc_v_application_exception_handler

③ portcontextRESTORE_CONTEXT → 新任务 mret

【异步中断:MachineTimer / 外设】

vector.S → freertos_risc_v_mtimer_interrupt_handler / freertos_risc_v_interrupt_handler

① portcontextSAVE_INTERRUPT_CONTEXT

② (如果是 MTIMER) portUPDATE_MTIMER_COMPARE_REGISTER 更新下一 Tick

  • call xTaskIncrementTick

  • 若返回 pdTRUE → call vTaskSwitchContext

③ portcontextRESTORE_CONTEXT → 新/原任务 mret

ECALL和MRET

ecall —— 主动触发一次「同步异常」
1) 指令功能
RISC-V 定义 ecall 为 Environment Call。
执行时不会继续走下一条指令,而是:
mcause ← 11(代表 Environment Call from M-mode)mepc ← 触发 ecall 的地址PC ← mtvec(向量基址)或向量槽2) 在 FreeRTOS 中的用途
portmacro.h: #define portYIELD() __asm volatile("ecall")
任何需要“自愿让出 CPU”的 API(vTaskDelay() / taskYIELD() 等)最终都会执行 portYIELD(),于是进入同步异常流程:
   ecall
      └→ freertos_risc_v_exception_handler
            ├─ portcontextSAVE_EXCEPTION_CONTEXT        // 把当前任务寄存器压栈
            ├─ if (mcause==11) vTaskSwitchContext();    // C 级调度,挑选下一任务
            └─ portcontextRESTORE_CONTEXT → mret
通过这种方式,不依赖硬件中断也能完成上下文切换。3) 为什么不用 SRET?
整个系统运行在 M-mode(特权最高),因此所有陷入都走 mret 系列;没有 S-mode/U-mode 就无需 sret/uret。
mret —— 从陷入返回、恢复上下文
1) 指令功能
Machine RETurn,从异常/中断处理器返回。
取出 mstatus.MPIE → MIE,恢复中断使能位;
PC ← mepc(异常入口保存的返回地址);
同时恢复 mstatus.MPP 等字段,可完成特权级切换。
2) 在 FreeRTOS 中的具体流程a) portcontextRESTORE_CONTEXT 最后一步执行 mret。b) 在执行前,汇编已把 新任务 的寄存器、mstatus、mepc 从该任务栈弹出:
      ... lw ra,  (sp)    // etc.
      ... lw mepc, offset(sp)
      ... lw mstatus, offset(sp)
      addi sp, sp, portCONTEXT_SIZE
      mret                  // ← 关键一步
c) mret 使 CPU 跳到新任务的 PC(mepc),并恢复 mstatus,因此完成了上下文切换。旧任务的寄存器仍留在它自己的栈顶,等待下次被恢复。3) 在中断场景的作用
若启用硬件 MTIMER,freertos_risc_v_mtimer_interrupt_handler 在增量比较寄存器、调用 xTaskIncrementTick() 后,也会通过 mret 返回(有可能切到别的任务)。

动物装饰