花 满 楼头像

RTA-OS 任务调度机制深度拆解:从 alarm 到期到任务切换的全链路(PowerPC e200)

芯片:国芯 CCFC3008(内核 z720n3,PowerPC e200 兼容,双发射) 软件:ETAS RTA-OS(AUTOSAR OS)+ MCAL V3.0.0,GreenHills MULTI 工具链 读完能带走什么:彻底搞清「一个周期任务是怎么被调起来、跑完、又怎么切走的」——任务状态机、alarm 字段、Os_Dispatch 的 setjmp/longjmp 切换原理,全部基于真实函数体逐行拆解。

0. 先纠正一个关键误解

问:tick 中断内是靠 SetEvent 把到期任务切到就绪的吗?

不是。 本工程的事件机制根本没启用——Os_Cfg.h:28 里 OS_NUM_EVENTS = 0,SetEvent/WaitEvent 只是宏存在、没有任何实例被调用。

tick 中断里真正把任务切到就绪的是这条链:

tick 中断 → IncrementCounter → counter 走字 → 遍历 alarm → Os_ActivateTaskKL(置就绪位)

SetEvent 是另一种让任务就绪的方式(事件驱动),本工程没用。两者并列,别混。

让任务进入就绪态的全部途径:

途径

机制

本工程是否使用

ActivateTask()

主动激活(直接置就绪位)

StartAuto 内部用 Os_ActivateTaskKL

ChainTask()

接力交接(指定接替者置就绪位)

未用

alarm 到期

时间驱动(IncrementCounter 里自动置就绪位)

主要方式

SetEvent()

事件驱动(唤醒 WaitEvent 的任务)

未用(OS_NUM_EVENTS=0)

1. 三大核心概念先分清:就绪 / 挂起 / 事件

这三个词容易串,务必先立住。

1.1 「就绪」是任务的调度状态

  • 就绪 = 任务在 Os_ReadyTasks 位图里对应 bit 为 1,可被调度器选中。
  • 判断逻辑(GetTaskState.c:1176)非常直白:
if (Os_RunningTask == TaskID)                    *State = RUNNING;   // 正在跑
else if (Os_ReadyTasks.p0 & TaskID->pset.p0)     *State = READY;     // 就绪
else                                             *State = SUSPENDED; // 挂起

1.2 「挂起」有两个完全不同的含义

这是最容易混的地方,务必区分:

谁"挂起"

英文

字段

含义

任务挂起

SUSPENDED

(任务状态)

任务不在运行、也不在就绪,不会被调度选中

alarm 挂起

running

Os_dyn_alarms[id].running

alarm 已激活、正在计时

SetAbsAlarm 里的"挂起"指的是 alarm 的 running 字段,不是任务状态:

Os_dyn_alarms[AlarmID].match  = start;   // 到期 tick 值
Os_dyn_alarms[AlarmID].period = cycle;   // 周期
Os_dyn_alarms[AlarmID].running = TRUE;   // ← "挂起" = alarm 开始计时

1.3 事件(Event)

  • 事件是任务间同步机制:一个任务 WaitEvent(mask) 挂起等待,另一个任务/ISR SetEvent 唤醒它。
  • 本工程 OS_NUM_EVENTS=0,事件完全没启用,所以任务状态机里没有 WAITING 态。理解 alarm 和 ActivateTask 就够了。

2. 任务状态机(本工程 = 三态)

因为没有事件,本工程任务只有 RUNNING / READY / SUSPENDED 三态(无 WAITING):

          ActivateTask / ChainTask / alarm到期
   SUSPENDED ───────────────────────────────▶ READY
      ▲                                          │
      │                                          │ Os_Dispatch 选中
      │              TerminateTask               ▼
      └───────────────────────────────────── RUNNING
  • SUSPENDED(挂起):初始态,任务"存在但没被激活"。
  • READY(就绪):被激活了,排队等调度。
  • RUNNING(运行):正在执行。

任务挂起的影响:只是"暂时不会被选中",任务本体还在,随时能被 ActivateTask/ChainTask/alarm 重新置就绪。挂起 ≠ 删除,也 ≠ 永久冻结。

3. alarm 的三个字段详解(含 cycle 的必要性)

3.1 三个字段

typedef struct Os_AlarmDynType_s {
  boolean running;   // 是否在计时("挂起")
  TickType match;    // 下一次到期的绝对 tick 值
  TickType period;   // 周期(cycle)
} Os_AlarmDynType;

3.2 match 是什么

match 是 counter 计数值的"绝对 tick 值",不是真实时间。 每次 IncrementCounter 让 counter +1,然后检查:

if (aref->running && (now == aref->match)) {  // counter 数到 match 了?
  触发...
}

3.3 有了 match 为什么还要 cycle?

关键:match 只表示"当前这一次"到期点,cycle 才表示"每隔多久重复一次"。

if (aref->period != 0U) {
  aref->match += aref->period;   // 周期 alarm:match 往后推一个 cycle,等待下次
} else {
  aref->running = FALSE;         // 单次 alarm:触发一次就停(running 清掉)
}
  • cycle = 0 → 单次触发,到点激活一次任务后 alarm 自己停掉(running=FALSE)。
  • cycle ≠ 0 → 周期触发,每次触发后 match += cycle,自动续期,任务周期性被激活。

所以 SetAbsAlarm(0, 1, 10) = "counter 数到 1 时第一次触发,之后每 10 个 tick 触发一次" → 这就是 10ms 周期任务的来源(1 tick = 1ms)。

3.4 alarm 如何映射到任务

在 Os_IncrementCounter_MillisecondCounter(Os_Cfg_Counters.c:1371):

cfg_index = (uint8)(cref->config & 3U);
if (cfg_index > 0U) {
  Os_ActivateTaskKL(Os_const_tasks[cfg_index - 1U]);  // 激活 config-1 号任务
}

Os_const_alarms 的 config 字段 = 任务下标 + 1(0 = 该 alarm 不关联任务)。本工程实际映射:

alarm

config

指向任务

StartAuto 调用

实际周期

Alarm10ms (0)

1

tasks[0] = 10ms

SetAbsAlarm(0,1,10)

10ms

Alarm1ms (1)

3

tasks[2] = 1ms

SetAbsAlarm(1,1,1)

1ms

Alarm5ms (2)

2

tasks[1] = 5ms

SetAbsAlarm(2,1,5)

5ms

注意名字有点绕:Alarm1ms 是 alarm 下标 1,但 config=3 指向 tasks[2](也就是 1ms 任务)。以 config 字段为准。


4. 完整触发路径(时间驱动的主干)

PIT0/STM 硬件 tick 溢出(每 1ms)
  → 进 ISR(GPT_PIT0CH0_ms / STM_GlbSch_ISR)
      → IncrementCounter(MillisecondCounter)          Os_main.c ISR 内
           → Os_IncrementCounter() = CounterID->advincr()
                → Os_IncrementCounter_MillisecondCounter()   Os_Cfg_Counters.c:1347
                     ├─ count++(wrap 到 65535)
                     ├─ 遍历 Os_dyn_alarms[0..2]:
                     │     if (running && now==match) {
                     │         cfg_index = config & 3
                     │         Os_ActivateTaskKL(tasks[cfg_index-1])   ← 置就绪位
                     │         match += period 或 running=FALSE
                     │     }
                     └─ if (want_dispatch) Os_Dispatch()   ← 这里做任务切换
      → CLEAR_PENDING_INTERRUPT()
  → rfi 返回

所以任务切换确实发生在 tick 中断内,但触发方式是"alarm 到期 → ActivateTaskKL → Os_Dispatch",不是 SetEvent。


5. ChainTask 的优先级问题

问:如果需要链式启动的任务不是当前就绪的最高优先级任务,会如何?

不会立即运行,会继续等待。 原因看 Os_Dispatch 的处理:

Os_ReadyTasks.p0 &= ~Os_RunningTask->pset.p0;   // 摘除当前任务
if (INVALID_TASK != Os_ChainTaskRef) {
  Os_ReadyTasks.p0 |= Os_ChainTaskRef->pset.p0; // ChainTask 只是"置就绪"
  Os_ChainTaskRef = INVALID_TASK;
}
} while (Os_ReadyTasks.p0 > previous_priority.t0);  // 下一轮重新选人

// 下一轮 do 开头:
Os_RunningTask = &Os_const_tasks0[31 - CLZ(Os_ReadyTasks.p0)];  // 永远选最高优先级

结论:ChainTask(B) 只保证 B 进入就绪态,不保证 B 立刻运行。下一轮 do-while 会重新用 CLZ 选最高优先级任务——如果 B 不是最高优先级,就先跑别的任务,B 继续在就绪队列里等着。

所以 ChainTask 的语义是"接力",不是"抢占"。它省的是"重新调度选择"这一步的开销,但不改变优先级规则。


6. Os_Dispatch 逐行拆解(核心)

(DispatchTask.c:1179)

FUNC(void, OS_CODE) Os_Dispatch(void) {
  const Os_tpmaskType previous_priority = Os_RunningTPMask;   // ① 保存当前优先级
  const TaskType previous_task = Os_RunningTask;              // ② 保存当前任务
  ...
  do {
    // ③ 选人:取就绪位图中最高优先级任务
    Os_RunningTask = &Os_const_tasks0[31U - OS_CLZ(Os_ReadyTasks.p0)];
    //    OS_CLZ = 前导零计数,31-CLZ = 最高置位 bit = 最高优先级
    Os_RunningTPMask = Os_RunningTask->tpmask;                // ④ 提升当前运行优先级

    // ⑤ setjmp:第一次返回 0,进任务体
    if (0 == Os_setjmp(&Os_RunningTask->dynamic->terminate_jump_buf)) {
      OS_INTC_CPR = 0;                 // 开中断(允许更高优先级抢占)
      Os_RunningTask->entry_function(); // ← 跑 TASK(xx) 函数体
    }
    // ⑥ TerminateTask() 里 longjmp 回这里,setjmp 返 1(非0),跳过任务体
    OS_INTC_CPR = 62;                  // 关中断
    Os_ReadyTasks.p0 &= ~Os_RunningTask->pset.p0;  // ⑦ 摘除就绪位
    if (Os_ChainTaskRef != INVALID) {              // ⑧ 处理 ChainTask
      Os_ReadyTasks.p0 |= Os_ChainTaskRef->pset.p0;
      Os_ChainTaskRef = INVALID_TASK;
    }
  } while (Os_ReadyTasks.p0 > previous_priority.t0);  // ⑨ 还有更高优先级继续

  // ⑩ 恢复现场
  Os_RunningTPMask = previous_priority;
  Os_RunningTask = previous_task;
}

核心机制 = setjmp / longjmp:

  1. 第一次执行到 Os_setjmp 返回 0 → 进 if 块 → 跑 entry_function()。
  1. 任务末尾 TerminateTask() → Os_longjmp(..., 1) → 跳回 setjmp 点,此时返回 1 → 跳过任务体 → 继续清理。
  1. 这就是任务"不 return、靠 TerminateTask 结束"的原因。

Os_RunningTPMask(优先级天花板):记录"当前正在跑的最高优先级",want_dispatch = (就绪优先级 > 运行优先级)。这是 AUTOSAR immediate priority ceiling 的单核简化。


7. 异常归属:内核 IVOR vs OS handler

IVOR 是内核硬件寄存器(无法替换),但"落点 handler"是 OS 自己的。

  1. IVOR0~15 是 PowerPC e200 内核硬件,各对应一类异常(IVOR1 = machine check 等),OS 改不了"寄存器存在"这件事。
  1. OS 通过 IVPR 把向量表基址指向自己的 Os_CPU_vectors(Os_write_IVORs():OS_MTSPR(63, &Os_CPU_vectors))。
  1. 异常发生时,硬件按 IVPR + IVORn 跳到 OS 写的 handler:
Os_CPU_vectors:                       ; Os_Vectors.s L500
  e_b  Os_Exception_handler           ; IVOR0
  e_b  Os_Mem_Exception_handler       ; IVOR1 (machine check)
  e_b  Os_Mem_Exception_handler       ; IVOR2 (ISI)
  e_b  Os_Mem_Exception_handler       ; IVOR3 (DSI)
  e_b  Os_Exception_handler           ; IVOR4 ~ IVOR11

handler 重开 ME 位后跳 Os_ProtectionLog。原工程 vector_table.c 已被 #if 0 废弃,异常完全由 OS 接管。

7.1 为什么 IVOR0/411 共用同一个 handler、IVOR13 也共用

关键在 Os_Vectors.s:534-550 的真实实现:

Os_Exception_handler:                 ; IVOR0, 4~11 都跳这
  se_li       %r3, 18                 ; r3 = 错误码 18
  e_b         Os_SetMSRBits

Os_Mem_Exception_handler:             ; IVOR1~3 都跳这
  se_li       %r3, 14                 ; r3 = 错误码 14
Os_SetMSRBits:
  mfspr       %r0, 572                ; 读 MCSR(机器检查综合征)
  mtspr       572, %r0                ; ★ 写回 = 「写 1 清零」,清掉已记录的 machine check 位
  mfmsr       %r0
  e_or2i      %r0, OS_ME_BIT          ; 重开 MSR[ME],避免 handler 内再触发机器检查
  mtmsr       %r0
  e_b         Os_ProtectionLog        ; r3 仍带着 18 或 14 传下去

对照 Os.h 的错误码定义(345~349 行):

r3

错误码

含义

14

E_OS_PROTECTION_MEMORY

存储/机器类异常(IVOR1 machine check、IVOR2 ISI、IVOR3 DSI)

18

E_OS_PROTECTION_EXCEPTION

其它保护异常(IVOR0 Critical、IVOR4 External、IVOR5~11 等)

所以「共用」是合理且故意的:

  1. 这些异常 handler 的唯一职责是记错误 → 关系统,不做「恢复现场继续跑」的精细处理。
  1. 既然行为完全一样,就没必要每个 IVOR 写一份,只需要用 r3 传一个不同的错误码区分「是什么异常」,上报给同一个 Os_ProtectionLog。
  1. 分组依据就是异常大类:IVOR1~3 都是「存储/机器」类 → 14;其余 → 18。

7.2 Os_ProtectionLog 函数体

ProtectionSupport.c:1177-1183:

FUNC(void, OS_CODE) Os_ProtectionLog(StatusType os_status)
{
  // 1. 记下进入前的 imask,把 CPR 抬到 62(屏蔽 INTC 中断)
  Os_imaskType previous_imask = (EE 置位 ? (OS_INTC_CPR | OS_EE_BIT) : OS_INTC_CPR);
  OS_CLEAR_EE(); OS_INTC_CPR = 62U; OS_SET_EE();

  // 2. 本工程没配 ProtectionHook,直接关系统
  Os_ShutdownOS(os_status);           // ★ 参数就是 r3 传进来的 14 / 18

  // 3. 理论上 ShutdownOS 不返回;若返回则恢复 imask
  ...
}

要点:

  • os_status = r3 = 14 或 18,就是上面汇编 se_li 塞进去的那个。
  • 它不是 hook、不是可恢复的异常处理,而是 AUTOSAR 的「保护机制」:检测到异常后直接把 OS 关掉(Os_ShutdownOS)。
  • 本工程 ShutdownHook 未配置保护 hook,所以 Os_ProtectionLog 里直接 shutdown。

注意:PIT0/STM 等外设中断走另一张 Os_INTC_vectors(INTC 硬件向量模式),与 Os_CPU_vectors 是两套表,别混。详见《中断篇》。


8. 什么时候要软件主动切换(不等 tick)

本工程应用层只调了 TerminateTask(三个任务各一次),公开的 ActivateTask/Schedule/ChainTask 一个没调,是纯时间驱动。

需要软件主动切换的场景:

场景

API

为什么不能等 tick

事件同步

SetEvent/WaitEvent

事件发生后立即唤醒,实时性要求高

长任务让权

Schedule()

任务跑太久,主动让出 CPU

接力执行

ChainTask(B)

A 做完必须马上 B

生产者唤醒消费者

ActivateTask()

数据就绪立即激活

释放资源后

ReleaseResource()

释放锁后可能有更高优先级该跑

一句话:tick 负责时间触发(周期任务),这些 API 负责事件触发(实时响应)。事件发生后不能干等下一个 tick。


9. 完整知识脉络(串起所有概念)

┌──────────────────────────────────────────────────────────────┐
│ 静态层(Flash,只读):const 配置表                            │
│   Os_const_tasks[]   ← 任务定义(入口/优先级/栈/调度组)        │
│   Os_const_alarms[]  ← alarm 定义(config=任务下标+1)          │
│   Os_const_counters[]← counter 定义(advincr 函数指针)         │
└──────────────────────────┬───────────────────────────────────┘
                           │ dynamic 指针配对
┌──────────────────────────▼───────────────────────────────────┐
│ 动态层(RAM,可写):dyn 状态数组                               │
│   Os_dyn_tasks[]     ← 任务 setjmp 现场                       │
│   Os_dyn_alarms[]    ← running / match / period              │
│   Os_dyn_counters[]  ← sw.count 当前计数值                    │
│   Os_dyn_appstate[]  ← OS-Application 状态                   │
└──────────────────────────┬───────────────────────────────────┘
                           │
         ┌─────────────────┼──────────────────────┐
         ▼                 ▼                      ▼
   时间驱动(tick)     事件驱动(SetEvent)      主动驱动(ActivateTask/ChainTask)
         │                 │                      │
         └─────────┬───────┴──────────────────────┘
                   ▼
         Os_ReadyTasks.p0 置位(任务就绪)
                   ▼
         want_dispatch = 就绪优先级 > 当前优先级
                   ▼
              Os_Dispatch()          ← setjmp/longjmp 切换
                   ▼
          entry_function() 跑任务体
                   ▼
         TerminateTask → longjmp 回调度器
                   ▼
         重新选最高优先级,循环

三句话总结:

  1. const 表是"配置",dyn 数组是"状态",靠 dynamic 指针配对。任务"创建"发生在编译期(const 表),"激活"发生在运行时(就绪位置位)。
  1. "就绪"是唯一被调度器关心的状态:Os_ReadyTasks 位图 bit 置 1 = 可被选中。alarm 到期 / ActivateTask / ChainTask / SetEvent 都是"让任务就绪"的不同触发方式。
  1. 任务切换 = Os_Dispatch 里的 setjmp/longjmp,触发点最典型是 tick 中断内(时间驱动),也可由任务主动调 API(事件驱动)。

附:你可能还需要搞清楚的几个点

A. 任务优先级如何编码在位图里

每个任务有 pset(priority set,位掩码)。Os_ReadyTasks.p0 是 32 位位图,bit 位置 = 优先级。Os_Dispatch 里 31 - CLZ(Os_ReadyTasks.p0) 就是找最高置位 bit。位图里 bit 位越高 = 优先级越高,所以一个 bit 只能对应一个优先级(本工程 3 个任务 3 个优先级,互不冲突)。

B. Os_ActivateTaskKL 和公开 ActivateTask 的区别

Os_ActivateTaskKL

ActivateTask(公开 API)

是否调度

否,只置就绪位

是,置位后判断 want_dispatch 才 Os_Dispatch

谁调用

OS 内部(StartAuto、IncrementCounter)

应用层

KL 含义

Kernel Lock(已持锁,不再加锁)

内部会先加锁/关中断

C. tick 来源与 counter 的关系

IncrementCounter(MillisecondCounter) 由 ISR 调用,但 counter 不关心 tick 来自 PIT 还是 STM——那是 Target.c 用宏三选一决定的硬件层。OS 只看到"ISR 里有人调 IncrementCounter",然后 counter 走字、查 alarm。这就是 MCAL 分层的好处:OS 和具体定时器解耦。

D. 为什么任务函数体不写 return

因为任务不是被 call 进来的,而是被 Os_Dispatch 用 setjmp/longjmp 管理的。任务结束必须走 TerminateTask()(longjmp 回调度器)。如果任务函数体写 return,会返回给 Os_Dispatch 里 entry_function() 调用点之后的代码——但那里没有正常的返回路径设计,行为是未定义的。任务要么 TerminateTask,要么 ChainTask,不能 return。

E. 单核 vs 多核(SMP)

本工程 OS_NUM_OS_CORES = 1(单核)。代码里有 #if OS_NUM_OS_CORES == 2 的分支(Os_main.c:35)——多核时每个核独立跑 Os_Dispatch,任务通过 core_id 绑定到特定核。


10. 一句话总结

RTA-OS 是静态配置 + 运行时位图驱动的调度模型。任务的「创建」是编译期 const 表,「激活」是就绪位图 Os_ReadyTasks 置位;tick 中断里 alarm 到期 → Os_ActivateTaskKL 置位 → Os_Dispatch 用 setjmp/longjmp 完成上下文切换。任务结束靠 TerminateTask(longjmp 回调度器),而不是函数 return。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/niushijia007/article/details/168435324

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--