ARMv7-A小系统实践(七):GIC、定时器与一次完整的IRQ

本章记录什么

这一章沿着一枚定时器中断走完整条链:Private Timer 产生事件,GIC Distributor 和本核 CPU Interface 判断是否交付,处理器进入 IRQ,软件读取 IAR 完成应答,设备 handler 清除中断源,最后把原始 IAR 写回 EOIR。目标是理解一个状态机,而不是记住几组地址。

在这一章之前做了什么

上一章已经能够安全进入和离开 IRQ,但入口正确并不代表设备中断能够重复工作。还需要解决三层使能、优先级、目标 CPU、Pending/Active 状态以及设备侧状态。任何一层遗漏,现象都可能只是“没有输出”或“只进入一次”。

本章会先解释 SGI、PPI、SPI 和 GIC 两大组成,再用 ID29 建立周期 tick。所有关键寄存器语义、分发器骨架和排错顺序都在文章内展开,不要求读者另开工程才能补齐上下文。

系列目录 · 上一篇:异常入口、独立中断栈与恢复

本章的阅读顺序

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

动手前的基础:GIC、定时器和完整 IRQ 链

第二部分:GICv1 与 Zynq-7000 中断集成

9. GIC 的三类中断

1
2
3
SGI:ID 0–15
PPI:ID 16–31
SPI:ID 32 及以上

9.1 SGI

Software Generated Interrupt:

  • 软件向 GIC 写寄存器产生;
  • 可指定本核、其他核或一组核;
  • SMP 常用于 reschedule IPI、call-function、停止另一核。

9.2 PPI

Private Peripheral Interrupt:

  • 与每个 CPU 私有外设或私有信号关联;
  • enable、pending、active 等相关状态按 CPU banked;
  • 不像 SPI 那样通过 target 字节任意路由。

CPU0 和 CPU1 都可看到 ID29,但分别代表本核 Private Timer 状态,不是两核共享同一个 timer pending 位。

9.3 SPI

Shared Peripheral Interrupt:

  • 来自 UART、TTC、GPIO、网卡、PL 等共享外设;
  • Distributor 维护共享状态;
  • 可配置目标 CPU;
  • 需要明确 edge/level、优先级和目标核。

10. Distributor 与 CPU Interface

10.1 Distributor

共享 Distributor 负责:

  • 中断源使能;
  • pending/active 管理;
  • 优先级;
  • SPI 目标 CPU;
  • edge/level 类型;
  • 从候选中选出可交付给各 CPU 的最高优先级中断。

10.2 每核 CPU Interface

每个 CPU Interface 负责:

  • 接收 Distributor 为本核选出的中断;
  • 用 Priority Mask 再过滤;
  • 向本核输出 IRQ/FIQ;
  • 提供 GICC_IAR acknowledge;
  • 接收 GICC_EOIR 完成通知;
  • 维护本核当前运行优先级。

CPU0 和 CPU1 从相同私有外设地址访问时,会到达各自的 banked CPU Interface 实例。

10.3 GIC 优先级方向

1
数值越小,优先级越高

例如 0x20 高于 0xA0。Zynq 只实现优先级字段的高若干位,低位写入后可能读回为 0。初始化阶段可把 GICC_PMR 设到宽松范围,先打通链路,再设计正式优先级层次。


11. Zynq-7000 关键实际地址

模块 基址
SCU 0xF8F00000
GIC CPU Interface 0xF8F00100
Global Timer 0xF8F00200
CPU Private Timer / Private Watchdog 0xF8F00600
GIC Distributor 0xF8F01000

常用 CPU Interface 寄存器:

偏移 旧名称 通用名称 作用
0x000 ICCICR GICC_CTLR 本核接口开关
0x004 ICCPMR GICC_PMR 优先级门槛
0x008 ICCBPR GICC_BPR 优先级分组
0x00C ICCIAR GICC_IAR 读取 ID 并 acknowledge
0x010 ICCEOIR GICC_EOIR 报告处理完成

常用 Distributor 寄存器族:

偏移族 名称 作用
0x000 GICD_CTLR Distributor 开关
0x100+n*4 GICD_ISENABLERn 写 1 使能
0x180+n*4 GICD_ICENABLERn 写 1 禁用
0x280+n*4 GICD_ICPENDRn 写 1 清 pending
0x400+ID GICD_IPRIORITYR 每 ID 优先级字节
0x800+ID GICD_ITARGETSR SPI 目标 CPU 字节
0xC00+... GICD_ICFGR edge/level 配置

旧手册使用 ICD.../ICC... 命名,新文档常用 GICD.../GICC...。不要误以为是两套硬件。


12. Zynq 中与本节相关的中断 ID

来源 类型 ID
Global Timer comparator PPI 27
CPU Private Timer PPI 29
Private Watchdog PPI 30
TTC0 Counter 0/1/2 SPI 42 / 43 / 44
TTC1 Counter 0/1/2 SPI 69 / 70 / 71

使能 ID29:

1
2
register_index = 29 / 32 = 0
bit_index = 29 % 32 = 29

即本核:

1
GICD_ISENABLER0 = 1u << 29;

因为 ID0–31 的相关状态按 CPU banked,CPU1 启动时仍要为自己配置 PPI;CPU0 的写入不能代替 CPU1 本核初始化。


13. GICC_IAR:读取本身就是 acknowledge

典型:

1
2
uint32_t iar = GICC_IAR;
uint32_t id = iar & 0x3ffu;

读取的语义不是普通“查询寄存器”:

  • 返回当前最高优先级、满足交付条件的中断;
  • 对有效 ID 执行 acknowledge;
  • 相应中断由 pending 转为 active,或按状态机更新;
  • 本核 running priority 相应改变。

对于 SGI,iar 高位还可能携带源 CPU 信息。因此结束时推荐写回完整 iar,不只写低 10 位 ID。

特殊/伪中断 ID

ID 1020–1023 是特殊值范围,最常见 spurious 是 1023。若读到非有效普通 ID:

1
2
3
if (id >= 1020u) {
return; /* 不为未 acknowledge 的普通中断写 EOI */
}

spurious 可能来自请求在 CPU 读 IAR 前消失、优先级条件改变或没有合格 pending 中断。它不是一个应派发到设备 handler 的真实 ID。


14. GICC_EOIR:结束 GIC 侧的处理

对每次有效 IAR acknowledge,软件必须有匹配的 EOI:

1
GICC_EOIR = iar;

它告诉 CPU Interface/GIC:

1
本 CPU 对这个 active interrupt 的处理已经完成

在 GICv1 的本节模型中,EOI 会完成运行优先级下降及对应 active 处理。若允许嵌套中断,EOI 必须遵守架构规定的活动中断嵌套次序;不要随意乱序结束。

EOI 不等于清设备

这是整个 GIC 学习最重要的区分:

1
2
3
4
5
清 Timer/UART/网卡状态
通知设备:“产生事件的条件已处理”

写 GICC_EOIR
通知 GIC:“CPU 对 active interrupt 的服务已完成”

对于 level-sensitive 中断:

1
2
3
4
5
只 EOI、不清设备
→ 设备电平仍有效
→ GIC 再次 pending
→ CPU 返回后立刻重进
→ 中断风暴

只清设备、不 EOI:

1
2
3
4
设备不再请求
但 GIC 仍认为该 ID active
→ 优先级/后续交付异常
→ 常见表现是只进入一次或其他中断被阻塞

常见处理顺序:

1
2
3
4
IAR
→ handler
→ 清设备源
→ EOI

某些设备要求更早清源,具体以设备手册为准,但两个动作都必须存在。


15. edge 与 level:为什么清源行为不同

level-sensitive

只要设备线保持有效,GIC 就认为请求条件仍存在。软件通常必须在 EOI 前让设备撤销电平。

edge-triggered

GIC 记录一个边沿事件为 pending。即使外部线已经恢复,pending 事件仍需服务。设备内部可能仍有状态位、FIFO 或新事件,所以 handler 仍必须按设备规则确认和清理。

不能仅靠 GIC 配置猜设备行为。UG585 Table 7-4 给出 Zynq PS/PL SPI 连接和敏感性,实际初始化要与 SoC 连接一致。


16. 单核与双核初始化边界

16.1 CPU0 执行一次的共享初始化

1
2
3
4
5
6
关闭 Distributor
配置共享 SPI 的类型
配置共享 SPI 优先级
配置 SPI 目标 CPU
清理可安全清理的旧共享 pending 状态
打开 Distributor

16.2 每个 CPU 都要执行的本核初始化

1
2
3
4
5
6
7
准备本核各模式栈和 per-CPU 数据
设置向量环境
配置本核 GICC_PMR/GICC_BPR
使能本核 CPU Interface
配置本核 SGI/PPI enable 与优先级
初始化本核 Private Timer 或 comparator
完成必要同步后再清 CPSR.I

第一版只启动 CPU0 时,不要提前:

  • 把共享 SPI 路由给尚未就绪的 CPU1;
  • 向 CPU1 发 SGI;
  • 假设 CPU1 的 SP_irq、CPU Interface 或 PPI 已初始化。

第三部分:UG585 Chapter 8 的完整定时器地图

17. 第 8 章到底包含哪些内容

准确目录:

1
2
3
4
5
6
8.1 Introduction / System Diagram
8.2 CPU Private Timers and Watchdog Timers
8.3 Global Timer
8.4 System Watchdog Timer
8.5 Triple Timer Counters
8.6 I/O Signals

学习优先级:

模块 当前需要程度 原因
CPU Private Timer 精读 最简单的单核周期 tick,PPI 29
Global Timer 精读 RTEMS 实际选择,共享 64 位时间基准,PPI 27
Private Watchdog 认识 与每核 private timer 同区,但不是首选系统 tick
System Watchdog 认识 系统健康监控与复位输出
TTC 掌握选型 独立外设计时器,SPI 42–44/69–71
I/O Signals 用到外部引脚再查 TTC 波形/外部时钟及 Watchdog 信号

所以纠正页码后不意味着读者现在要背完 Watchdog/TTC 的每个寄存器。需要能说出每类定时器属于谁、适合什么任务、怎样进 GIC。


18. CPU Private Timer

每个 Cortex-A9 CPU 有自己的 32 位递减 Private Timer:

  • 每核独立;
  • 自动重装;
  • 可产生 PPI 29;
  • 寄存器从当前核看是自己的 banked 实例;
  • 很适合第一版单核系统 tick。

基址:

1
0xF8F00600
偏移 寄存器 作用
0x00 Load 重装值
0x04 Counter 当前递减值
0x08 Control prescaler、IRQ、auto-reload、enable
0x0C Interrupt Status 到期状态,W1C

Control 关键位:

1
2
3
4
[15:8] Prescaler
[2] IRQ enable
[1] Auto reload
[0] Timer enable

Interrupt Status bit0:

  • counter 到期后置位;
  • 是 sticky 状态;
  • 1 清除,写 0 不是清除。

周期公式

设 Private Timer 输入时钟为 PERIPHCLK

1
2
period =
(prescaler + 1) × (load + 1) / PERIPHCLK

目标 tick:

1
2
load =
PERIPHCLK / ((prescaler + 1) × tick_hz) - 1

Zynq 中相关私有外设时钟与 CPU 时钟配置相关,常见为 CPU 频率的一半。真正移植不能根据开发板宣传频率硬编码,必须从 FSBL/时钟寄存器/BSP 配置确认实际 PERIPHCLK

例:

1
2
3
4
5
6
PERIPHCLK = 333,000,000 Hz
prescaler = 0
tick_hz = 1000

load = 333000000 / 1000 - 1
= 332999

最小初始化顺序

1
2
3
4
5
6
7
8
9
10
11
12
PT_CONTROL = 0;             /* 先停 */
PT_STATUS = 1; /* W1C 清旧事件 */
PT_LOAD = load;

gic_set_priority(29, 0xA0);
gic_enable_id_for_this_cpu(29);

PT_CONTROL =
(prescaler << 8)
| (1u << 2) /* IRQ enable */
| (1u << 1) /* auto reload */
| (1u << 0); /* timer enable */

handler:

1
2
3
4
5
static void private_timer_handler(void)
{
PT_STATUS = 1u;
++ticks;
}

19. Private Watchdog

Private Watchdog 与每核 Private Timer 位于同一私有外设区域,每核有自己的实例。它可工作在:

  • timer 模式;
  • watchdog 模式;
  • 到期产生中断;
  • watchdog 模式下进一步触发复位行为。

Watchdog 从普通 timer 模式切到 watchdog 模式通常有受保护的写序列,避免软件误操作。它的核心用途是检测某个 CPU/软件路径长时间失去响应,不是第一版周期 tick 的首选。

本节需要知道:

1
2
3
Private Timer ID29
Private Watchdog ID30
二者都属于每核 PPI

具体 watchdog disable、reset status 和魔数序列,在真正实现看门狗时再精读 MPCore TRM 4.2.5–4.2.10。


20. Global Timer

Global Timer 的核心结构:

  • 一个双核共享的 64 位递增 counter;
  • 每个 CPU 有自己的 comparator 相关视图/状态;
  • 支持 comparator 中断;
  • 支持 auto-increment;
  • 中断为本核 PPI 27;
  • 适合做统一时间基准和每核时钟事件。

基址:

1
0xF8F00200
偏移 作用
0x00 Counter Low
0x04 Counter High
0x08 Control
0x0C Interrupt Status,W1C
0x10 Comparator Low
0x14 Comparator High
0x18 Auto-increment

典型流程:

1
2
3
4
5
6
读取共享 64 位 counter
设置 comparator = counter + interval
设置 auto_increment = interval
使能 comparator、IRQ、auto-increment、timer
使能本核 PPI 27
每次到期写状态位清事件

为什么 RTEMS 选 Global Timer

本地参考:

1
rtems-6.2/bsps/arm/shared/clock/clock-a9mpcore.c

它使用:

1
2
Global Timer base = 0xF8F00200
PPI ID = 27

优势:

  • 64 位 counter 不易快速回绕;
  • 双核共享同一时间基准;
  • comparator 和 auto-increment 同时支持 tick;
  • 可兼作 timecounter。

Private Timer 更适合先学通最小 IRQ;正式移植若准备走向 SMP,Global Timer 是很值得优先评估的实现。

读取或更新 64 位 MMIO 时,要遵循 TRM 推荐的一致性方法,不能在 32 位 CPU 上把两个独立读取想当然视为原子 64 位操作。


21. System Watchdog Timer

UG585 8.4 描述的是 Zynq PS 的 System Watchdog,不是 Cortex-A9 每核 Private Watchdog。

它的特点包括:

  • 面向整个系统健康监控;
  • 可选择内部或外部时钟来源;
  • 可产生复位输出;
  • 有专门的使能、模式、状态和重启编程序列;
  • 与 MIO/EMIO 等外部信号连接相关。

移植阶段的使用顺序通常是:

1
2
3
4
系统先稳定启动和处理中断
→ 再启用 watchdog
→ 设计周期喂狗点
→ 验证故意停止喂狗后确实按预期复位

若启动早期已有固件打开 watchdog,必须先识别并妥善接管,否则调试时会出现“固定时间莫名复位”。


22. Triple Timer Counters(TTC)

Zynq 有两组 TTC,每组包含三个独立的 16 位 counter:

1
2
TTC0: Counter 0/1/2 → SPI 42/43/44
TTC1: Counter 0/1/2 → SPI 69/70/71

TTC 能提供:

  • 内部或外部时钟输入;
  • prescaler;
  • interval/overflow/match 事件;
  • 递增或递减计数;
  • 波形输出;
  • 共享外设形式的 SPI 中断。

它与 Private Timer 的工程区别:

Private Timer TTC
每核私有 PS 共享外设
PPI 29 SPI 42–44/69–71
32 位递减,链路简单 16 位、多模式、外部 I/O 更灵活
适合最小 per-CPU tick 适合板级计时、波形、独立时钟或共享 tick

使用 TTC 时,除了计数器自身寄存器,还必须配置:

  • 对应 SPI enable;
  • SPI priority;
  • SPI target CPU;
  • edge/level 类型按 UG585 连接;
  • TTC 自身事件状态清除;
  • 时钟源与 prescaler。

23. 定时器选型结论

当前学习与移植建议:

1
2
3
4
5
6
7
8
9
10
11
为了最快验证 IRQ:
CPU Private Timer,ID29

为了参考 RTEMS 并面向 SMP/统一时间:
Global Timer,ID27

为了独立外设时钟、波形或共享 SPI:
TTC

为了系统失效复位:
System Watchdog 或按需求使用 Private Watchdog

第一版不需要同时驱动所有计时器。懂得完整地图,是为了遇到现有固件、BSP 或原定目标选型时能判断,而不是增加实现工作量。


第四部分:从 Private Timer 到任务继续的完整过程

24. 一次 ID29 IRQ 的逐步状态变化

阶段 A:设备

1
2
3
Private Timer counter 到 0
→ Interrupt Status bit0 = 1
→ 若 IRQ enable=1,向本核输出 PPI 29 请求

阶段 B:GIC pending 与交付

1
2
3
4
GIC 看到本核 ID29 请求
→ 若 ID29 enable,置为 pending
→ Distributor/CPU Interface 结合 priority、PMR、当前 running priority 选择
→ 向当前 Cortex-A9 核断言 IRQ

阶段 C:ARM 架构异常入口

前提是 CPSR.I 允许 IRQ。CPU:

1
2
3
4
5
CPSR → SPSR_irq
返回信息 → LR_irq
切 IRQ 模式并屏蔽普通 IRQ
PC → VBAR + 0x18
当前 SP → SP_irq

阶段 D:汇编保存

1
2
3
4
修正返回 PC
保存 r0-r12、返回 PC、SPSR
保证栈对齐
必要时切换到 per-CPU interrupt stack

阶段 E:GIC acknowledge

1
2
iar = GICC_IAR;
id = iar & 0x3ff;

得到 ID29,GIC 将其置 active。

阶段 F:设备 handler

1
2
PT_STATUS = 1;       /* W1C 清设备事件 */
clock_tick();

clock_tick() 更新系统时间和时间片,必要时只设置:

1
need_reschedule = true

阶段 G:GIC 完成

1
GICC_EOIR = iar;

结束 GIC active 状态。

阶段 H:IRQ 尾部

1
2
3
4
降低嵌套/dispatch disable 计数
若最外层 IRQ 且 need_reschedule
调 scheduler 选择 heir
在统一出口切换/替换待恢复现场

阶段 I:异常返回

1
2
3
4
恢复通用寄存器
恢复 PC 和 CPSR
恢复异常前模式与 SP bank
原任务或新选中的任务继续

25. 一个教学用 Dispatcher 骨架

下面只表达寄存器语义,不是完整生产驱动:

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
#define MMIO32(a) (*(volatile uint32_t *)(uintptr_t)(a))

#define GICC_BASE 0xF8F00100u
#define GICD_BASE 0xF8F01000u
#define PT_BASE 0xF8F00600u

#define GICC_IAR MMIO32(GICC_BASE + 0x00Cu)
#define GICC_EOIR MMIO32(GICC_BASE + 0x010u)
#define PT_STATUS MMIO32(PT_BASE + 0x00Cu)

void irq_dispatch(arm_irq_frame *frame)
{
uint32_t iar = GICC_IAR;
uint32_t id = iar & 0x3ffu;

if (id >= 1020u) {
return;
}

switch (id) {
case 29:
PT_STATUS = 1u;
clock_tick(frame);
break;
default:
handle_unexpected_irq(id, frame);
break;
}

GICC_EOIR = iar;
}

生产实现还要确定:

  • GIC security/group 配置;
  • 嵌套中断策略;
  • 未知有效 ID 的屏蔽与日志策略;
  • MMIO 内存属性和屏障;
  • handler 注册表并发;
  • per-CPU 数据;
  • EOI 前后调度边界;
  • 中断亲和性;
  • 统计和超时保护。

volatile 只防止编译器删除/合并访问,不自动提供设备访问所需全部屏障和顺序保证。


26. 初始化顺序:不要先开总闸再补配置

建议第一版:

1
2
3
4
5
6
7
8
9
10
11
12
1. 屏蔽 CPU IRQ/FIQ
2. 建立 CPU0 的 SVC/IRQ/Abort/Undefined 等栈
3. 安装向量并设置 VBAR
4. 初始化 UART,具备最小可观察性
5. 关闭并配置 GIC Distributor
6. 配置 CPU0 的 CPU Interface、PMR
7. 停止并清理 Private Timer 旧状态
8. 设置 ID29 priority 和 enable
9. 配置 timer load/auto-reload/IRQ enable
10. 打开 GIC Distributor 和 CPU Interface
11. 完成必要 DSB/ISB
12. 最后 CPSIE I

实际某些模块开关的先后可根据复位状态调整,但原则是:

在 CPU 能接受 IRQ 之前,向量、栈、设备状态、GIC 和 handler 必须全部就绪。

启动由 FSBL/U-Boot 交接时,不能盲信所有寄存器处于上电默认值。先读取和记录现状,明确由谁负责复位/接管。


27. WFI:休眠不是中断处理

空闲任务常执行:

1
wfi

它让处理器等待事件/中断条件,降低空转。需要区分:

1
2
3
处理器从 WFI 唤醒

IRQ 满足屏蔽、优先级和路由条件并真正进入 handler

两者条件相关但不是一句“醒了就一定执行了 handler”可以概括。调试时仍要检查 CPSR.I、GIC enable、PMR 和设备状态。


把 Pending、Active、清源与 EOI 彻底分开

二、第一层:CPU0 与 CPU1 各有自己的 Private Timer

双核上不是:

1
2
3
CPU0 ─┐
├─ 共同使用一个 Private Timer ID29
CPU1 ─┘

而是:

1
2
3
CPU0 Private Timer ─→ CPU0 的 PPI29

CPU1 Private Timer ─→ CPU1 的 PPI29

编号相同不等于设备相同。

这类似于:

1
2
CPU0 有自己的 SP_irq
CPU1 也有自己的 SP_irq

名字相同,是因为每个 CPU 都采用相同的架构编号;实际物理状态属于各自 CPU。

每核独立的内容

对 ID29,至少要形成下面的图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CPU0
PT_LOAD
PT_COUNTER
PT_CONTROL
PT_STATUS
GIC enable[29]
GIC pending[29]
GIC active[29]
GICC_IAR / GICC_EOIR

CPU1
PT_LOAD
PT_COUNTER
PT_CONTROL
PT_STATUS
GIC enable[29]
GIC pending[29]
GIC active[29]
GICC_IAR / GICC_EOIR

所以 CPU0 初始化 ID29 后,CPU1 仍然需要:

1
2
3
4
初始化 CPU1 Private Timer
使能 CPU1 banked PPI29
初始化 CPU1 CPU Interface
准备 CPU1 IRQ 栈和异常入口环境

与 SPI 的对照

假设 UART 是一个 SPI:

1
2
3
4
5
一个 UART 设备

一个共享 SPI ID

Distributor 根据 target 选择 CPU0 或 CPU1

Private Timer 则是两套设备、两套 PPI 状态,不需要用 SPI target 在两核之间争抢同一个事件。


三、第二层:GIC 中断状态机

第一次只需掌握四种状态:

状态 含义
Inactive 没有等待处理,也没有正在处理
Pending 中断事件已经到达,等待 CPU acknowledge
Active CPU 已通过 IAR acknowledge,正在处理
Active + Pending 正在处理期间又来了一个新事件

一次正常中断

1
2
3
4
5
6
7
8
9
10
Inactive
│ Timer 到期、请求进入 GIC

Pending
│ CPU 读取 GICC_IAR

Active
│ CPU 写 GICC_EOIR

Inactive

如果设备源没有清除:

1
2
3
4
5
Active
│ 设备请求仍保持有效
│ CPU 写 EOI

重新 Pending

这就是中断风暴的根本原因之一。

IAR 的位置

CPU 已经进入 IRQ 向量之后,软件才读取 IAR:

1
2
3
4
GIC 先向 CPU 断言 IRQ
→ CPU 进入 VBAR+0x18
→ 汇编保存现场
→ 软件读取 IAR

IAR 不负责唤醒 GIC。GIC 一直在运行。IAR 的含义是:

CPU 正式领取当前最高优先级的有效中断。

读取有效 IAR 后:

1
2
3
返回 ID
pending → active
本核 running priority 更新

EOI 的位置

handler 完成后:

1
GICC_EOIR = iar

含义是:

CPU 告诉 GIC,这次已经领取的 active interrupt 处理结束。

每次有效 IAR 与 EOI 要一一配对。


四、第三层:设备清源与 GIC EOI

把它们想成两个不同的“工单系统”。

Private Timer 工单

1
PT_STATUS = 1

通知对象是 Private Timer:

读者的到期事件已经被软件看见,请清除 sticky status。

它不通知另一个 CPU,也不直接修改 GIC 的 active 状态。

GIC 工单

1
GICC_EOIR = iar

通知对象是 GIC:

CPU 对刚才 IAR 领取的中断已经处理完毕。

它不会替读者清 Private Timer、UART、网卡或 TTC 内部的事件状态。

两个动作的最小表

动作 通知谁 解决什么
PT_STATUS = 1 Private Timer 清设备事件/撤销请求条件
GICC_EOIR = iar GIC 结束 active 与运行优先级状态

五、ID1023 到底是什么

读取 IAR 返回 1023,表示:

当前没有满足条件、可以被本次读取 acknowledge 的有效 pending 中断。

可能出现的窗口包括:

  • IRQ 请求在 CPU 读取 IAR 前消失。
  • 中断优先级或屏蔽条件发生变化。
  • 软件进入公共 IRQ 入口时已经没有合格中断。
  • 多核竞争可能是某些共享中断场景中的原因之一,但不是 1023 的固定含义。

因此不能说:

1
1023 = 一定被另一个 CPU 处理了

正确处理:

1
2
3
4
5
6
uint32_t iar = GICC_IAR;
uint32_t id = iar & 0x3ffu;

if (id >= 1020u) {
return;
}

没有有效 acknowledge,就没有普通 active interrupt 需要 EOI。


六、ID29 为什么是 ISENABLER0.bit29

每个 ISENABLERn 管 32 个 ID:

1
2
3
4
ISENABLER0:ID 0–31
ISENABLER1:ID 32–63
ISENABLER2:ID 64–95
...

通用计算:

1
2
index = id / 32;
bit = id % 32;

ID29:

1
2
index = 29 / 32 = 0
bit = 29 % 32 = 29

所以:

1
GICD_ISENABLER0 = 1u << 29;

ID42:

1
2
index = 42 / 32 = 1
bit = 42 % 32 = 10

所以:

1
GICD_ISENABLER1 = 1u << 10;

注意:

  • ID29 是 PPI,ISENABLER0 的对应状态对每核 banked。
  • ID42 是 SPI,对应 Distributor 共享状态,还需要配置目标 CPU。

七、一条完整链只分五层记

不要一次背十二步。先记五层:

1
2
3
4
5
设备
→ GIC
→ CPU 异常入口
→ 软件 handler
→ 异常返回/可选调度

第 1 层:设备

1
2
3
CPU0 Private Timer counter 到 0
→ PT_STATUS=1
→ 向 GIC 提交 CPU0 PPI29

第 2 层:GIC

1
2
3
CPU0 ID29 → pending
→ 检查 enable、priority、PMR
→ CPU0 CPU Interface 断言 IRQ

第 3 层:CPU 异常入口

1
2
3
4
5
CPSR → SPSR_irq
返回信息 → LR_irq
切 IRQ 模式,看到 SP_irq
PC → VBAR+0x18
汇编保存 r0-r12、返回 PC、SPSR

第 4 层:软件 handler

1
2
3
4
5
6
7
8
读取 IAR
→ 得到 29
→ pending 变 active
→ dispatcher 调 timer handler
→ PT_STATUS 写 1 清设备源
→ 更新 tick
→ 必要时 need_reschedule=1
→ EOI 写回完整 iar

第 5 层:返回或调度

1
2
3
4
IRQ 尾部检查 need_reschedule
→ 不切换:恢复原任务
→ 要切换:选择 heir,恢复 heir 的最终现场
→ RFE/等价序列恢复 PC+CPSR

一句话版本

Timer 产生 PPI29,GIC 先 pending 并向 CPU 发 IRQ;CPU 进入统一 IRQ 向量,软件读 IAR 才把 ID29 领成 active;handler 清 Timer 状态,EOI 结束 GIC active;IRQ 尾部可调度,最后 RFE 恢复 PC 和 CPSR。


按故障现象逐层定位

十、完整 IRQ Dispatcher 骨架

下面代码是教学伪代码,不是可直接提交的完整驱动。它暂时假设启动环境和 GIC security/group 配置与当前执行状态匹配。

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
#define MMIO32(addr) \
(*(volatile uint32_t *)(uintptr_t)(addr))

#define GICC_BASE 0xF8F00100u
#define GICD_BASE 0xF8F01000u
#define PT_BASE 0xF8F00600u

#define GICC_CTLR MMIO32(GICC_BASE + 0x000)
#define GICC_PMR MMIO32(GICC_BASE + 0x004)
#define GICC_BPR MMIO32(GICC_BASE + 0x008)
#define GICC_IAR MMIO32(GICC_BASE + 0x00C)
#define GICC_EOIR MMIO32(GICC_BASE + 0x010)

#define GICD_CTLR MMIO32(GICD_BASE + 0x000)
#define GICD_ISENABLER0 MMIO32(GICD_BASE + 0x100)

#define PT_LOAD MMIO32(PT_BASE + 0x00)
#define PT_CONTROL MMIO32(PT_BASE + 0x08)
#define PT_STATUS MMIO32(PT_BASE + 0x0C)

static volatile uint64_t ticks;

void irq_dispatch(void)
{
uint32_t iar = GICC_IAR;
uint32_t id = iar & 0x3ffu;

if (id >= 1020u) {
return;
}

if (id == 29u) {
PT_STATUS = 1u;
++ticks;
} else {
unexpected_irq(id);
}

GICC_EOIR = iar;
}

注意:

  • 入口汇编已经保存异常现场并满足 AAPCS,C dispatcher 才能安全运行。
  • volatile 防止编译器把 MMIO 访问缓存或删除,但它不替代必要的硬件 memory barrier。
  • 真正初始化完成、执行必要 DSB/ISB 后,才能 CPSIE I
  • 对未知有效 ID 也必须采取明确策略并匹配 EOI,不能直接永远挂起 active 状态。
  • security group、初始固件状态和寄存器访问权限必须在实际启动环境中确认。

十一、从定时器到任务抢占

最小 tick 只做:

1
ticks++

接入调度器后:

1
2
3
4
5
6
定时器 handler
→ 更新系统时间和当前任务时间片
→ scheduler 判断 need_reschedule
→ IRQ 尾部检查标志
→ 若需要,执行任务 dispatch/context switch
→ 恢复最终选中任务的异常现场

为什么通常在 IRQ 尾部统一调度,而不是设备 handler 中直接随意切换:

  • 此时异常现场布局已知;
  • 中断嵌套层数可检查;
  • GIC active 状态和设备清源可以先正确收尾;
  • 多个 handler 只需设置调度请求;
  • 能避免在任意驱动函数深处切走导致栈和锁状态混乱。

这正是原定目标所说两块移植工作的汇合:

1
2
3
4
5
中断控制器移植
+
上下文调度移植

IRQ 尾部抢占式任务切换

十二、建议的真实实现顺序

阶段 1:只验证 GIC

1
2
3
4
向量和 IRQ 栈就绪
→ 初始化 Distributor/CPU Interface
→ 暂不开定时器
→ 用软件 pending 或已知外设验证一个 ID

阶段 2:只做可观察 tick

1
2
3
4
配置 Private Timer
→ handler 清状态
→ ticks++
→ 主循环偶尔打印 ticks

不要每次中断都打印,UART 很慢,会严重扰动时序。

阶段 3:主动上下文切换

两个任务主动 yield,先证明普通上下文正确。

阶段 4:tick 请求调度

handler 只设置 need_reschedule,IRQ 尾部执行安全调度。

阶段 5:SMP

最后才:

  • 启动 CPU1;
  • 初始化 CPU1 的 banked PPI、CPU Interface、栈和 per-CPU 数据;
  • 使用 SGI 做核间 reschedule;
  • 设计每核 tick 或统一 tick;
  • 处理锁、屏障、Cache 一致性和任务归属。

十三、Debug 检查表

完全进不了 IRQ

依次查:

  1. CPSR.I 是否仍为 1;
  2. VBAR 和 IRQ 向量是否正确;
  3. SP_irq 是否有效;
  4. GICD_CTLR 是否 enable;
  5. 本核 GICC_CTLR 是否 enable;
  6. GICC_PMR 是否放行优先级;
  7. ID29 是否在本核 enable;
  8. timer Control 的 timer/IRQ 位是否打开;
  9. timer Counter 是否递减。

进入一次后再也不进

查:

  • 是否写 1 清 Private Timer Status;
  • 是否写回 GICC_EOIR;
  • auto-reload 是否开启;
  • 是否误关 CPSR.I。

返回后立刻再次进入

查:

  • 设备状态是否真正清除;
  • 清源寄存器是否 W1C,却错误写了 0;
  • level-sensitive 外设电平是否仍有效;
  • EOI 和清源顺序是否正确。

双核时只有一个核正常

查:

  • CPU1 是否执行了本核 CPU Interface 初始化;
  • CPU1 是否写了 banked PPI enable;
  • CPU1 的 private timer/comparator 是否初始化;
  • CPU1 的 IRQ/SVC 栈和 per-CPU 指针是否正确;
  • SPI target mask 是否指向期望 CPU。

十四、十个自检问题

  1. 四类手册分别解决什么? Cortex-A9 Core TRM 说明单核实现,MPCore TRM 说明簇级 SCU/GIC/Timer,UG585 说明 Zynq 集成与物理地址,GIC 架构规范裁决通用状态机语义。
  2. 三类中断怎样区分? SGI 为 0~15,由软件指定目标核;PPI 为 16~31,每核私有;SPI 为 32~1019,由 Distributor 配置候选目标核。
  3. 为什么两个核都可以出现 ID29? 每个核都有自己的 Private Timer,ID0~31 的相关 GIC 状态也是每核 banked;编号相同不等于共享同一事件。
  4. 为什么初始化分成共享和每核两部分? Distributor 的共享 SPI 配置只做一次,GICC、SGI/PPI banked 状态、Private Timer 和模式栈则由每个 CPU 分别建立。
  5. 读 IAR 发生什么? 返回最高优先级有效中断,并完成 acknowledge,使该中断进入 Active 处理,同时更新运行优先级。
  6. 为什么 EOI 写回完整 IAR? 一次有效 acknowledge 必须匹配一次完成操作;SGI 的 IAR 还可能含源 CPU 信息,保留完整值最稳妥。
  7. ID1023 怎样处理? 它表示本次没有可交付的有效中断,不调用普通 handler,也不按普通有效中断写 EOI。
  8. 设备清源与 EOI 分别通知谁? 前者通知具体外设到期事件已处理,后者通知 GIC 当前 Active 服务结束,二者缺一都会破坏重复中断。
  9. ID29 位于哪个使能位? 29/32=029%32=29,因此是 GICD_ISENABLER0.bit29;CPU1 仍需在本核写自己的 banked PPI 使能。
  10. Timer load 怎样计算? load = PERIPHCLK / ((prescaler+1) × tick_hz) - 1。例如 333 MHz、prescaler 为 0、1 kHz tick 得到 332999。

十五、用一句话复述整条链

Private Timer 到期并设置状态,GIC 将 ID29 置为 Pending 并向本核交付 IRQ,处理器进入向量,入口汇编建立异常帧,软件读 IAR 使中断进入 Active,handler 清 Timer 状态并可设置 need_reschedule,随后写 EOIR,IRQ 尾部选择返回现场,最终恢复 PC 与 CPSR。

GIC 分发、Private Timer 与 SGI 的完整实现

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

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
#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) == 64u, "IRQ frame size");
_Static_assert(offsetof(CPU_Interrupt_frame, r) == 0u, "IRQ r0 offset");
_Static_assert(offsetof(CPU_Interrupt_frame, svc_lr) == 52u, "IRQ SVC LR offset");
_Static_assert(offsetof(CPU_Interrupt_frame, return_pc) == 56u, "IRQ PC offset");
_Static_assert(offsetof(CPU_Interrupt_frame, cpsr) == 60u, "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();
}

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;
}

CPU_Interrupt_frame *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 frame;
}

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);
}

从规则走到实际实现

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

三层使能,一条路径

1
2
3
4
5
6
7
8
9
Private Timer 倒计时到期
↓ PT_STATUS 置位,设备中断输出有效
GIC Distributor
↓ 检查该中断使能、Pending、优先级及相关路由
当前核 GIC CPU Interface
↓ 检查优先级屏蔽与运行优先级
Cortex-A9
↓ IRQ 允许时进入 IRQ 异常向量
irq_entry → irq_dispatch → 对应设备 handler

因此“开 IRQ”至少有三层含义:设备允许产生中断,GIC 接受并转发,CPU 没有屏蔽。只做 cpsie i 无法代替前两层。

中断还受安全分组、路由和实现配置影响。本工程在已满足基本访问权限的启动环境中,使用一套较简单的使能路径,没有实现通用安全世界中断管理。

SGI、PPI、SPI 不只是编号不同

类型 ID 范围 所属关系 本工程例子
SGI 0–15 软件生成,可指定目标 CPU SGI0 核间握手或调度请求
PPI 16–31 每 CPU 私有外设中断 Private Timer:29
SPI 32 起 共享外设中断,可路由到 CPU UART、TTC、PL 中断等

相同的 PPI29,在 CPU0 和 CPU1 上对应各自的 Private Timer。若只初始化 CPU0 的 banked enable,不代表 CPU1 的 PPI29 也已经打开。

GICD 虽然总体是共享 Distributor,但 SGI/PPI 相关寄存器的一部分是 banked。GICC 则是本核接口:两个核使用相同 MMIO 地址访问,也可能看到各自不同的状态。不要用“地址相同”推断“所有寄存器都是全局同一份”。

工程 irq_table[160] 的长度是软件容量,不能据此宣称芯片上全部 160 个 ID 都实际连接了设备。具体实现 ID 与保留范围要查 SoC 中断表。

几个真正需要对上的地址

本工程 private peripheral base 为 0xF8F00000

模块 相对偏移 实际基址
SCU 0x0000 0xF8F00000
GIC CPU Interface 0x0100 0xF8F00100
Global Timer 0x0200 0xF8F00200
Private Timer 0x0600 0xF8F00600
GIC Distributor 0x1000 0xF8F01000
L2C-310 0x2000 0xF8F02000

例如 GICC_IAR 相对 CPU Interface 基址偏移 0x0c,因此地址为 0xF8F0010c。不要把手册里的寄存器偏移直接当成 SoC 的绝对地址。

Pending 与 Active 描述不同阶段

一个简化的非嵌套中断状态过程:

1
2
3
4
Inactive
└─ 收到事件 → Pending
└─ CPU 读 IAR 应答 → Active
└─ EOI → Inactive

如果 Active 期间又有新事件或电平仍有效,还可能出现 Active and Pending,结束当前服务后重新投递。

Pending 表示有待处理请求;Active 表示已经被 CPU 接手处理。二者不是“中断开/关”的同义词。使能位则控制是否允许参与投递,是另一个维度。

优先级通常数值越小越高。GICC_PMR 是门槛,运行优先级与 BPR 又影响抢占判定。仅把 PMR 写宽松,并不会跳过 Distributor 或设备的使能。

当前实现禁止 IRQ 嵌套,因此虽然初始化了优先级和 BPR,却没有提供多层 handler 抢占流程。

读 IAR 是有副作用的

基础版 irq_dispatch() 读取一次 IAR,把完整结果保存到局部变量:

1
2
const uint32_t iar = *register32(GICC_BASE + GICC_IAR);
const uint32_t id = iar & 0x3ffu;

IAR 的读取不仅报告 ID,还执行 acknowledge。它不是随便查看 Pending 的无副作用状态寄存器。

所以不能这样调试:

1
2
print_id(read_iar());
dispatch(read_iar()); /* 错误示意:第二次读取不保证还是同一次中断 */

GDB 中对 IAR 做 x/wx 也可能改变硬件状态。优先查看代码已保存的 iarlast_irq_id,不要让调试动作抢走系统应答。

IAR 除了 ID,还可能包含 SGI 来源 CPU 信息。工程最后写回 EOIR 的是原始完整 IAR,而不是只写掩码后的 id。

设备清源和 GIC EOI 缺一不可

Private Timer handler 很短:

1
2
3
private_timer_acknowledge();  /* PT_STATUS 写 1 清除 */
++tick_count;
scheduler_request();

其中 PT_STATUS 是 W1C。写 0 不会清除,写入旧值并按普通 R/W 位理解也可能不符合寄存器语义。

设备清源后,分发器执行:

1
2
3
4
arm_data_sync_barrier();
*register32(GICC_BASE + GICC_EOIR) = iar;
arm_data_sync_barrier();
return scheduler_irq_tail(frame);

在当前 GICv1 使用方式下,EOI 完成控制器侧本次服务的结束处理,包括相应运行优先级/活动状态更新。不能把 GICv2 某些配置中 priority drop 与 deactivate 分开的写法套进来。

只清设备、不写 EOI:设备安静了,GIC 仍可能认为该中断处于服务中,后续投递受阻。

只写 EOI、不清设备:若中断线仍有效,就可能立即再进 IRQ,表现为中断风暴。

DSB 用于满足这里的访问完成和排序需求。某些真实外设还可能要求特定读回或额外同步步骤,要遵循设备手册,不能假定所有芯片只插一条屏障都足够。

Spurious ID 的处理

本路径把 ID1023 作为 spurious,即此次没有可正常处理的有效中断。不能用 1023 当 handler 数组索引,也不能为它机械写一次 EOIR。

基础版直接返回当前 frame;精简版返回“不切换”的结果。若以后扩展到其他 GIC 版本、安全状态或特殊 ID,需要按对应规范补齐保留值处理,不能只保留一个 1023 特判就宣称通用。

Private Timer 怎样产生周期事件

基础版的启动流程如下,PT_* 用符号化寄存器简写;实际 MMIO 访问封装在 irq.c 中:

1
2
3
4
5
6
zynq_irq_register(29u, private_timer_handler, NULL);
zynq_irq_enable(29u, 0xa0u);
PT_CONTROL = 0u;
PT_STATUS = 1u;
PT_LOAD = reload_value;
PT_CONTROL = ENABLE | AUTO_RELOAD | IRQ_ENABLE;

最后再在合适位置允许 CPU IRQ。先建立向量、模式栈、handler 表和 GIC,再启动事件源,可以避免第一下 Timer 提前到来。

Private Timer 是递减计数器。周期取决于输入时钟、预分频、load 值和计数器的到零/重载规则。只看源码常量无法直接得到真实秒数。

精简版命名为 HALF_SECOND 的常量取值 166666667,来自约 333.333 MHz 的时钟假设。这是配置意图。QEMU 模型与实际 PS 时钟配置可能不同,需要用可信时基测量;日志打印“0.5s”不是周期证明。

Zynq 的定时器不只有一种

定时器 特征 当前用途
A9 Private Timer 每核独立,递减,可周期 IRQ tick、抢占实验
A9 Global Timer 64 位递增计数器,支持比较功能 基础版仪表盘轮询
Private Watchdog 每核看门狗相关功能 未作为本系列调度源
System Watchdog SoC 级监控 未接入
TTC 多通道计数、间隔等功能 后续外设计时可考虑

Cortex-A9 Global Timer 不能与其他 ARM 处理器上的 Generic Timer 混为一谈。它们的寄存器模型和中断连接不同。

基础版在抢占自检结束后停止 Private Timer、屏蔽 IRQ,进入永久协同演示。因此旧 tick 会冻结。仪表盘增加 Global Timer 轮询是为了提供独立的动态计数,不会重新触发旧的抢占调度器。

当前只显示 Global Timer 低 32 位原始计数。它会回绕,不是系统运行秒数;若要做长期时间,读取 64 位计数需要正确处理 high/low 跨越,常见方式是 high→low→high 并在高位变化时重读。

handler 注册表只是最小分发框架

注册表把 ID → handler + argument 分开,使 Timer 与 SGI 共用一个分发流程:

1
2
IAR → ID → irq_table[id] → handler
→ 清源 → 完整 IAR 写 EOIR → IRQ tail

但它还没有动态注销时的并发保护、共享 IRQ 的多个 handler、复杂的设备生命周期管理和未知中断抑制。未知中断仅增加计数时,如果源始终不清,仍可能反复进入;完善驱动框架需要另外定义隔离策略。

从现象定位层次

现象 优先检查
Timer counter 不动 时钟、Timer enable、MMIO 地址
状态位置位但没有 IRQ 设备 IRQ enable、GIC enable/PMR、CPSR.I
进 IRQ 却没有 handler IAR 是否只读一次、ID 是否正确、表项是否注册
只来一次 自动重载、清源、EOIR
无休止重进 IRQ 设备源未清、W1C 写法、触发方式
调试后行为变化 是否读取了 IAR、串口延迟是否改变时序

资料定位:本地 UG585 v1.12.2 第 7 章 Interrupts、第 8 章 Timers;Cortex-A9 MPCore TRM 第 3、4 章;GIC 规范中的 Interrupt Acknowledge RegisterEnd of Interrupt Register。第 8 章不能只读最初几页就认为涵盖了 TTC 和看门狗。

下一篇:IRQ 尾部调度、精简现场与 RTEMS

重点回看

重复工作的 IRQ 依赖设备、GIC 和 CPU 三层共同闭环。读 IAR 会把中断从 Pending 推向 Active,设备 handler 负责清源,EOIR 负责结束 GIC 侧服务,二者不能互相替代。PPI29 是每核私有状态,Distributor 与 GICC 的初始化归属也不同。

本章总结

重复工作的 IRQ 依赖设备、GIC 和 CPU 三层共同闭环。读 IAR 会把中断从 Pending 推向 Active,设备 handler 负责清源,EOIR 负责结束 GIC 侧服务,二者不能互相替代。PPI29 是每核私有状态,Distributor 与 GICC 的初始化归属也不同。

下一章让 Timer IRQ 提出调度请求,并比较完整 frame、精简 frame 与 RTEMS 的处理方式。

ARMv7-A小系统实践(七):GIC、定时器与一次完整的IRQ

https://goko-son626.github.io/post/armv7a-06-gic-timers.html

作者

GoKo Mell

发布于

2026-06-27

更新于

2026-06-27

许可协议

评论

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