!Attention
本crate包为AI4OS内测的T2L1实验包,尚在敏捷开发迭代中,包括但不限于文档完善、评分机制开发、报告提交等工作,未完成工作会紧跟列出:
TODO:
- 完善《原理驱动+进度满足型学习者原理驱动者型学习文档》
- 练习题开发
- 评分进度cli程序开发
- 提交实践报告
与uart2 crate的对比
参考uart2 crate之后,我发现我的设计与uart2的设计主要差别在于:
- 我的实验尽可能基于ch1的架构而来,保持了对sbi crate的依赖,只在ch1基础上对输出部分做了改造:也即采用串口(MMIO)的方式输出字符。 而uart2的crate则自包含,将sbi的汇编以及rust handler代码也包含在内,推测目的是让学生直接了解特权级切换和上下文保存机制。 但根据我个人的学习历程,在学习ch1的时候我就已经同步通读了sbi的代码,不需要再重复自包含,所以我选择同ch1一样的架构,依赖完整也可以直接运行。
- 我的实验对于串口的一些处理机制还不够完善,个人认为可以加到实验里让同学们自行完善一部分
第一章: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 工具链
|
验证安装:
2. 添加 RISC-V 64 编译目标
3. 安装 QEMU 模拟器
Ubuntu/Debian:
macOS(Homebrew):
验证安装:
编译与运行
编译
编译过程为交叉编译,目标平台为 riscv64gc-unknown-none-elf,由 .cargo/config.toml 配置指定。
运行
该命令会自动启动 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 访问强制编译器:
- 按代码顺序执行每次访问
- 不缓存寄存器值
- 不进行冗余访问优化
示例:
// 错误:编译器可能优化掉第二次读取
let status = ptr.read_volatile;
if status & TX_READY != 0
// 正确:每次读取都是显式的
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 初始化步骤如下:
- 禁用中断:
IER = 0(使用轮询方式,不启用中断) - 设置波特率:进入 LCR_BAUD_LATCH 模式,设置波特率为 38.4K
- 设置数据格式:
LCR = LCR_EIGHT_BITS(8 位数据位,1 位停止位,无奇偶校验) - 启用 FIFO:
FCR = FIFO_ENABLE | FIFO_CLEAR(启用 16 字节 FIFO 并清空)
轮询式 I/O
轮询(Polling)是一种简单的 I/O 处理方式,CPU 不断检查设备状态,直到设备就绪。
字符输出流程:
- 等待发送器空闲:读取 LSR 寄存器,检查
TX_IDLE标志 - 写入字符:向 THR 寄存器写入字符
- 重复:继续等待并写入下一个字符
优缺点:
- 优点:实现简单,无需中断处理机制
- 缺点:CPU 利用率低,在等待期间忙循环
代码解读
项目配置
.cargo/config.toml 配置交叉编译和 QEMU runner:
[]
= "riscv64gc-unknown-none-elf"
[]
= [
"qemu-system-riscv64",
"-machine", "virt",
"-nographic",
"-bios", "none",
"-kernel",
]
Cargo.toml 中的关键依赖:
[]
= { = "../tg-rcore-tutorial-uart", = "0.1.0-preview.1" }
= { = "../tg-sbi", = "0.4.2-preview.1", = ["nobios"] }
UART 驱动使用
在 src/main.rs 中:
use Uart;
use shutdown;
unsafe extern "C" !
UART 驱动实现
tg-rcore-tutorial-uart/src/uart.rs 中的核心函数:
本章小结
本章通过实现 UART 驱动,深入学习了硬件设备驱动的基本原理:
- MMIO 机制:理解了设备寄存器通过内存地址映射的访问方式,掌握了
volatile关键字在防止编译器优化中的关键作用。 - UART 驱动实现:学习了 NS16550A 串口芯片的寄存器布局,掌握了初始化流程、字符发送的轮询等待机制。
- 驱动 vs. SBI 对比:理解了直接硬件操作与通过固件抽象层调用的本质区别,能够根据场景选择合适的技术方案。
- 轮询式 I/O:掌握了简单的轮询实现方式,为后续学习中断驱动、DMA 等高级 I/O 技术打下基础。
这是操作系统设备驱动开发的第一步,后续章节将在此基础上引入中断机制、块设备驱动、文件系统等更复杂的设备管理技术。
思考题
volatile的必要性:如果不使用volatile访问 UART 寄存器,可能会发生什么问题?请结合编译器优化策略和硬件寄存器特性进行分析。- 轮询 vs. 中断:轮询方式和中断驱动方式在 CPU 利用率、响应延迟、实现复杂度等方面有何区别?在什么场景下你会选择轮询方式?
- 波特率计算:UART 初始化代码中设置了波特率为 38.4K,这个值是如何计算出来的?如果希望改为 115200 波特率,需要修改哪些寄存器值?
- 地址硬编码:UART 基地址
0x10000000在代码中是硬编码的,这种方式有什么优缺点?在实际的操作系统中,如何更灵活地管理设备地址? - 错误处理:当前的 UART 驱动缺乏错误处理机制(如超时、奇偶校验错误等)。如果要增强鲁棒性,需要考虑哪些错误情况?如何设计相应的错误处理机制?
参考资料
- xv6-riscv uart.c - MIT xv6-riscv 的 UART 驱动实现
- NS16550A Datasheet - NS16550A 数据手册
- Rustonomicon: Volatile - Rust 中 volatile 访问的官方说明
- rCore-Tutorial 设备驱动章节 - rCore-Tutorial 中关于设备驱动的讲解
依赖
| 依赖 | 说明 |
|---|---|
tg-rcore-tutorial-uart |
NS16550A UART 驱动实现,提供字符输入输出 |
tg-sbi |
SBI 调用封装(仅用于关机功能) |
License
Licensed under GNU GENERAL PUBLIC LICENSE, Version 3.0.