tg-rcore-tutorial-uart1 0.1.0-preview.1

NS16550A UART driver for S-Mode in rCore tutorial.
Documentation
  • Coverage
  • 100%
    14 out of 14 items documented0 out of 8 items with examples
  • Size
  • Source code size: 1.37 MB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 399.37 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • rcore-os/tg-rcore-tutorial
    11 30 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • yydawx

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.rsread_reg / write_reg 输出稳定,无编译器优化问题
寄存器定义 src/uart.rs 的常量定义(UART0RHRTHR 等) 可对照 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:

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

Windows:

https://rustup.rs 下载并运行 rustup-init.exe

验证安装:

rustc --version    # 应显示 rustc 1.xx.x
cargo --version    # 应显示 cargo 1.xx.x

1.2 添加 RISC-V 64 编译目标

由于驱动可能用于 RISC-V 64 裸机平台,建议添加对应的编译目标:

rustup target add riscv64gc-unknown-none-elf

1.3 安装 QEMU 模拟器(可选)

如需在 QEMU 中测试驱动,需要安装 qemu-system-riscv64(建议版本 >= 7.0)。

Ubuntu / Debian:

sudo apt update
sudo apt install qemu-system-misc

macOS(Homebrew):

brew install qemu

验证安装:

qemu-system-riscv64 --version

二、使用方式

2.1 添加依赖

Cargo.toml 中添加:

[dependencies]
tg-rcore-tutorial-uart = { path = "../tg-rcore-tutorial-uart", version = "0.1.0-preview.1" }

2.2 基本使用

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 处理中使用同步输出

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 通过普通的内存读写指令(如 ldsd)访问这些地址,实际上是在与设备寄存器交互。

与端口 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 初始化序列

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 字符输出

pub fn put_char(c: u8) {
    while unsafe { read_reg(LSR) } & lsr::TX_IDLE == 0 {} // 等待发送器空闲
    unsafe { write_reg(THR, c) }; // 写入字符
}

4.4 非阻塞字符输入

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 访问函数

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 后,驱动应该如何优化?

参考资料

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.