3. QEMU RISC-V 固件链接脚本定制
任务说明
本案例要求在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
串口初始化时序
关键点说明
- QEMU virt机器上电后从0x80000000开始执行,因此链接脚本将代码段定位在此地址
- PL011的波特率分频值基于QEMU固定的24MHz参考时钟计算
- 发送字符前必须检查FR寄存器的TXFF位,确保发送FIFO未满
- 字符串以
\n结尾,QEMU的串口仿真会自动转换为终端换行