wyp-tg-rcore-tutorial-ch1-clock 0.1.3

Chapter 1 extended: A minimal kernel crate with clock interrupt support for learning RISC-V interrupt mechanism.
wyp-tg-rcore-tutorial-ch1-clock-0.1.3 is not a library.

第一章扩展:时钟中断

本项目在第一章(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.shcargo run,看到定时输出的字符
  • 能解释 sstatus.SIEsie.STIE 的区别与关系
  • 能说明时钟中断从硬件触发到 trap_handler 执行的完整流程
  • 能解释 M-mode 如何将 MTI 转发为 S-mode 的 STI

概念-源码-测试三联表

核心概念 源码入口 自测方式(命令/现象)
中断向量设置 main.rsstvec::write 打印 stvec 寄存器值验证
中断使能 main.rssie::set_stimersstatus::set_sie 打印 siesstatus 验证
时钟中断处理 main.rstrap_handler 观察定时输出的字符
SBI 定时器 tg-sbiset_timer 修改 TIMER_INTERVAL 观察频率变化

一、环境准备

1.1 安装 Rust 工具链

Linux / macOS / WSL:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

验证安装:

rustc --version    # 要求 >= 1.85.0(支持 edition 2024)
cargo --version

1.2 添加 RISC-V 64 编译目标

rustup target add riscv64gc-unknown-none-elf

1.3 安装 QEMU 模拟器

Ubuntu / Debian:

sudo apt update
sudo apt install qemu-system-misc

macOS(Homebrew):

brew install qemu

验证:

qemu-system-riscv64 --version    # 建议 >= 7.0

1.4 获取源代码

git clone https://github.com/rcore-os/tg-rcore-tutorial.git
cd tg-rcore-tutorial/wyp-tg-rcore-tutorial-ch1-clock

二、编译与运行

2.1 编译

cargo build --release

编译过程会自动:

  1. 检测目标架构为 riscv64
  2. 生成链接脚本,设置内存布局
  3. 交叉编译生成 ELF 可执行文件

2.2 运行

方式一:使用脚本

./run.sh

方式二:使用 cargo

cargo run --release

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

关键点

  1. 硬件定时器中断首先在 M-mode 触发(MTI)
  2. M-mode 必须主动设置 mip.STIP 才能触发 S-mode 中断
  3. 设置 mtimecmp = 最大值 可以暂时禁用硬件中断
  4. 每次 set_timer 后需要重新启用 mie.MTIE

四、开发步骤

详细的开发指南请参阅 doc/dev-guide-clock-interrupt.md

步骤 1:创建陷阱入口

#[naked]
#[no_mangle]
pub unsafe extern "C" fn trap_entry() -> ! {
    core::arch::asm!(
        "addi sp, sp, -32*8",
        "sd x1, 1*8(sp)",
        // ... 保存其他寄存器 ...
        "call trap_handler",
        // ... 恢复寄存器 ...
        "sret",
        options(noreturn)
    );
}

步骤 2:编写中断处理函数

#[no_mangle]
pub fn trap_handler() {
    let scause = scause::read();
    match scause.cause() {
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            handle_timer_interrupt();
        }
        _ => panic!("Unhandled trap!"),
    }
}

步骤 3:初始化中断配置

pub fn rust_main() -> ! {
    unsafe {
        stvec::write(trap_entry as usize, stvec::TrapMode::Direct);
        sie::set_stie();      // 启用定时器中断
        sstatus::set_sie();   // 启用全局中断(重要!)
    }
    set_timer(time::read() + TIMER_INTERVAL);
    loop { unsafe { riscv::asm::wfi() }; }
}

步骤 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_timermie.MTIE 是否恢复 1 每次设置定时器时恢复

调试技巧

  1. 分层调试:先确保 S-mode 配置正确,再检查 M-mode
  2. 添加标记:在关键位置输出字符(如 'M' 表示进入 M-mode)
  3. 检查 CSR:打印关键 CSR 寄存器的值
  4. 单步执行:使用 QEMU 的 -s -S 配合 GDB 调试

六、本章小结

通过本章的学习和实践,你理解了:

  1. 中断层次结构:RISC-V 中断从硬件层到软件层的完整链路
  2. 两层使能开关sstatus.SIE(全局)与 sie.STIE(局部)的关系
  3. M-mode 转发机制:MTI 如何通过 mip.STIP 转发为 STI
  4. 时钟中断流程:从设置定时器到处理中断的完整过程

这些知识是理解第三章多道程序和抢占式调度的基础。


七、思考题

  1. 为什么需要两层中断使能开关? 只用一层会有什么问题?

  2. M-mode 为什么要转发中断给 S-mode? 为什么不直接在 M-mode 处理所有中断?

  3. 如果 set_timer 后忘记重新启用 mie.MTIE,会发生什么?

  4. 时钟中断的精度受哪些因素影响? 如何提高精度?


参考资料


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.