3. 串口输出Hello World

串口输出Hello World 学习卡片

任务说明

本案例要求在QEMU模拟的RISC-V virt机器上,通过内存映射寄存器初始化UART(PL011),并向终端输出"Hello World"字符串。目标验证串口驱动代码的正确性以及QEMU串口仿真链路是否打通。

硬件背景

QEMU的virt机器使用PL011作为串口控制器,其寄存器基地址为0x10000000。PL011的关键寄存器包括:

  • DR(0x000):数据寄存器,写入即发送字符
  • FR(0x018):标志寄存器,bit5为TXFF(发送FIFO满)
  • IBRD(0x024):整数波特率寄存器
  • FBRD(0x028):小数波特率寄存器
  • LCRH(0x02C):线路控制寄存器,需设置数据位、使能FIFO
  • CR(0x030):控制寄存器,使能UART、TX

代码实现

以下代码实现完整的UART初始化和字符串输出功能,可直接编译运行于QEMU virt机器:


// uart_hello.c
#include <stdint.h>

#define UART_BASE 0x10000000UL

// PL011寄存器偏移
#define UART_DR   0x000
#define UART_FR   0x018
#define UART_IBRD 0x024
#define UART_FBRD 0x028
#define UART_LCRH 0x02C
#define UART_CR   0x030

// FR寄存器标志位
#define FR_TXFF   (1 << 5)

static volatile uint32_t *uart_reg(uint32_t offset) {
    return (volatile uint32_t *)(UART_BASE + offset);
}

// 初始化PL011:8位数据、无校验、1停止位、使能FIFO
static void uart_init(void) {
    // 先禁用UART
    *uart_reg(UART_CR) = 0x0;

    // 设置波特率(QEMU默认时钟24MHz,目标115200)
    // IBRD = 24000000 / (16 * 115200) = 13.02 → 13
    // FBRD = (0.02 * 64) + 0.5 = 1.78 → 1
    *uart_reg(UART_IBRD) = 13;
    *uart_reg(UART_FBRD) = 1;

    // 线路控制:8位数据、使能FIFO(LCRH = 0x60)
    *uart_reg(UART_LCRH) = 0x60;

    // 使能UART、TX(CR = 0x301)
    *uart_reg(UART_CR) = 0x301;
}

// 发送单个字符(忙等待直到TX FIFO有空位)
static void uart_putc(char c) {
    // 等待发送FIFO不满
    while (*uart_reg(UART_FR) & FR_TXFF)
        ;
    *uart_reg(UART_DR) = (uint32_t)c;
}

// 输出字符串
static void uart_puts(const char *str) {
    while (*str) {
        uart_putc(*str++);
    }
}

// QEMU virt机器入口(_start跳转至此)
void main(void) {
    uart_init();
    uart_puts("Hello World\n");
    // 输出完成后进入死循环,避免main返回后执行未定义代码
    while (1)
        ;
}

链接脚本

需要链接脚本将代码放置在QEMU期望的入口地址(0x80000000):


/* link.ld */
OUTPUT_ARCH("riscv")
ENTRY(_start)

SECTIONS
{
    . = 0x80000000;
    .text : {
        *(.text._start)
        *(.text*)
    }
    .rodata : {
        *(.rodata*)
    }
    .data : {
        *(.data*)
    }
    .bss : {
        *(.bss*)
    }
}

启动汇编

提供最小的启动代码,设置栈指针后跳转到main:


# start.S
.section .text._start
.globl _start
_start:
    # 设置栈指针(指向DRAM顶部)
    li sp, 0x80000000
    # 栈向下增长,预留空间
    li t0, 0x100000
    sub sp, sp, t0

    # 跳转到C代码
    call main

# 不应返回,若返回则死循环
loop:
    j loop

编译与运行

使用RISC-V交叉编译工具链编译,并在QEMU中运行:


# 编译(假设riscv64-unknown-elf-gcc已安装)
riscv64-unknown-elf-gcc -march=rv64gc -mabi=lp64 -nostdlib -ffreestanding \
    -T link.ld -o uart_hello.elf start.S uart_hello.c

# 在QEMU中运行(无图形界面,串口输出到终端)
qemu-system-riscv64 -machine virt -nographic -kernel uart_hello.elf

验证方法

运行上述QEMU命令后,观察终端输出。判定标准:

  • 终端显示Hello World且无乱码
  • 程序不崩溃、不产生异常(QEMU无错误退出)
  • 串口输出完整,无字符丢失或重复

预期输出为:


Hello World

串口初始化时序

PL011 UART 初始化与发送时序 软件 1. 写 CR = 0x0(禁用UART) 2. 写 IBRD=13, FBRD=1(115200波特) 3. 写 LCRH = 0x60(8位数据+使能FIFO) 4. 写 CR = 0x301(使能UART+TX) 5. 轮询FR[TXFF]→写DR(逐字符发送) 准备阶段 配置阶段 发送阶段

关键点说明

  • QEMU virt机器上电后从0x80000000开始执行,因此链接脚本将代码段定位在此地址
  • PL011的波特率分频值基于QEMU固定的24MHz参考时钟计算
  • 发送字符前必须检查FR寄存器的TXFF位,确保发送FIFO未满
  • 字符串以\n结尾,QEMU的串口仿真会自动转换为终端换行