ARMv7-A小系统实践(一):从Cortex-A9与Zynq-7020开始

本章记录什么

这一章不急着写第一条汇编,而是先回答一个更容易被忽略的问题:要在 ARMv7-A、Cortex-A9 和 Zynq-7020 上做出一个能启动、能切换任务、能响应中断的小系统,究竟需要建立哪些知识和软件部件?如果一开始不把层次分清,后面很容易把 ARMv7-A 的架构规定、Cortex-A9 的实现细节和 Zynq-7020 的片上地址混成一张表,遇到故障时也不知道该查哪一本手册。

本系列的目标不是照抄一份现成工程,也不是把 x86_64 汇编逐行翻译成 ARM 汇编。真正要迁移的是系统能力:处理器从复位入口进入 C 环境,任务拥有独立栈和可恢复现场,定时器能够触发 IRQ,GIC 能够完成中断状态闭环,MMU 与 Cache 按内存属性工作,第二个处理器核能够被安全释放。每一项能力都要给出可观察证据和明确边界。

在这一章之前做了什么

动手前已经具备一个很小的 x86_64 参考系统和两套 ARM 侧参照:一套是按同样功能合同搭建的 Zynq-7020 教学实现,另一套是 RTEMS 6.2 的成熟 ARM 端口。前者适合看最小闭环,后者适合验证设计方向。官方架构手册、Cortex-A9 MPCore 技术参考手册和 Zynq-7000 技术参考手册负责裁决寄存器和硬件语义。

接下来的文章会从处理器模式开始,逐步走到 ABI、启动、任务上下文、异常现场、GIC、抢占、MMU、Cache、SMP 和调试。读者只看博客也能顺着这条链完成推演;文中出现的源码路径只用于说明工程如何组织,不作为理解前提。

本章的阅读顺序

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

先把平台、资料和实现边界讲清楚

一、先给结论

原定目标的安排是合理的。对于一个小型实时系统的 CPU/BSP 移植,以下三条确实是主干:

  1. 启动:CPU 如何从复位或上级 bootloader 交接点进入 C 环境。
  2. 上下文与调度:如何创建任务现场、保存当前任务、恢复下一个任务。
  3. 中断与异常:如何进入异常、保存现场、通过中断控制器分发、返回或触发调度。

学会这三条,可以说已经抓住了 RTOS ARM 移植的骨架,但不能说学完了全部 ARM 架构。一个可运行的最小移植还需要:

  • 编译选项和 ABI;
  • 链接脚本与内存布局;
  • 异常向量和各模式栈;
  • 串口,用于观察启动和故障;
  • 定时器,用于产生系统 tick;
  • 中断控制器 GIC;
  • 必要的板级时钟、引脚和装载约定。

MMU、Cache、内存屏障、原子操作、FPU 和 SMP 也属于 Cortex-A9 系统的重要特性,但不应全部塞进第一版。最快的做法是先得到一个单核、可打印、可定时中断、可切换两个任务的系统,再逐项增加复杂度。

交流中的“终端控制器”暂按“中断控制器”理解。如果原定目标原话确实是 UART/终端控制器,则 UART 本来也包含在最小启动阶段,不会漏掉。


二、三类参照分别解决什么

参考系统分为三类。第一类是小型 x86_64 系统,用来提取与架构无关的功能合同,例如任务初始化、上下文切换、中断分发和时钟 tick。第二类是 ARM 侧的最小教学实现,用来观察一条路径怎样以最少代码闭环。第三类是 RTEMS 6.2 这样的成熟实现,用来检查异常帧、GIC 和上下文设计是否遗漏工程条件。

三类材料不能互相替代。x86_64 代码能够说明“系统想完成什么”,却不能决定 ARM 的异常返回指令;教学实现便于推演,却没有成熟内核的全部边界;RTEMS 的代码足够完整,却不适合作为第一份逐行照抄的模板。寄存器、页表、GIC 和 SoC 地址的最终语义仍由官方手册裁决。

三、阅读官方资料时先判断问题属于哪一层

  • 处理器模式、CPSR/SPSR、异常返回、页表格式:查 ARMv7-A 架构手册。
  • Cache 层级、SCU、Private Timer、Global Timer:查 Cortex-A9 或 Cortex-A9 MPCore 技术参考手册。
  • GIC 与定时器在 Zynq 中的实际地址、时钟和复位关系:查 Zynq-7000 技术参考手册。
  • C 与汇编之间的寄存器和栈合同:查 AAPCS32。

文章会把真正需要的规则直接讲清楚,手册链接和章节用于追溯依据。这样既不会让读者在英文手册中失去主线,也不会把文章中的简化描述误当成唯一规范。

四、最快且依赖正确的学习顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
寄存器模式

AAPCS32

普通任务上下文与汇编

异常现场与中断处理

最小启动并进入 C / UART

GIC + 定时器 tick

两个任务的上下文切换

中断返回路径上的抢占调度

异常诊断

MMU / Cache / SMP(按需求)

为什么启动实践放在四项理论之后:四项当前只做第一轮,耗时不应很长;先理解 ABI、现场和异常后,再写启动代码,能避免把每条汇编当成孤立口诀。

为什么实际实现先启动、再 GIC、最后任务切换:所有后续调试都依赖可靠的 C 环境和串口;抢占调度又依赖中断和普通上下文两条路径都正确。


五、分阶段目标与验收标准

阶段 A:完成原定的四项

当前进度:

  • 寄存器模式与 banked registers:第一轮完成
  • AAPCS32:第一轮完成,验收约 7/10
  • 上下文与汇编:当前阶段
  • 中断和异常处理:下一阶段

验收不是背诵手册,而是能解释:

  • 普通任务切换为什么只保存 ABI 要求长期保持的状态;
  • 异常为何必须保存更多状态;
  • 新任务为什么可以通过“伪造一个可恢复的现场”第一次运行;
  • IRQ 从设备到 GIC、向量、入口、C handler、EOI、异常返回的完整链路。

阶段 B:移植前接口清单

输出一张表,把 x86_64 平台文件分为:

  • 架构无关,可直接保留;
  • 接口保留、实现重写;
  • x86 专用,在 ARM 删除或替换;
  • 新增的 Zynq BSP 文件。

验收:每个待移植函数都有调用者、输入、输出、保存规则和 ARM 实现归属。

阶段 C:单核最小启动

第一版建议:

  • ARM state 或 Thumb state 选择一种并全工程一致;
  • 单核 CPU0;
  • 由现有 FSBL/U-Boot 完成 DDR 和底层时钟初始化;
  • 暂不主动开启 MMU、Cache、FPU、SMP;
  • 初始化 SVC/IRQ 等必要栈;
  • 清零 .bss,设置 SP,进入 C;
  • UART 输出一个固定启动标志。

验收:

1
2
每次复位都能稳定打印:
BOOT -> STACK OK -> BSS OK -> C ENTRY

阶段 D:异常向量、GIC 和定时器

完成:

  • 安装异常向量;
  • 初始化 GIC Distributor 和 CPU Interface;
  • 配置一个 Zynq 定时器;
  • acknowledge 中断号;
  • 调用注册的 handler;
  • 清设备中断源并写 EOI;
  • 正确返回被中断代码。

验收:

  • 定时器计数持续增加;
  • 主循环未被破坏;
  • 不发生重复进入、假死或只触发一次;
  • 能打印中断号和必要现场。

阶段 E:普通任务上下文

完成:

  • ARM Context_Control
  • _CPU_Context_Initialize()
  • _CPU_Context_switch(run, heir)
  • _CPU_Context_restore() 或等价入口;
  • 两个独立任务栈。

先做主动让出式切换,不依赖定时器。

验收:

1
2
3
4
5
task A: 1
task B: 1
task A: 2
task B: 2
...

且每个任务的局部变量、SP 和 callee-saved 寄存器保持独立。

阶段 F:中断驱动的抢占调度

把系统 tick 与调度器连接:

  • IRQ 保存异常现场;
  • tick handler 决定是否需要调度;
  • 在安全位置选择 heir;
  • 保存被中断任务状态;
  • 恢复另一个任务;
  • 通过正确的异常返回恢复 PC/CPSR。

验收:两个不主动 yield 的忙循环任务都能持续获得 CPU。

阶段 G:异常诊断

至少支持 Undefined、SVC、Prefetch Abort、Data Abort、IRQ:

  • 异常类型;
  • 故障 PC;
  • CPSR/SPSR;
  • 通用寄存器;
  • Abort 状态和地址寄存器;
  • 可读的停止或恢复策略。

验收:主动执行未定义指令和非法访问时,可以稳定输出正确故障位置。

阶段 H:按需求扩展

推荐顺序:

  1. Cache;
  2. MMU 和页表;
  3. 用户态/系统调用;
  4. VFP/NEON 上下文;
  5. SMP、SGI 和核间调度。

这些项目不是“会 ARM 移植”的第一道门槛,但是真正进入工程复杂系统时必须继续补齐。


六、学习方法:每轮只追求一个闭环

每个主题采用四步:

  1. 画状态:寄存器或内存中原来是什么。
  2. 逐指令走一遍:每条汇编改变了什么。
  3. 说出合同:调用者、被调用者、硬件和 OS 各负责什么。
  4. 设计验收:怎样用串口、反汇编或 GDB 证明它正确。

不要把“能在文档里找到答案”当作最终验收。移植真正需要的是:

看到一段未知架构的上下文或中断代码,能根据 ABI 和硬件规则判断它为什么保存这些状态、哪里可能出错、应怎样验证。


七、当前立即执行的计划

现在只推进第三项“上下文与汇编”,暂不开始写移植代码:

  1. 阅读上下文调度与汇编的基础推演。
  2. 对照 srh_x64_mini 的 x86_64 保存集合,回忆 SysV ABI 中的 callee-saved 思想。
  3. 对照 RTEMS ARM 的 stm/ldm 保存集合,使用 AAPCS32 解释它。
  4. 手工走一遍任务 A → B → A 的 SP、LR/PC 变化。
  5. 回答文末自检题,再进入第四项中断和异常。

用一条执行主线看完整系列

第一部分:一条统一主线

三、从任务 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 一起恢复。


实现前必须写下的五份合同

第二部分:五份必须明确的软件合同

四、启动合同

启动代码必须提供一个可靠 C 环境:

1
2
3
4
5
6
7
确认 CPU 当前模式和指令状态
屏蔽尚未准备好的 IRQ/FIQ
为 SVC/IRQ/Abort/Undefined 等模式设置栈
设置 VBAR
准备 .data/.bss
保证 C 入口栈对齐
进入内核 C 入口

第一版可只启动 CPU0。CPU1 的栈、GIC CPU Interface、PPI 和 per-CPU 数据没有准备好之前,不应接收中断或运行任务。

五、C/汇编 ABI 合同

每个汇编函数都应明确:

1
2
3
4
5
6
输入参数在哪
返回值在哪
会破坏哪些寄存器
必须保持哪些寄存器
调用 C 前 SP 是否 8 字节对齐
ARM/Thumb 状态是否一致

看到一次 BL,就问:

调用后还要使用的 r0-r3/r12/LR 旧值,是否已经保存?

六、任务上下文合同

Context_Control 与任务栈不是同一个对象:

1
2
3
Context_Control 保存少量恢复入口状态
其中的 sp 指向任务自己的栈区
任务栈保存调用链、局部变量和栈帧

最小 ARM 普通上下文通常围绕:

1
r4-r11、sp、resume_pc

真实实现还可能增加 IRQ 状态策略、thread ID、VFP、SMP 执行标志等。

七、异常帧合同

异常可发生在任意指令点,因此入口必须保护被中断代码可能正在使用的状态:

1
2
3
4
r0-r12
返回 PC
异常前 CPSR
必要的 LR/栈/扩展状态

C 结构体布局、汇编保存顺序和恢复顺序必须完全一致,最好用 offsetof 静态断言和最终反汇编验证。

八、设备/GIC 合同

每个中断 handler 必须明确两个收尾动作:

1
2
怎样清设备源
怎样对有效 IAR 做匹配 EOI

还要明确:

1
2
3
4
5
6
ID
SGI/PPI/SPI 类型
edge/level
priority
target CPU
共享初始化还是每核初始化

编码前先做一次能力审计

路线确定后,还需要逐项核对参考系统原来具有什么、ARM 侧已经替换什么,以及哪些能力不能仅凭启动输出就宣称完成。

二、原始 x64 基线的真实能力审计

审计对象是最初的 x86_64 小系统基线,ARM 侧后来增加的文件不计入原系统能力。

模块 x86_64 基线状态 证据与判断 ARM 侧起点 ARM 侧处理
启动交接 已实现 Multiboot2、32 位入口、GDT、Long Mode QEMU 直接加载 ELF 用 Zynq/QEMU 入口与 FSBL/U-Boot/JTAG 合同替换,不照搬 Multiboot
启动页表/MMU 已实现 临时页表进入 Long Mode;随后按 ELF/内存图建立页表并加载 CR3 完全缺失 必做 ARM short-descriptor 页表与 CP15 MMU
物理内存发现 已实现 解析 Multiboot memory map 固定 16 MiB 链接假设 用 Zynq 平台内存表替代;QEMU/真板参数分离
启动页分配 已实现 boot_mm_page_alloc() 单向页分配,仅供启动映射 缺失 根据 ARM 静态表需求做最小启动内存区,不扩成完整伙伴/Slab
高半区/直接映射 已实现 x64 利用 64 位虚拟空间建立 direct-map 和 16 MiB dynamic-map 缺失 ARM 32 位不能照搬地址,采用等价的受保护 identity map + 预留 heap 区
MMIO Cache 属性 已实现 framebuffer/LAPIC/IOAPIC 页设置 PCD 缺失 UART、SLCR、GIC/Timer、SCU、L2C-310 全部映射为 Device/XN
通用 Cache 驱动 未实现 只有配置宏/CMake 残留路径,实际源码不存在 缺失 ARM 必做 L1 invalidate/clean、I/D Cache enable 和 barrier API
L2 Cache 未实现 x64 不存在独立 L2 控制器驱动 缺失 增加 Zynq L2C-310 检测/初始化;QEMU 不支持时明确 SKIP
异常/中断入口 已实现 IDT、统一汇编帧、ISR 数组分发 已有 ARM IRQ 帧,但只硬编码 Timer 改成可注册的 GIC handler 表,并增强 Abort 诊断
Timer 已实现 PIT 校准 LAPIC Timer,ISR 只增加 tick Private Timer 已连续触发 保留设备替换,Timer handler 增加 need_reschedule
普通上下文 已实现 两个任务主动 _CPU_Context_switch() 已实现并验证 保留作同步切换验收
抢占调度 未实现 Timer 不调度;任务主动让出 CPU 未实现 这是最后一轮必做且超过 x64 基线的闭环补全
独立中断栈 未真正启用 IDT 的 IST=0,声明的上下界未接入 CPU0 独立 sp_irq 已验证 保留并扩为每 CPU 独立 IRQ 栈
SMP/AP 启动 未实现 smp.h 和 API 声明存在,源码无实现 只启 CPU0 做 CPU1 启动、per-CPU 栈、GICC 和 SGI 验证,不做全 SMP 调度
核间中断 只有声明 lapic_send_ipi() 仅在头文件声明 缺失 用 GIC SGI0 完成 CPU0↔CPU1 handshake
Fault 诊断 部分实现 page fault/未知向量能打印完整 x64 frame 只打印 vector/lr 增加 DFSR/DFAR/IFSR/IFAR、SPSR 和关键寄存器
FPU/VFP 上下文 未实现 x64 is_fp 被忽略,实际 switch 不保存 FP 未实现 不纳入等价必做,文档明确限制
通用调度/Heap/VFS 源码不存在 CMake 引用大量缺失目录,不是可运行功能 不存在 不在两天内凭空重写一个完整 OS

三、最终实现范围

A. 保留并明确“复用/替换”边界

保留的功能合同:

  • _CPU_Context_Initialize()_CPU_Context_switch()_CPU_Context_restore()
  • ISR 注册/分发概念;
  • Timer tick 与任务调度概念;
  • 两个任务及其独立栈的验收模型;
  • 启动阶段建立可运行内存环境后进入 kernel 的顺序。

必须替换的平台实现:

1
2
3
4
5
6
Multiboot/GDT/Long Mode  → Zynq入口、ARM模式栈、VBAR
x86四级页表/CR3 → ARMv7 short-descriptor、TTBR0/TTBCR/DACR/SCTLR
IDT/LAPIC/PIT → ARM向量、GIC、Private Timer
x86 context/iretq → ARM context、SPSR/LR、异常返回
PCD Cache属性 → ARM Normal/Device/Shareable/XN属性
LAPIC IPI声明 → GIC SGI0实际收发

B. MMU 与内存属性(必做)

新增 16 KiB 对齐的一级转换表,并按 ARMv7-A short-descriptor 规则配置:

  1. DDR:Normal、Write-Back/Write-Allocate、Shareable;
  2. 代码/只读区:只读、可执行;
  3. data/bss/heap/stack:可读写、XN;
  4. UART、SLCR、SCU/GIC/Timer、L2C-310:Device、Shareable、XN;
  5. 未映射区域保持 Fault descriptor;
  6. 设置 TTBCR、TTBR0、DACR,失效 TLB 后使能 SCTLR.M
  7. 增加 VA→PA/PAR 查询验收,证明正常地址可翻译、未映射地址返回 fault。

x64 的 512 GiB 高半区不能机械复制到 32 位 ARM。最终保持现有物理地址的 identity map,
但实现同样的核心功能:代码/数据权限、内存类型、MMIO 隔离和可控翻译表。

C. Cache 与一致性(必做 L1,条件验证 L2)

  1. 读取 SCTLR/CLIDR/CCSIDR
  2. 开启前按 set/way 失效 L1 D-Cache;
  3. 失效 I-Cache 和 branch predictor;
  4. DSB/ISB 串联维护操作;
  5. 设置 SCTLR.C/I/Z,验证读回位;
  6. 提供 range clean/invalidate 和 instruction sync 最小 API;
  7. SMP 时设置 ACTLR.SMP 并先启用 SCU;
  8. 检测并初始化 Zynq L2C-310。若 QEMU 模型不提供有效 L2 寄存器,输出明确 SKIP
    不伪造成功;真板流程和寄存器验证写入 README。

D. 通用 IRQ 与异常诊断(必做)

  1. 把 ID29 硬编码分支改成 irq_register(id, handler, arg)
  2. handler 表覆盖 Zynq/GIC 实际 ID 范围并拒绝非法 ID;
  3. Timer、SGI 分别注册为普通 handler;
  4. 保持完整 IAR 写回 EOIR 和设备先清源;
  5. Abort handler 输出 DFSR/DFAR/IFSR/IFAR、SPSR、LR 和 frame;
  6. spurious 1023、未注册中断、异常返回分别有明确路径。

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 栈深度
三个变量混在一起,不利于形成可验证的最小闭环。

F. Cortex-A9 双核底座(建议纳入最终轮)

这部分超过 x86_64 基线已有能力,但符合 Zynq-7020 平台事实:

  1. QEMU 改为 -smp 2
  2. _start 读取 MPIDR,CPU0/CPU1 走不同早期路径;
  3. 为每核分配 SVC/System/IRQ 等模式栈,禁止两个核共用 sp_irq
  4. CPU1 等待 CPU0 完成 .bss、页表和共享初始化;
  5. CPU0 通过 release flag + SEV 启动 CPU1;保留 Zynq 0xfffffff0 kick address 真板接口;
  6. 两核分别设置 VBAR、MMU、L1 Cache、ACTLR.SMP 和 GICC;
  7. Distributor 只由 CPU0 初始化;
  8. 注册 SGI0,完成 CPU0→CPU1→CPU0 的计数/确认;
  9. CPU1 完成验收后进入 WFI idle,CPU0 运行抢占调度。

这证明多核启动、per-CPU 栈、共享内存可见性和 IPI 通路;不等于两个核共享一个
完整调度器。真正 SMP 调度还需要 per-CPU scheduler、锁、原子操作、任务迁移和 TLB
shootdown,原 x64 mini 本身也没有这些实现。

四、实施顺序

按下面顺序推进,每一步都必须保持前一步仍能运行:

  1. 功能矩阵固化与通用 IRQ 注册:先消除 ID29 硬编码,增强异常输出。
  2. MMU/内存属性:只开 MMU,Cache 暂关;验证翻译表与 UART/GIC 仍可访问。
  3. L1 Cache/SCU/L2 hook:逐级开启并读回控制位,重新跑 IRQ 与上下文测试。
  4. 任务私有异步上下文:实现 IRQ frame ↔ task context 保存/恢复。
  5. Timer 抢占闭环:busy-loop A/B 无主动 switch,按 tick 交替。
  6. CPU1 与 SGI:per-CPU 栈、release、GICC、SGI ping-pong。
  7. 综合回归:启动、MMU、Cache、主动切换、抢占、IRQ 栈、三次 Timer、CPU1/SGI。
  8. 文档交付:更新 27/28/README/学习记录 和最终功能等价矩阵。

五、预计新增或修改的代码

预计新增:

1
2
3
4
5
6
7
8
include/mmu.h          ARM页表、属性和翻译查询接口
include/cache.h L1/L2维护与控制状态接口
include/scheduler.h task完整异步现场、need_reschedule和轮转接口
include/smp.h per-CPU状态、CPU1 release和SGI接口
src/mmu.c 页表构造、CP15设置和PAR验收
src/cache.c / cache.S set/way维护、SCTLR/ACTLR/SCU/L2控制
src/scheduler.c 当前/下一个任务和IRQ尾部决策
src/smp.c CPU1初始化、GIC SGI与握手

预计修改:

1
2
3
4
5
6
7
8
9
start.S                MPIDR分流、per-CPU模式栈、MMU/Cache启动、完整IRQ现场
irq.c / irq.h 通用注册、Timer/SGI分发、返回next frame
context.c/.S/.h 保留普通context,并增加任务完整异步context合同
kernel.c 所有阶段验收和最终PASS/FAIL汇总
linker.ld 页表、per-CPU栈、heap/nocache区和权限边界
CMakeLists.txt 新模块、SMP=2和构建选项
run_qemu*.sh 单核回归/双核最终验收和GDB参数
README.md 实现、替换关系、真板差异和限制
27/28/01 Bug、逐步教程和学习记录

六、最终验收清单

只有以下证据同时出现,才写“QEMU 功能等价移植完成”:

  • ELF 从 _start 进入 C,.bss 与 per-CPU 栈正确;
  • SCTLR.M=1,TTBR0 指向 16 KiB 对齐页表;
  • DDR、代码、数据、MMIO 属性检查通过;
  • 一个合法 VA 翻译成功,一个未映射 VA 的 PAR fault 成功;
  • SCTLR.C/I=1,L1 Cache 维护路径执行;
  • UART、GIC、Timer 在 MMU+Cache 开启后仍连续工作;
  • 主动 boot→A→B→A→boot 回归通过;
  • 两个 busy-loop 任务在不主动让出 CPU 时被 Timer 抢占;
  • 抢占后每个任务的寄存器/栈局部状态连续;
  • 任务 frame 位于任务私有栈,handler SP 与哨兵位于对应 CPU 独立 IRQ 软件栈;
  • CPU0、CPU1 的 MPIDR、栈范围和启动握手正确;
  • SGI0 CPU0→CPU1→CPU0 收发成功;
  • GDB 1234 能断到 _start/kernel_main 并识别两颗 CPU;
  • readelf 保持代码段不可写、数据段不可执行;
  • 文档逐项标注“x64复用、ARM替换、额外补强、尚未实现”。

从规则走到实际实现

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

三个名字,三个层次

层次 对应对象 决定什么
指令集与系统架构 ARMv7-A 指令语义、处理器模式、异常模型、页表格式、系统寄存器
处理器实现 Cortex-A9 / Cortex-A9 MPCore 流水线、Cache、SCU、处理器私有外设以及实现选项
SoC 集成 Zynq-7020 双核 A9、DDR 控制器、UART、时钟、复位、PS/PL 互连、实际地址
软件平台 当前裸机小系统 链接地址、栈布局、任务模型、中断分发与调度策略

讨论 CPSR.I 时,问题属于 ARM 架构;讨论 SCU 时,需要查 MPCore 手册;讨论 UART0 为什么位于 0xE0000000,则必须查 Zynq 的地址表。

Cortex-A9 的基本编程模型和 MPCore 集成手册都要用。多核平台并不意味着单个 CPU 的技术手册失去作用:每个核仍然有自己的 CP15 寄存器、MMU、TLB 和 L1 Cache。MPCore 手册是在这些内容上补充共享与核间机制。

Zynq-7020 的 PS 包含两个 Cortex-A9 核;PL 是可编程逻辑。串口、GIC、Private Timer 和这套小系统的核心实验都可以围绕 PS 完成,不要求先做一套 PL 显示电路。Zynq-7000 家族还有单核型号,不能把“7020 是双核”扩展为整个产品家族都双核。型号关系见 UG585 的介绍

这套系统做到哪里

目标是把最重要的平台机制接起来:

1
2
3
4
5
6
7
8
9
ELF 装载
→ 启动入口与每核模式栈
→ 进入 C、初始化 UART
→ 建立页表、开启 MMU/Cache
→ 普通任务现场保存与恢复
→ 异常向量、GIC、Timer
→ 独立中断栈与 IRQ 尾部调度
→ CPU1 启动、SGI 往返
→ 长期运行的任务演示

这里的系统是一个内核实验底座。它包含任务切换与中断调度所需的真实汇编路径,但没有完整的用户态进程、文件系统、动态内存分配、驱动框架和通用 SMP 调度器。阅读时保留这条边界,很多看似过于简单的设计就容易解释:固定的两任务轮转,是为了先验证现场恢复,不是在实现通用优先级调度策略。

实际使用的三份源码

目录编号容易造成误解,先明确本文的称呼。

本文简称 实际目录 用途
基础版 64 字节完整 IRQ 现场 先用直接、易证明的方案建立正确闭环
精简版 32 字节 IRQ 现场 真正换任务时再保存 r4-r11;三个任务混合调度
RTEMS 参考树 完整 RTEMS 6.2 用于对照成熟实现的异常和上下文路径

基础版目录名保留了最初的移植来源,但不能据此认为其中仍是 x86 汇编。当前 src/start.Ssrc/context_switch.S 都是 Cortex-A9 的 ARM 状态代码。

前两者用于验证较少的约束,RTEMS 要处理更完整的线程状态、嵌套、中断调度限制、浮点、多核竞争等问题。对比时应比较某条具体路径,不能仅根据文件长短评价系统效率。

文中的代码片段会标明来自哪一版。标注“示意”的代码用于解释原理,不当成可直接替换整个源文件的补丁。文章日期用于系列阅读排序,源码细节按整理时的实际文件核对。

从 x86 迁移,保留的是什么

CPU 架构变了,底层指令和硬件控制器不可能原封不动保留。真正能保留的是接口意图和验证方法。

平台职责 x86 常见实现 ARMv7-A / Zynq 实现
启动交接 Bootloader、Multiboot QEMU ELF 装载,真板另有 BootROM/FSBL 交接
处理器状态 控制寄存器、描述符表等 CPSR、模式栈、CP15
异常入口 IDT 与入口桩 VBAR、异常向量、IRQ 汇编
中断控制器 PIC / APIC GIC Distributor 与 CPU Interface
周期事件 平台 Timer A9 Private Timer
普通任务切换 保存 ABI 要求的状态 保存 r4-r11、SP、LR 与约定的控制状态
地址转换 x86 页表 ARMv7 short-descriptor 页表

_CPU_Context_switch(old, next) 这个名字可以保留。它表达旧任务将来能恢复,下一任务从自己的继续点执行。寄存器集合、偏移、栈布局和恢复指令必须重新设计。

资料应该怎样分工

架构事实以官方手册为依据;实现细节以当前源码为依据。二者有冲突时,要先确认芯片实现范围、编译选项和文档版本。

资料 主要查什么 官方入口
ARMv7-A/R Architecture Reference Manual,DDI 0406 模式、CPSR、异常、指令、VMSA ARM ARM
Cortex-A Series Programmer’s Guide,DEN0013 从软件角度串起架构知识 Arm 文档
AAPCS32 参数、返回值、寄存器责任、栈对齐 Arm 官方 ABI 仓库
Cortex-A9 TRM,DDI 0388 单核实现、CP15、Cache/MMU 细节 Cortex-A9 TRM
Cortex-A9 MPCore TRM,DDI 0407 SCU、GIC、Global/Private Timer MPCore TRM
GIC Architecture Specification,IHI 0048 Pending/Active、IAR、EOIR GIC 规范
UG585 SoC 地址、时钟、IRQ ID、启动交接 Zynq-7000 TRM
RTEMS 6.2 工程实现的边界条件 文档源码
QEMU 仿真板支持的设备与运行方式 xilinx-zynq-a9

本地 UG585 封面是 v1.12.2,日期为 2018-07-01;官方在线文档已有其他版本。因此引用时优先写章节、寄存器名和图名,不把某个 PDF 页码当成跨版本不变的定位方式。

另一个版本问题是 GIC:本地通用规范是 GICv2 文档,Zynq-7000 的集成描述对应 GICv1。IAR/EOIR 等共同语义可以对照,虚拟化接口、分离 deactivate 等新增机制不能自动套到这块芯片上。UG585 的 IP 版本表能确认这种差别。

源码从哪里开始读

不建议第一次就按文件名逐个读完。围绕执行顺序会更清楚:

1
2
3
4
5
6
7
8
9
CMakeLists.txt + linker.ld

src/start.S → src/kernel.c → src/uart.c

context.c + context_switch.S

start.S 的 irq_entry + irq.c + scheduler.c

mmu.c + cache.c + smp.c

kernel.c 是实验顺序的索引。irq.c 知道具体中断号和设备寄存器,scheduler.c 决定下一任务,start.S 执行现场恢复。这个分工在后面的优化中仍然成立。

系列目录

篇次 内容 核心问题
01 本篇:平台与资料 ARMv7-A、A9、Zynq 分别管什么
02 模式、寄存器与 CPSR 切模式以后,哪个寄存器变了
03 AAPCS 与关键汇编 C 与汇编怎样约定现场责任
04 启动、链接与 UART 第一条 C 语句执行前要准备什么
05 线程与协同上下文 更换 SP 和继续点怎样形成任务
06 异常入口与独立中断栈 被打断的现场属于谁
07 GIC 与 Timer 设备事件怎样变成一次完整 IRQ
08 抢占、精简现场与 RTEMS 哪些寄存器可以延迟保存
09 MMU 与页表 地址相同为什么仍要开 MMU
10 Cache 与内存顺序 数据已写入为什么还可能不可见
11 CPU1、SCU 与 SGI 第二个核怎样安全加入系统
12 调试与实现复盘 怎样用证据判断做到了哪一步

前半部分先建立能手工推演的 CPU 模型,后半部分再处理异步事件和多核可见性。遇到某个寄存器不熟时,可以回到对应篇章,而不必从头翻一遍手册。

从参考系统迁移时,什么保留、什么替换

平台移植不是把指令逐行改写。能跨架构保留的是任务、调度、时钟、分发等软件语义;必须替换的是启动入口、页表格式、异常入口、中断控制器和体系结构相关同步。下面把这条边界单独展开,避免后续文章把“重新实现平台层”误解成“没有迁移”。

第三部分:x64 小系统移植时保留什么、重写什么

九、保留的是内核语义

基础版教学实现 提取:

  • 内核 C 入口。
  • Context_Control 对上层的接口语义。
  • _CPU_Context_Initialize() 的输入和预期结果。
  • _CPU_Context_switch(run, heir) 的行为。
  • ISR 注册、分发和 tick 进入调度器的接口。
  • 任务创建、就绪队列和调度策略。

十、必须替换的是架构实现

x86_64 内容 ARM/Zynq 替代
Multiboot/GRUB 入口 Zynq 启动入口和 bootloader 交接
x86 保存寄存器/RFLAGS ARM r4-r11/sp/pc 与明确的 IRQ 状态策略
IDT ARM 异常向量和 VBAR
PIC/APIC Zynq GICv1
x86 timer Private Timer、Global Timer 或 TTC
iretq ARM RFE 或等价异常返回
x86 interrupt frame ARM 异常帧

不能逐指令翻译,只能按接口语义重写。

十一、RTEMS 怎样使用

RTEMS 6.2 是答案参考,不是复制来源。建议顺序:

1
2
3
4
5
先根据 x64 小系统写出 ARM 接口和伪代码
→ 自己确定最小数据结构
→ 对照 RTEMS 检查遗漏
→ 解释 RTEMS 多出的每个字段和指令
→ 只引入当前系统真正需要的部分

重点参考:

1
2
3
4
rtems-6.2/cpukit/score/cpu/arm/cpu_asm.S
rtems-6.2/cpukit/score/cpu/arm/arm_exc_interrupt.S
rtems-6.2/bsps/shared/dev/irq/arm-gicv2.c
rtems-6.2/bsps/arm/shared/clock/clock-a9mpcore.c

第四部分:移植 Debug 的固定顺序

十二、完全进不了 C

按顺序查:

1
2
3
4
5
6
7
镜像加载地址和入口
当前 ARM/Thumb 状态
SVC SP
.data/.bss
链接地址与运行地址
C 入口 ABI 对齐
UART 是否真的可用

十三、一开 IRQ 就崩

按顺序查:

1
2
3
4
5
6
VBAR
IRQ vector +0x18
SP_irq
入口保存布局
C 调用前 8 字节对齐
SPSR/返回 PC

十四、一次后再也不进

重点查:

1
2
3
4
设备源是否清除
有效 IAR 是否匹配 EOI
auto-reload/auto-increment
CPSR.I 是否恢复

十五、不断重进形成风暴

重点查:

1
2
3
4
5
W1C 是否错误写 0
level 源是否仍有效
EOI 前设备是否真正撤销请求
load 是否过小
是否清错每核 Timer 实例

十六、任务 A→B 成功,B→A 失败

重点查:

1
2
3
4
5
A_ctx 保存的 resume PC
A_ctx.sp
Context_Control 汇编偏移
callee-saved 保存集合
任务栈越界或重叠

十七、加日志或优化后才崩

重点查:

1
2
3
4
5
6
LR 是否跨 BL 保存
caller-saved 活跃值
r4-r11 是否被汇编破坏
SP 8 字节对齐
inline asm clobber
IRQ 栈空间

重点回看

这一章建立了系列的坐标系:ARMv7-A 定义架构规则,Cortex-A9/MPCore 给出具体处理器与多核实现,Zynq-7020 决定 SoC 地址和启动环境,小系统负责把它们组织成可验证的软件路径。后续每次实现都应先写清合同、再做最小闭环、最后记录证据与限制。

本章总结

这一章建立了系列的坐标系:ARMv7-A 定义架构规则,Cortex-A9/MPCore 给出具体处理器与多核实现,Zynq-7020 决定 SoC 地址和启动环境,小系统负责把它们组织成可验证的软件路径。后续每次实现都应先写清合同、再做最小闭环、最后记录证据与限制。

下一章从处理器模式与 banked 寄存器开始,因为启动栈、异常入口和上下文切换都建立在这组规则上。

ARMv7-A小系统实践(一):从Cortex-A9与Zynq-7020开始

https://goko-son626.github.io/post/armv7a-00-roadmap.html

作者

GoKo Mell

发布于

2026-06-13

更新于

2026-06-13

许可协议

评论

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