当前位置: 首页 > news >正文

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。通常有以下几种路径:

  1. FPGA原型验证平台:将E906的HDL代码综合并烧录到FPGA中,形成一块“软核”芯片。这是最接近真实芯片的体验,能进行硬件调试,但需要FPGA开发板,门槛和成本较高。
  2. 软件仿真平台:利用QEMU、Spike等指令集模拟器来模拟E906的执行行为。这种方式纯粹在软件层面运行,速度快,易于调试软件,但无法验证与真实硬件时序相关的特性。
  3. 商业仿真器:如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 --version

4.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} scripts
  • firmware/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编译过程中报错,提示缺少某个头文件或库,或者某个子模块下载失败。
  • 排查
    1. 检查依赖:回头仔细核对apt install那一步,确保所有列出的包都已成功安装。有时不同Linux发行版或版本,包名略有差异。
    2. 网络问题:如果是子模块下载失败(如git clone失败),可以尝试手动下载对应的tar包。工具链源码目录下有时会有cache文件夹,将下载好的包放进去再重试。或者配置git和curl的代理。
    3. 内存不足:编译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也启动了,但终端一片寂静。
  • 系统化排查
    1. 检查链接地址:确认virt.ld中定义的ORIGIN是否与QEMU的-kernel加载地址匹配。对于-kernel,QEMU默认加载地址是0x80000000。使用riscv32-unknown-elf-objdump -h firmware.elf查看各个段的起始地址(VMA)。
    2. 检查入口点:使用riscv32-unknown-elf-readelf -h firmware.elf查看ELF头中的Entry point address,它应该等于_start符号的地址(即.text段的起始地址)。
    3. 检查串口地址:QEMUvirt机器的NS16550串口寄存器基地址确实是0x10000000。但需要注意,该平台可能将串口映射到了“MMIO”区域,并且位宽可能是8位(volatile char *)而非32位。尝试将指针类型改为volatile char *,并通过偏移量访问THR寄存器(通常偏移0)。
    4. 简化测试:在main函数最开始,尝试用一个最简单的内存写入操作(如设置一个全局变量为特定值),然后通过QEMU的监视器(-S -s参数启动,然后用gdb连接)查看内存是否被正确修改,以判断程序是否真的在运行。
    5. 查看QEMU退出码:程序如果触发异常(如非法指令、访问非法地址),QEMU可能会静默退出。在run.sh脚本最后加上echo "QEMU exit code: $?"来查看。

6.4 与真实E906 SDK的衔接

  • 说明:本文搭建的是一个学习用的、通用的、基于QEMU virt平台的模拟环境。它与平头哥官方提供的E906 FPGA验证环境或芯片SDK存在差异,主要体现在:
    • 内存映射:真实E906开发板有固定的内存映射表,串口、定时器等外设的基地址不同。
    • 启动流程:可能包含BootROM、二级引导等更复杂的阶段。
    • 设备树:需要匹配硬件平台的真实.dts文件。
  • 迁移思路:当你需要迁移到真实硬件或FPGA时,需要:
    1. 替换链接脚本中的内存区域定义,使其与硬件设计一致。
    2. 替换启动文件中硬件相关的初始化代码(如时钟初始化、PLL配置等)。
    3. 使用官方提供的设备树文件(或根据硬件手册编写)。
    4. 调整外设驱动代码中的寄存器基地址。
    5. 使用JTAG调试器而非QEMU来加载和调试程序。

搭建好这个基础环境,就像是盖房子打好了地基和框架。虽然目前还只是在模拟器上运行一个简单的裸机程序,但我们已经拥有了完整的编译、链接、模拟执行的能力。更重要的是,我们理解了从源代码到处理器执行的完整工具链路径,以及如何为一个特定的处理器核配置运行环境。在接下来的实践中,我们将在这个平台上,逐步添加更复杂的功能,如中断处理、定时器驱动、以及移植一个小型实时操作系统(RTOS),从而不断深化对E906 RISC-V内核的理解。

http://www.jsqmd.com/news/1284896/

相关文章:

  • DIY磁流体:从打印机墨粉到动态艺术品的材料科学实践
  • SEO实战指南:从技术基建到内容策略的完整流量获取体系
  • Prompt工程实战:AI精准指令设计与优化技巧
  • 基于Bluno Beetle的避障小车:从硬件搭建、滤波算法到蓝牙调试全解析
  • Tkinter窗口图标设置全攻略:从原理到打包的完整解决方案
  • 2026 年更新:灌阳专业的帮我推荐一个好用的短视频获客软件平台哪家可靠,别再乱买了!这款能帮你搞定短视频获客的工具,到底选哪个才不踩坑-抖来豆包推广 - 企业推荐管【认证】
  • Starccm浮式风机七自由度耦合仿真实践
  • Cadence Allegro PCB设计:Snap Pick To捕捉功能原理与实战配置详解
  • AI论文降重技术解析与实操指南
  • 从波形看懂I2C:时序分析、硬件/软件实现与深度调试实战
  • CMDB在云原生时代的核心价值与选型指南
  • 人工筛选直播高光太耗时,能不能 AI 自动抓取直播卖点片段
  • CORS账号还是自建基站?FindCM长期成本评估模型
  • vLLM大模型推理部署实战:KV缓存优化与生产级API搭建
  • (2026最新)鹰潭本地漏水检测维修公司靠谱推荐:正规防水补漏上门维修-墙面/屋顶/外墙/暗管漏水检测精准定位 - 即刻修防水
  • 深入理解STM32 PWM:从时钟源到占空比的全链路解析与实战
  • Gmail邮件追踪技术解析:从追踪像素到智能跟进的实战指南
  • 西门子PLC高速计数器实战:从编码器脉冲采集到位置闭环控制
  • MySQL 慢查询排查完整流程
  • 如何解决MPC Video Renderer与PotPlayer兼容性问题:终极指南
  • Flutter跨平台开发:从环境搭建到首个应用实战
  • STM32 HAL库实现SPWM正弦波生成:从原理到工程实践
  • 物联网设备低功耗设计:NBM7100A能量管理方案解析
  • 仿BOSS招聘平台实现(5)
  • 实战解析:通用版阿卡迈逆向的核心技巧与避坑指南
  • 【C++重载操作符与转换】自增操作符和自减操作符
  • Windows内网信息收集实战:从系统命令到自动化工具链
  • Windows APK安装器:3分钟学会在电脑上直接运行安卓应用
  • 游戏音频提取完全指南:5步轻松解密ACB/AWB到WAV格式
  • 机器学习入门实战:从环境配置到完整项目的Python代码实现