AArch64 通用定时器

本文介绍 Arm A-profile 的通用定时器架构。

通用定时器为 Arm 内核提供了一个标准化的定时器框架。通用定时器包括一个系统计数器和一组每个内核的定时器,如下图所示:

system-counter

核心架构组成

  • **系统计数器 (System Counter)**:
    • 一个始终运行(Always-on)的全局设备,生成递增的计数值(56 到 64 位宽)。
    • 将计数值广播给系统内的所有 CPU 核心,为多核架构提供统一的时间视角。
  • **核心独立定时器 (Per-core Timers)**:
    • 每个 CPU 核心配有一组独立的定时器(比较器),通过将设定的目标值与广播的系统计数值进行对比,来触发中断或事件。

计数频率演进

  • Armv8.6-A / Armv9.1-A 之前:频率由系统设计自行决定,通常在 1MHz 到 50MHz 之间。
  • Armv8.6-A 与 Armv9.1-A 起:计数频率统一固定为 1GHz(即计数值每递增 1 代表 1 纳秒)。

1. 处理器定时器

定时器名称 何时存在 作用
EL1 Physical Timer Always EL1 软件使用的物理定时器,直接基于 Physical Counter,常用于 Linux 内核定时。
EL1 Virtual Timer Always EL1 软件使用的虚拟定时器,基于 Physical Counter - CNTVOFF,可供 Host OS 或 Guest OS 使用,实现虚拟时间隔离。
Non-secure EL2 Physical Timer Implements EL2 非安全 EL2 使用的物理定时器,主要供 Hypervisor 使用。
Non-secure EL2 Virtual Timer Implements EL2 非安全 EL2 使用的虚拟定时器,开启 VHE 时 Host OS 可直接使用,否则主要由 Hypervisor 使用。
EL3 Physical Timer Implements EL3 EL3 使用的物理定时器,供 Secure Monitor 或固件使用。
Secure EL2 Physical Timer Implements FEAT_SEL2 安全 EL2 使用的物理定时器,供 Secure Hypervisor 或 Secure Partition Manager 使用。
Secure EL2 Virtual Timer Implements FEAT_SEL2 安全 EL2 使用的虚拟定时器,供安全世界中的虚拟化环境(如 Secure Partitions)使用。

Physical Timer 面向真实硬件时间(Real Time);Virtual Timer 面向虚拟机时间(Virtual Time)。

对于普通裸机 Linux,这两者通常几乎没有区别,因为 CNTVOFF = 0,所以很多开发者甚至感觉不到 Virtual Timer 的存在。只有进入 KVM、Xen 等虚拟化环境后,Virtual Timer 的价值才真正体现出来。

  • 没有 VHE:Host Linux 在 EL1,Guest Linux 在 EL1,KVM 在 EL2;Host Linux 使用 EL1 Virtual/Physical Timer(Linux 通常使用 Virtual Timer)。

  • 开启 VHE:Host Linux 整体搬到 EL2,Guest Linux 仍在 EL1;Host Linux 使用 *EL2 Virtual Timer (CNTHV_)**,KVM/Hypervisor 使用 *EL2 Physical Timer (CNTHP_)**。没有两个 Host Linux,只是同一个 Host Linux 换了运行级别。

1.1 计数和频率

计数

系统通过 CNTPCT_EL0 寄存器向软件暴露当前的物理计数值。

在实际应用中,由于处理器存在乱序执行(Out-of-order execution)和推测性读取机制,直接读取该寄存器可能会导致读取到的时间戳与指令预期流转顺序不符。

1
2
3
4
5
6
loop:           // 轮询通信状态,以确定是否需要读取定时器
LDR X1, [X2] // 从内存地址 [X2] 加载数据到 X1 寄存器
CBZ x1, loop // 如果 X1 为 0(说明未准备好),则继续循环等待
ISB // 指令同步屏障。如果没有 ISB,CPU 可能会发生乱序推测读取,
// 导致在 [X2] 对应的内存写入 0 之前,就已经提前读取了 CNTPCT 的值
MRS X1, CNTPCT_EL0 // 读取物理计数器(CNTPCT_EL0)的值到 X1 寄存器

为了解决这一指令顺序交错问题,有两种解决思路:

  • 传统做法:在执行读取指令前手动插入指令同步屏障 ISB,强行清空流水线,以确保计时动作发生在特定内存或逻辑操作之后;
  • 新架构优化:从 Armv8.6-A 与 Armv9.1-A 扩展开始,架构引入了自同步版本的计数寄存器 CNTPCTSS_EL0,在硬件层面实现了读操作的同步,无需再手动添加屏障。

频率

系统通过 CNTFRQ_EL0 寄存器来汇报系统计数器的更新频率。

需要注意的是,该寄存器并非由硬件自动填入数值,而是需要软件干预。通常在系统刚启动时,运行在最高特权级(如 EL3)的底层固件(Firmware)负责将芯片实际的频率值写入该寄存器;后续运行的操作系统(OS)或应用软件只需读取 CNTFRQ_EL0,即可将抽象的计数值准确换算为真实的物理时间(如秒、毫秒或纳秒)。

1.2 定时器寄存器

在 Arm 架构中,每一个定时器都包含以下 3 个功能固定的系统寄存器

寄存器模板 目的 / 作用
<timer>_CTL_EL<x> 控制寄存器(Control register):用于使能/禁用定时器、屏蔽中断或查看中断状态。
<timer>_CVAL_EL<x> 比较值寄存器(Comparator value):存储 64 位的绝对目标计数值,当 System Counter >= CVAL 时触发中断。
<timer>_TVAL_EL<x> 定时器值寄存器(Timer value):存储 32 位的相对倒计时数值(CVAL = SystemCounter + TVAL),写入此寄存器可快速配置倒计时。

寄存器名称中的 <timer> 代表具体的定时器前缀,EL<x> 代表访问该寄存器所需的最低异常级别(Minimum Exception Level)。硬件为各特权级和安全状态下的定时器提供了独立的寄存器集合:

定时器名称 (Timer) 寄存器前缀 (Register prefix) 最低访问级别 (EL) 访问级别
EL1 physical timer (EL1 物理定时器) CNTP EL0 可授权 EL0 访问
EL1 virtual timer (EL1 虚拟定时器) CNTV EL0 可授权 EL0 访问
Non-secure EL2 physical timer (非安全 EL2 物理定时器) CNTHP EL2 不开放给 EL1
Non-secure EL2 virtual timer (非安全 EL2 虚拟定时器) CNTHV EL2 不开放给 EL1
EL3 physical timer (EL3 物理定时器) CNTPS EL1 (注意这个) 可授权 Secure EL1 访问
Secure EL2 physical timer (安全 EL2 物理定时器) CNTHPS EL2 不开放给 EL1
Secure EL2 virtual timer (安全 EL2 虚拟定时器) CNTHVS EL2 不开放给 EL1

注意:寄存器名后缀 _ELx 表示该系统寄存器接口定义在哪一级异常级(lowest architectural access level),而不是 Timer 属于哪个 EL

1.3 访问定时器

对于某些计时器,可以配置哪些异常级别可以访问该计时器:

  • EL1 物理和虚拟定时器权限:

    • EL0 对这些定时器的访问由 CNTKCTL_EL1 控制。
  • EL2 物理和虚拟定时器权限:

    • HCR_EL2.{TGE,E2H}=={1,1} (表示 VHE 开启),EL0 对这些定时器的访问由 CNTKCTL_EL2 控制。这些定时器是作为 Armv8.1-A 虚拟化主机扩展支持的一部分而添加的。
  • EL3 物理定时器权限:

    • 防越权拦截机制

      运行在安全操作系统/应用(S.EL1)的代码如果尝试访问 EL3 物理定时器,可通过配置控制位 SCR_EL3.ST = 1 将该访问强制捕获(Trap)并重定向回 EL3 固件处理,由 EL3 决定是否代为执行。

    • Armv8.4-A 硬隔离强化

      从 Armv8.4-A 架构开始,一旦开启安全虚拟化(即 SCR_EL3.EEL2 == 1),硬件将彻底封锁低级别安全软件(S.EL1 / S.EL2)对 EL3 物理定时器的访问。此时任何越权访问都会直接触发“未定义指令异常”(UNDEF),从硬件层面禁止访问。

1.4 配置定时器

架构提供了基于“绝对时间”和“相对倒计时”的两种编程模型,两者在底层本质上操作的是同一套定时器硬件:

  • 比较值寄存器模式(CVAL

    CVAL 是一个 64 位的绝对比较寄存器。软件向其写入一个目标时刻值,当系统的总计数值等于或超过该值(即 System Count >= CVAL)时,定时器触发中断。这种模式适合“需要在特定系统时刻 Y 触发事件”的场景。

  • 定时器值寄存器模式(TVAL

    TVAL 是一个 32 位的相对倒计时寄存器。软件向其写入需要等待的 Tick 数 X(即“在 X 个时钟周期后触发”),处理器会在内部自动读取当前系统计数值,将两者相加后填入 CVAL(即计算出 CVAL = TVAL + System Counter)。

需要注意的是,TVAL 是一个带符号(Signed)的递减数值。当定时器到达指定时间并触发中断后,TVAL 仍会继续向负数递减,这方便软件在响应中断时,能够精准计算出“定时器已经超时过去了多久”。

1.5 中断

通用定时器的中断具有严格的核内私有属性(Private Peripheral Interrupt, PPI),即某个 CPU 核心的定时器中断只能传递给该核心本身,无法跨核触发。这意味着每个 CPU 核心在硬件层面都拥有一套独立的定时器中断映射。


中断控制与触发机制

定时器中断的生成主要通过控制寄存器 CTL 进行管理。要使能中断,软件必须将 ENABLE 置 1 并清除掩码位 IMASK(设为 0)。当满足条件 CVAL <= System Count 时,ISTATUS 位会被拉高,定时器随即向中断控制器(通常为 GIC)发出中断信号。

需要注意的是,定时器中断属于电平触发(Level-sensitive)。一旦触发条件成立,中断信号会持续拉高,直到软件采取以下三种措施之一来消除中断状态:

  • IMASK 设为 1(屏蔽中断)
  • ENABLE 清零(禁用定时器)
  • 重新写入 TVALCVAL,使当前不满足超时条件

开发提示:在编写定时器中断处理程序(Interrupt Handler)时,软件必须先清除或重新配置定时器的超时状态,然后再去清除 GIC 中断控制器中的中断状态。如果顺序颠倒,GIC 会因为检测到低端持续拉高的电平信号而再次误触发同个中断。


SBSA 推荐的中断 ID(INTID)映射

为了保持跨平台架构的兼容性,服务器基本系统架构(SBSA)定义了一套标准的中断编号规范。这些 INTID 全部位属于 PPI 范围,对各个内核私有(例如每个 CPU 核心看到自己的 EL1 物理定时器中断号均为 30):

定时器类型 SBSA 推荐 INTID
EL1 物理定时器 (EL1 Physical Timer) 30
EL1 虚拟定时器 (EL1 Virtual Timer) 27
非安全 EL2 物理定时器 (Non-secure EL2 Physical Timer) 26
非安全 EL2 虚拟定时器 (Non-secure EL2 Virtual Timer) 28
EL3 物理定时器 (EL3 Physical Timer) 29
安全 EL2 物理定时器 (Secure EL2 Physical Timer) 20
安全 EL2 虚拟定时器 (Secure EL2 Virtual Timer) 19

1.6 定时器虚拟化

物理定时器(例如 EL3 物理定时器 CNTPS )会与系统计数器提供的计数值进行比较。该值称为物理计数,由 CNTPCT_EL0 报告。

虚拟计时器(例如 EL1 虚拟计时器、 CNTV )会与虚拟计数进行比较。

images_02_System-Counter-block-diagram

虚拟定时器 主要使用者 硬件比较寄存器 对应的时间偏移寄存器 控制/配置级别
EL1 虚拟定时器 (最常用) 普通虚拟机(Guest OS) CNTV_CVAL_EL0 / TVAL CNTVOFF_EL2 NS-EL2 (Hypervisor) 配置
非安全 EL2 虚拟定时器 (Armv8.1 VHE) VHE 模式下的宿主机/嵌套虚拟机 CNTHV_CVAL_EL2 / TVAL CNTHVOFF_EL2 NS-EL2 配置
安全 EL1 虚拟定时器 (Armv8.4 Secure EL2) 安全虚拟机 / Secure Partition CNTPSV_CVAL_EL1 / TVAL CNTSOFF_EL2 Secure EL2 配置
安全 EL2 虚拟定时器 (Armv8.4 Secure EL2) Secure Hypervisor CNTHVS_CVAL_EL2 / TVAL CNTHS OFF 体系结构定义 Secure EL2/EL3 配置

1.7 Event stream(事件流)

事件流(Event Stream)的应用场景

在 Arm 架构中,当线程需要等待某个短期资源可用时,通常会执行 WFE 指令让 CPU 核心进入低功耗状态,直到接收到“事件”才会被唤醒。生成唤醒事件的方式主要有三种:

  • 其他 CPU 核心执行 SEV(Send Event)指令
  • 核心清除自身的全局独占监视器(Global Exclusive Monitor)
  • 使用通用定时器定期生成的“事件流”

核心作用:由于资源等待可能是非确定的,定时器产生的事件流可以作为超时保底机制(Timeout),确保 CPU 核心在低功耗状态下的停留时间是有界限的(Bounded),防止因长时间拿不到资源而导致内核永久卡死。


控制寄存器与触发机制

事件流既可以基于物理计数(CNTPCT_EL0)生成,也可以基于虚拟计数(CNTVCT_EL0)生成。其行为主要由 CNTKCTL_EL1(控制虚拟计数事件流)或 CNTHCTL_EL2(控制物理计数事件流)进行配置,每个控制寄存器包含以下三个关键控制项:

  • EVNTEN(使能开关):控制是否开启定时器的事件流生成功能。
  • EVNTI(事件触发频率):指定计数器的某一位(Bit)。当选定的位发生翻转时,触发一次事件。例如:若 EVNTI 设为 3,则每当计数器的 Bit[3] 发生变化时,硬件就会自动生成一个事件。
  • EVNTDIR(触发方向):控制事件是在选定位从 1 -> 0(下降沿)还是从 0 -> 1(上升沿)变化时产生。

1.8 总结表

Timer Registers Typically used by Trappable? Using counter INTID
EL1 Physical Timer CNTP_<>_EL0 EL0 and EL1 To EL2 CNTPCT_EL0 30
EL2 Non-secure Physical Timer CNTHP_<>_EL2 NS.EL2 CNTPCT_EL0 26
EL2 Secure Physical Timer CNTHPS_<>_EL2 S.EL2 CNTPCT_EL0 20
EL3 Physical Timer CNTPS_<>_EL1 S.EL1 and EL3 To EL3 CNTPCT_EL0 29
EL1 Virtual Timer CNTV_<>_EL0** EL0 and EL1 CNTPCT_EL0 27
EL2 Non-secure Virtual Timer CNTHV_<>_EL2 NS.EL2 CNTPCT_EL0 28
EL2 Secure Virtual Timer CNTHVS_<>_EL2 S.EL2 CNTPCT_EL0* 19

安全 EL2 虚拟定时器的计数机制: 对于安全 EL2 虚拟定时器(CNTHVS),其虚拟偏移量(CNTVOFFSET_EL2)在硬件行为上恒等于 0。因此,虽然该定时器逻辑上对比的是虚拟计数,但实际效果等同于使用物理计数(CNTPCT_EL0)。

VHE 模式下的寄存器重定向(Redirection): 在使能 VHE(Virtualization Host Extensions,即 HCR_EL2.E2H == 1)时,访问 CNTP_<>_EL0CNTV_<>_EL0 会被硬件自动重定向到对应的 EL2 寄存器,以便宿主机(Host OS)直接在 EL2 运行。

2. 系统计数器

为了提供稳定一致的时间基准,系统计数器的运行频率是恒定的。架构定义了两个核心概念来描述其频率属性:

  • 有效频率(Effective Frequency):定义了计数器每递增 1 代表的实际物理时间(如 1GHz 代表每单位 1ns)。有效频率由 CNTFRQ_EL0 寄存器报告,且在系统运行期间不允许改变。从 Armv8.6/Armv9.1-A 开始,有效频率固定为 1GHz(更早版本通常在 1~50MHz)。
  • 计数器分辨率(Counter Resolution):代表计数器硬件实际更新/递增的频率。与有效频率不同,分辨率允许随时间动态调整(例如系统进入低功耗状态时降低更新频率)。例如有效频率为 1GHz 但实际时钟跑在 100MHz 时,计数器每 10ns 会直接一次性加 10。

简单说,count不一定是每次加1的,也可能一次加10,每次步进的长度叫做计数器分辨率。

2.1 计数器缩放

在 Armv8.4-A 之前,系统计数器在每个时钟 Tick 只能固定递增 1。引入缩放功能后,每次 Tick 允许递增指定的数值 X,从而实现动态加快或放慢计数值增长速率的效果。

缩放机制

硬件数据结构(88-bit 内部存储): 为了精确支持小数级别的缩放步进,硬件内部将计数器扩展到了 88 位定点数

  • 高 64 位(整数部分):导出给 CPU 核心(如通过 CNTPCT_EL0 寄存器)作为可视计数值。
  • 低 24 位(小数部分):仅由系统计数器内部维护,用于精度累加。

images_04_bit-integer-part-of-count-image

这个是内部计数器,不是直接给程序读取的,程序还是读 CNTPCT_EL0。

**配置寄存器 CNTSCR**: 缩放步进值由 32 位的 CNTSCR 寄存器设定(包含 8 位整数部分和 24 位小数部分)。

  • 例如:配置为 0x0180_0000 时,代表每个 Tick 递增 1.5(整数 1 + 小数 0.5)。经过两次 Tick 后,内部计数值变为 3.0,CPU 读到的整数计数值由 1 阶跃至 3
Tick Internal counter value (Integer part / Fractional part) Exported counter value (Visible via CNTPCT_EL0)
0 0x0000_0000_0000_0000_0000_00 0x0000_0000_0000_0000
1 0x0000_0000_0000_0001_8000_00 0x0000_0000_0000_0001
2 0x0000_0000_0000_0003_0000_00 0x0000_0000_0000_0003
3 0x0000_0000_0000_0004_8000_00 0x0000_0000_0000_0004
4 0x0000_0000_0000_0006_0000_00 0x0000_0000_0000_0006
5 0x0000_0000_0000_0007_8000_00 0x0000_0000_0000_0007
6 0x0000_0000_0000_0009_0000_00 0x0000_0000_0000_0009

安全约束: 缩放步进仅允许在系统计数器被禁用(Disabled)时配置。在运行过程中修改缩放因子会导致读取到未定义的错误数值。

从软件和 CPU 的视角来看,计数器缩放的效果就是“让时间走得更快或更慢”。

理解缩放机制

软件视角:时间被“缩放”了(Scaling)

软件(比如操作系统内核)是通过读取 CNTPCT_EL0 等寄存器的计数值,再结合固定频率 CNTFRQ_EL0(比如 1GHz)来计算时间的:

经过的时间 = (当前读到的计数值 - 起始读到的计数值) ÷ 固定的系统频率

  • 正常情况(步进 = 1):真实物理世界过去了 1 秒,硬件敲了 1,000,000,000 次 Tick,计数值增加了 $1,000,000,000$。软件算出:“过去了 1 秒”。
  • 开启缩放(比如步进设为 2):真实物理世界过去了 1 秒,硬件同样敲了 1,000,000,000 次 Tick,但因为每次加 2,计数值增加了 $2,000,000,000$。软件按公式一算:“居然过去了 2 秒!”

👉 所以对软件来说,时间被加速了 2 倍。

硬件与芯片设计的本质视角:消除“时钟频率不匹配”

在物理实际上,真实的自然时间(物理世界的时间)是不可能被芯片改变的。Arm 设计这个机制的根本目的,不是为了玩“时光倒流/加速”的魔术,而是为了解决现实硬件设计中的“换算”麻烦

  1. 晶振频率不规则

    假设你的板子上接了一个很怪异的物理晶振,实际频率是 1.333GHz,但软件规范要求标准的系统时间必须按 1GHz 来算。

    • 以前(没有缩放机制): 软件每次读取计数值后,都必须在代码里手动跑一遍乘除法换算(比如:真实时间 = 读到的计数值 × (1.0 ÷ 1.333))。频繁进行这类小数或大数除法非常消耗 CPU 性能,大幅拉低了系统效率。
    • 现在(有了缩放机制): 硬件直接把步进寄存器 CNTSCR 设置为 0.75(因为 1.0 ÷ 1.333 约为 0.75)。这样硬件在每次 Tick 时只自动累加 0.75,直接在源头就把数值换算好了,对齐到标准的 1GHz 节奏。软件读出来直接用就行,完全不需要再做任何额外计算!
  2. 仿真与调试(Simulation)

    在电脑上用软件(如 QEMU)模拟芯片运行(仿真测试)时,仿真器的速度往往只有真实物理芯片的几十分之一。通过缩放机制,可以把仿真环境里的时间按比例放慢,让跑在里面的操作系统以为时间还在正常流逝,从而顺畅地进行断点调试和性能分析。

我的理解:缩放机制是为了解决晶振频率无法被整除的问题。

2.2 基础编程

两个核心寄存器帧(Register Frames)

Arm 推荐的 system counter 硬件接口划分为了两个独立的内存映射区(帧):

  1. CNTControlBase(控制帧):
    • 作用:用于对系统计数器进行完整配置与控制。
    • 安全策略:在支持 TrustZone 的系统上,该帧仅允许安全访问(Secure Access Only)
  2. CNTReadBase(只读帧):
    • 作用:是 CNTControlBase 的简化副本,仅包含 CNTCV(计数值)寄存器。
    • 安全策略允许非安全访问(Non-secure Access)。这意味着非安全软件(如普通 Guest OS 或应用程序)可以读取当前的系统时间,但无法更改或破坏系统计数器的配置。

CNTControlBase 中的关键寄存器

寄存器 功能描述
CNTCR 控制寄存器:用于使能/禁用计数器、启用缩放(Scaling)、选择更新频率以及控制调试挂起(Halt-on-debug)。
CNTSCR 缩放步进寄存器:设置开启缩放时每次 Tick 的增加值(Armv8.4-A 或更高版本支持)。
CNTID ID 寄存器:报告当前硬件实现了哪些定时器特性。
CNTSR 状态寄存器:报告定时器当前处于运行状态还是停止状态。
CNTCV 计数值寄存器:报告当前的系统计数值(仅返回整数部分)。
CNTFID<n> 频率选择寄存器:报告系统支持的各种更新频率。

系统的启动初始化流程

要成功启动并运行系统计数器,软件必须依次完成以下两个步骤:

  1. 选择更新频率(通过配置频率相关寄存器)。
  2. CNTCR 中的计数器使能位(Counter Enable)置位。

3. 外部定时器

除了 CPU 核心内部集成的定时器(Processor Timers)外,系统(SoC)还可以包含额外的外部定时器。

这些外部定时器(Timer)挂载在系统总线/互联网络(Interconnect)上,独立于 CPU 核心(PE)。

images_06_Example-additional-external-timer

它们挂载在系统总线上,通过 MMIO(内存映射) 方式进行配置访问。

其控制接口与 CPU 内部定时器类似,发出的超时中断通常作为 SPI(共享外设中断) 交由 GIC 分发给各个 CPU 处理。