ARMv7-A小系统实践(八):从完整IRQ现场到按需切换,再看RTEMS

本章记录什么

这一章把主动切换与 IRQ 返回路径合并成抢占式调度,并比较两种异常帧方案:基础版在每次 IRQ 保存 64 字节完整现场;精简版常规 IRQ 只保存 32 字节 caller-saved 现场,只有确实切换任务时才保存 r4-r11。最后用 RTEMS 6.2 的 ARM 端口检查这些选择背后的工程条件。

在这一章之前做了什么

此时已经有两个彼此独立但尚未连接的能力:普通函数边界上的协同任务切换,以及能够重复进入的 Timer IRQ。抢占的关键不是在 handler 里直接调用普通 context switch,而是让定时器只提出调度请求,由最外层 IRQ 尾部决定恢复哪一个任务的异常现场。

本章会先用完整 frame 建立容易证明正确的版本,再讨论何时可以优化。性能数字不是重点;重点是每省掉一组保存动作,都必须说明是谁继续承担寄存器保护责任。

系列目录 · 上一篇:GIC、Private Timer 与 IRQ 状态机

本章的阅读顺序

先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。

把主动切换和 IRQ 抢占放进同一张图

三、从任务 A 到任务 B,再被 Timer 抢占

假设系统已经启动 CPU0,当前任务 A 正常运行,任务 B 处于 ready 状态。

3.1 任务 A 调普通 C 函数

1
result = helper(arg0, arg1);

AAPCS32 规定:

1
2
3
4
5
6
arg0 → r0
arg1 → r1
返回值 → r0
r0-r3、r12、lr 可被调用破坏
r4-r11、sp 由被调用者保持
公共调用边界 sp 必须 8 字节对齐

这里没有处理器模式切换,也没有 SPSR。

3.2 任务 A 主动让出 CPU

A 调用:

1
context_switch(&A_ctx, &B_ctx);

正常函数边界上可以利用 AAPCS,保存主干是:

1
2
3
A 的 r4-r11
A 的 sp
A 的继续地址(调用 context_switch 时的 lr)

再从 B_ctx 恢复:

1
2
3
B 的 r4-r11
B 的 sp
B 的继续地址 → pc

恢复 B 的 SP 后换成 B 的栈;恢复 B 的 PC 后从 B 以前暂停的位置继续。

3.3 新任务 B 第一次运行

新任务没有“以前暂停的位置”,初始化代码必须人为构造:

1
2
3
4
5
对齐后的新任务栈顶
entry 地址
arg
thread_start_trampoline
任务函数返回后的 thread_exit

恢复 B_ctx 后先跳到 trampoline,由 trampoline 按 AAPCS 把 arg 放入 r0,调用 entry(arg)

3.4 Timer 在任务 B 任意位置到期

这次不是普通函数边界,不能只保存 callee-saved:

1
2
3
4
Private Timer 到期
→ CPU0 PPI29 Pending
→ GIC 向 CPU0 断言 IRQ
→ CPU0 进入 VBAR+0x18

硬件完成:

1
2
3
4
5
6
CPSR → SPSR_irq
返回信息 → LR_irq
模式 → IRQ
I 位 → 1
当前可见 SP → SP_irq
PC → IRQ vector

软件入口继续保存 r0-r12、返回 PC 和 SPSR,建立完整异常帧。

3.5 IAR、handler、清源与 EOI

1
2
3
4
5
6
7
8
9
10
11
12
读取 GICC_IAR
→ 返回 ID29
→ ID29 Pending → Active

调用 Private Timer handler
→ PT_STATUS = 1
→ 只清 Private Timer 设备源
→ 更新 tick
→ 必要时 need_reschedule = 1

写 GICC_EOIR = iar
→ 只告诉 GIC 这次 Active interrupt 已处理完

3.6 IRQ 尾部是否切回 A

1
2
3
4
5
6
need_reschedule = 0
→ 恢复 B 的异常现场,B 继续

need_reschedule = 1
→ scheduler 选择 heir
→ 若选中 A,保存/衔接 B 的现场并恢复 A

最终用 RFE 或等价序列同时恢复:

1
2
PC
CPSR

恢复 CPSR 后,模式、IRQ 屏蔽和寄存器 bank 一起恢复。


基础版完整异常帧的设计

5. 同步上下文与异步 IRQ 现场

普通 _CPU_Context_switch(old, new) 发生在函数调用边界,按 AAPCS 只保存
r4-r11、sp、lr、cpsr。IRQ 可发生在任意指令边界,必须保存完整的共享工作寄存器。

协同任务 A/B 各自拥有栈和 Context_Control。任务主动调用 _CPU_Context_switch()
时,汇编把当前寄存器、SP 和返回位置写入自己的 context,再载入另一任务的 context;
以后切回来时会从这次函数调用之后继续执行。Context_Control 是保存现场的容器,
切换动作不是 C 结构体赋值。该演示没有就绪队列、阻塞/唤醒和优先级,只是与原 x86
示例同语义的双任务协同切换闭环。

本实现的任务私有 64 字节 frame 为:

1
2
3
4
+0..+48   r0-r12
+52 lr_svc
+56 return_pc(已经完成 LR_irq - 4)
+60 原 CPSR(来自 SPSR_irq)

IRQ 入口先用 SRSDB 把返回 PC/CPSR 放到被打断任务的 SVC 栈,再保存
r0-r12,lr_svc。随后 C 分发器切到当前 CPU 的独立 IRQ 软件栈运行。这样:

  • 可恢复 frame 属于任务私有栈,任务 A/B 不会争用一份异常帧;
  • C handler 的深层调用不会消耗任务栈;
  • 调度器只在最外层 IRQ 尾部返回另一个任务的 frame;
  • 恢复使用 RFE 同时装回 PC 和 CPSR。

不要在共享 IRQ 栈里直接调用普通 _CPU_Context_switch():普通 context 不含
r0-r3/r12/异步返回 PC,而且共享栈上的 frame 可能被下一次 IRQ 覆盖。

第一版明确禁止 IRQ 嵌套。IRQ 嵌套需要额外维护嵌套深度、优先级屏蔽和 frame 所有权。

6. GIC、Timer 与 SGI

所有中断遵循同一主干:

1
2
3
4
5
6
7
读取 GICC_IAR
→ interrupt_id = IAR[9:0]
→ 查 irq_table[id]
→ handler 清具体设备源/更新状态
→ DSB
→ GICC_EOIR = 原始完整 IAR
→ IRQ tail 可选调度

PPI29 的 handler 对 Private Timer STATUS 写 1 清源;SGI0 没有外设状态需要清。
1023 是 spurious ID,不能写成普通设备中断处理。GIC Distributor 只由 CPU0
初始化,GICC、PPI/SGI banked 状态和 IRQ 栈则按 CPU 初始化。

精简版 caller-saved 异常帧的设计

5. 同步上下文与异步 IRQ 现场

普通 _CPU_Context_switch(old, new) 发生在函数调用边界,按 AAPCS 只保存
r4-r11、sp、lr、cpsr。IRQ 可发生在任意指令边界,必须保存完整的共享工作寄存器。

原有协同任务 A/B 各自拥有栈和 Context_Control,但当前已通过预处理禁用。任务主动调用 _CPU_Context_switch()
时,汇编把当前寄存器、SP 和返回位置写入自己的 context,再载入另一任务的 context;
以后切回来时会从这次函数调用之后继续执行。Context_Control 是保存现场的容器,
切换动作不是 C 结构体赋值。该演示没有就绪队列、阻塞/唤醒和优先级,只是与原 x86
示例同语义的双任务协同切换闭环。

本实现的任务私有 32 字节 IRQ frame 仅含 caller-saved 状态:

1
2
3
4
5
+0..+12   r0-r3
+16 r12
+20 lr_svc
+24 return_pc(已经完成 LR_irq - 4)
+28 原 CPSR(来自 SPSR_irq)

IRQ 入口先用 SRSDB 把返回 PC/CPSR 放到被打断任务的 SVC 栈,再保存
r0-r3,r12,lr_svc。随后 C 分发器切到当前 CPU 的独立 IRQ 软件栈运行。若 IRQ
直接返回,C 函数按 AAPCS 自行保护 r4-r11;若调度器选择另一任务,IRQ 尾部才将当前
r4-r11 写入旧任务控制块,并从新任务控制块载入它们。这样避免了每个IRQ都重复保存
callee-saved寄存器,同时发生切换时仍保存完整任务状态。

这样:

  • 可恢复 frame 属于任务私有栈,任务 A/B 不会争用一份异常帧;
  • C handler 的深层调用不会消耗任务栈;
  • 调度器只在最外层 IRQ 尾部返回另一个任务的 frame;
  • 恢复使用 RFE 同时装回 PC 和 CPSR。

不要在共享 IRQ 栈里直接调用普通 _CPU_Context_switch():普通 context 不含
r0-r3/r12/异步返回 PC,而且共享栈上的 frame 可能被下一次 IRQ 覆盖。

第一版明确禁止 IRQ 嵌套。IRQ 嵌套需要额外维护嵌套深度、优先级屏蔽和 frame 所有权。

6. GIC、Timer 与 SGI

所有中断遵循同一主干:

1
2
3
4
5
6
7
读取 GICC_IAR
→ interrupt_id = IAR[9:0]
→ 查 irq_table[id]
→ handler 清具体设备源/更新状态
→ DSB
→ GICC_EOIR = 原始完整 IAR
→ IRQ tail 可选调度

PPI29 的 handler 对 Private Timer STATUS 写 1 清源;SGI0 没有外设状态需要清。
1023 是 spurious ID,不能写成普通设备中断处理。GIC Distributor 只由 CPU0
初始化,GICC、PPI/SGI banked 状态和 IRQ 栈则按 CPU 初始化。

最终收尾时必须审计的抢占条件

E. 独立 IRQ 栈上的 Timer 抢占(必做、最后一轮核心)

不能在共享 sp_irq 上直接调用当前普通 Context switch。计划采用任务私有的完整异步
上下文:

  1. 任务运行在 System 模式,使每个任务拥有自己的 sp_usr/lr_usr
  2. IRQ 入口在独立 IRQ 栈构造完整 frame;
  3. 保存 r0-r12、sp_usr、lr_usr、PC、CPSR 到当前任务私有结构;
  4. Timer handler 增加 tick 并设置 need_reschedule
  5. 只在最外层 IRQ 尾部选择 next task;
  6. 把 next 的完整现场装回返回 frame;
  7. 异常返回直接进入 next,而不是把 A 的 frame 长期留在共享 IRQ 栈;
  8. 两个任务都只做忙循环、不主动调用 switch,仍能由 Timer 周期性交替;
  9. 同时保留原来的普通 A→B→A 主动切换测试,证明同步/异步两套合同都成立。

第一版继续禁止 IRQ 嵌套。嵌套与抢占同时引入会让 frame 所有权、优先级和 IRQ 栈深度
三个变量混在一起,不利于两天内形成可靠闭环。

精简版 IRQ 尾部调度器

下面给出文章对应的完整实现片段。即使没有配套仓库,也可以沿着注释、寄存器访问和调用关系逐行阅读。代码之后的分析只讨论本章主题,不要求预先掌握其他目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <stddef.h>
#include <stdint.h>

#include "platform.h"
#include "scheduler.h"
#include "scheduler_offsets.h"

enum {
SCHEDULER_TASK_COUNT = 3u,
ARM_CPSR_MODE_SVC = 0x13u,
ARM_CPSR_FIQ_DISABLE = 1u << 6
};

typedef struct {
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
CPU_Interrupt_frame *frame;
} Scheduler_Task;

struct Scheduler_Irq_switch {
Scheduler_Task *from;
Scheduler_Task *to;
};

_Static_assert(offsetof(Scheduler_Task, r4) == SCHEDULER_TASK_R4, "task r4 offset");
_Static_assert(offsetof(Scheduler_Task, r11) == SCHEDULER_TASK_R11, "task r11 offset");
_Static_assert(offsetof(Scheduler_Task, frame) == SCHEDULER_TASK_FRAME, "task frame offset");
_Static_assert(offsetof(struct Scheduler_Irq_switch, from) == SCHEDULER_IRQ_SWITCH_FROM, "switch from offset");
_Static_assert(offsetof(struct Scheduler_Irq_switch, to) == SCHEDULER_IRQ_SWITCH_TO, "switch to offset");

static Scheduler_Task tasks[SCHEDULER_TASK_COUNT];
static volatile uint32_t current_task;
static volatile uint32_t need_reschedule;
static volatile uint32_t switch_count;
static volatile uint32_t timer_preempt_count;
static volatile uint32_t irq_return_count;
static volatile uint32_t scheduler_active;
static struct Scheduler_Irq_switch irq_switch;

extern void scheduler_restore_first(CPU_Interrupt_frame *frame) __attribute__((noreturn));

static CPU_Interrupt_frame *prepare_initial_frame(
void *stack,
size_t stack_size,
Scheduler_Entry entry,
void *argument,
void (*exit_handler)(void)
)
{
uintptr_t top = (uintptr_t)stack + stack_size;
top &= ~(uintptr_t)0x7u;
CPU_Interrupt_frame *frame = (CPU_Interrupt_frame *)(top - sizeof(*frame));

for (uint32_t index = 0u; index < 4u; ++index) {
frame->r[index] = 0u;
}
frame->r12 = 0u;
frame->r[0] = (uint32_t)(uintptr_t)argument;
frame->svc_lr = (uint32_t)(uintptr_t)exit_handler;
frame->return_pc = (uint32_t)(uintptr_t)entry;
frame->cpsr = ARM_CPSR_MODE_SVC | ARM_CPSR_FIQ_DISABLE;
return frame;
}

void scheduler_initialize(
void *task0_stack,
size_t task0_stack_size,
Scheduler_Entry task0_entry,
void *task0_argument,
void *task1_stack,
size_t task1_stack_size,
Scheduler_Entry task1_entry,
void *task1_argument,
void *task2_stack,
size_t task2_stack_size,
Scheduler_Entry task2_entry,
void *task2_argument,
void (*exit_handler)(void)
)
{
tasks[0].frame = prepare_initial_frame(
task0_stack,
task0_stack_size,
task0_entry,
task0_argument,
exit_handler
);
tasks[1].frame = prepare_initial_frame(
task1_stack,
task1_stack_size,
task1_entry,
task1_argument,
exit_handler
);
tasks[2].frame = prepare_initial_frame(
task2_stack,
task2_stack_size,
task2_entry,
task2_argument,
exit_handler
);
current_task = 0u;
need_reschedule = 0u;
switch_count = 0u;
timer_preempt_count = 0u;
irq_return_count = 0u;
scheduler_active = 1u;
}

void scheduler_request(void)
{
if (scheduler_active != 0u) {
need_reschedule = 1u;
}
}

void scheduler_request_from_timer(void)
{
if (scheduler_active != 0u) {
++timer_preempt_count;
need_reschedule = 1u;
}
}

void scheduler_yield(void)
{
scheduler_request();
zynq_irq_send_sgi(ZYNQ_SGI_IRQ_ID, 1u << (arm_cpu_index() & 1u));
}

void scheduler_interrupt_return_only(void)
{
zynq_irq_send_sgi(
ZYNQ_SCHEDULER_RETURN_IRQ_ID,
1u << (arm_cpu_index() & 1u)
);
}

Scheduler_Irq_switch *scheduler_irq_tail(CPU_Interrupt_frame *current_frame)
{
if (scheduler_active == 0u) {
return NULL;
}

tasks[current_task].frame = current_frame;
if (need_reschedule == 0u) {
++irq_return_count;
return NULL;
}

need_reschedule = 0u;
irq_switch.from = &tasks[current_task];
current_task = (current_task + 1u) % SCHEDULER_TASK_COUNT;
irq_switch.to = &tasks[current_task];
++switch_count;
return &irq_switch;
}

uint32_t scheduler_irq_return_count(void)
{
return irq_return_count;
}

uint32_t scheduler_switch_count(void)
{
return switch_count;
}

uint32_t scheduler_timer_preempt_count(void)
{
return timer_preempt_count;
}

void scheduler_start(void)
{
scheduler_restore_first(tasks[0].frame);
}

从规则走到实际实现

下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。

先明确比较对象

本篇对照三种实现层次:

1
2
3
基础版教学实现              64 字节 IRQ frame
精简版教学实现 32 字节 IRQ frame + TCB 中的 r4-r11
RTEMS 6.2 成熟的 IRQ/线程调度分层

RTEMS 不是精简版的另一个名字,32 字节的设计也不能直接归到 RTEMS 头上。三者有相近思路,但数据结构、控制流和适用范围各不相同。

基础版:选一个 frame,异常返回就换了任务

基础版的 Scheduler_Task 只有一个 frame 指针。两个任务各自有栈,栈顶预先构造可恢复的完整异常帧。

1
2
3
typedef struct {
CPU_Interrupt_frame *frame;
} Scheduler_Task;

初始化 frame 时:

1
2
3
4
frame->r[0] = (uint32_t)(uintptr_t)argument;
frame->svc_lr = (uint32_t)(uintptr_t)exit_handler;
frame->return_pc = (uint32_t)(uintptr_t)entry;
frame->cpsr = 0x13u | (1u << 6);

其余整数寄存器清零,CPSR.I 为零,允许任务启动后接受 IRQ。这次首次启动直接从 RFE 恢复到 entry,不需要普通 context 那种 trampoline;因为 r0、PC、CPSR 都能显式放进异常帧。

Timer handler 清设备源、tick++,然后只设置 need_reschedule。真正选任务发生在 EOIR 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CPU_Interrupt_frame *scheduler_irq_tail(CPU_Interrupt_frame *frame)
{
if (scheduler_active == 0u)
return frame;

tasks[current_task].frame = frame;
if (need_reschedule == 0u)
return frame;

need_reschedule = 0u;
current_task ^= 1u;
++switch_count;
return tasks[current_task].frame;
}

这段代码来自基础版,省去了与逻辑无关的排版。返回 A 的 frame 就恢复 A;返回 B 的 frame 就恢复 B。现场已经完整,汇编不需要理解为什么选 B。

抢占路径与主动调用路径在哪里不同

协同式任务把切换点放在普通函数调用边界,编译器参与保护调用者仍需要的临时值。抢占任务可以在乘法、循环条件判断甚至 C 函数内部任意指令之间被打断。

基础版用两个不主动调用 _CPU_Context_switch 的 busy-loop 任务测试 Timer 抢占。如果两个任务都有进展,并出现多次 switch_count,才说明 CPU 的确经由 IRQ 尾部切换,而不是任务自己礼让。

不过“两个计数都增长”仍不足以证明所有寄存器都正确。一个专门的上下文压力测试还应让 r0-r12、条件标志以及局部变量保持可检查模式,验证切回后状态一致。普通业务输出只是第一层证据。

64 字节保存有什么重复

每次 IRQ 都执行:

1
2
3
stmdb sp!, {r0-r12, lr}
...
ldmia sp!, {r0-r12, lr}

再加上 SRS/RFE 的两个字,总现场 64 字节。其中 r4-r11 是普通 C 函数必须保持的寄存器。

当 handler 按 AAPCS 返回,并且入口汇编没有破坏 r4-r11 时,这些寄存器在 irq_dispatch 返回后已经等于被打断时的值。若随后继续原任务,把它们再从内存加载一次,在外层保护逻辑上就出现了可省略部分。

这不等于 C handler 从不使用 r4-r11。它可以使用,只是必须先保存并在返回前恢复。

精简版:常规 IRQ 只保留 32 字节

精简版 frame 为:

1
2
3
4
5
6
7
typedef struct {
uint32_t r[4]; /* r0-r3 */
uint32_t r12;
uint32_t svc_lr;
uint32_t return_pc;
uint32_t cpsr;
} CPU_Interrupt_frame;

布局变成:

1
2
3
4
5
6
+0..12  r0-r3
+16 r12
+20 lr_svc
+24 return_pc
+28 cpsr
总计 32 字节

r4-r11 没消失,而是把“必须跨任务保存它们”的责任延后到实际切换时。任务控制块增加八个字段:

1
2
3
4
5
6
7
8
9
typedef struct {
uint32_t r4, r5, r6, r7, r8, r9, r10, r11;
CPU_Interrupt_frame *frame;
} Scheduler_Task;

struct Scheduler_Irq_switch {
Scheduler_Task *from;
Scheduler_Task *to;
};

frame 指针偏移为 32,from/to 的偏移为 0/4。编译期断言验证这套布局。

对于暂停任务,32 字节 frame 加 32 字节 r4-r11 保存区仍构成 64 字节的核心整数状态。优化主要减少“不切换 IRQ”中的外层寄存器访存,不是把整个任务现场永久砍掉一半。

精简入口逐段展开

来源:精简版 src/start.S。先修正返回地址,把异常状态转到任务 SVC 栈:

1
2
3
4
5
sub   lr, lr, #4
srsdb sp!, #0x13
cps #0x13
stmdb sp!, {r0-r3, r12, lr}
mov r0, sp

随后选择本核中断栈,这一段仍只能使用已保护的 scratch 寄存器:

1
2
3
4
5
mrc   p15, 0, r1, c0, c0, 5
and r1, r1, #0x3
cmp r1, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top

现在要保存“原 frame 在哪里”。不能期待 r0 跨 C 调用保持,因为 r0 本来就用于返回值;也不能随便使用 r4,原任务的 r4 还没有外层保存。代码在中断栈上留 8 字节:

1
2
3
4
5
sub sp, sp, #8
str r0, [sp]
bl irq_dispatch
ldr r1, [sp]
add sp, sp, #8

实际只存一个指针,留 8 字节是为了保持调用边界的栈对齐。r1 取回原 frame,r0 则是调度结果。

不切任务

1
2
3
4
5
6
7
8
cmp r0, #0
beq .Lirq_return_same
...
.Lirq_return_same:
mov sp, r1
.Lirq_restore_frame:
ldmia sp!, {r0-r3, r12, lr}
rfefd sp!

精简版用 NULL 表示继续原任务。r4-r11 一直由 ABI 保持,恢复剩余 frame 就完整回到原执行流。

确实切任务

1
2
3
4
5
6
7
ldr   r2, [r0, #SCHEDULER_IRQ_SWITCH_FROM]
stmia r2, {r4-r11}

ldr r2, [r0, #SCHEDULER_IRQ_SWITCH_TO]
ldmia r2, {r4-r11}
ldr sp, [r2, #SCHEDULER_TASK_FRAME]
b .Lirq_restore_frame

关键时机是 bl irq_dispatch 已经返回。此时 C 调用链已恢复旧任务的 r4-r11,所以写到 from 对象的是旧任务真实现场,而不是 handler 的临时变量。

载入 to 的 r4-r11 后,再去恢复 to 的 32 字节 frame。完整状态由两个内存位置组合起来,不能只换 frame 指针而遗漏 callee-saved 部分。

优化成立需要哪些条件

条件可以逐项检查:

  1. IRQ 入口到保存点之前,没有未经保护地改动 r4-r11;
  2. 所有被调用 C/汇编 handler 遵守同一 ABI;
  3. 中断栈调用结束后,原 r4-r11 已恢复;
  4. from/to 对象不重叠,偏移与汇编一致;
  5. 新任务的 frame 和 r4-r11 有定义好的初始状态;
  6. 不允许在这段半切换过程中发生重入;
  7. 当前任务模型仍为 SVC、整数上下文、CPU0 单核运行调度器。

当前 Scheduler_Task 是静态数组,首次初始化时其中的 r4-r11 字段来自 BSS 零值;但 scheduler_restore_first 只恢复 32 字节 frame,没有装入 TCB 中的 r4-r11。因此第一个任务首次进入时可能继承启动路径的这些寄存器值,不能宣称所有任务初始 r4-r11 都为零。普通 C 函数入口并不要求它们必须为零,真正要保持的是每个任务运行后的自身值。

scheduler_initialize 也没有在重复调用时显式重置所有这些字段。首次启动与“销毁后反复重建任务”是不同要求,后者要补上统一的初始化与生命周期处理。

这条检查比简单记住“callee-saved 不用保存”更可靠。它们仍然必须保存,只是保存责任可以跨 C 调用链和调度路径分担。

SGI yield 与 Timer 共用出口

精简版的三个任务都通过同一个 preempt_task 函数运行,参数区分任务。主动让出接口:

1
2
3
4
5
void scheduler_yield(void)
{
scheduler_request();
zynq_irq_send_sgi(ZYNQ_SGI_IRQ_ID, 1u << (arm_cpu_index() & 1u));
}

它先记录请求,再向本核发 SGI0。硬件接收 SGI 后仍走同一 IRQ 入口,最后在 IRQ tail 轮转到下一个任务。

Timer 到期则调用 scheduler_request_from_timer,增加 timer_preempt_count 并请求调度。SGI1 专门走不请求切换的 handler,验证“不换任务”出口。

1
2
3
4
5
任务主动 yield ── SGI0 ──┐
├─ irq_entry → 分发 → EOI → IRQ tail
定时器抢占 ───── PPI29 ──┤

只验证返回 ───── SGI1 ───┘

主动触发 SGI 的请求来源是协同式的,但现场仍按 IRQ 路径保存。它与普通 BL 调用 _CPU_Context_switch 的实现方式不同。

日志里的 cooperative_switches 实际读取总 switch_count,其中包含 Timer 导致的切换;不能把这个名字当成严格分类。多个调度请求还可能合并为一个 need_reschedule 标志,因而请求次数和切换次数并不总是一一对应。

RTEMS 为什么又多保存 r7、r9

本地参考文件:

1
2
3
cpukit/score/cpu/arm/arm_exc_interrupt.S
cpukit/score/cpu/arm/cpu_asm.S
bsps/shared/dev/irq/arm-gicv2.c

文件名中的 ARMV4 表示这条共享代码路径的历史/兼容组织方式,不表示 Cortex-A9 被当成 ARMv4 芯片。必须结合构建宏看实际采用的分支;这里讨论支持 Store Return State 的分支。

RTEMS 除 volatile 寄存器和 LR 外,还保存入口内部要使用的两个 non-volatile 寄存器:r7 用于栈对齐调整,r9 用于保留原 SP 等跨调用状态。因为入口自己改了它们,就必须先保护旧值。

在这个分支,不计可选 VFP 与对齐填充时,SRS 两个字加 push 八个寄存器,共 40 字节。它不是本地精简版的 32 字节布局,不能共用那套 offsetof。

RTEMS 随后根据被打断 SP 的低位修正对齐,取得 per-CPU 控制块,只在最外层切换到该核中断栈,并维护:

1
2
3
4
ISR nest level
thread dispatch disable level
dispatch needed
ISR dispatch disable

这些字段决定能否在当前返回点做线程调度。

参考树的 GIC 分发器还会循环读取 IAR,在合法中断 handler 周围允许再进入中断,返回后恢复屏蔽状态并写回完整 IAR。它与小系统始终保持 IRQ 屏蔽、一次处理一个应答的路径不同。这也解释了为什么 RTEMS 入口必须维护嵌套层级,不能只比较最外层保存指令的条数。

RTEMS 怎样在 IRQ 后使用普通 context switch

关键是调用位置。RTEMS 完成 BSP 分发后恢复原来的栈位置,降低嵌套和调度禁止层级,在条件满足时调用 _Thread_Do_dispatch。

如果这个函数触发普通 _CPU_Context_switch,普通 context 保存的是调度函数调用链所需的 callee-saved 状态。旧线程的 volatile 异常现场仍留在它自己的栈上。

1
2
3
4
5
6
7
8
9
线程 A 的原执行流
→ IRQ frame:保护异步现场
→ 调度调用链:普通 ABI 规则
→ 普通 context switch 保存这条调用链
... B 运行 ...
→ A 的普通 context 恢复
→ A 的调度调用返回
→ 弹出 A 的 IRQ frame
→ 异常返回到 A 原指令流

因此“IRQ 后绝不能调用普通 context switch”过于绝对。真正不能做的是:没有保存异步状态、没有明确栈归属,就在共享 IRQ 栈里直接拿普通 context 当完整 IRQ frame。

RTEMS 把两层现场组合起来,小系统精简版则在 IRQ 尾部直接按 from/to 保存 r4-r11。两条路线都需要能闭合的恢复链,不能只剪下其中几行。

还有哪些 RTEMS 处理而小系统没有处理

RTEMS 的配置分支还包括 VFP 保存、TLS、SMP 线程执行归属、dispatch 状态和独占监视器清理。尤其 CLREX 不是无用装饰:中断或切换不能让一个执行流错误继承另一个执行流留下的独占预留状态。

本地小系统的对应入口没有这些完整机制。当前实验不使用原子锁、用户态或浮点任务时,能观察到整数调度;未来加入 LDREX/STREX、VFP 或跨核迁移,需要扩充合同与验证。

RTEMS 源码可从 官方仓库按上述路径查阅。本篇分析的是本地 VERSION 标为 6.2 的代码,不以不断变化的主分支代替版本依据。

“更高效”到底证明了多少

路径 基础版外层保存 精简版外层保存
IRQ 后回原任务 完整 64 字节现场 32 字节 frame,r4-r11 由调用约定保持
IRQ 后换任务 完整现场,换 frame 32 字节 frame,再保存/恢复 TCB 的 r4-r11
实现复杂度 指针选择简单 多 from/to 合同、两部分状态
测量结论 已能运行 已能运行,但没有统一条件的周期基准

常规返回路径少了外层 r4-r11 的固定存取,这是代码可以直接证明的事实。C 编译器是否在内部又保存这些寄存器、Cache 是否命中、Timer/GIC 成本和调度频率,都会影响最后周期。

若每次 IRQ 都切任务,收益与“不切换 IRQ 很多”的场景也不同。再加上两版任务数量和串口输出不同,直接比较总运行时间没有意义。后续要量化,应固定编译优化、关闭日志,在相同硬件/时基上分别测 entry→return 与实际切换,并统计分布。

下一篇:MMU 与页表

精简异常入口的完整汇编

这一版把每次 IRQ 必存的 caller-saved 现场与真正切换时才保存的 callee-saved 现场分开。阅读时分别沿“不切换”和“切换”两条出口核对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
.syntax unified
.cpu cortex-a9
.arm

#include "smp_constants.h"
#include "scheduler_offsets.h"

.section .vectors, "ax", %progbits
.align 5
.global _vectors
.type _vectors, %function
_vectors:
b _start
b undefined_handler
b svc_handler
b prefetch_abort_handler
b data_abort_handler
b reserved_handler
b irq_entry
b fiq_handler
.size _vectors, . - _vectors

.section .text.startup, "ax", %progbits
.align 4
.global _start
.type _start, %function
_start:
@ 启动期间先关 IRQ/FIQ,避免各模式栈尚未建立时进入异常。
cpsid if

@ VBAR 指向本镜像的异常向量表。
ldr r0, =_vectors
mcr p15, 0, r0, c12, c0, 0
dsb
isb

@ QEMU的两个核从同一入口启动;MPIDR.Aff0作为每核栈和路径索引。
mrc p15, 0, r4, c0, c0, 5
and r4, r4, #0x3

@ ARMv7-A 的异常模式拥有 banked SP,必须逐个切换模式后赋值。
cps #0x1b
cmp r4, #0
ldreq sp, =__stack_und_cpu0_top
ldrne sp, =__stack_und_cpu1_top

cps #0x17
cmp r4, #0
ldreq sp, =__stack_abt_cpu0_top
ldrne sp, =__stack_abt_cpu1_top

cps #0x12
cmp r4, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top

cps #0x11
cmp r4, #0
ldreq sp, =__stack_fiq_cpu0_top
ldrne sp, =__stack_fiq_cpu1_top

cps #0x13
cmp r4, #0
ldreq sp, =__stack_svc_cpu0_top
ldrne sp, =__stack_svc_cpu1_top

cmp r4, #0
bne .Lsecondary_wait

@ 裸机环境没有 C 运行库,启动代码负责清零 .bss。
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
.Lclear_bss:
cmp r0, r1
strlo r2, [r0], #4
blo .Lclear_bss

bl kernel_main

.Lhalt:
wfi
b .Lhalt

.Lsecondary_wait:
@ CPU1不能和CPU0同时清.bss或建页表,先在无Cache状态等待发布。
ldr r0, =secondary_release
ldr r2, =ZYNQ_SECONDARY_GO
.Lsecondary_wait_loop:
ldr r1, [r0]
cmp r1, r2
beq .Lsecondary_go
wfe
b .Lsecondary_wait_loop
.Lsecondary_go:
bl secondary_kernel_main
b .Lhalt
.size _start, . - _start

.macro FATAL_VECTOR name, number
.align 2
.type \name, %function
\name:
mov r1, lr
mrs r2, spsr
mov r0, #\number
b platform_exception_halt
.size \name, . - \name
.endm

FATAL_VECTOR undefined_handler, 1
FATAL_VECTOR svc_handler, 2
FATAL_VECTOR prefetch_abort_handler, 3
FATAL_VECTOR data_abort_handler, 4
FATAL_VECTOR reserved_handler, 5
FATAL_VECTOR fiq_handler, 7

.align 2
.global irq_entry
.type irq_entry, %function
irq_entry:
@ 修正PC,把PC/CPSR压入被打断任务的SVC栈
sub lr, lr, #4
srsdb sp!, #0x13 @ 把 LR_irq把 SPSR_irq压到 SP_svc 指向的栈
cps #0x13

@ 只保存caller-saved;r4-r11仅在实际任务切换时由IRQ尾部保存。
stmdb sp!, {r0-r3, r12, lr}
mov r0, sp

@ 切换到CPU专用软件IRQ栈;禁止嵌套,所以每次从栈顶开始
mrc p15, 0, r1, c0, c0, 5
and r1, r1, #0x3
cmp r1, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top
@ 把当前r0(svc下的sp) 保存到IRQ栈,返回NULL不切换
sub sp, sp, #8
str r0, [sp]
bl irq_dispatch
ldr r1, [sp]
add sp, sp, #8
cmp r0, #0
beq .Lirq_return_same

@ 任务切换:此时C调用已按AAPCS恢复当前任务的r4-r11,保存到旧任务控制块。
ldr r2, [r0, #SCHEDULER_IRQ_SWITCH_FROM]
stmia r2, {r4-r11}

@ 载入新任务的callee-saved寄存器与其caller-saved IRQ frame。
ldr r2, [r0, #SCHEDULER_IRQ_SWITCH_TO]
ldmia r2, {r4-r11}
ldr sp, [r2, #SCHEDULER_TASK_FRAME]
b .Lirq_restore_frame

.Lirq_return_same:
mov sp, r1
.Lirq_restore_frame:
ldmia sp!, {r0-r3, r12, lr}
rfefd sp!
.size irq_entry, . - irq_entry

.section .note.GNU-stack, "", %progbits

精简版的中断分发与调度请求

完整 C 路径展示了 SGI 主动让出、Timer 抢占、SGI1 直接返回以及 EOI 的共同主干。重点不是代码行数,而是调度请求只在最外层 IRQ 尾部兑现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#include <stddef.h>
#include <stdint.h>

#include "irq.h"
#include "cache.h"
#include "platform.h"
#include "scheduler.h"

extern uint8_t __stack_irq_cpu0_bottom[];
extern uint8_t __stack_irq_cpu0_top[];
extern uint8_t __stack_irq_cpu1_bottom[];
extern uint8_t __stack_irq_cpu1_top[];

enum {
GICC_BASE = ZYNQ_A9_PRIVATE_PERIPHERAL_BASE + 0x0100u,
PRIVATE_TIMER_BASE = ZYNQ_A9_PRIVATE_PERIPHERAL_BASE + 0x0600u,
GICD_BASE = ZYNQ_A9_PRIVATE_PERIPHERAL_BASE + 0x1000u,

GICC_CTLR = 0x000u,
GICC_PMR = 0x004u,
GICC_BPR = 0x008u,
GICC_IAR = 0x00cu,
GICC_EOIR = 0x010u,

GICD_CTLR = 0x000u,
GICD_ISENABLER = 0x100u,
GICD_ICENABLER = 0x180u,
GICD_ICPENDR = 0x280u,
GICD_ICACTIVER = 0x380u,
GICD_IPRIORITYR = 0x400u,
GICD_SGIR = 0xf00u,

PRIVATE_TIMER_LOAD = 0x00u,
PRIVATE_TIMER_CONTROL = 0x08u,
PRIVATE_TIMER_STATUS = 0x0cu,

PRIVATE_TIMER_ENABLE = 1u << 0,
PRIVATE_TIMER_AUTO_RELOAD = 1u << 1,
PRIVATE_TIMER_IRQ_ENABLE = 1u << 2,

/* 栈向低地址增长,在最低地址放 4 个哨兵字检测越界。 */
IRQ_STACK_GUARD_WORDS = 4u,
IRQ_STACK_GUARD_VALUE = 0x49525147u
};

_Static_assert(sizeof(CPU_Interrupt_frame) == 32u, "IRQ frame size");
_Static_assert(offsetof(CPU_Interrupt_frame, r) == 0u, "IRQ r0 offset");
_Static_assert(offsetof(CPU_Interrupt_frame, r12) == 16u, "IRQ r12 offset");
_Static_assert(offsetof(CPU_Interrupt_frame, svc_lr) == 20u, "IRQ SVC LR offset");
_Static_assert(offsetof(CPU_Interrupt_frame, return_pc) == 24u, "IRQ PC offset");
_Static_assert(offsetof(CPU_Interrupt_frame, cpsr) == 28u, "IRQ CPSR offset");

static volatile uint32_t tick_count;
static volatile uint32_t last_irq_id = GIC_SPURIOUS_IRQ_ID;
static volatile uint32_t unhandled_irq_count;
static volatile uint32_t irq_stack_minimum_sp[2];
static volatile uint32_t irq_stack_last_frame[2];
static volatile uint32_t irq_stack_valid[2];

typedef struct {
zynq_irq_handler handler;
void *argument;
} Irq_Entry;

static Irq_Entry irq_table[ZYNQ_GIC_MAX_IRQS];

static volatile uint32_t *register32(uint32_t address)
{
return (volatile uint32_t *)(uintptr_t)address;
}

static volatile uint8_t *register8(uint32_t address)
{
return (volatile uint8_t *)(uintptr_t)address;
}

static void private_timer_acknowledge(void)
{
/* PT_STATUS is write-one-to-clear: write 1 after observing ID29. */
*register32(PRIVATE_TIMER_BASE + PRIVATE_TIMER_STATUS) = 1u;
}

static void private_timer_handler(
uint32_t interrupt_id,
void *argument,
CPU_Interrupt_frame *frame
)
{
(void)interrupt_id;
(void)argument;
(void)frame;
private_timer_acknowledge();
++tick_count;
scheduler_request_from_timer();
}

static uintptr_t irq_stack_bottom_for_cpu(uint32_t cpu)
{
return cpu == 0u ? (uintptr_t)__stack_irq_cpu0_bottom :
(uintptr_t)__stack_irq_cpu1_bottom;
}

static uintptr_t irq_stack_top_for_cpu(uint32_t cpu)
{
return cpu == 0u ? (uintptr_t)__stack_irq_cpu0_top :
(uintptr_t)__stack_irq_cpu1_top;
}

static volatile uint32_t *irq_stack_guard(uint32_t cpu)
{
return (volatile uint32_t *)irq_stack_bottom_for_cpu(cpu);
}

static uint32_t irq_stack_guard_is_valid(uint32_t cpu)
{
volatile uint32_t *guard = irq_stack_guard(cpu);

for (uint32_t index = 0u; index < IRQ_STACK_GUARD_WORDS; ++index) {
if (guard[index] != IRQ_STACK_GUARD_VALUE) {
return 0u;
}
}
return 1u;
}

static void irq_stack_monitor_initialize(uint32_t cpu)
{
volatile uint32_t *guard = irq_stack_guard(cpu);

for (uint32_t index = 0u; index < IRQ_STACK_GUARD_WORDS; ++index) {
guard[index] = IRQ_STACK_GUARD_VALUE;
}

irq_stack_minimum_sp[cpu] = (uint32_t)irq_stack_top_for_cpu(cpu);
irq_stack_last_frame[cpu] = 0u;
irq_stack_valid[cpu] = 1u;
}

static void irq_stack_monitor_record(const CPU_Interrupt_frame *frame)
{
const uint32_t cpu = arm_cpu_index() & 1u;
const uintptr_t bottom = irq_stack_bottom_for_cpu(cpu);
const uintptr_t usable_bottom = bottom + IRQ_STACK_GUARD_WORDS * sizeof(uint32_t);
const uintptr_t top = irq_stack_top_for_cpu(cpu);
const uintptr_t current_sp = arm_read_sp();
const uintptr_t frame_address = (uintptr_t)frame;

irq_stack_last_frame[cpu] = (uint32_t)frame_address;
if (current_sp < irq_stack_minimum_sp[cpu]) {
irq_stack_minimum_sp[cpu] = (uint32_t)current_sp;
}

/* frame属于任务栈;只有C handler的SP必须落在独立软件IRQ栈。 */
if (current_sp < usable_bottom || current_sp >= top ||
frame_address == 0u || irq_stack_guard_is_valid(cpu) == 0u) {
irq_stack_valid[cpu] = 0u;
}
}

void zynq_irq_initialize(void)
{
arm_disable_irq();
irq_stack_monitor_initialize(0u);

for (uint32_t index = 0u; index < ZYNQ_GIC_MAX_IRQS; ++index) {
irq_table[index].handler = NULL;
irq_table[index].argument = NULL;
}

*register32(GICC_BASE + GICC_CTLR) = 0u;
*register32(GICD_BASE + GICD_CTLR) = 0u;

for (uint32_t word = 0u; word < ZYNQ_GIC_MAX_IRQS / 32u; ++word) {
*register32(GICD_BASE + GICD_ICENABLER + word * 4u) = 0xffffffffu;
*register32(GICD_BASE + GICD_ICPENDR + word * 4u) = 0xffffffffu;
*register32(GICD_BASE + GICD_ICACTIVER + word * 4u) = 0xffffffffu;
}

*register32(GICC_BASE + GICC_PMR) = 0xffu;
*register32(GICC_BASE + GICC_BPR) = 0u;
*register32(GICD_BASE + GICD_CTLR) = 1u;
*register32(GICC_BASE + GICC_CTLR) = 1u;

arm_data_sync_barrier();
arm_instruction_sync_barrier();
}

void zynq_irq_initialize_cpu_interface(void)
{
const uint32_t cpu = arm_cpu_index() & 1u;
irq_stack_monitor_initialize(cpu);
*register32(GICC_BASE + GICC_CTLR) = 0u;
*register32(GICC_BASE + GICC_PMR) = 0xffu;
*register32(GICC_BASE + GICC_BPR) = 0u;
*register32(GICC_BASE + GICC_CTLR) = 1u;
arm_data_sync_barrier();
arm_instruction_sync_barrier();
}

int zynq_irq_register(
uint32_t interrupt_id,
zynq_irq_handler handler,
void *argument
)
{
if (interrupt_id >= ZYNQ_GIC_MAX_IRQS || handler == NULL) {
return -1;
}

irq_table[interrupt_id].handler = handler;
irq_table[interrupt_id].argument = argument;
zynq_cache_clean_range(&irq_table[interrupt_id], sizeof(irq_table[interrupt_id]));
arm_data_sync_barrier();
return 0;
}

int zynq_irq_enable(uint32_t interrupt_id, uint8_t priority)
{
if (interrupt_id >= ZYNQ_GIC_MAX_IRQS) {
return -1;
}

const uint32_t word_offset = (interrupt_id / 32u) * 4u;
const uint32_t mask = 1u << (interrupt_id % 32u);
*register8(GICD_BASE + GICD_IPRIORITYR + interrupt_id) = priority;
*register32(GICD_BASE + GICD_ICPENDR + word_offset) = mask;
*register32(GICD_BASE + GICD_ICACTIVER + word_offset) = mask;
*register32(GICD_BASE + GICD_ISENABLER + word_offset) = mask;
arm_data_sync_barrier();
return 0;
}

void zynq_irq_send_sgi(uint32_t interrupt_id, uint32_t target_mask)
{
if (interrupt_id < 16u) {
*register32(GICD_BASE + GICD_SGIR) =
((target_mask & 0xffu) << 16) | interrupt_id;
arm_data_sync_barrier();
}
}

void zynq_private_timer_start(uint32_t reload_value)
{
(void)zynq_irq_register(ZYNQ_PRIVATE_TIMER_IRQ_ID, private_timer_handler, NULL);
(void)zynq_irq_enable(ZYNQ_PRIVATE_TIMER_IRQ_ID, 0xa0u);
*register32(PRIVATE_TIMER_BASE + PRIVATE_TIMER_CONTROL) = 0u;
private_timer_acknowledge();
*register32(PRIVATE_TIMER_BASE + PRIVATE_TIMER_LOAD) = reload_value;
*register32(PRIVATE_TIMER_BASE + PRIVATE_TIMER_CONTROL) =
PRIVATE_TIMER_ENABLE | PRIVATE_TIMER_AUTO_RELOAD | PRIVATE_TIMER_IRQ_ENABLE;
arm_data_sync_barrier();
}

void zynq_private_timer_stop(void)
{
*register32(PRIVATE_TIMER_BASE + PRIVATE_TIMER_CONTROL) = 0u;
private_timer_acknowledge();
arm_data_sync_barrier();
}

uint32_t zynq_tick_count(void)
{
return tick_count;
}

uint32_t zynq_last_irq_id(void)
{
return last_irq_id;
}

uint32_t zynq_unhandled_irq_count(void)
{
return unhandled_irq_count;
}

uint32_t zynq_irq_stack_bottom(void)
{
return (uint32_t)irq_stack_bottom_for_cpu(arm_cpu_index() & 1u);
}

uint32_t zynq_irq_stack_top(void)
{
return (uint32_t)irq_stack_top_for_cpu(arm_cpu_index() & 1u);
}

uint32_t zynq_irq_stack_minimum_sp(void)
{
return irq_stack_minimum_sp[arm_cpu_index() & 1u];
}

uint32_t zynq_irq_stack_last_frame(void)
{
return irq_stack_last_frame[arm_cpu_index() & 1u];
}

uint32_t zynq_irq_stack_used_bytes(void)
{
return zynq_irq_stack_top() - irq_stack_minimum_sp[arm_cpu_index() & 1u];
}

uint32_t zynq_irq_stack_is_valid(void)
{
const uint32_t cpu = arm_cpu_index() & 1u;
return irq_stack_valid[cpu] != 0u && irq_stack_guard_is_valid(cpu) != 0u;
}

Scheduler_Irq_switch *irq_dispatch(CPU_Interrupt_frame *frame)
{
/* C处理运行在每核独立IRQ软件栈;frame仍位于被打断任务的SVC栈。 */
irq_stack_monitor_record(frame);

const uint32_t iar = *register32(GICC_BASE + GICC_IAR);
const uint32_t interrupt_id = iar & 0x3ffu;

last_irq_id = interrupt_id;

if (interrupt_id == GIC_SPURIOUS_IRQ_ID) {
return NULL;
}

if (interrupt_id < ZYNQ_GIC_MAX_IRQS &&
irq_table[interrupt_id].handler != NULL) {
irq_table[interrupt_id].handler(
interrupt_id,
irq_table[interrupt_id].argument,
frame
);
} else {
++unhandled_irq_count;
}

/* Preserve the complete IAR value: it can include the source CPU field. */
arm_data_sync_barrier();
*register32(GICC_BASE + GICC_EOIR) = iar;
arm_data_sync_barrier();
return scheduler_irq_tail(frame);
}

重点回看

抢占不是在 handler 中盲目调用普通切换函数,而是在 IRQ 尾部选择另一个任务 frame。64 字节方案先保证清楚正确;32 字节方案利用 AAPCS,把 r4-r11 推迟到真正切换时保存。优化只有在不切换与切换两条返回路径都验证后才成立。

本章总结

抢占不是在 handler 中盲目调用普通切换函数,而是在 IRQ 尾部选择另一个任务 frame。64 字节方案先保证清楚正确;32 字节方案利用 AAPCS,把 r4-r11 推迟到真正切换时保存。优化只有在不切换与切换两条返回路径都验证后才成立。

下一章开始处理地址转换,为代码、数据、栈、页表与 MMIO 建立可检查的权限和内存类型。

ARMv7-A小系统实践(八):从完整IRQ现场到按需切换,再看RTEMS

https://goko-son626.github.io/post/armv7a-07-preemption-rtems.html

作者

GoKo Mell

发布于

2026-06-28

更新于

2026-06-28

许可协议

评论

:D 一言句子获取中...