芯片:国芯 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 是另一种让任务就绪的方式(事件驱动),本工程没用。两者并列,别混。
让任务进入就绪态的全部途径:
|
途径 |
机制 |
本工程是否使用 |
|
|
主动激活(直接置就绪位) |
StartAuto 内部用 |
|
|
接力交接(指定接替者置就绪位) |
未用 |
|
alarm 到期 |
时间驱动(IncrementCounter 里自动置就绪位) |
主要方式 |
|
|
事件驱动(唤醒 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 |
|
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)挂起等待,另一个任务/ISRSetEvent唤醒它。
- 本工程
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 |
|
10ms |
|
Alarm1ms (1) |
3 |
tasks[2] = 1ms |
|
1ms |
|
Alarm5ms (2) |
2 |
tasks[1] = 5ms |
|
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:
- 第一次执行到
Os_setjmp返回 0 → 进if块 → 跑entry_function()。
- 任务末尾
TerminateTask()→Os_longjmp(..., 1)→ 跳回 setjmp 点,此时返回 1 → 跳过任务体 → 继续清理。
- 这就是任务"不 return、靠 TerminateTask 结束"的原因。
Os_RunningTPMask(优先级天花板):记录"当前正在跑的最高优先级",want_dispatch = (就绪优先级 > 运行优先级)。这是 AUTOSAR immediate priority ceiling 的单核简化。
7. 异常归属:内核 IVOR vs OS handler
IVOR 是内核硬件寄存器(无法替换),但"落点 handler"是 OS 自己的。
- IVOR0~15 是 PowerPC e200 内核硬件,各对应一类异常(IVOR1 = machine check 等),OS 改不了"寄存器存在"这件事。
- OS 通过 IVPR 把向量表基址指向自己的
Os_CPU_vectors(Os_write_IVORs():OS_MTSPR(63, &Os_CPU_vectors))。
- 异常发生时,硬件按
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 |
|
存储/机器类异常(IVOR1 machine check、IVOR2 ISI、IVOR3 DSI) |
|
18 |
|
其它保护异常(IVOR0 Critical、IVOR4 External、IVOR5~11 等) |
所以「共用」是合理且故意的:
- 这些异常 handler 的唯一职责是记错误 → 关系统,不做「恢复现场继续跑」的精细处理。
- 既然行为完全一样,就没必要每个 IVOR 写一份,只需要用 r3 传一个不同的错误码区分「是什么异常」,上报给同一个
Os_ProtectionLog。
- 分组依据就是异常大类: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 |
|
事件同步 |
|
事件发生后立即唤醒,实时性要求高 |
|
长任务让权 |
|
任务跑太久,主动让出 CPU |
|
接力执行 |
|
A 做完必须马上 B |
|
生产者唤醒消费者 |
|
数据就绪立即激活 |
|
释放资源后 |
|
释放锁后可能有更高优先级该跑 |
一句话: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 回调度器
▼
重新选最高优先级,循环
三句话总结:
- const 表是"配置",dyn 数组是"状态",靠 dynamic 指针配对。任务"创建"发生在编译期(const 表),"激活"发生在运行时(就绪位置位)。
- "就绪"是唯一被调度器关心的状态:
Os_ReadyTasks位图 bit 置 1 = 可被选中。alarm 到期 / ActivateTask / ChainTask / SetEvent 都是"让任务就绪"的不同触发方式。
- 任务切换 =
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 的区别
|
|
| |
|
是否调度 |
否,只置就绪位 |
是,置位后判断 want_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



