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 移植,以下三条确实是主干:
- 启动:CPU 如何从复位或上级 bootloader 交接点进入 C 环境。
- 上下文与调度:如何创建任务现场、保存当前任务、恢复下一个任务。
- 中断与异常:如何进入异常、保存现场、通过中断控制器分发、返回或触发调度。
学会这三条,可以说已经抓住了 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 | |
为什么启动实践放在四项理论之后:四项当前只做第一轮,耗时不应很长;先理解 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 | |
阶段 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 | |
且每个任务的局部变量、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:按需求扩展
推荐顺序:
- Cache;
- MMU 和页表;
- 用户态/系统调用;
- VFP/NEON 上下文;
- SMP、SGI 和核间调度。
这些项目不是“会 ARM 移植”的第一道门槛,但是真正进入工程复杂系统时必须继续补齐。
六、学习方法:每轮只追求一个闭环
每个主题采用四步:
- 画状态:寄存器或内存中原来是什么。
- 逐指令走一遍:每条汇编改变了什么。
- 说出合同:调用者、被调用者、硬件和 OS 各负责什么。
- 设计验收:怎样用串口、反汇编或 GDB 证明它正确。
不要把“能在文档里找到答案”当作最终验收。移植真正需要的是:
看到一段未知架构的上下文或中断代码,能根据 ABI 和硬件规则判断它为什么保存这些状态、哪里可能出错、应怎样验证。
七、当前立即执行的计划
现在只推进第三项“上下文与汇编”,暂不开始写移植代码:
- 阅读上下文调度与汇编的基础推演。
- 对照
srh_x64_mini的 x86_64 保存集合,回忆 SysV ABI 中的 callee-saved 思想。 - 对照 RTEMS ARM 的
stm/ldm保存集合,使用 AAPCS32 解释它。 - 手工走一遍任务 A → B → A 的 SP、LR/PC 变化。
- 回答文末自检题,再进入第四项中断和异常。
用一条执行主线看完整系列
第一部分:一条统一主线
三、从任务 A 到任务 B,再被 Timer 抢占
假设系统已经启动 CPU0,当前任务 A 正常运行,任务 B 处于 ready 状态。
3.1 任务 A 调普通 C 函数
1 | |
AAPCS32 规定:
1 | |
这里没有处理器模式切换,也没有 SPSR。
3.2 任务 A 主动让出 CPU
A 调用:
1 | |
正常函数边界上可以利用 AAPCS,保存主干是:
1 | |
再从 B_ctx 恢复:
1 | |
恢复 B 的 SP 后换成 B 的栈;恢复 B 的 PC 后从 B 以前暂停的位置继续。
3.3 新任务 B 第一次运行
新任务没有“以前暂停的位置”,初始化代码必须人为构造:
1 | |
恢复 B_ctx 后先跳到 trampoline,由 trampoline 按 AAPCS 把 arg 放入 r0,调用 entry(arg)。
3.4 Timer 在任务 B 任意位置到期
这次不是普通函数边界,不能只保存 callee-saved:
1 | |
硬件完成:
1 | |
软件入口继续保存 r0-r12、返回 PC 和 SPSR,建立完整异常帧。
3.5 IAR、handler、清源与 EOI
1 | |
3.6 IRQ 尾部是否切回 A
1 | |
最终用 RFE 或等价序列同时恢复:
1 | |
恢复 CPSR 后,模式、IRQ 屏蔽和寄存器 bank 一起恢复。
实现前必须写下的五份合同
第二部分:五份必须明确的软件合同
四、启动合同
启动代码必须提供一个可靠 C 环境:
1 | |
第一版可只启动 CPU0。CPU1 的栈、GIC CPU Interface、PPI 和 per-CPU 数据没有准备好之前,不应接收中断或运行任务。
五、C/汇编 ABI 合同
每个汇编函数都应明确:
1 | |
看到一次 BL,就问:
调用后还要使用的 r0-r3/r12/LR 旧值,是否已经保存?
六、任务上下文合同
Context_Control 与任务栈不是同一个对象:
1 | |
最小 ARM 普通上下文通常围绕:
1 | |
真实实现还可能增加 IRQ 状态策略、thread ID、VFP、SMP 执行标志等。
七、异常帧合同
异常可发生在任意指令点,因此入口必须保护被中断代码可能正在使用的状态:
1 | |
C 结构体布局、汇编保存顺序和恢复顺序必须完全一致,最好用 offsetof 静态断言和最终反汇编验证。
八、设备/GIC 合同
每个中断 handler 必须明确两个收尾动作:
1 | |
还要明确:
1 | |
编码前先做一次能力审计
路线确定后,还需要逐项核对参考系统原来具有什么、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 | |
B. MMU 与内存属性(必做)
新增 16 KiB 对齐的一级转换表,并按 ARMv7-A short-descriptor 规则配置:
- DDR:Normal、Write-Back/Write-Allocate、Shareable;
- 代码/只读区:只读、可执行;
- data/bss/heap/stack:可读写、XN;
- UART、SLCR、SCU/GIC/Timer、L2C-310:Device、Shareable、XN;
- 未映射区域保持 Fault descriptor;
- 设置
TTBCR、TTBR0、DACR,失效 TLB 后使能SCTLR.M; - 增加 VA→PA/PAR 查询验收,证明正常地址可翻译、未映射地址返回 fault。
x64 的 512 GiB 高半区不能机械复制到 32 位 ARM。最终保持现有物理地址的 identity map,
但实现同样的核心功能:代码/数据权限、内存类型、MMIO 隔离和可控翻译表。
C. Cache 与一致性(必做 L1,条件验证 L2)
- 读取
SCTLR/CLIDR/CCSIDR; - 开启前按 set/way 失效 L1 D-Cache;
- 失效 I-Cache 和 branch predictor;
- 用
DSB/ISB串联维护操作; - 设置
SCTLR.C/I/Z,验证读回位; - 提供 range clean/invalidate 和 instruction sync 最小 API;
- SMP 时设置
ACTLR.SMP并先启用 SCU; - 检测并初始化 Zynq L2C-310。若 QEMU 模型不提供有效 L2 寄存器,输出明确
SKIP,
不伪造成功;真板流程和寄存器验证写入 README。
D. 通用 IRQ 与异常诊断(必做)
- 把 ID29 硬编码分支改成
irq_register(id, handler, arg); - handler 表覆盖 Zynq/GIC 实际 ID 范围并拒绝非法 ID;
- Timer、SGI 分别注册为普通 handler;
- 保持完整 IAR 写回 EOIR 和设备先清源;
- Abort handler 输出 DFSR/DFAR/IFSR/IFAR、SPSR、LR 和 frame;
- spurious 1023、未注册中断、异常返回分别有明确路径。
E. 独立 IRQ 栈上的 Timer 抢占(必做、最后一轮核心)
不能在共享 sp_irq 上直接调用当前普通 Context switch。计划采用任务私有的完整异步
上下文:
- 任务运行在 System 模式,使每个任务拥有自己的
sp_usr/lr_usr; - IRQ 入口在独立 IRQ 栈构造完整 frame;
- 保存
r0-r12、sp_usr、lr_usr、PC、CPSR到当前任务私有结构; - Timer handler 增加 tick 并设置
need_reschedule; - 只在最外层 IRQ 尾部选择 next task;
- 把 next 的完整现场装回返回 frame;
- 异常返回直接进入 next,而不是把 A 的 frame 长期留在共享 IRQ 栈;
- 两个任务都只做忙循环、不主动调用 switch,仍能由 Timer 周期性交替;
- 同时保留原来的普通 A→B→A 主动切换测试,证明同步/异步两套合同都成立。
第一版继续禁止 IRQ 嵌套。嵌套与抢占同时引入会让 frame 所有权、优先级和 IRQ 栈深度
三个变量混在一起,不利于形成可验证的最小闭环。
F. Cortex-A9 双核底座(建议纳入最终轮)
这部分超过 x86_64 基线已有能力,但符合 Zynq-7020 平台事实:
- QEMU 改为
-smp 2; _start读取 MPIDR,CPU0/CPU1 走不同早期路径;- 为每核分配 SVC/System/IRQ 等模式栈,禁止两个核共用
sp_irq; - CPU1 等待 CPU0 完成
.bss、页表和共享初始化; - CPU0 通过 release flag +
SEV启动 CPU1;保留 Zynq0xfffffff0kick address 真板接口; - 两核分别设置 VBAR、MMU、L1 Cache、ACTLR.SMP 和 GICC;
- Distributor 只由 CPU0 初始化;
- 注册 SGI0,完成 CPU0→CPU1→CPU0 的计数/确认;
- CPU1 完成验收后进入 WFI idle,CPU0 运行抢占调度。
这证明多核启动、per-CPU 栈、共享内存可见性和 IPI 通路;不等于两个核共享一个
完整调度器。真正 SMP 调度还需要 per-CPU scheduler、锁、原子操作、任务迁移和 TLB
shootdown,原 x64 mini 本身也没有这些实现。
四、实施顺序
按下面顺序推进,每一步都必须保持前一步仍能运行:
- 功能矩阵固化与通用 IRQ 注册:先消除 ID29 硬编码,增强异常输出。
- MMU/内存属性:只开 MMU,Cache 暂关;验证翻译表与 UART/GIC 仍可访问。
- L1 Cache/SCU/L2 hook:逐级开启并读回控制位,重新跑 IRQ 与上下文测试。
- 任务私有异步上下文:实现 IRQ frame ↔ task context 保存/恢复。
- Timer 抢占闭环:busy-loop A/B 无主动 switch,按 tick 交替。
- CPU1 与 SGI:per-CPU 栈、release、GICC、SGI ping-pong。
- 综合回归:启动、MMU、Cache、主动切换、抢占、IRQ 栈、三次 Timer、CPU1/SGI。
- 文档交付:更新
27/28/README/学习记录和最终功能等价矩阵。
五、预计新增或修改的代码
预计新增:
1 | |
预计修改:
1 | |
六、最终验收清单
只有以下证据同时出现,才写“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 | |
这里的系统是一个内核实验底座。它包含任务切换与中断调度所需的真实汇编路径,但没有完整的用户态进程、文件系统、动态内存分配、驱动框架和通用 SMP 调度器。阅读时保留这条边界,很多看似过于简单的设计就容易解释:固定的两任务轮转,是为了先验证现场恢复,不是在实现通用优先级调度策略。
实际使用的三份源码
目录编号容易造成误解,先明确本文的称呼。
| 本文简称 | 实际目录 | 用途 |
|---|---|---|
| 基础版 | 64 字节完整 IRQ 现场 | 先用直接、易证明的方案建立正确闭环 |
| 精简版 | 32 字节 IRQ 现场 | 真正换任务时再保存 r4-r11;三个任务混合调度 |
| RTEMS 参考树 | 完整 RTEMS 6.2 | 用于对照成熟实现的异常和上下文路径 |
基础版目录名保留了最初的移植来源,但不能据此认为其中仍是 x86 汇编。当前 src/start.S、src/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 | |
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 | |
重点参考:
1 | |
第四部分:移植 Debug 的固定顺序
十二、完全进不了 C
按顺序查:
1 | |
十三、一开 IRQ 就崩
按顺序查:
1 | |
十四、一次后再也不进
重点查:
1 | |
十五、不断重进形成风暴
重点查:
1 | |
十六、任务 A→B 成功,B→A 失败
重点查:
1 | |
十七、加日志或优化后才崩
重点查:
1 | |
重点回看
这一章建立了系列的坐标系:ARMv7-A 定义架构规则,Cortex-A9/MPCore 给出具体处理器与多核实现,Zynq-7020 决定 SoC 地址和启动环境,小系统负责把它们组织成可验证的软件路径。后续每次实现都应先写清合同、再做最小闭环、最后记录证据与限制。
本章总结
这一章建立了系列的坐标系:ARMv7-A 定义架构规则,Cortex-A9/MPCore 给出具体处理器与多核实现,Zynq-7020 决定 SoC 地址和启动环境,小系统负责把它们组织成可验证的软件路径。后续每次实现都应先写清合同、再做最小闭环、最后记录证据与限制。
下一章从处理器模式与 banked 寄存器开始,因为启动栈、异常入口和上下文切换都建立在这组规则上。
ARMv7-A小系统实践(一):从Cortex-A9与Zynq-7020开始

