# NS16550A UART 串口驱动库
`tg-rcore-tutorial-uart` 是一个用于 RISC-V S-Mode 的 NS16550A 兼容串口驱动库,提供内存映射 I/O(MMIO)方式的字符输入输出,适用于裸机环境和操作系统内核。
通过本驱动库的学习和使用,你将掌握:
- UART 串口硬件的工作原理和通信协议
- 内存映射 I/O(MMIO)的访问机制与 `volatile` 语义的重要性
- NS16550A 寄存器的定义与初始化流程
- 轮询式字符输入输出的实现方法
- 如何为裸机环境编写硬件设备驱动
## 项目结构
```
tg-rcore-tutorial-uart/
├── Cargo.toml # 项目配置与依赖
├── README.md # 本文档
└── src/
├── lib.rs # 库入口,模块导出
└── uart.rs # UART 驱动实现:寄存器定义、初始化、字符 I/O
```
<a id="source-nav"></a>
## 源码阅读导航索引
[返回根文档导航总表](../../README.md#chapters-source-nav-map)
建议把源码阅读聚焦在一个文件:`src/uart.rs`。
| 1 | `Uart::init()` | UART 初始化流程如何配置波特率、数据位和 FIFO? |
| 2 | `Uart::put_char()` | 轮询式输出如何通过 LSR 寄存器等待发送器空闲? |
| 3 | `Uart::try_get_char()` | 非阻塞输入如何检测接收器就绪标志? |
| 4 | `read_reg` / `write_reg` | MMIO 访问为何必须使用 `volatile` 操作? |
配套建议:结合 QEMU virt 机器的 UART 硬件文档,理解寄存器映射与功能。
## DoD 验收标准(驱动库完成判据)
- [ ] 能在依赖本驱动的程序(如 `tg-rcore-tutorial-ch1-uart`)中执行 `cargo run`,看到串口输出
- [ ] 能解释 MMIO 与端口 I/O 的区别,以及 `volatile` 在设备驱动中的作用
- [ ] 能说明 UART 初始化序列中每个寄存器写入的目的(IER、LCR、FCR)
- [ ] 能描述轮询输出与中断驱动输出的优缺点
## 概念-源码-测试三联表
| MMIO 访问 | `src/uart.rs` 的 `read_reg` / `write_reg` | 输出稳定,无编译器优化问题 |
| 寄存器定义 | `src/uart.rs` 的常量定义(`UART0`、`RHR`、`THR` 等) | 可对照 NS16550A 数据手册验证偏移量 |
| 轮询输出 | `Uart::put_char` 等待 `LSR_TX_IDLE` | 每个字符可靠输出,无丢失 |
| 非阻塞输入 | `Uart::try_get_char` 检查 `LSR_RX_READY` | 有输入时返回 `Some(c)`,无输入时返回 `None` |
## 一、环境准备
### 1.1 安装 Rust 工具链
本项目使用 Rust 语言编写,需要通过 rustup 安装 Rust 工具链。
**Linux / macOS / WSL:**
```bash
```
**Windows:**
从 [https://rustup.rs](https://rustup.rs) 下载并运行 `rustup-init.exe`。
验证安装:
```bash
rustc --version # 应显示 rustc 1.xx.x
cargo --version # 应显示 cargo 1.xx.x
```
### 1.2 添加 RISC-V 64 编译目标
由于驱动可能用于 RISC-V 64 裸机平台,建议添加对应的编译目标:
```bash
rustup target add riscv64gc-unknown-none-elf
```
### 1.3 安装 QEMU 模拟器(可选)
如需在 QEMU 中测试驱动,需要安装 `qemu-system-riscv64`(建议版本 >= 7.0)。
**Ubuntu / Debian:**
```bash
sudo apt update
sudo apt install qemu-system-misc
```
**macOS(Homebrew):**
```bash
brew install qemu
```
**验证安装:**
```bash
qemu-system-riscv64 --version
```
## 二、使用方式
### 2.1 添加依赖
在 `Cargo.toml` 中添加:
```toml
[dependencies]
tg-rcore-tutorial-uart = { path = "../tg-rcore-tutorial-uart", version = "0.1.0-preview.1" }
```
### 2.2 基本使用
```rust
use tg_rcore_tutorial_uart::Uart;
// 初始化 UART 硬件
let _ = Uart::init();
// 输出字符串
Uart::put_str("Hello from UART!\n");
// 输出单个字符
Uart::put_char(b'A');
// 尝试读取字符(非阻塞)
if let Some(c) = Uart::try_get_char() {
Uart::put_char(c); // 回显
}
```
### 2.3 在 panic 处理中使用同步输出
```rust
use tg_rcore_tutorial_uart::Uart;
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
// 使用同步输出确保 panic 信息能发出
let _ = Uart::put_char_sync(b'P');
let _ = Uart::put_char_sync(b'A');
let _ = Uart::put_char_sync(b'N');
let _ = Uart::put_char_sync(b'I');
let _ = Uart::put_char_sync(b'C');
loop {}
}
```
## 三、核心概念
### 3.1 内存映射 I/O(MMIO)
MMIO 是一种将设备寄存器映射到内存地址空间的硬件设计。CPU 通过普通的内存读写指令(如 `ld`、`sd`)访问这些地址,实际上是在与设备寄存器交互。
**与端口 I/O 的区别:**
- **端口 I/O**:使用专门的 `in`/`out` 指令,地址空间独立
- **MMIO**:使用内存访问指令,地址空间与物理内存统一
RISC-V 架构主要采用 MMIO 方式,设备寄存器被映射到特定的物理地址区间(如 `0x10000000`)。
### 3.2 volatile 语义
编译器在进行优化时,可能会认为对同一内存地址的多次读取是冗余的,从而合并或删除某些访问。对于设备寄存器,这种优化会导致错误,因为寄存器的值可能随时被硬件改变。
`volatile` 关键字(在 Rust 中为 `read_volatile` / `write_volatile`)告诉编译器:
- 每次访问都必须执行,不可优化掉
- 访问顺序必须严格按代码顺序进行
- 不假设该地址的内容在两次访问之间保持不变
### 3.3 UART 通信协议
UART 是一种异步串行通信协议,包含以下要素:
- **波特率**:每秒传输的符号数(如 38400 bps)
- **数据位**:每个字符的位数(通常为 8 位)
- **停止位**:字符结束的标志(通常为 1 位)
- **奇偶校验位**:错误检测位(可选)
NS16550A 是 PC 兼容的 UART 芯片,支持 FIFO 缓冲和中断,广泛应用于虚拟机和嵌入式系统。
## 四、代码解读
### 4.1 寄存器定义
```rust
/// QEMU virt 机器 UART0 基地址
const UART0: usize = 0x1000_0000;
/// 寄存器偏移量(某些寄存器读写时有不同含义)
const RHR: usize = 0; // 接收保持寄存器(只读)
const THR: usize = 0; // 发送保持寄存器(只写)
const IER: usize = 1; // 中断使能寄存器
const FCR: usize = 2; // FIFO 控制寄存器(只写)
const LCR: usize = 3; // 线路控制寄存器
const LSR: usize = 5; // 线路状态寄存器
```
### 4.2 初始化序列
```rust
pub fn init() -> Result<()> {
unsafe { write_reg(IER, 0) }; // 禁用中断
unsafe { write_reg(LCR, lcr::BAUD_LATCH) }; // 进入波特率设置模式
unsafe { write_reg(0, 0x03) }; // 波特率除数低位(38400 bps)
unsafe { write_reg(1, 0x00) }; // 波特率除数高位
unsafe { write_reg(LCR, lcr::EIGHT_BITS) }; // 8 数据位,无奇偶校验
unsafe { write_reg(FCR, fcr::FIFO_ENABLE | fcr::FIFO_CLEAR) }; // 启用并清空 FIFO
Ok(())
}
```
### 4.3 字符输出
```rust
pub fn put_char(c: u8) {
while unsafe { read_reg(LSR) } & lsr::TX_IDLE == 0 {} // 等待发送器空闲
unsafe { write_reg(THR, c) }; // 写入字符
}
```
### 4.4 非阻塞字符输入
```rust
pub fn try_get_char() -> Option<u8> {
if unsafe { read_reg(LSR) } & lsr::RX_READY != 0 { // 检查接收器就绪
Some(unsafe { read_reg(RHR) }) // 读取字符
} else {
None
}
}
```
### 4.5 volatile 访问函数
```rust
unsafe fn read_reg(offset: usize) -> u8 {
let addr = UART0 + offset;
unsafe { (addr as *const u8).read_volatile() }
}
unsafe fn write_reg(offset: usize, value: u8) {
let addr = UART0 + offset;
unsafe { (addr as *mut u8).write_volatile(value) }
}
```
## 五、驱动设计总结
通过本驱动库的学习和实践,你将掌握硬件设备驱动开发的核心技能:
1. **理解了 MMIO 机制**:设备寄存器通过内存地址映射,使用 `volatile` 访问确保可靠性
2. **熟悉了 UART 硬件**:NS16550A 的寄存器布局、初始化流程和通信协议
3. **实现了轮询式 I/O**:通过状态寄存器轮询实现可靠的字符输入输出
4. **掌握了驱动设计模式**:提供同步/异步接口,支持 panic 场景的可靠输出
这是操作系统设备驱动开发的第一步——在后续章节中,你将在此基础上学习中断驱动、DMA、设备树等更高级的设备管理技术。
## 六、思考题
1. **为什么 `put_char` 中需要轮询 `LSR_TX_IDLE` 标志?** 如果不检查直接写入 THR 寄存器,会发生什么问题?
2. **`volatile` 访问是否足以保证多核环境下的设备访问安全?** 还需要哪些同步机制?
3. **如何将本驱动改为中断驱动模式?** 需要修改哪些部分?中断处理函数如何与驱动交互?
4. **FIFO 的作用是什么?** 启用 FIFO 后,驱动应该如何优化?
## 参考资料
- [NS16550A Datasheet](https://www.nxp.com/docs/en/data-sheet/PC16550D.pdf)
- [xv6-riscv uart.c](https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/uart.c)
- [rCore-Tutorial 设备驱动章节](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter9/2device-driver-1.html)
- [Rustonomicon: Volatile](https://doc.rust-lang.org/nomicon/volatility.html)
- [Memory-mapped I/O in RISC-V](https://five-embeddev.com/quickref/MMIO.html)
## Dependencies
| 无 | 本驱动不依赖其他 crate,是纯硬件抽象层 |
## 附录: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 位数据位
---
## License
Licensed under GNU GENERAL PUBLIC LICENSE, Version 3.0.