RISC-V E906移植实战:从零搭建QEMU裸机开发环境
1. 项目缘起:为什么选择E906进行移植?
在嵌入式开发领域,RISC-V架构以其开放、灵活的特性,正成为越来越多开发者探索和学习的焦点。而E906,作为平头哥半导体(T-Head)推出的一款面向低功耗、高能效应用场景的32位RISC-V内核,因其完整的开源IP、成熟的工具链以及活跃的社区支持,成为了许多开发者入门RISC-V软硬件协同开发的首选平台之一。
我之所以启动这个E906移植项目,并非一时兴起。在过往的项目中,我经常遇到需要在特定硬件平台上验证算法、驱动或系统级功能的需求。传统的做法是依赖现成的评估板或仿真器,但这往往受限于硬件资源、成本或供货周期。更重要的是,一个“黑盒”式的开发板,很难让你真正理解从处理器上电复位到第一条指令执行,再到操作系统启动的完整过程。这种理解上的缺失,在遇到底层疑难问题时,往往会成为排查的瓶颈。
因此,我的目标很明确:在一个完全由自己搭建的、可控的软硬件实验环境中,将E906 RISC-V内核成功运行起来。这不仅仅是为了点亮一个LED,更是为了打通从RISC-V处理器核、总线、外设到上层应用软件的完整链路,建立起对嵌入式系统启动流程、内存映射、中断处理和工具链使用的第一手认知。这个过程,对于希望深入理解计算机体系结构、有志于从事芯片底层软件(BSP/Firmware)开发,或计划基于RISC-V进行定制化设计的工程师来说,价值巨大。
接下来,我将完整记录这次移植之旅的第一步:项目整体规划、开发环境搭建以及实验平台的构建。我会尽量详述每一个环节背后的考量和具体操作,特别是那些容易踩坑的细节。
2. 项目全景图:我们要移植什么?目标是什么?
在动手写第一行代码或敲第一个命令之前,清晰地定义项目范围至关重要。这能避免我们在复杂的细节中迷失方向。
2.1 核心组件解析
本次移植的核心是平头哥E906 RISC-V处理器核。它不是一块实体的芯片,而是一个以硬件描述语言(HDL)编写的IP(知识产权核)。我们需要一个载体来“装载”并运行这个IP。通常有以下几种路径:
- FPGA原型验证平台:将E906的HDL代码综合并烧录到FPGA中,形成一块“软核”芯片。这是最接近真实芯片的体验,能进行硬件调试,但需要FPGA开发板,门槛和成本较高。
- 软件仿真平台:利用QEMU、Spike等指令集模拟器来模拟E906的执行行为。这种方式纯粹在软件层面运行,速度快,易于调试软件,但无法验证与真实硬件时序相关的特性。
- 商业仿真器:如Synopsys VCS、Cadence Xcelium等,用于芯片前端的仿真验证,功能强大但license昂贵,不适合个人学习。
考虑到学习的普适性和低成本,我将选择“软件仿真平台”作为本次实验的主要环境。具体来说,是使用QEMU这个开源的机器模拟器。我们可以在QEMU中配置一个模拟的“机器”(machine),使其CPU模型行为与E906保持一致,并为其配置符合E906典型内存映射的外设(如UART、CLINT等)。这样,我们编译出的RISC-V程序就可以在这个模拟的“E906机器”上运行了。
2.2 项目目标分解
我们的终极目标是让一个简单的裸机程序(例如通过串口打印“Hello, E906!”)在模拟的E906平台上成功运行。拆解开来,需要完成以下关键目标:
- 目标A:构建完整的RISC-V GNU工具链。我们需要一套能生成针对E906(具体是RV32IMA[F]D[C]架构)指令集的交叉编译器、汇编器、链接器等。
- 目标B:准备E906相关的软件支持包。这主要包括E906的启动引导代码(Bootloader)和基础设备树(Device Tree Blob, DTB)。Bootloader负责最底层的硬件初始化(如设置栈指针、清零BSS段);DTB则向操作系统(或裸机程序)描述模拟平台的内存布局、外设信息等。
- 目标C:配置并编译QEMU系统模拟器。我们需要一个支持RISC-V 32位架构,并且能够模拟我们所需外设的QEMU版本。
- 目标D:编写、编译并链接我们的第一个裸机测试程序。
- 目标E:将程序加载到QEMU中并运行,通过串口观察输出。
本次分享将聚焦于目标A、B、C,即完成开发环境与实验平台的搭建,为后续的编程与调试铺平道路。
3. 开发环境搭建:打造专属的RISC-V工具链
工欲善其事,必先利其器。一个稳定、高效且版本匹配的工具链是项目成功的基石。我强烈建议在Linux环境下进行此项工作,Ubuntu 20.04/22.04或CentOS 7/8都是不错的选择。我使用的是Ubuntu 22.04 LTS。
3.1 系统基础依赖安装
首先,更新系统并安装编译工具链所必需的软件包。这一步看似简单,但缺少任何一个包都可能导致后续编译诡异失败。
sudo apt update sudo apt upgrade -y sudo apt install -y autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev git ninja-build pkg-config libglib2.0-dev libpixman-1-dev libslirp-dev这里解释几个关键包的作用:
autoconf,automake,libtool: 用于处理项目的自动配置脚本。bison,flex: 语法分析器生成器,编译某些组件(如GCC)时需要。texinfo: 用于生成GNU风格的文档。gperf: 完美的哈希函数生成器。libmpc-dev,libmpfr-dev,libgmp-dev: 高精度数学运算库,GCC编译所需。libglib2.0-dev,libpixman-1-dev: QEMU编译的核心依赖。ninja-build: 一种比make更快的构建系统,QEMU推荐使用。
3.2 获取并编译RISC-V GNU工具链
我们可以从GitHub上官方仓库获取源码并自行编译。虽然有些Linux发行版提供了预编译的riscv64-unknown-elf-gcc包,但自行编译能确保获得针对RV32IM架构的完整工具链(包括Newlib库),并且版本可控。
# 1. 选择一个合适的工作目录并进入 mkdir -p ~/riscv-e906-project cd ~/riscv-e906-project # 2. 克隆工具链源码仓库 (这是一个包含所有子模块的大仓库) git clone --recursive https://github.com/riscv-collab/riscv-gnu-toolchain.git cd riscv-gnu-toolchain # 3. 配置编译选项 # 我们目标是裸机环境(elf),架构是rv32im,使用newlib作为C库。 ./configure --prefix=/opt/riscv --with-arch=rv32im --with-abi=ilp32 # --prefix=/opt/riscv 指定安装目录,建议选一个系统路径,方便后续使用。 # --with-arch=rv32im 指定目标架构。E906支持M标准扩展(整数乘除法),根据需求可添加F/D/C(单双精度浮点、压缩指令)。 # --with-abi=ilp32 指定应用二进制接口。rv32对应ilp32(int, long, pointer都是32位)。 # 4. 开始编译(这是一个漫长过程,取决于CPU核心数,可能需1-2小时) make -j$(nproc)注意:编译过程会从网络下载依赖(如GCC、Binutils、Newlib的特定版本源码)。请保持网络通畅。如果遇到下载失败,可以尝试手动下载对应的tar包放到工具链源码目录的
cache文件夹下(如果有的话),或者使用代理。
编译成功后,工具链会被安装到/opt/riscv目录下。我们需要将它的bin目录加入系统的PATH环境变量。
echo 'export PATH=/opt/riscv/bin:$PATH' >> ~/.bashrc source ~/.bashrc验证安装:
riscv32-unknown-elf-gcc --version如果成功,你会看到类似riscv32-unknown-elf-gcc (GCC) x.x.x的输出。至此,我们的核心编译武器就准备好了。
4. 实验平台构建:定制化QEMU与获取硬件支持包
有了编译器,我们还需要一个“虚拟机”来运行程序。这就是QEMU的任务。同时,我们需要E906特定的启动代码来描述这个“虚拟机”的硬件信息。
4.1 编译支持RISC-V的QEMU系统模式
我们需要的QEMU是“系统模式”(qemu-system-riscv32),它能模拟整个计算机系统,包括CPU、内存、总线、外设等。
# 返回项目根目录 cd ~/riscv-e906-project # 1. 克隆QEMU源码(建议使用稳定版本分支) git clone -b stable-8.2 https://github.com/qemu/qemu.git qemu-8.2 cd qemu-8.2 # 2. 创建并进入构建目录(推荐out-of-tree build) mkdir build cd build # 3. 配置编译选项 ../configure --target-list=riscv32-softmmu --prefix=/opt/qemu-8.2 # --target-list=riscv32-softmmu 指定我们只编译RISC-V 32位系统模拟器,加快编译速度。 # --prefix 指定安装路径。 # 4. 编译并安装 make -j$(nproc) sudo make install安装后,将QEMU路径加入PATH:
echo 'export PATH=/opt/qemu-8.2/bin:$PATH' >> ~/.bashrc source ~/.bashrc验证:
qemu-system-riscv32 --version4.2 获取E906硬件支持包(关键步骤)
这是整个搭建过程中最具平台特异性的一步。E906的硬件支持包,通常包含:
- 链接脚本(.ld文件):定义程序的内存布局,如代码段(.text)、数据段(.data/.bss)的加载地址和运行地址。这对于裸机程序至关重要,必须与QEMU模拟的内存映射一致。
- 启动文件(startup.S或crt0.S):用汇编编写的入口代码,负责设置栈指针、清零BSS段、初始化数据段,最后跳转到C语言的
main函数。 - 设备树源文件(.dts):描述硬件平台资源(内存范围、中断控制器、串口地址等)的文本文件。
- 可能需要的驱动头文件或简易外设操作代码。
平头哥通常会在其GitHub仓库(如T-head-Semi)或芯片SDK中提供这些资源。由于直接使用官方SDK可能涉及复杂的依赖,为了简化学习,我们可以采用一种“模拟兼容”的策略。
策略:使用QEMU官方支持的“virt”机器平台,并使其配置与E906典型配置对齐。QEMU的riscv32-virt机器是一个通用的RISC-V虚拟平台,我们可以通过命令行参数灵活配置其CPU特性、内存大小和设备树。我们可以自己编写一个简单的、符合virt平台内存映射的链接脚本和启动文件。
4.2.1 创建项目目录结构
cd ~/riscv-e906-project mkdir -p firmware/{src,include,ld} scriptsfirmware/src: 存放应用程序源码(.c, .S)。firmware/include: 存放头文件。firmware/ld: 存放链接脚本。scripts: 存放编译、运行脚本。
4.2.2 编写链接脚本 (firmware/ld/virt.ld)
/* 定义内存区域 */ MEMORY { /* QEMU virt机器通常将ROM(或Flash)映射到0x80000000, RAM映射到0x80000000 + ROM_SIZE */ /* 为了简单,我们让代码和数据都放在RAM里,入口地址为0x80000000 */ ram (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* 定义程序段如何映射到内存区域 */ SECTIONS { /* 程序入口点符号 */ . = ORIGIN(ram); .text : { /* 启动代码放在最前面 */ *(.text.start) *(.text .text.*) } > ram .rodata : { *(.rodata .rodata.*) } > ram .data : { *(.data .data.*) } > ram .bss : { *(.bss .bss.*) *(COMMON) } > ram /* 栈指针初始位置,通常设置在RAM末尾 */ /* _stack_top = ORIGIN(ram) + LENGTH(ram); */ }这个脚本定义了程序从0x80000000地址开始加载和运行。_stack_top可以在启动文件里计算。
4.2.3 编写启动汇编文件 (firmware/src/start.S)
.section .text.start .global _start _start: /* 1. 设置栈指针: 假设RAM大小为128M,栈顶设在末尾 */ li sp, 0x88000000 # 0x80000000 + 128M /* 2. 清零BSS段 */ la a0, _sbss la a1, _ebss bgeu a0, a1, clear_bss_done clear_bss_loop: sw zero, 0(a0) addi a0, a0, 4 bltu a0, a1, clear_bss_loop clear_bss_done: /* 3. 复制数据段从加载地址(LMA)到运行地址(VMA) */ /* 对于我们的简单内存模型,LMA=VMA,此步可省略,但保留框架 */ la a0, _sdata la a1, _edata la a2, _sidata bgeu a0, a1, copy_data_done copy_data_loop: lw t0, 0(a2) sw t0, 0(a0) addi a0, a0, 4 addi a2, a2, 4 bltu a0, a1, copy_data_loop copy_data_done: /* 4. 跳转到C主函数 */ call main /* 5. 主函数返回后,进入死循环 */ 1: j 1b /* 提供链接脚本需要的符号 */ .global _sbss, _ebss, _sdata, _edata, _sidata这个启动文件完成了最基本的初始化工作。其中_sbss,_ebss等符号需要在链接脚本中定义,我们稍后更新。
4.2.4 更新链接脚本以定义符号在virt.ld的.bss和.data段后添加:
_sbss = .; .bss : { ... } > ram _ebss = .; _sidata = LOADADDR(.data); _sdata = .; .data : { ... } > ram _edata = .; . = ALIGN(8); _end = .;5. 整合验证:编译一个最小可执行文件并运行
现在,让我们用已有的工具链和平台代码,编译一个最简单的C程序,并在QEMU中验证整个环境是否通畅。
5.1 编写测试程序 (firmware/src/main.c)
// 一个简单的串口输出函数 // QEMU virt平台的NS16550兼容串口寄存器映射在0x10000000 volatile unsigned int *uart_tx = (unsigned int *)0x10000000; void uart_putc(char c) { while (*uart_tx & 0x80000000); // 等待发送就绪位(简化模型,实际位可能不同) *uart_tx = c; } void uart_puts(const char *s) { while (*s) { uart_putc(*s++); } } // C入口函数 int main(void) { uart_puts("Hello, E906 from QEMU-virt!\n"); while (1) { // 空循环 } return 0; // 不会执行到这里 }5.2 编写编译脚本 (scripts/build.sh)
#!/bin/bash PROJECT_ROOT=$(cd `dirname $0`/..; pwd) FIRMWARE_DIR=$PROJECT_ROOT/firmware TOOLCHAIN_PREFIX=riscv32-unknown-elf- CC=${TOOLCHAIN_PREFIX}gcc AS=${TOOLCHAIN_PREFIX}as LD=${TOOLCHAIN_PREFIX}ld OBJCOPY=${TOOLCHAIN_PREFIX}objcopy CFLAGS="-march=rv32im -mabi=ilp32 -nostdlib -ffreestanding -O2 -I$FIRMWARE_DIR/include" LDFLAGS="-T $FIRMWARE_DIR/ld/virt.ld" # 编译启动文件 $AS -march=rv32im -o $FIRMWARE_DIR/src/start.o $FIRMWARE_DIR/src/start.S # 编译主程序 $CC $CFLAGS -c -o $FIRMWARE_DIR/src/main.o $FIRMWARE_DIR/src/main.c # 链接 $LD $LDFLAGS -o $FIRMWARE_DIR/firmware.elf $FIRMWARE_DIR/src/start.o $FIRMWARE_DIR/src/main.o # 生成原始二进制文件(用于可能的烧录) $OBJCOPY -O binary $FIRMWARE_DIR/firmware.elf $FIRMWARE_DIR/firmware.bin echo "Build complete: firmware.elf, firmware.bin"5.3 编写QEMU启动脚本 (scripts/run.sh)
#!/bin/bash PROJECT_ROOT=$(cd `dirname $0`/..; pwd) FIRMWARE_ELF=$PROJECT_ROOT/firmware/firmware.elf qemu-system-riscv32 \ -machine virt \ -cpu rv32 \ -m 128M \ -nographic \ -serial mon:stdio \ -bios none \ -kernel $FIRMWARE_ELF-machine virt: 指定使用virt虚拟平台。-cpu rv32: 指定CPU为32位RISC-V,符合E906基线。-m 128M: 指定128MB内存,与链接脚本匹配。-nographic -serial mon:stdio: 将串口输出重定向到当前终端。-bios none: 不加载任何BIOS,直接从-kernel指定地址执行。-kernel $FIRMWARE_ELF: 将我们的ELF文件作为内核加载,QEMU会将其加载到内存(默认从0x80000000开始)并跳转到入口点_start。
5.4 执行验证
cd ~/riscv-e906-project chmod +x scripts/*.sh ./scripts/build.sh ./scripts/run.sh如果一切顺利,你将在终端看到输出:Hello, E906 from QEMU-virt!。恭喜你,一个基于QEMU模拟的、为E906风格配置的RISC-V裸机运行环境已经成功搭建并运行了第一个程序!
6. 环境搭建中的常见陷阱与解决思路
这个过程看似步骤清晰,但实际操作中极易遇到各种问题。以下是我在搭建过程中遇到或预见到的一些典型“坑”及其应对策略。
6.1 工具链编译失败
- 问题:
make编译过程中报错,提示缺少某个头文件或库,或者某个子模块下载失败。 - 排查:
- 检查依赖:回头仔细核对
apt install那一步,确保所有列出的包都已成功安装。有时不同Linux发行版或版本,包名略有差异。 - 网络问题:如果是子模块下载失败(如
git clone失败),可以尝试手动下载对应的tar包。工具链源码目录下有时会有cache文件夹,将下载好的包放进去再重试。或者配置git和curl的代理。 - 内存不足:编译GCC是非常消耗内存的过程,如果虚拟机内存小于4GB,可能会在编译某个阶段因内存耗尽而被系统杀死(Killed)。建议分配至少8GB内存给编译环境。
- 检查依赖:回头仔细核对
6.2 QEMU编译或运行报错
- 问题:
../configure时提示缺少libslirp等库。 - 解决:确保安装了
libslirp-dev。如果发行版仓库没有,可能需要从源码编译安装libslirp。 - 问题:运行
qemu-system-riscv32时提示Unable to load the RISC-V firmware "opensbi-riscv32-virt-fw_jump.bin"。 - 解决:我们使用了
-bios none和-kernel直接加载ELF,所以不需要OpenSBI。这个警告可以忽略,或者通过添加-bios none参数显式指定。如果仍想使用OpenSBI,需要下载或编译对应的固件,并通过-bios参数加载,此时我们的程序将作为“payload”被OpenSBI加载,入口地址和启动流程会有所不同。
6.3 程序编译成功但QEMU无输出
- 问题:最令人头疼的情况。程序似乎正常编译链接,QEMU也启动了,但终端一片寂静。
- 系统化排查:
- 检查链接地址:确认
virt.ld中定义的ORIGIN是否与QEMU的-kernel加载地址匹配。对于-kernel,QEMU默认加载地址是0x80000000。使用riscv32-unknown-elf-objdump -h firmware.elf查看各个段的起始地址(VMA)。 - 检查入口点:使用
riscv32-unknown-elf-readelf -h firmware.elf查看ELF头中的Entry point address,它应该等于_start符号的地址(即.text段的起始地址)。 - 检查串口地址:QEMU
virt机器的NS16550串口寄存器基地址确实是0x10000000。但需要注意,该平台可能将串口映射到了“MMIO”区域,并且位宽可能是8位(volatile char *)而非32位。尝试将指针类型改为volatile char *,并通过偏移量访问THR寄存器(通常偏移0)。 - 简化测试:在
main函数最开始,尝试用一个最简单的内存写入操作(如设置一个全局变量为特定值),然后通过QEMU的监视器(-S -s参数启动,然后用gdb连接)查看内存是否被正确修改,以判断程序是否真的在运行。 - 查看QEMU退出码:程序如果触发异常(如非法指令、访问非法地址),QEMU可能会静默退出。在
run.sh脚本最后加上echo "QEMU exit code: $?"来查看。
- 检查链接地址:确认
6.4 与真实E906 SDK的衔接
- 说明:本文搭建的是一个学习用的、通用的、基于QEMU virt平台的模拟环境。它与平头哥官方提供的E906 FPGA验证环境或芯片SDK存在差异,主要体现在:
- 内存映射:真实E906开发板有固定的内存映射表,串口、定时器等外设的基地址不同。
- 启动流程:可能包含BootROM、二级引导等更复杂的阶段。
- 设备树:需要匹配硬件平台的真实
.dts文件。
- 迁移思路:当你需要迁移到真实硬件或FPGA时,需要:
- 替换链接脚本中的内存区域定义,使其与硬件设计一致。
- 替换启动文件中硬件相关的初始化代码(如时钟初始化、PLL配置等)。
- 使用官方提供的设备树文件(或根据硬件手册编写)。
- 调整外设驱动代码中的寄存器基地址。
- 使用JTAG调试器而非QEMU来加载和调试程序。
搭建好这个基础环境,就像是盖房子打好了地基和框架。虽然目前还只是在模拟器上运行一个简单的裸机程序,但我们已经拥有了完整的编译、链接、模拟执行的能力。更重要的是,我们理解了从源代码到处理器执行的完整工具链路径,以及如何为一个特定的处理器核配置运行环境。在接下来的实践中,我们将在这个平台上,逐步添加更复杂的功能,如中断处理、定时器驱动、以及移植一个小型实时操作系统(RTOS),从而不断深化对E906 RISC-V内核的理解。