# !Attention
本crate包为AI4OS内测的T2L1实验包,尚在敏捷开发迭代中,包括但不限于文档完善、评分机制开发、报告提交等工作,未完成工作会紧跟列出:
# TODO:
* [ ] 完善《原理驱动+进度满足型学习者原理驱动者型学习文档》
* [ ] 练习题开发
* [ ] 评分进度cli程序开发
* [ ] 提交实践报告
# 与uart2 crate的对比
参考uart2 crate之后,我发现我的设计与uart2的设计主要差别在于:
1. 我的实验尽可能基于ch1的架构而来,保持了对sbi crate的依赖,只在ch1基础上对输出部分做了改造:也即采用串口(MMIO)的方式输出字符。
而uart2的crate则自包含,将sbi的汇编以及rust handler代码也包含在内,推测目的是让学生直接了解特权级切换和上下文保存机制。
但根据我个人的学习历程,在学习ch1的时候我就已经同步通读了sbi的代码,不需要再重复自包含,所以我选择同ch1一样的架构,依赖完整也可以直接运行。
2. 我的实验对于串口的一些处理机制还不够完善,个人认为可以加到实验里让同学们自行完善一部分
# 第一章:UART 驱动实践
本章通过实现一个基于 UART(Universal Asynchronous Receiver/Transmitter)驱动的裸机程序,学习硬件设备驱动的基本原理和编程技巧。你将了解到如何通过内存映射 I/O(MMIO)直接操作串口硬件,并对比传统的 SBI 调用方式的差异。
## 学习目标
通过本章的学习和实践,你将掌握:
- UART 串口硬件的工作原理和寄存器布局
- 内存映射 I/O(MMIO)的访问机制与 `volatile` 关键字的必要性
- UART 驱动与 SBI 调用在终端输出上的实现差异
- 轮询式 I/O 的实现方法与适用场景
- 裸机环境下设备驱动的集成方法
## 项目结构
```
tg-rcore-tutorial-ch1-uart/
├── .cargo/
│ └── config.toml # Cargo 配置:交叉编译目标和 QEMU runner
├── build.rs # 构建脚本:自动生成链接脚本
├── Cargo.toml # 项目配置与依赖
├── README.md # 本文档
├── rust-toolchain.toml # Rust 工具链配置
└── src/
└── main.rs # 程序源码:入口、主函数、panic 处理
```
## 源码阅读导航
建议按以下顺序阅读代码,聚焦 UART 驱动的实现与使用:
| 1 | `src/main.rs` 的 `rust_main` 函数 | 如何使用 UART 驱动进行字符输出? |
| 2 | `tg-rcore-tutorial-uart/src/uart.rs` 的 `Uart::init` | UART 初始化流程涉及哪些关键寄存器? |
| 3 | `uart.rs` 的 `Uart::put_char` | 轮询等待发送器空闲的机制如何实现? |
| 4 | `uart.rs` 的 `read_reg` / `write_reg` | 为什么需要使用`volatile` 访问? |
| 5 | `src/main.rs` 的 `panic_handler` | panic 时如何通过串口输出调试信息? |
## DoD 验收标准(本章完成判据)
- [ ] 在 `tg-rcore-tutorial-ch1-uart` 目录执行 `cargo run`,看到 "Hello from UART!" 输出并正常关机
- [ ] 能够解释 MMIO 与 SBI 调用在终端输出实现上的本质区别
- [ ] 能够说明 UART 初始化流程中 IER、LCR、FCR 等寄存器的作用
- [ ] 能够从源码分析字符输出的轮询等待机制
- [ ] 能够描述 `volatile` 关键字在设备驱动中的必要性
## 环境准备
### 1. 安装 Rust 工具链
```bash
```
验证安装:
```bash
rustc --version
cargo --version
```
### 2. 添加 RISC-V 64 编译目标
```bash
rustup target add riscv64gc-unknown-none-elf
```
### 3. 安装 QEMU 模拟器
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install qemu-system-misc
```
**macOS(Homebrew):**
```bash
brew install qemu
```
验证安装:
```bash
qemu-system-riscv64 --version
```
## 编译与运行
### 编译
```bash
cargo build
```
编译过程为交叉编译,目标平台为 `riscv64gc-unknown-none-elf`,由 `.cargo/config.toml` 配置指定。
### 运行
```bash
cargo run
```
该命令会自动启动 QEMU 虚拟机,加载编译后的内核。预期输出:
```
Hello from UART!
```
## 核心概念
### UART 驱动 vs. SBI 调用
在操作系统开发中,终端输出有两种常见的实现方式:
| **SBI 调用** | 通过 SBI(Supervisor Binary Interface)固件提供的服务,如`console_putchar` | 简单、稳定,但存在上下文切换开销 | 快速原型、教学演示、基础功能 |
| **UART 驱动** | 直接通过内存映射 I/O 操作 UART 硬件寄存器 | 更低延迟、更高控制权,但需要处理硬件细节 | 生产环境、性能敏感场景、自定义硬件 |
**关键区别:**
- SBI 调用是**软件抽象层**,内核通过 ecall 指令陷入 M-mode,由固件处理硬件细节
- UART 驱动是**硬件直接操作**,内核通过内存读写指令直接访问设备寄存器
### 内存映射 I/O(MMIO)
MMIO 是一种硬件设计,将设备寄存器映射到处理器的内存地址空间。CPU 通过普通的内存读写指令(`ld`/`sd`)访问这些地址,实际上是在与设备寄存器交互。
在 QEMU virt 平台中,NS16550A 兼容串口被映射到地址 `0x10000000`。
#### volatile 关键字的重要性
编译器在进行优化时,可能会认为对同一内存地址的多次读取是冗余的,从而合并或删除某些访问。对于设备寄存器,这种优化会导致错误,因为寄存器的值可能随时被硬件改变。
`volatile` 访问强制编译器:
1. 按代码顺序执行每次访问
2. 不缓存寄存器值
3. 不进行冗余访问优化
示例:
```rust
// 错误:编译器可能优化掉第二次读取
let status = ptr.read_volatile();
if status & TX_READY != 0 {
// 编译器可能认为 status 未改变,直接重用
}
// 正确:每次读取都是显式的
while ptr.read_volatile() & TX_READY == 0 {}
```
### UART 硬件与寄存器
UART(通用异步收发器)是一种异步串行通信接口,负责将并行数据转换为串行数据发送,并将接收到的串行数据转换为并行数据。
#### NS16550A 关键寄存器
| **RHR** | 0 | 读 | 接收保持寄存器:存放接收到的字符 |
| **THR** | 0 | 写 | 发送保持寄存器:写入待发送的字符 |
| **IER** | 1 | 读写 | 中断使能寄存器:控制哪些事件产生中断 |
| **FCR** | 2 | 写 | FIFO 控制寄存器:启用/清空 FIFO |
| **LCR** | 3 | 读写 | 线路控制寄存器:设置数据位、停止位、奇偶校验 |
| **LSR** | 5 | 读 | 线路状态寄存器:反映发送/接收状态 |
#### 关键标志位
- `LSR_RX_READY` (bit 0):接收器有数据可读
- `LSR_TX_IDLE` (bit 5):发送器空闲,可接受新字符
- `LCR_BAUD_LATCH` (bit 7):置 1 时访问波特率除数寄存器
- `LCR_EIGHT_BITS` (bits 1:0 = 11):8 位数据位
### UART 初始化流程
参考 xv6 实现,UART 初始化步骤如下:
1. **禁用中断**:`IER = 0`(使用轮询方式,不启用中断)
2. **设置波特率**:进入 LCR_BAUD_LATCH 模式,设置波特率为 38.4K
3. **设置数据格式**:`LCR = LCR_EIGHT_BITS`(8 位数据位,1 位停止位,无奇偶校验)
4. **启用 FIFO**:`FCR = FIFO_ENABLE | FIFO_CLEAR`(启用 16 字节 FIFO 并清空)
### 轮询式 I/O
轮询(Polling)是一种简单的 I/O 处理方式,CPU 不断检查设备状态,直到设备就绪。
**字符输出流程:**
1. **等待发送器空闲**:读取 LSR 寄存器,检查 `TX_IDLE` 标志
2. **写入字符**:向 THR 寄存器写入字符
3. **重复**:继续等待并写入下一个字符
**优缺点:**
- 优点:实现简单,无需中断处理机制
- 缺点:CPU 利用率低,在等待期间忙循环
## 代码解读
### 项目配置
`.cargo/config.toml` 配置交叉编译和 QEMU runner:
```toml
[build]
target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
runner = [
"qemu-system-riscv64",
"-machine", "virt",
"-nographic",
"-bios", "none",
"-kernel",
]
```
`Cargo.toml` 中的关键依赖:
```toml
[dependencies]
tg-rcore-tutorial-uart = { path = "../tg-rcore-tutorial-uart", version = "0.1.0-preview.1" }
tg-sbi = { path = "../tg-sbi", version = "0.4.2-preview.1", features = ["nobios"] }
```
### UART 驱动使用
在 `src/main.rs` 中:
```rust
use tg_rcore_tutorial_uart::Uart;
use tg_sbi::shutdown;
#[no_mangle]
unsafe extern "C" fn rust_main() -> ! {
// 初始化 UART
Uart::init();
// 通过 UART 输出字符串
Uart::put_str("Hello from UART!\n");
// 尝试读取字符(非阻塞)
for _ in 0..8 {
if let Some(c) = Uart::try_get_char() {
Uart::put_char(c); // 回显
}
}
// 使用 SBI 关机(仅此功能仍依赖 SBI)
shutdown(false);
}
```
### UART 驱动实现
`tg-rcore-tutorial-uart/src/uart.rs` 中的核心函数:
```rust
impl Uart {
/// 初始化 UART 硬件
pub fn init() {
unsafe {
// 禁用中断
Self::write_reg(Reg::IER, 0x00);
// 设置波特率
Self::write_reg(Reg::LCR, LCR_BAUD_LATCH);
Self::write_reg(Reg::DLL, 0x03); // 38.4K 波特率
Self::write_reg(Reg::DLM, 0x00);
// 8 位数据位,1 位停止位,无奇偶校验
Self::write_reg(Reg::LCR, LCR_EIGHT_BITS);
// 启用 FIFO
Self::write_reg(Reg::FCR, FIFO_ENABLE | FIFO_CLEAR);
}
}
/// 输出单个字符(阻塞等待)
pub fn put_char(c: u8) {
unsafe {
// 等待发送器空闲
while Self::read_reg(Reg::LSR) & LSR_TX_IDLE == 0 {}
// 写入字符
Self::write_reg(Reg::THR, c);
}
}
/// 读取寄存器(volatile 访问)
unsafe fn read_reg(reg: Reg) -> u8 {
let ptr = (UART_BASE + reg as usize) as *const u8;
ptr.read_volatile()
}
/// 写入寄存器(volatile 访问)
unsafe fn write_reg(reg: Reg, value: u8) {
let ptr = (UART_BASE + reg as usize) as *mut u8;
ptr.write_volatile(value);
}
}
```
## 本章小结
本章通过实现 UART 驱动,深入学习了硬件设备驱动的基本原理:
1. **MMIO 机制**:理解了设备寄存器通过内存地址映射的访问方式,掌握了 `volatile` 关键字在防止编译器优化中的关键作用。
2. **UART 驱动实现**:学习了 NS16550A 串口芯片的寄存器布局,掌握了初始化流程、字符发送的轮询等待机制。
3. **驱动 vs. SBI 对比**:理解了直接硬件操作与通过固件抽象层调用的本质区别,能够根据场景选择合适的技术方案。
4. **轮询式 I/O**:掌握了简单的轮询实现方式,为后续学习中断驱动、DMA 等高级 I/O 技术打下基础。
这是操作系统设备驱动开发的第一步,后续章节将在此基础上引入中断机制、块设备驱动、文件系统等更复杂的设备管理技术。
## 思考题
1. **`volatile` 的必要性**:如果不使用 `volatile` 访问 UART 寄存器,可能会发生什么问题?请结合编译器优化策略和硬件寄存器特性进行分析。
2. **轮询 vs. 中断**:轮询方式和中断驱动方式在 CPU 利用率、响应延迟、实现复杂度等方面有何区别?在什么场景下你会选择轮询方式?
3. **波特率计算**:UART 初始化代码中设置了波特率为 38.4K,这个值是如何计算出来的?如果希望改为 115200 波特率,需要修改哪些寄存器值?
4. **地址硬编码**:UART 基地址 `0x10000000` 在代码中是硬编码的,这种方式有什么优缺点?在实际的操作系统中,如何更灵活地管理设备地址?
5. **错误处理**:当前的 UART 驱动缺乏错误处理机制(如超时、奇偶校验错误等)。如果要增强鲁棒性,需要考虑哪些错误情况?如何设计相应的错误处理机制?
## 参考资料
- [xv6-riscv uart.c](https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/uart.c) - MIT xv6-riscv 的 UART 驱动实现
- [NS16550A Datasheet](https://www.nxp.com/docs/en/data-sheet/PC16550D.pdf) - NS16550A 数据手册
- [Rustonomicon: Volatile](https://doc.rust-lang.org/nomicon/volatility.html) - Rust 中 volatile 访问的官方说明
- [rCore-Tutorial 设备驱动章节](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter9/2device-driver-1.html) - rCore-Tutorial 中关于设备驱动的讲解
## 依赖
| `tg-rcore-tutorial-uart` | NS16550A UART 驱动实现,提供字符输入输出 |
| `tg-sbi` | SBI 调用封装(仅用于关机功能) |
---
## License
Licensed under GNU GENERAL PUBLIC LICENSE, Version 3.0.