RISC-V timer 在 Linux 中的实现
Author: Yu Liao yuliao0214@gmail.com
Revisor: lzufalcon falcon@tinylab.org
Project: RISC-V Linux 内核剖析
RISC-V timer 相关寄存器
mtime & mtimecmp 寄存器
按照 RISC-V 定义,系统需要提供两个 64 位的 M 模式寄存器 mtime
和 mtimecmp
,并通过 MMIO 方式映射到地址空间。
mtime
需要以固定的频率递增,并在发生溢出时回绕。当 mtime
大于或等于 mtimecmp
时,由核内中断控制器 (CLINT, Core-Local Interrupt Controller) 产生 timer 中断。中断的使能由 mie
寄存器中的 MTIE
和 STIE
位控制,mip
中的 MPIE
和 SPIE
则指示了 timer 中断是否处于 pending。在 RV32 中读取 mtimecmp
结果为低 32 位, mtimecmp
的高 32 位需要读取 mtimecmph
得到。
在 RISC-V 特权 ISA 规范 的 3.2.1 Machine Timer Registers (mtime and mtimecmp) 中详细介绍了这部分。
time CSR
RISC-V 还定义了一个 64 位非特权 CSR 寄存器 time
,time
计数器是前面提到的 mtime
的只读映射。同样,在 RV32 中 timeh
CSR 是 mtime
高 32 位的只读映射,对于 M 模式和 S 模式它们都是可读写的。
在 RISC-V 特权 ISA 规范 的 2.2 CSR Listing 和 3.1.11 Machine Counter-Enable Register (mcounteren) 可以找到这块的规范。
htimedelta & htimedeltah 寄存器
在增加虚拟化扩展以后,特权模式会发生一定变化,如下图(来源于参考文档 5)所示:
相应地,timer 支持也进行了如下扩展:
htimedelta
和 htimedeltah
是 Hypervisor 扩展里的 CSR,在 VS/VU 模式下读取 time
结果是真正的 host 中的 time
加上 htimedelta
。同样的,对于 RV32 htimedelta
保存了低 32 位,高 32 位保存在 htimedeltah
。
在 RISC-V 特权 ISA 规范 的 8.2.7 Hypervisor Time Delta Registers (htimedelta, htimedeltah) 中详细介绍了这部分。
Sstc 扩展
由于 mtimecmp
只能在 M 模式下访问,对于 S/HS 模式下的内核和 VU/VS 模式下的虚拟机需要通过 SBI 才能访问,会造成较大的中断延迟和性能开销。为了解决这一问题,RISC-V 新增了 Sstc 拓展支持(已批准但尚未最终集成到规范中)。
Sstc 扩展为 HS 模式和 VS 模式分别新增了 stimecmp
和 vstimecmp
寄存器,当 $time >= stimecmp$ (HS)或 $time+htimedelta >= vstimecmp$ (VS)时会产生 timer 中断,不再需要通过 SBI 陷入其他模式。
详见 RISC-V “stimecmp / vstimecmp” 扩展 。
Linux timer 实现
Linux 将底层时钟硬件抽象为两类设备:clockevent 和 clocksource,前者用来在未来指定的时间产生中断,通常用作定时器;后者则用于维护自系统启动以来所经过的时间。
当前 Linux 为 RISC-V 根据内核运行模式实现了两套驱动,代码路径为 drivers/clocksource/timer-riscv.c 和 drivers/clocksource/timer-clint.c。
本文代码基于最新的 Linux v5.18-rc4 和 OpenSBI v1.0,截止目前 Linux 对 Sstc 扩展的支持还没有合入主线内核,社区已有相关补丁:Add Sstc extension support。
mtime
频率由设备树 CPU 节点中的 timebase-frequency 定义,不同平台都各不相同,如 Kendryte K210 的频率是 7.8 MHz,平头哥 C910 的频率是 3 MHz,SiFive Unmatched A00 频率为 1 MHz。
NoMMU timer-clint.c
timer-clint.c 驱动适用于 NoMMU 系统,内核运行在 M 模式下,通过 CONFIG_CLINT_TIMER 使能该驱动。RV64 下 clocksource 是通过直接读取 mtime
寄存器实现的,RV32 系统需要分两次读取,并需要考虑产生进位的情况。
1 |
|
clint_get_cycles/clint_get_cycles_hi
直接通过内存访问寄存器。
1 |
|
clockevent 是通过使能 mie
的 TIMER 中断,并向 mtimecmp
寄存器写入期望的计数值实现的。
1 |
|
MMU timer-riscv.c
timer-riscv.c 驱动适用于有 MMU 的场景,内核运行在 S/HS 模式下,通过 CONFIG_RISCV_TIMER 可以使能该驱动。和 timer-riscv.c 的驱动相比,本质上也是访问 mtime
和 mtimecmp
寄存器,不过由于 S 模式下无法直接访问它们,需要通过其他方式间接完成。
RV64 的 clocksource 是通过 csrr 直接读取 time
寄存器实现的;在 RV32 系统由于一条指令无法读完,需要分两次读取 time
和 timeh
, 并考虑可能发生进位的情况。前面提到 time
和 timeh
这两个 CSR 是 mtime
寄存器的映射,因此频率与精度和 mtime
是一致的。
1 |
|
clockevent 则是通过 SBI 间接访问 mtimecmp
实现的。
1 |
|
这里以 OpenSBI 来分析,如果不支持 Sstc 扩展则调用在 SBI 中注册的 timer_event_start
函数写入 mtimecmp
,这个需要具体平台自己去实现。
1 |
|
在支持 Sstc 扩展后,可以直接访问 stimecmp
寄存器,避免通过 SBI 调用的方式产生的开销。社区已开展相关工作:RISC-V: Prefer sstc extension if available。
KVM vcpu_timer.c
在 VS 模式下读取 time
时,KVM 会返回真正的 time
加上 htimedelta
。
1 |
|
在 VS 模式下设置 mtimecmp
时,KVM 会开启一个已经创建好的高精度定时器,并把定时器的到期时间设置为写入 mtimecmp
值对应的 ns。
1 |
|
在定时器到期后,KVM 会为 Guest 产生 TIMER 中断。
1 |
|
因此 VS 模式设置时钟事件需要通过 SBI 调用进入 HS 模式然后再进入 M 模式,会产生较大的开销。同样,在支持 Sstc 扩展后,可以直接访问 vstimecmp
并产生 timer 中断,社区目前已经开展了相关的工作:RISC-V: KVM: Support sstc extension。
参考文档
- RISC-V Platform
- RISC-V ISA Specification
- RISC-V “stimecmp / vstimecmp” Extension
- 基于 FPGA 与 RISC-V 的嵌入式系统设计
- RISC-V虚拟化扩展