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
源码阅读导航索引
建议把源码阅读聚焦在一个文件: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:
|
Windows:
从 https://rustup.rs 下载并运行 rustup-init.exe。
验证安装:
1.2 添加 RISC-V 64 编译目标
由于驱动可能用于 RISC-V 64 裸机平台,建议添加对应的编译目标:
1.3 安装 QEMU 模拟器(可选)
如需在 QEMU 中测试驱动,需要安装 qemu-system-riscv64(建议版本 >= 7.0)。
Ubuntu / Debian:
macOS(Homebrew):
验证安装:
二、使用方式
2.1 添加依赖
在 Cargo.toml 中添加:
[]
= { = "../tg-rcore-tutorial-uart", = "0.1.0-preview.1" }
2.2 基本使用
use Uart;
// 初始化 UART 硬件
let _ = init;
// 输出字符串
put_str;
// 输出单个字符
put_char;
// 尝试读取字符(非阻塞)
if let Some = try_get_char
2.3 在 panic 处理中使用同步输出
use Uart;
!
三、核心概念
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 寄存器定义
/// 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 初始化序列
4.3 字符输出
4.4 非阻塞字符输入
4.5 volatile 访问函数
unsafe
unsafe
五、驱动设计总结
通过本驱动库的学习和实践,你将掌握硬件设备驱动开发的核心技能:
- 理解了 MMIO 机制:设备寄存器通过内存地址映射,使用
volatile访问确保可靠性 - 熟悉了 UART 硬件:NS16550A 的寄存器布局、初始化流程和通信协议
- 实现了轮询式 I/O:通过状态寄存器轮询实现可靠的字符输入输出
- 掌握了驱动设计模式:提供同步/异步接口,支持 panic 场景的可靠输出
这是操作系统设备驱动开发的第一步——在后续章节中,你将在此基础上学习中断驱动、DMA、设备树等更高级的设备管理技术。
六、思考题
-
为什么
put_char中需要轮询LSR_TX_IDLE标志? 如果不检查直接写入 THR 寄存器,会发生什么问题? -
volatile访问是否足以保证多核环境下的设备访问安全? 还需要哪些同步机制? -
如何将本驱动改为中断驱动模式? 需要修改哪些部分?中断处理函数如何与驱动交互?
-
FIFO 的作用是什么? 启用 FIFO 后,驱动应该如何优化?
参考资料
- NS16550A Datasheet
- xv6-riscv uart.c
- rCore-Tutorial 设备驱动章节
- Rustonomicon: Volatile
- Memory-mapped I/O in RISC-V
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.