CLINT定时器 — Core Local Interruptor
机器定时器中断与软件中断
📖 CLINT架构
CLINT(Core Local Interruptor)提供两个关键功能:定时器中断和核间中断(IPI):
CLINT寄存器映射 (QEMU virt):
┌─────────────────────────────────────────┐
│ 0x0000-0x3FFF msip (每Hart 4字节) │
│ 写1触发软件中断, 写0取消 │
│─────────────────────────────────────────│
│ 0x4000-0xBFFF mtimecmp (每Hart 8字节) │
│ 64位比较值, mtime>=mtimecmp时触发MTI │
│─────────────────────────────────────────│
│ 0xBFF8-0xBFFF mtime (全局, 8字节) │
│ 64位计数器, 每tick递增1 │
└─────────────────────────────────────────┘
mtime特性:
• 所有Hart共享同一个mtime计数器
• 每个Hart有独立的mtimecmp
• mtime频率通常为10MHz (100ns/tick)
• mtime持续递增, 不会停止
| 中断 | 触发条件 | mip位 | 清除方式 |
|---|
| MTI | mtime >= mtimecmp | bit 7 | 写mtimecmp到未来值 |
| MSI | msip寄存器写1 | bit 3 | 写msip为0 |
🔬 定时器中断的精度
定时器中断时序:
mtime: 0 1 2 ... 99 100 101 ...
mtimecmp: 100
↑
mti: ───────────────────╱──────
触发!
精度 = mtime时钟周期 = 1/freq
freq = 10MHz → 精度 = 100ns
freq = 1MHz → 精度 = 1μs
Linux定时器精度:
CONFIG_HZ = 250 → jiffy = 4ms
但通过mtimecmp可以实现纳秒级定时器!
mtime是所有Hart共享的,但mtimecmp是每个Hart独立的。这意味着不同核心可以设置不同的定时器中断时间,实现独立调度。
🖥️ Verilog实现:CLINT定时器
// Lesson 08: CLINT — Core Local Interruptor (Timer)
module clint(
input wire clk, rst_n,
input wire [63:0] mtimecmp,
output reg [63:0] mtime,
output reg mti,
input wire msip,
output reg msi
);
always @(posedge clk or negedge rst_n)
if (!rst_n) mtime <= 0; else mtime <= mtime + 1;
always @(*) begin
mti = (mtime >= mtimecmp);
msi = msip;
end
endmodule
Verilator仿真验证通过 — 定时器中断触发正确
代码解析
- mtime计数器:每时钟周期递增,复位清零
- 比较器:mtime >= mtimecmp时拉高MTI
- MSI直通:软件中断直接由msip控制
- 电平触发:MTI保持高直到mtimecmp被更新
🧪 实验练习
- 实现mtimecmp写入接口:通过内存映射寄存器更新比较值
- 添加mtime分频:支持不同的mtime时钟频率
- 实现多Hart CLINT:每个核心独立的mtimecmp和msip
- 添加定时器中断处理:在trap handler中读取mtimecmp并设置下一次中断
定时器中断触发正确
思考题:为什么mtime必须持续递增而不能在mtimecmp匹配时停止?如果mtime停止了,操作系统的时钟会怎样?
参考资料:RISC-V Privileged Spec §3.1.10 | SiFive CLINT手册 | Linux RISC-V时钟源
🔗 CLINT与操作系统时钟
CLINT的mtime是Linux RISC-V时钟子系统的核心:
Linux RISC-V时钟架构:
┌─────────────────────────────────────────┐
│ 用户空间 │
│ clock_gettime() → VDSO (无ecall!) │
│ gettimeofday() → VDSO │
├─────────────────────────────────────────┤
│ 内核空间 │
│ sched_clock() → mtime读取 │
│ timer中断 → mtimecmp设置 │
│ tick处理 → HZ=250, jiffy=4ms │
├─────────────────────────────────────────┤
│ 硬件 │
│ mtime → 10MHz计数器 │
│ mtimecmp → 下次中断时间 │
│ mip.MTIP → 定时器中断pending │
└─────────────────────────────────────────┘
VDSO优化:
传统: gettimeofday() → ecall → 内核读取mtime
VDSO: gettimeofday() → 直接读mtime(用户映射)
关键: mtime被映射到用户空间只读页面
省去了ecall的50+时钟周期开销!
高精度定时器(hrtimer)
Linux的hrtimer利用mtimecmp实现纳秒级定时精度:
- 传统定时器:基于jiffy(4ms@250HZ),精度低
- hrtimer:直接设置mtimecmp,精度100ns(@10MHz)
- nanosleep()、timer_create()等POSIX定时器都基于hrtimer
🎯 本课与整体课程的关系
CLINT知识图谱:
06 中断 → 07 PLIC → 08 CLINT (本课)
↓
CLINT提供时间基准和核间通信
↓
09 SV39 — 虚拟内存(新主题)
10 TLB — 地址翻译缓存
📚 延伸阅读与参考资料
| 资料 | 内容 | 链接 |
| RISC-V特权规范 | CSR、Trap、中断完整定义 | riscv.org/specifications |
| RISC-V手册 | 中文版免费教材 | crva.ict.ac.cn |
| OpenSBI源码 | M-mode固件参考实现 | github.com/riscv/opensbi |
| Linux RISC-V | 内核移植与驱动 | kernel.org |
| BOOM处理器 | UC Berkeley开源OoO核心 | github.com/riscv-boom/riscv-boom |
| 香山处理器 | 中科院开源高性能核心 | github.com/OpenXiangShan |
相关课程
| 课程范围 | 课程号 | 主题 |
| 特权架构 | 01-06 | 特权级→CSR→ecall→mret→trap→中断 |
| 内存系统 | 07-12 | PLIC→CLINT→SV39→TLB→直接映射→组相联 |
| 算术单元 | 13-14 | Booth乘法器→恢复余数除法 |
| 乱序执行 | 15-19 | OoO→ROB→寄存器重命名→记分牌→Tomasulo |
| 分支预测 | 20-21 | 2位预测器→BTB |
| RISC-V扩展 | 22-26 | RVC→RVM→RVA→RVF→RVD |
| 系统集成 | 27-30 | PMP→解码器→SoC→启动流程 |
实验环境搭建
建议使用以下环境进行实验:
- Verilator 5.020:Verilog仿真和lint(本课程使用的验证工具)
- QEMU virt:RISC-V全系统模拟器,支持M/S/U特权级
- Spike:RISC-V ISA模拟器,适合指令级调试
- OpenOCD + GDB:硬件调试工具链
- Chisel/FIRRTL:BOOM和Rocket使用的硬件描述语言
📊 RISC-V定时器与时间同步
mtime是RISC-V的时间基准,在不同场景下有不同用途:
| 场景 | 频率 | 精度 | 用途 |
| QEMU virt | 10MHz | 100ns | 开发/测试 |
| SiFive FE310 | 32kHz RTC | 31μs | 低功耗嵌入式 |
| SiFive U540 | 1MHz | 1μs | 多核服务器 |
| 香山南湖 | 50MHz | 20ns | 高性能计算 |
核间中断(IPI)的用途
CLINT的msip寄存器实现核间中断(Inter-Processor Interrupt),用于:
- IPI_RESCHEDULE:通知其他核心重新调度进程
- IPI_CALL_FUNC:请求其他核心执行函数(smp_call_function)
- IPI_CPU_STOP:停止其他核心(关机/调试)
- IPI_IRQ_WORK:延迟中断处理(在另一个核心上执行)
- TLB flush:通知其他核心刷新TLB(通过IPI+SFENCE.VMA)
🔬 CLINT的实现细节
CLINT的mtime计数器在硬件中有一些微妙的实现细节:
mtime计数器实现:
┌─────────────────────────────────────────┐
│ mtime (64位计数器): │
│ • 通常由独立的时钟域驱动 │
│ • 在多核系统中全局共享 │
│ • 读取是原子的(64位一次性读出) │
│ │
│ 时钟域交叉: │
│ mtime时钟域 ──→ 处理器时钟域 │
│ │ │ │
│ mtimecmp比较 mip.MTIP设置 │
│ (可能跨时钟域) (同步到处理器时钟) │
│ │
│ 64位原子读取: │
│ RV32: 需要两次32位读取 │
│ lo = mtime[31:0] │
│ hi = mtime[63:32] │
│ // 可能不一致! │
│ 解决: 读hi两次, 如果不同则重试 │
│ RV64: 单次64位读取, 天然原子 │
└─────────────────────────────────────────┘
mtimecmp写入的原子性:
RV32: 必须先写低32位, 再写高32位
原因: 如果先写高32位清零, mtimecmp暂时
变得很小, 会误触发MTI中断!