第一章扩展:时钟中断
本项目在第一章(ch1)的基础上,添加时钟中断支持,实现定时输出字符的功能。作为从第一章到第三章的过渡实验,帮助学生理解 RISC-V 中断机制的基础知识。
通过本章的学习和实践,你将理解:
- RISC-V 中断机制的层次结构
- 时钟中断的初始化和处理流程
- CSR 寄存器(stvec、sie、sstatus、scause)的作用
- SBI 调用
set_timer的工作原理 - M-mode 与 S-mode 之间的中断转发机制
前置知识:建议先完成第一章(ch1)的学习,理解裸机启动、SBI 调用等基础概念。
项目结构
wyp-tg-rcore-tutorial-ch1-clock/
├── .cargo/
│ └── config.toml # Cargo 配置:交叉编译目标
├── .gitignore # Git 忽略规则
├── build.rs # 构建脚本:生成链接脚本
├── Cargo.toml # 项目配置与依赖
├── exercise.md # 实验练习题
├── README.md # 本文档
├── run.sh # 运行脚本
├── rust-toolchain.toml # Rust 工具链配置
├── doc/
│ └── dev-guide-clock-interrupt.md # 开发手册
└── src/
└── main.rs # 内核源码:时钟中断初始化与处理
源码阅读导航索引
建议按"启动初始化 → 中断配置 → 陷阱处理"顺序阅读。
| 阅读顺序 | 位置 | 重点问题 |
|---|---|---|
| 1 | _start |
裸机入口如何设置栈并跳转到 Rust? |
| 2 | rust_main |
时钟中断需要配置哪些 CSR 寄存器? |
| 3 | trap_entry |
中断入口如何保存和恢复寄存器? |
| 4 | trap_handler |
如何识别时钟中断并处理? |
配套建议:阅读 doc/dev-guide-clock-interrupt.md 理解完整的中断流程和 M-mode 转发机制。
DoD 验收标准(本章完成判据)
- 能执行
./run.sh或cargo run,看到定时输出的字符 - 能解释
sstatus.SIE与sie.STIE的区别与关系 - 能说明时钟中断从硬件触发到
trap_handler执行的完整流程 - 能解释 M-mode 如何将 MTI 转发为 S-mode 的 STI
概念-源码-测试三联表
| 核心概念 | 源码入口 | 自测方式(命令/现象) |
|---|---|---|
| 中断向量设置 | main.rs 的 stvec::write |
打印 stvec 寄存器值验证 |
| 中断使能 | main.rs 的 sie::set_stimer 和 sstatus::set_sie |
打印 sie 和 sstatus 验证 |
| 时钟中断处理 | main.rs 的 trap_handler |
观察定时输出的字符 |
| SBI 定时器 | tg-sbi 的 set_timer |
修改 TIMER_INTERVAL 观察频率变化 |
一、环境准备
1.1 安装 Rust 工具链
Linux / macOS / WSL:
|
验证安装:
1.2 添加 RISC-V 64 编译目标
1.3 安装 QEMU 模拟器
Ubuntu / Debian:
macOS(Homebrew):
验证:
1.4 获取源代码
二、编译与运行
2.1 编译
编译过程会自动:
- 检测目标架构为
riscv64 - 生成链接脚本,设置内存布局
- 交叉编译生成 ELF 可执行文件
2.2 运行
方式一:使用脚本
方式二:使用 cargo
2.3 预期输出
Clock interrupt demo starting...
Timer initialized, waiting for interrupts...
Each 'x' represents a clock interrupt (~0.8 seconds)
Press Ctrl+A then X to exit QEMU
xxxxxxxxxx
10
xxxxxxxxxx
20
...
每约 0.8 秒输出一个 x,每 10 次输出一行计数。
三、操作系统核心概念
3.1 RISC-V 中断层次结构
RISC-V 的中断控制是分层级的:
flowchart TB
subgraph HW["硬件层"]
mtime["mtime<br/>机器时间寄存器(只读)"]
mtimecmp["mtimecmp<br/>机器时间比较器(读写)"]
end
subgraph MMODE["M-mode 层"]
mstatus_mie["mstatus.MIE<br/>M-mode 全局中断开关"]
mie_mtie["mie.MTIE<br/>M-mode 定时器中断使能"]
mip_flags["mip.MTIP/STIP<br/>中断挂起位"]
end
subgraph SMODE["S-mode 层"]
sstatus_sie["sstatus.SIE<br/>S-mode 全局中断开关"]
sie_stie["sie.STIE<br/>S-mode 定时器中断使能"]
sip_stip["sip.STIP<br/>S-mode 中断挂起位"]
end
subgraph SW["软件层"]
trap_handler["trap_handler()<br/>中断处理函数"]
end
HW --> MMODE
MMODE -->|"SBI 负责转发"| SMODE
SMODE --> SW
3.2 关键 CSR 寄存器
| 寄存器 | 作用 | 关键位 |
|---|---|---|
stvec |
S-mode 陷阱向量地址 | BASE, MODE |
sstatus |
S-mode 状态寄存器 | SIE (第1位) |
sie |
S-mode 中断使能 | STIE (第5位) |
scause |
陷阱原因 | Exception Code |
mstatus |
M-mode 状态寄存器 | MIE (第3位) |
mie |
M-mode 中断使能 | MTIE (第7位) |
mip |
M-mode 中断挂起 | STIP (第5位), MTIP (第7位) |
3.3 中断使能的两层开关
RISC-V 中断使能是"与"的关系:
中断触发 = sstatus.SIE && sie.STIE
| 场景 | sstatus.SIE | sie.STIE | 结果 |
|---|---|---|---|
| 都开启 | 1 | 1 | ✓ 中断触发 |
| 全局关闭 | 0 | 1 | ✗ 不触发 |
| 局部关闭 | 1 | 0 | ✗ 不触发 |
| 都关闭 | 0 | 0 | ✗ 不触发 |
常见错误:只设置了
sie.STIE而忘记设置sstatus.SIE,导致中断无法触发。
3.4 时钟中断流程
sequenceDiagram
participant S as S-mode (内核)
participant M as M-mode (SBI)
participant HW as 硬件
S->>M: set_timer(间隔)
M->>HW: 设置 mtimecmp
Note over HW: 等待 mtime >= mtimecmp
HW->>M: MTI 触发
M->>M: 设置 mip.STIP = 1
M-->>S: 返回 S-mode
Note over S: STI 触发 (因为 STIP=1)
S->>S: 进入 trap_handler
S->>S: 处理时钟中断
S->>M: set_timer(新间隔)
M->>M: 清除 STIP,设置新 mtimecmp
M-->>S: 返回
3.5 M-mode 中断转发机制
这是理解时钟中断的关键:硬件定时器中断首先在 M-mode 触发(MTI),SBI 需要手动转发给 S-mode。
flowchart TB
HW["[硬件] mtime >= mtimecmp"] --> MTI
subgraph MMODE["M-mode"]
MTI["MTI 触发"]
MTI -->|"mie.MTIE = 0"| IGNORE["忽略,不处理"]
MTI -->|"mie.MTIE = 1"| MTRAP["进入 M-mode trap"]
MTRAP --> SBI_HANDLE["SBI 中断处理"]
SBI_HANDLE --> STIP["1. 设置 mip.STIP = 1<br/>注入 S-mode 中断"]
SBI_HANDLE --> MAXCMP["2. 设置 mtimecmp = 最大值<br/>暂时禁用硬件中断"]
SBI_HANDLE --> MRET["3. mret 返回"]
end
MRET --> STI
subgraph SMODE["S-mode"]
STI["STI 触发"]
STI -->|"sstatus.SIE && sie.STIE"| STRAP["进入 S-mode trap"]
STRAP --> HANDLER["trap_handler()"]
end
关键点:
- 硬件定时器中断首先在 M-mode 触发(MTI)
- M-mode 必须主动设置
mip.STIP才能触发 S-mode 中断 - 设置
mtimecmp = 最大值可以暂时禁用硬件中断 - 每次
set_timer后需要重新启用mie.MTIE
四、开发步骤
详细的开发指南请参阅 doc/dev-guide-clock-interrupt.md。
步骤 1:创建陷阱入口
pub unsafe extern "C" !
步骤 2:编写中断处理函数
步骤 3:初始化中断配置
!
步骤 4:确保 SBI 正确转发
在 tg-sbi 的 M-mode 启动代码中启用 mie.MTIE,并在 MTI 处理中设置 mip.STIP。
五、常见问题排查
如果时钟中断不工作,按以下顺序排查:
S-mode 排查
| 检查项 | 命令/方法 | 预期值 |
|---|---|---|
stvec 是否设置 |
println!("{}", stvec::read().bits) |
非 0 的有效地址 |
sie.STIE 是否启用 |
println!("{}", sie::read().stie()) |
true |
sstatus.SIE 是否启用 |
println!("{}", sstatus::read().sie()) |
true |
M-mode 排查
| 检查项 | 预期值 | 说明 |
|---|---|---|
mie.MTIE 是否启用 |
1 | SBI 初始化时设置 |
| M-mode trap 是否被调用 | 是 | 在 m_trap 入口添加调试输出 |
mip.STIP 是否被设置 |
1 | 处理 MTI 时设置 |
set_timer 后 mie.MTIE 是否恢复 |
1 | 每次设置定时器时恢复 |
调试技巧
- 分层调试:先确保 S-mode 配置正确,再检查 M-mode
- 添加标记:在关键位置输出字符(如 'M' 表示进入 M-mode)
- 检查 CSR:打印关键 CSR 寄存器的值
- 单步执行:使用 QEMU 的
-s -S配合 GDB 调试
六、本章小结
通过本章的学习和实践,你理解了:
- 中断层次结构:RISC-V 中断从硬件层到软件层的完整链路
- 两层使能开关:
sstatus.SIE(全局)与sie.STIE(局部)的关系 - M-mode 转发机制:MTI 如何通过
mip.STIP转发为 STI - 时钟中断流程:从设置定时器到处理中断的完整过程
这些知识是理解第三章多道程序和抢占式调度的基础。
七、思考题
-
为什么需要两层中断使能开关? 只用一层会有什么问题?
-
M-mode 为什么要转发中断给 S-mode? 为什么不直接在 M-mode 处理所有中断?
-
如果
set_timer后忘记重新启用mie.MTIE,会发生什么? -
时钟中断的精度受哪些因素影响? 如何提高精度?
参考资料
- RISC-V Privileged Architecture Specification
- rCore-Tutorial-Book 第三章:中断机制
- RISC-V Reader 中文版
- 项目文档:doc/dev-guide-clock-interrupt.md
Dependencies
| 依赖 | 说明 |
|---|---|
riscv |
RISC-V CSR 寄存器访问(sie、scause、time、sstatus、stvec) |
tg-sbi |
SBI 调用封装,包括 set_timer、console_putchar |
tg-console |
控制台输出(print! / println!) |
License
Licensed under GNU GENERAL PUBLIC LICENSE, Version 3.0.