本章记录什么 这一章沿着一枚定时器中断走完整条链: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 优先级方向
例如 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 ; }
spurious 可能来自请求在 CPU 读 IAR 前消失、优先级条件改变或没有合格 pending 中断。它不是一个应派发到设备 handler 的真实 ID。
14. GICC_EOIR:结束 GIC 侧的处理 对每次有效 IAR acknowledge,软件必须有匹配的 EOI:
它告诉 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。
基址:
偏移
寄存器
作用
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 ; PT_LOAD = load; gic_set_priority(29 , 0xA0 ); gic_enable_id_for_this_cpu(29 ); PT_CONTROL = (prescaler << 8 ) | (1u << 2 ) | (1u << 1 ) | (1u << 0 );
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;
适合做统一时间基准和每核时钟事件。
基址:
偏移
作用
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 ; clock_tick();
clock_tick() 更新系统时间和时间片,必要时只设置:
阶段 G:GIC 完成
结束 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 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 完成后:
含义是:
CPU 告诉 GIC,这次已经领取的 active interrupt 处理结束。
每次有效 IAR 与 EOI 要一一配对。
四、第三层:设备清源与 GIC EOI 把它们想成两个不同的“工单系统”。
Private Timer 工单
通知对象是 Private Timer:
读者的到期事件已经被软件看见,请清除 sticky status。
它不通知另一个 CPU,也不直接修改 GIC 的 active 状态。
GIC 工单
通知对象是 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 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 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 依次查:
CPSR.I 是否仍为 1;
VBAR 和 IRQ 向量是否正确;
SP_irq 是否有效;
GICD_CTLR 是否 enable;
本核 GICC_CTLR 是否 enable;
GICC_PMR 是否放行优先级;
ID29 是否在本核 enable;
timer Control 的 timer/IRQ 位是否打开;
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。
十四、十个自检问题
四类手册分别解决什么? Cortex-A9 Core TRM 说明单核实现,MPCore TRM 说明簇级 SCU/GIC/Timer,UG585 说明 Zynq 集成与物理地址,GIC 架构规范裁决通用状态机语义。
三类中断怎样区分? SGI 为 0~15,由软件指定目标核;PPI 为 16~31,每核私有;SPI 为 32~1019,由 Distributor 配置候选目标核。
为什么两个核都可以出现 ID29? 每个核都有自己的 Private Timer,ID0~31 的相关 GIC 状态也是每核 banked;编号相同不等于共享同一事件。
为什么初始化分成共享和每核两部分? Distributor 的共享 SPI 配置只做一次,GICC、SGI/PPI banked 状态、Private Timer 和模式栈则由每个 CPU 分别建立。
读 IAR 发生什么? 返回最高优先级有效中断,并完成 acknowledge,使该中断进入 Active 处理,同时更新运行优先级。
为什么 EOI 写回完整 IAR? 一次有效 acknowledge 必须匹配一次完成操作;SGI 的 IAR 还可能含源 CPU 信息,保留完整值最稳妥。
ID1023 怎样处理? 它表示本次没有可交付的有效中断,不调用普通 handler,也不按普通有效中断写 EOI。
设备清源与 EOI 分别通知谁? 前者通知具体外设到期事件已处理,后者通知 GIC 当前 Active 服务结束,二者缺一都会破坏重复中断。
ID29 位于哪个使能位? 29/32=0、29%32=29,因此是 GICD_ISENABLER0.bit29;CPU1 仍需在本核写自己的 banked PPI 使能。
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 , 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 ) { *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; } 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) { 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; } 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 也可能改变硬件状态。优先查看代码已保存的 iar 或 last_irq_id,不要让调试动作抢走系统应答。
IAR 除了 ID,还可能包含 SGI 来源 CPU 信息。工程最后写回 EOIR 的是原始完整 IAR,而不是只写掩码后的 id。
设备清源和 GIC EOI 缺一不可 Private Timer handler 很短:
1 2 3 private_timer_acknowledge(); ++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 Register 与 End 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 的处理方式。