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-archrv32im --with-abiilp32 # --prefix/opt/riscv 指定安装目录建议选一个系统路径方便后续使用。 # --with-archrv32im 指定目标架构。E906支持M标准扩展整数乘除法根据需求可添加F/D/C单双精度浮点、压缩指令。 # --with-abiilp32 指定应用二进制接口。rv32对应ilp32int, 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-listriscv32-softmmu --prefix/opt/qemu-8.2 # --target-listriscv32-softmmu 指定我们只编译RISC-V 32位系统模拟器加快编译速度。 # --prefix 指定安装路径。 # 4. 编译并安装 make -j$(nproc) sudo make install安装后将QEMU路径加入PATHecho 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) */ /* 对于我们的简单内存模型LMAVMA此步可省略但保留框架 */ 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_PREFIXriscv32-unknown-elf- CC${TOOLCHAIN_PREFIX}gcc AS${TOOLCHAIN_PREFIX}as LD${TOOLCHAIN_PREFIX}ld OBJCOPY${TOOLCHAIN_PREFIX}objcopy CFLAGS-marchrv32im -mabiilp32 -nostdlib -ffreestanding -O2 -I$FIRMWARE_DIR/include LDFLAGS-T $FIRMWARE_DIR/ld/virt.ld # 编译启动文件 $AS -marchrv32im -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.bin5.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加载地址匹配。对于-kernelQEMU默认加载地址是0x80000000。使用riscv32-unknown-elf-objdump -h firmware.elf查看各个段的起始地址VMA。检查入口点使用riscv32-unknown-elf-readelf -h firmware.elf查看ELF头中的Entry point address它应该等于_start符号的地址即.text段的起始地址。检查串口地址QEMUvirt机器的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内核的理解。

本月热点