#uart #tutorial #driver #bios #virt #qemu #fifo #bare-metal #s-mode #ns16550a

app tg-rcore-tutorial-ch1-uart2

Chapter 1 UART2: A minimal bare-metal application using a fully-initialized NS16550A UART driver in S-mode (-bios none)

2 releases

Uses new Rust 2024

0.1.0-preview.2 Mar 11, 2026

#573 in Hardware support

GPL-3.0 license

55KB
178 lines

tg-rcore-tutorial-ch1-uart2:裸机串口驱动内核

目录


1. 项目概述

1.1 项目目标

本项目是一个最小化的裸机操作系统内核,运行在 QEMU RISC-V 64 位虚拟平台(virt machine)上。核心目标是:

不依赖 SBI(Supervisor Binary Interface) 的情况下,直接通过 MMIO(Memory-Mapped I/O) 操作 NS16550A 串口硬件,输出 "hello world" 字符串。

1.2 为什么需要这个项目?

传统方式下,RISC-V S-mode 内核通过 SBI 调用(类似于系统调用)请求 M-mode 的固件(如 OpenSBI)来完成 I/O 操作。但在深入理解操作系统的道路上,我们需要了解:

  1. 硬件是如何被软件直接控制的
  2. CPU 是如何从 M-mode 切换到 S-mode 的
  3. 设备驱动程序的基本结构是什么

本项目正是回答这些问题的最佳实践。

1.3 项目特点

┌─────────────────────────────────────────────────────────────┐
│                     项目特点一览                             │
├─────────────────┬───────────────────────────────────────────┤
│ 运行模式        │ -bios none(无 SBI 固件)                  │
│ 特权级          │ M-mode → S-mode                           │
│ I/O 方式        │ MMIO(直接内存映射 I/O)                   │
│ 串口硬件        │ NS16550A 兼容 UART                        │
│ 波特率          │ 115200 bps                               │
│ 帧格式          │ 8N1(8位数据,无校验,1位停止位)          │
│ 编程语言        │ Rust + 汇编                              │
└─────────────────┴───────────────────────────────────────────┘

2. 整体架构设计

2.1 系统分层架构

graph TB
    subgraph Hardware["硬件层"]
        UART["NS16550A UART<br/>(0x10000000)"]
        TEST["QEMU Test Device<br/>(0x100000)"]
        CPU["RISC-V CPU<br/>(M-mode / S-mode)"]
    end
    
    subgraph MMode["M-mode 固件层"]
        MEntry["m_entry.asm<br/>启动代码"]
        PMP["PMP 配置"]
        MStatus["mstatus 配置"]
    end
    
    subgraph SMode["S-mode 内核层"]
        Entry["_start 入口<br/>(栈初始化)"]
        Main["rust_main()<br/>主函数"]
        UARTDrv["uart.rs<br/>串口驱动"]
        Shutdown["shutdown()<br/>退出 QEMU"]
    end
    
    CPU --> MEntry
    MEntry --> PMP
    MEntry --> MStatus
    MEntry -->|"mret"| Entry
    Entry --> Main
    Main --> UARTDrv
    Main --> Shutdown
    UARTDrv -->|"MMIO"| UART
    Shutdown -->|"MMIO"| TEST

2.2 模块依赖关系

graph LR
    subgraph Source["源文件"]
        main["main.rs"]
        uart["uart.rs"]
        mentry["m_entry.asm"]
        build["build.rs"]
    end
    
    subgraph Runtime["运行时"]
        mcode["M-mode 代码"]
        scode["S-mode 代码"]
        driver["串口驱动"]
    end
    
    main --> uart
    main --> mentry
    build -->|"生成"| ld["linker.ld"]
    mentry --> mcode
    main --> scode
    uart --> driver

2.3 文件结构说明

tg-rcore-tutorial-ch1-uart2/
├── Cargo.toml                 # Rust 项目配置
├── rust-toolchain.toml        # 工具链版本锁定
├── build.rs                   # 构建脚本(生成链接脚本)
├── .cargo/
│   └── config.toml            # Cargo 配置(目标平台、QEMU 参数)
└── src/
    ├── main.rs                # S-mode 内核入口
    ├── uart.rs                # NS16550A 串口驱动
    └── m_entry.asm            # M-mode 启动汇编代码

3. 启动流程详解

3.1 完整启动时序

sequenceDiagram
    participant QEMU as QEMU
    participant M as M-mode<br/>(m_entry.asm)
    participant S as S-mode<br/>(main.rs)
    participant UART as UART 硬件
    participant TEST as Test Device
    
    QEMU->>M: 上电,PC = 0x80000000
    Note over M: 1. 建立 M-mode 栈
    Note over M: 2. 配置 mstatus.MPP = S-mode
    Note over M: 3. 设置 mepc = _start
    Note over M: 4. 配置 PMP 全访问
    Note over M: 5. 委托中断/异常给 S-mode
    M->>S: mret(切换到 S-mode)
    Note over S: PC = 0x80200000 (_start)
    Note over S: 1. 设置 S-mode 栈指针
    S->>S: call rust_main
    S->>UART: uart::init() - MMIO 初始化
    UART-->>S: 寄存器配置完成
    S->>UART: uart::puts("hello world\n")
    UART-->>QEMU: 串口输出字符
    S->>TEST: shutdown(false) - 写入 0x5555
    QEMU->>QEMU: 退出模拟

3.2 M-mode 启动代码解析

当 QEMU 以 -bios none 启动时,CPU 从地址 0x80000000 开始执行,处于 M-mode(Machine Mode)m_entry.asm 完成以下工作:

┌─────────────────────────────────────────────────────────────────┐
│                    M-mode 启动流程                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  0x80000000: _m_start                                          │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 1. 建立 M-mode 专用栈               │                       │
│  │    la   sp, m_stack_top            │                       │
│  │    csrw mscratch, sp               │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 2. 配置 mstatus                     │                       │
│  │    MPP=01(返回到 S-mode)          │                       │
│  │    li   t0, (1 << 11)              │                       │
│  │    csrw mstatus, t0                │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 3. 设置返回地址                     │                       │
│  │    la   t0, _start                 │                       │
│  │    csrw mepc, t0                   │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 4. 配置中断/异常委托                │                       │
│  │    csrw mideleg, 0xffff            │                       │
│  │    csrw medeleg, 0xffff            │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 5. 配置 PMP(物理内存保护)         │                       │
│  │    pmpaddr0 = -1(全地址空间)      │                       │
│  │    pmpcfg0  = 0x0fTOR + RWX)    │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 6. 允许 S-mode 读取性能计数器       │                       │
│  │    csrw mcounteren, -1             │                       │
│  └─────────────────────────────────────┘                       │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 7. mret 跳转到 S-mode               │                       │
│  │    → PC = mepc = _start            │                       │
│  │    → 特权级 = S-mode               │                       │
│  └─────────────────────────────────────┘                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 关键 CSR 寄存器说明

┌─────────────────────────────────────────────────────────────────────┐
│                    mstatus 寄存器布局                                │
├─────┬─────┬─────┬─────┬─────┬─────┬────────────────────────────────┤
│ Bit │ 12111098   │ 说明                           │
├─────┼─────┼─────┼─────┼─────┼─────┼────────────────────────────────┤
│     │ MPP[1] MPP[0]FSMIEMPIE│                               │
│     │     ↓     ↓   │     │     │     │                              │
│     │     0     1   │     │     │     │ ← 我们设置 MPP=01 (S-mode)  │
└─────┴─────┴─────┴─────┴─────┴─────┴────────────────────────────────┘

MPP (Machine Previous Privilege) 值:
  00 = User mode (U-mode)
  01 = Supervisor mode (S-mode)  ← 我们使用这个
  11 = Machine mode (M-mode)

4. 内存布局

4.1 物理内存映射

物理地址空间布局
┌────────────────────────────────────────────────────────────────────┐
│ 地址              │ 内容                │ 说明                      │
├───────────────────┼─────────────────────┼───────────────────────────┤
│ 0x0000_0000ROM/Boot ROM        │(-bios none 时不存在)    │
│        ~          │                     │                           │
│ 0x0010_0000VIRT_TESTQEMU 退出控制寄存器       │
├───────────────────┼─────────────────────┼───────────────────────────┤
│ 0x1000_0000UART0NS16550A 串口基地址       │
│        ~          │                     │                           │
├───────────────────┼─────────────────────┼───────────────────────────┤
│ 0x8000_0000       │ M-mode 区域         │ M-mode 代码和数据         │
│                   │   .text.m_entry     │   - M-mode 启动代码       │
│                   │   .text.m_trap      │   - M-mode 陷阱处理       │
│                   │   .bss.m_stack      │   - M-mode 栈 (4KB)       │
│                   │   .bss.m_data       │   - M-mode 数据           │
├───────────────────┼─────────────────────┼───────────────────────────┤
│ 0x8020_0000       │ S-mode 区域         │ S-mode 内核               │
│                   │   .text.entry       │   - _start 入口           │
│                   │   .text             │   - 内核代码              │
│                   │   .rodata           │   - 只读数据              │
│                   │   .data             │   - 可读写数据            │
│                   │   .bss              │   - 未初始化数据 + 栈     │
└───────────────────┴─────────────────────┴───────────────────────────┘

4.2 内核镜像结构

内核镜像在内存中的布局
                  物理地址
                    │
    0x80000000 ─────┼────────────────────────────────────
                    │  ┌─────────────────────────────┐
                    │  │  .text.m_entry              │
                    │  │  M-mode 启动代码            │
                    │  │  (~52 bytes)                │
                    │  └─────────────────────────────┘
                    │
    0x80000040 ─────┼────────────────────────────────────
                    │  ┌─────────────────────────────┐
                    │  │  .bss.m_stack               │
                    │  │  M-mode 栈 (4096 bytes)     │
                    │  └─────────────────────────────┘
                    │
                    │  ... (对齐填充) ...0x80200000 ─────┼────────────────────────────────────
                    │  ┌─────────────────────────────┐
                    │  │  .text.entry                │
                    │  │  S-mode 入口 _start         │
                    │  │  (naked 函数,设置栈)       │
                    │  ├─────────────────────────────┤
                    │  │  .text                      │
                    │  │  rust_main                  │
                    │  │  uart::init / puts          │
                    │  │  shutdown                   │
                    │  │  panic_handler              │
                    │  ├─────────────────────────────┤
                    │  │  .rodata                    │
                    │  │  字符串字面量               │
                    │  │  "hello world\n"            │
                    │  ├─────────────────────────────┤
                    │  │  .bss.uninit                │
                    │  │  S-mode 栈 (4096 bytes)     │
                    │  └─────────────────────────────┘
                    │
                    ▼

5. 串口驱动设计与实现

5.1 NS16550A UART 简介

NS16550A 是一款经典的 UART(Universal Asynchronous Receiver-Transmitter,通用异步收发器)控制器,广泛用于串口通信。QEMU virt 平台模拟了一个 NS16550A 兼容的 UART,映射到物理地址 0x1000_0000

5.2 寄存器布局

UART 寄存器映射 (基址: 0x1000_0000)
┌────────┬─────────────────┬─────────────────┬─────────────────────────┐
│ 偏移   │ DLAB=0 读       │ DLAB=0 写       │ DLAB=1                  │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +0RBRTHRDLL                     │
│        │ 接收缓冲寄存器   │ 发送保持寄存器   │ 波特率除数低字节         │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +1IERIERDLM                     │
│        │ 中断使能寄存器   │                 │ 波特率除数高字节         │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +2IIRFCR(同左)                  │
│        │ 中断标识寄存器   │ FIFO 控制寄存器 │                         │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +3LCRLCR             │                         │
│        │ 线路控制寄存器   │                 │                         │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +4MCRMCR             │                         │
│        │ 调制解调器控制   │                 │                         │
├────────┼─────────────────┼─────────────────┼─────────────────────────┤
│ +5LSR             │ —               │                         │
│        │ 线路状态寄存器   │                 │                         │
└────────┴─────────────────┴─────────────────┴─────────────────────────┘

DLAB (Divisor Latch Access Bit): LCR 寄存器的第 7DLAB=0: 访问 RBR/THR/IER(正常操作模式)
  DLAB=1: 访问 DLL/DLM(设置波特率模式)

5.3 关键寄存器详解

5.3.1 LCR (Line Control Register) - 线路控制寄存器

LCR 寄存器位定义
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Bit │  7654321:0  │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│     │DLABSBCSPEPSPENSTBWLS │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

我们使用的值: 0x03 (8N1)0x80 (设置波特率)

字段说明:
  DLAB: 除数锁存访问位
    0 = 正常 RBR/THR/IER 访问
    1 = 访问 DLL/DLM(波特率除数)
  
  WLS (Word Length Select): 字长
    00 = 5 bits
    01 = 6 bits
    10 = 7 bits
    11 = 8 bits  ← 我们使用 8STB (Stop Bits): 停止位
    0 = 1 stop bit  ← 我们使用 11 = 2 stop bits (5位字长时为 1.5)

  PEN (Parity Enable): 校验使能
    0 = 无校验  ← 我们不使用校验
    1 = 有校验

8N1 配置: LCR = 0b00000011 = 0x03
  - 8 位数据
  - 无校验 (Parity = None)
  - 1 位停止位

5.3.2 LSR (Line Status Register) - 线路状态寄存器

LSR 寄存器位定义
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Bit │  76543210  │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│     │ RFETEMTTHREBIFEPEOEDR  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

我们主要使用:THRE (Bit 5)

关键字段:
  THRE (Transmit Holding Register Empty): 发送保持寄存器空
    0 = THR 有数据,不能写入新数据
    1 = THR 为空,可以写入新数据  ← 等待这个位变成 1

  TEMT (Transmitter Empty): 发送器空
    1 = THR 和移位寄存器都为空

  DR (Data Ready): 数据就绪
    1 = RBR 中有接收到的数据

5.3.3 FCR (FIFO Control Register) - FIFO 控制寄存器

FCR 寄存器位定义(只写)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Bit │  76543210  │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│     │  RTL[1:0]00DMAXMITRCVEN  │
│     │           │     │     │ MODERSTRST │     │
└─────┴───────────┴─────┴─────┴─────┴─────┴─────┴─────┘

我们使用的值: 0xC7

字段说明:
  EN (Bit 0): FIFO 使能
    1 = 使能 FIFO  ← 使能

  RCV RST (Bit 1): 接收 FIFO 复位
    1 = 清空接收 FIFO  ← 复位

  XMIT RST (Bit 2): 发送 FIFO 复位
    1 = 清空发送 FIFO  ← 复位

  RTL (Bits 7:6): 接收触发级别
    00 = 1 字节
    01 = 4 字节
    10 = 8 字节
    11 = 14 字节  ← 设置为 14 字节

0xC7 = 0b11000111:
  - 使能 FIFO
  - 复位接收 FIFO
  - 复位发送 FIFO
  - 触发阈值 14 字节

5.4 波特率计算

波特率除数计算
┌────────────────────────────────────────────────────────────────┐
│                                                                │
│  公式: Divisor = UART_CLK / (16 × Baud_Rate)                   │
│                                                                │
│  QEMU virt 平台参数:                                           │
│    UART_CLK  = 3,686,400 Hz (3.6864 MHz)                      │
│    Baud_Rate = 115,200 bps                                    │
│                                                                │
│  计算:                                                         │
│    Divisor = 3,686,400 / (16 × 115,200)                       │
│           = 3,686,400 / 1,843,200                             │
│           = 2                                                 │
│                                                                │
│  因此:                                                         │
│    DLL = 0x02 (低字节)                                         │
│    DLM = 0x00 (高字节)                                         │
│                                                                │
└────────────────────────────────────────────────────────────────┘

5.5 初始化序列

flowchart TD
    START[开始初始化] --> STEP1[1. 禁用中断<br/>IER = 0x00]
    STEP1 --> STEP2[2. 进入波特率设置模式<br/>LCR = 0x80 (DLAB=1)]
    STEP2 --> STEP3[3. 设置波特率除数<br/>DLL = 0x02, DLM = 0x00]
    STEP3 --> STEP4[4. 设置帧格式 8N1<br/>LCR = 0x03 (DLAB=0)]
    STEP4 --> STEP5[5. 配置 FIFO<br/>FCR = 0xC7]
    STEP5 --> STEP6[6. 配置调制解调器<br/>MCR = 0x0B]
    STEP6 --> STEP7[7. 清除挂起状态<br/>读取 LSRRBR]
    STEP7 --> DONE[初始化完成]

5.6 字符发送流程

发送一个字符的流程 (putchar)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  putchar(c)                                                     │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────┐                           │
│  │ 读取 LSR 寄存器                 │                           │
│  │ lsr = read_reg(LSR)            │                           │
│  └─────────────────────────────────┘                           │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────┐                           │
│  │ LSR.THRE == 1 ?                 │                           │
│  │ (发送保持寄存器是否为空)         │                           │
│  └─────────────────────────────────┘                           │
│       │                                                         │
│       ├── 否 ──► 循环等待(忙等待)────────────┐               │
│       │                                        │               │
│       ▼ 是                                     │               │
│  ┌─────────────────────────────────┐          │               │
│  │ 写入 THR 寄存器                  │          │               │
│  │ write_reg(THR, c)               │          │               │
│  └─────────────────────────────────┘          │               │
│       │                                        │               │
│       ▼                                        │               │
│  [字符被 UART 硬件发送]                        │               │
│       │                                        │               │
│       ▼                                        │               │
│  [返回] ◄──────────────────────────────────────┘               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.7 MMIO 操作原理

内存映射 I/O (MMIO) 示意图
┌─────────────────────────────────────────────────────────────────┐
│                         CPU                                     │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  Rust 代码                                                 │ │
│  │                                                           │ │
│  │  // 写入 UART 的 THR 寄存器                                │ │
│  │  (0x10000000 as *mut u8).write_volatile(byte);            │ │
│  │       │                                                   │ │
│  │       │ 指针解引用 + volatile 写                          │ │
│  │       ▼                                                   │ │
│  └───────┼───────────────────────────────────────────────────┘ │
│          │                                                      │
│          ▼                                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │              CPU 内存控制器 / 总线                         │ │
│  │                                                           │ │
│  │  检测到地址 0x10000000                                    │ │
│  │  → 这是一个 MMIO 地址,不访问 RAM                         │ │
│  │  → 路由到 UART 设备                                       │ │
│  └───────────────────────────────────────────────────────────┘ │
│          │                                                      │
│          ▼                                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │              UART 硬件 (NS16550A)                          │ │
│  │                                                           │ │
│  │  ┌─────┬─────┬─────┬─────┬─────┬─────┐                   │ │
│  │  │ RBRIERIIRLCRMCRLSR │                   │ │
│  │  │ THR │     │ FCR │     │     │     │                   │ │
│  │  └─────┴─────┴─────┴─────┴─────┴─────┘                   │ │
│  │     ↑                                                     │ │
│  │     │                                                     │ │
│  │  写入 THR → 字符进入发送队列 → 串口输出                    │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

为什么使用 write_volatile / read_volatile?
  1. 防止编译器优化:MMIO 操作不能被优化掉
  2. 保证执行顺序:编译器不会重排 volatile 操作
  3. 每次都真正访问内存:不会使用寄存器缓存

6. 构建系统

6.1 构建流程

flowchart LR
    A[cargo build] --> B[编译 build.rs]
    B --> C[生成 linker.ld]
    C --> D[编译 Rust 源码]
    D --> E[汇编 m_entry.asm]
    E --> F[链接]
    F --> G[生成 ELF 二进制]
    
    subgraph 链接脚本
        C
    end
    
    subgraph 编译产物
        G
    end

6.2 链接脚本解析

链接脚本由 build.rs 在编译时生成,定义了内存布局:

# 链接脚本核心结构

OUTPUT_ARCH(riscv)           # 目标架构:RISC-V
ENTRY(_m_start)              # 程序入口点

# 内存基地址
M_BASE_ADDRESS = 0x80000000; # M-mode 区域
S_BASE_ADDRESS = 0x80200000; # S-mode 区域

SECTIONS {
    # M-mode 区域(从 0x80000000 开始)
    . = M_BASE_ADDRESS;
    .text.m_entry : { *(.text.m_entry) }  # M-mode 启动代码
    .text.m_trap  : { *(.text.m_trap)  }  # M-mode 陷阱处理
    .bss.m_stack  : { *(.bss.m_stack)  }  # M-mode 栈
    .bss.m_data   : { *(.bss.m_data)   }  # M-mode 数据

    # S-mode 区域(从 0x80200000 开始)
    . = S_BASE_ADDRESS;
    .text : {
        *(.text.entry)     # S-mode 入口 (_start)
        *(.text .text.*)   # 其他代码
    }
    .rodata : { ... }      # 只读数据
    .data   : { ... }      # 可读写数据
    .bss    : { ... }      # 未初始化数据(含栈)
}

6.3 QEMU 运行参数

# .cargo/config.toml 中的 runner 配置

runner = [
    "qemu-system-riscv64",  # QEMU RISC-V 64 位模拟器
    "-machine", "virt",     # 使用 virt 虚拟机
    "-nographic",           # 不使用图形界面(使用串口)
    "-bios", "none",        # 不加载 BIOS/SBI 固件
    "-kernel",              # 后面跟内核文件路径
]
QEMU 启动流程
┌────────────────────────────────────────────────────────────────┐
│                                                                │
│  qemu-system-riscv64 -machine virt -nographic -bios none      │
│                                                                │
│       │                                                        │
│       ▼                                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 1. 初始化 virt 机器                                       │ │
│  │    - 创建 RISC-V CPU                                     │ │
│  │    - 映射 UART0x10000000                             │ │
│  │    - 映射 RAM0x80000000                              │ │
│  └──────────────────────────────────────────────────────────┘ │
│       │                                                        │
│       ▼                                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 2. -bios none: 不加载任何固件                             │ │
│  │    PC 直接从 0x80000000 开始执行                          │ │
│  │    处于 M-mode                                           │ │
│  └──────────────────────────────────────────────────────────┘ │
│       │                                                        │
│       ▼                                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 3. 加载内核 ELFRAM                                    │ │
│  │    - .text.m_entry → 0x80000000                          │ │
│  │    - .text.entry → 0x80200000                            │ │
│  └──────────────────────────────────────────────────────────┘ │
│       │                                                        │
│       ▼                                                        │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 4. CPU0x80000000 开始执行                             │ │
│  │    → 执行 _m_start (m_entry.asm)                         │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                │
└────────────────────────────────────────────────────────────────┘

7. 动手实践

7.1 环境准备

# 1. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 安装 QEMU
# Ubuntu/Debian:
sudo apt install qemu-system-riscv64

# macOS:
brew install qemu

# Arch Linux:
sudo pacman -S qemu-system-riscv

7.2 编译与运行

# 进入项目目录
cd tg-rcore-tutorial-ch1-uart2

# 编译
cargo build

# 运行(输出 "hello world" 后自动退出)
cargo run

7.3 调试技巧

# 1. 查看反汇编
cargo objdump -- -d

# 2. 查看符号表
cargo nm

# 3. 查看 ELF 文件头
cargo readelf -- -h

# 4. 查看段信息
cargo readelf -- -S

# 5. 使用 GDB 调试
# 终端 1: 启动 QEMU 等待调试
qemu-system-riscv64 -machine virt -nographic -bios none -kernel target/riscv64gc-unknown-none-elf/debug/tg-rcore-tutorial-ch1-uart2 -s -S

# 终端 2: 启动 GDB
riscv64-unknown-elf-gdb target/riscv64gc-unknown-none-elf/debug/tg-rcore-tutorial-ch1-uart2
(gdb) target remote :1234
(gdb) break _m_start
(gdb) break _start
(gdb) break rust_main
(gdb) continue

7.4 扩展练习

  1. 修改输出内容:将 "hello world" 改为你的名字
  2. 添加数字输出:实现 putnum(n: u32) 输出十进制数字
  3. 实现 getchar():从串口读取一个字符
  4. 实现简单的 shell:读取用户输入并回显

附录:完整代码索引

文件 主要内容 行数
src/main.rs S-mode 入口、主函数、panic 处理 ~97
src/uart.rs NS16550A 驱动实现 ~110
src/m_entry.asm M-mode 启动汇编 ~52
build.rs 链接脚本生成 ~65

参考资料

No runtime deps