
做嵌入式 SoC 这几年我越来越觉得 RISC-V 阵营里最值得先跑起来的核心就是 Ibex。它来自 lowRISC 项目是一个 32 位的 RISC-V 核心主打低功耗、小面积同时保留了不少能真正落地的配置项。第一次听说 Ibex 的人常以为它只是个教学用的玩具核直到你把它下载到 FPGA 上看到一串 C 程序在内部 SRAM 里跑通你才会意识到这玩意儿完全可以进入你的 MCU 工程。这篇文章围绕“Running an Ibex RISC-V Core”这件事展开适合三类人看一是想快速评估开源 RISC-V 核的 FPGA 工程师二是准备在自己的 SoC 里集成 Ibex 的芯片验证同学三是刚入门的硬件爱好者。我会把我实际跑 Ibex、改配置、调总线、上板点灯的过程和踩过的坑都写出来尽量让每个步骤都能照着操作。1. 为什么选 Ibex以及它到底适合什么场景1.1 Ibex 的定位不是“玩具核”是能落地的控制器级核心Ibex 的 RTL 全部使用 SystemVerilog默认配置下只用了两级流水线指令集支持 RV32IMC/RV32EMC没有 MMU。听起来很简单但这恰恰是它适合做单片机/控制器的原因。很多人在选型时拿它和 Rocket、CVA6 比其实不太公平——Rocket 目标是能跑 Linux 的应用核Ibex 的目标是替代传统 MCU 里那颗 8051 或 Cortex-M0。面积和功耗不是一个量级周围配套 IP中断、总线、Debug的复杂度也完全不同。我自己的判断标准是如果你的需求是“一块小芯片里嵌一个能执行 C 程序的控制核心还要低功耗、好集成”Ibex 是当下 RISC-V 开源核里最务实的选择之一。有朋友问过我这颗核到底有没有经过量产验证。严格说RISC-V 开源核的“量产”问题很复杂不能简单说某颗核被量产。Ibex 在 Google 的 OpenTitan 计划里作为 root of trust 芯片的 CPU 核心OpenTitan 本身是经过多次流片和实验验证的许多 MCU 级 SoC 也拿它当低功耗控制核。把它理解为“经过大量流片验证、可放心集成”比纠结“某个具体型号是否量产”更实际。1.2 拿到一个 Ibex 项目时先想清楚这四件事配置裁剪。Ibex 有大量参数比如是否支持乘法除法扩展、是否支持压缩指令、是否要分支预测、是否要 PMP。不要一上来开满特性。比如RV32IMC是常见配置既有乘法除法又有压缩指令代码密度和性能均衡如果只做简单控制RV32EMC可能更省面积。存储方案。Ibex 没有 MMU它对内存的要求是“够用就好”。要提前决定指令和数据是挂在同一块 SRAM 还是分开要不要 CacheIbex 本身没有指令 Cache所以你的 SoC 需要一块足够放的 bootloader/应用程序的 SRAM。外设总线。Ibex 的访存接口不是 AXI是一组带 req/gnt/valid 的握手信号。你需要一个 bridge/adapter 把它转成 AXI、AXI-Lite 或 TileLink。如果忽略这一步后面 SoC 接线会非常痛苦。调试能力。Ibex 支持 RISC-V Debug Module但要启用RV_DM并配 JTAG 或就地的debug_req接口。如果你只做功能仿真不启用也没关系如果上板强烈建议开启省得用 LED 点灯看程序跑没跑。2. 运行前必须搞清楚的环境准备与源码结构2.1 源码获取和分支选择先拉代码这个不复杂git clone https://github.com/lowRISC/ibex.git cd ibex git checkout master默认分支是开发主线更新很快。如果你是做正式项目建议不要追最新代码而是看官方 Releases 里相对稳定的 tag。Ibex 的对外接口偶尔会变比如中断信号、配置参数、FuseSoC target 名称都有可能在不同版本之间不兼容。锁版本不仅能降低你的维护成本还能让团队同事用同一套环境避免“我这边跑得好好的怎么你那边编译就挂了”这种问题。Ibex 官方推荐的 IP 管理和仿真流程是 FuseSoC。先装好它pip install fusesoc然后安装一两款仿真器。大部分时候我用 Verilator而如果用商业仿真器需要自己在环境里装好 Questa 或 ModelSim并且把可执行文件所在目录加进PATH。在 Ubuntu 上直接sudo apt install verilator就能装到系统自带版本但如果系统自带版本太旧编译 Ibex 时会出现一堆 SystemVerilog 语法报错。建议去 Verilator 官方仓库装 5.x 或者更新的源码版本编译时间虽然长一点但是省事。2.2 工具链选型Verilator、Questa/ModelSim 和 Vivado 各自的分工很多新手会把仿真器和综合工具混在一起其实它们负责的阶段完全不同。工具用途我的建议VerilatorRTL 编译 仿真 testbench默认首选速度快Ibex 自带流程支持好Questa/ModelSimUVM 验证环境、波形分析适合需要看波形、做覆盖率时用Vivado综合、实现、生成比特流上板前必须做时序分析和在线调试RISC-V GCC toolchain编译 C/汇编程序用 bare-metal 工具链不要用 Linux 工具链为什么我把 Verilator 放在第一位因为 Ibex 自带 top-level testbench 的 C/DPI 封装Verilator 能把 SystemVerilog 直接转成 C 模型跑程序级仿真比传统事件驱动仿真器快一个数量级。你可以快速编译、快速跑测试反复验证 C 程序逻辑。等确认功能没问题再拿 Vivado 做时序收敛效率会高很多。安装 RISC-V 工具链时要注意riscv64-linux-gnu-gcc这类 Linux 工具链面向带 MMU 的 Linux 用户态程序不适合裸机 Ibex。你要用的是riscv32-unknown-elf-gcc或riscv32-embedded-elf-gcc它们才支持-marchrv32imc这类裸机交叉编译参数。2.3 Ibex 目录里有哪些关键文件别一上来就 panic拿到 Ibex 仓库后目录结构一眼看不完但是真正重要的其实是这几个地方rtl/存放所有 RTL 源文件其中ibex_core.sv是处理器核心ibex_top.sv是带寄存器接口、中断同步、Debug 模块的完整顶层。dv/测试向量、testbench 和验证环境。examples/sw/示例软件程序可以直接用工具链编译。examples/fpga/FPGA 工程示例可以作为上板参考。这里有一个重要建议不要直接改rtl/目录下的源文件作为你自己的 SoC 设计。如果改坏了以后官方更新没法合并。最佳实践是把它当作外部 IP通过参数配置和 wrapper 接起来。但凡需要修功能先怀疑是你 wrapper 接错了再怀疑 Ibex 本身。3. 在仿真环境里把 Ibex 跑起来验证“处理器核心”不是一堆死代码3.1 核心仿真命令与参数Ibex 的测试流程不是直接丢给仿真器就跑它依赖 FuseSoC 做 IP 拆分和依赖生成。最简单的方式是cd ibex make sim这个目标会自动调用 FuseSoC用 Verilator 构建 sim target并跑一个默认的 smoke test。第一次运行会拉取 FuseSoC 依赖并编译 RTL时间比较久之后就快多了。跑完如果看到类似PASSED的输出说明你的环境和 Ibex 自身都是好的。你也可以用 FuseSoC 直接指定目标fusesoc run --targetsim lowrisc:ibex:ibex_sim两种方式本质一样。这里有个小坑如果你之前用过其他 FuseSoC 工程~/.cache/fusesoc里有旧的 core library 索引可能会导致找不到lowrisc:ibex:ibex_sim。解决办法是在 Ibex 仓库根目录重新执行fusesoc library add ibex .3.2 读懂仿真输出从 Testbench 到指令执行Ibex 的 testbench 会实例化一颗 ibex_core连接 memory然后把你自己编译的 ELF 加载进模拟内存。程序跑完后testbench 会判断是否执行到预设的终止条件。所以你看到PASSED不是因为仿真器觉得“看起来没报错”而是 testbench 真的等到了程序写某个特定的 memory 地址或者触发某个 CSR 状态。理解这一点很重要。很多人发现修改 C 程序后仿真一直不结束就开始怀疑 Verilator 坏了。其实大概率是你新写的程序里没有触发退出条件testbench 只能一直等。如果你要看波形可以试试在仿真命令行里加WAVES1或者直接在 testbench 里$dumpvars不同版本开关不一样以仓库文档为准。我习惯先用 VCD 看 PC、instr_addr_o、instr_rvalid_i这几根信号确认程序真的在跑。3.3 自定义软件测试程序跑一个简单的 C 程序想验证自己的逻辑不能一直跑官方自带的仿真。你需要写一个最简单的 C 程序比如往某个地址写一个数#define TEST_ADDR 0x00100000 int main(void) { volatile unsigned int *p (volatile unsigned int *)TEST_ADDR; *p 0x12345678; while (1); return 0; }然后使用裸机工具链编译riscv32-unknown-elf-gcc -marchrv32imc -mabiilp32 -O2 -nostdlib -T link.ld -o prog.elf prog.c这里最关键的是link.ld里的内存基地址必须和 testbench 里的 memory 起始地址一致。如果基地址写错程序会被加载到错误位置CPU 从 reset vector 取第一条指令时就取到了垃圾数据然后表现为“跑飞”。Ibex 仓库在examples/sw/里也放了一些示例比如simple。你可以把自己的 C 程序放进去执行make -C examples/sw/simple生成 ELF 后再交给仿真脚本。这样能避开自己写链接脚本的麻烦。注意编译 RISC-V 裸机程序时尽量用-marchrv32imc而不是-marchrv32i。默认配置带乘法除法扩展编译 libgcc 里的某些操作会用到乘除指令。如果你把核配置成 RV32E工具链参数也得相应改成-marchrv32emc否则编译出来的指令 CPU 不认。4. 把 Ibex 部署到真实 FPGA 上的关键步骤4.1 选择 FPGA 板卡与 Vivado 工程如果没特别偏好我建议选 Digilent Arty A7-35TArtix-7 35T 的资源足够跑一个 50MHz 到 80MHz 的最小系统。Ibex 的 FPGA 例程里针对这类板卡有过参考工程。用 Vivado 2019.1 及以上版本直接打开即可或者用 FuseSoC 生成工程但手工创建也不难。在创建工程时要把 Ibex 当成一个外部 IP放到单独目录。如果直接在 Block Design 里手拉总线你会发现 Ibex 的接口并不是 AXI而是类似 SRAM 的握手信号Block Design 里要加 adapter 才能连接到 AXI 总线上。最简单的起步方案是纯 RTL 顶层把ibex_top.sv实例化然后用几根 wire 把它和 Block RAM、UART、GPIO 连在一起。4.2 连接 Boot ROM、RAM、GPIO 最小系统最小系统至少三部分一块从固定地址开始的 SRAM 或者 BRAM一段上电后 CPU 跳到 reset vector 的 boot ROM一组可见的 LED/GPIO。Ibex 的复位地址由顶层端口reset_addr_i决定通常要设成 boot ROM 的基地址。boot ROM 里放一条跳转指令或者一段最简单的拷贝程序把主程序从 SPI Flash/UART 拷到 SRAM再跳过去执行。连线时最常犯的错误是把rst_ni接成高电平复位。注意信号名里的n表示低电平有效。如果你把复位按键按下时输出高电平直接接到rst_ni上会发现核心一上电就处于复位状态程序永远跑不起来。正确做法是接一个反相器或者用按键的低有效引脚。另外ibex_top的访存接口分为指令口和数据口每个口都有req、gnt、addr、rdata、rvalid等信号。做 SoC 时要把这两个口分别连到 Xbar 或 memory 上不能只连一个。指令口和数据口的基地址可以不连续但必须保证 RAM 覆盖两个口的访问范围。4.3 时序约束与上板调试为什么 core 会“跑飞”上板最常见的问题是程序跑飞。你烧进去的 bitstream 看起来能工作但 LED 不按预期闪CPU 完全不受控。这时候别急着怀疑核坏了先检查三件事时钟是否稳定、复位释放是否足够长、BRAM 读延迟是否匹配。Ibex 的访存接口是组合读地址加下一拍读数据的时序如果你在 BRAM 上加了输出寄存器相当于多了一拍延迟但 core 侧没有等待机制数据就会错位。这类问题在仿真里不容易暴露因为仿真 memory 模型比较理想时序完全符合 core 预期。解决方法是把 BRAM 的 primitive 延迟配到和 core 接口一致或者使用带 registered read 的控制器时插入等待状态。上板调试时我强烈建议在 Vivado 里加一个 ILA把instr_addr_o、instr_rvalid_i、data_addr_o、data_rdata_i这些信号抓下来。程序跑飞之前 PC 是什么跑飞之后跳到哪里一目了然。没有 ILA就只能靠 LED 点灯猜状态效率太低。注意Ibex 的默认配置里没有 MMU也没有指令 Cache。所以如果你的主程序很大比如塞了几十 KB 的数组BRAM 容量不够程序会跑到未映射地址然后异常。这种“跑飞”其实不是 bug而是你忘了规划内存大小。5. 集成到 SoC 时绕不开的总线与中断问题5.1 两种主流集成方式直接内存映射和 TileLink/AXI 桥接Ibex 的访存接口本质上是一组 master 握手不是完整总线协议。最简单的方式是把指令 master 和数据 master 接到一个 Xbar 的入口Xbar 内部分别挂 ROM、RAM、外设。这种“直接内存映射”的方式最适合小规模 SoC外设数量不多地址空间自己规划即可。如果 SoC 里已经有一整套 AXI 总线网络就需要写一个ibex_to_axiadapter把 req/gnt 握手转换成 AXI 的 AW/W/B/AR/R 通道。注意指令和数据在 AXI 上是两个独立 masterID 不能混用否则乱序返回后 core 会拿到错误的数据。写 adapter 时我建议先在仿真里用定向激励测一遍单拍读写、连续读写、outstanding 读。Ibex 的指令口在取指时会有比较强的突发性如果你的 adapter 不支持 back-to-back request性能会很难看。数据口则要保证写响应顺序不然状态机容易卡死。5.2 中断控制器的接入与优先级处理Ibex 顶层里有irq_i[31:0]这是一个多路中断输入另外还有单独的irq_software_i、irq_timer_i、irq_external_i。如果你在 SoC 里跑 RTOS推荐用标准 PLIC把外设中断汇聚后给irq_i定时器用 CLINT 生成irq_timer_i软件中断用于多核唤醒或核间通信。集成时最容易漏掉的是中断同步。Ibex 的中断输入在顶层已经做了一部分同步但如果你把异步外设中断直接怼到irq_i上还是可能出现亚稳态问题。稳妥做法是每个中断源先经过两级同步器和边沿检测再进入 PLIC。中断优先级这块我建议把 timer 中断放在最高优先级因为 RTOS 的调度节拍不能丢。外部中断按外设紧急程度配置可以用 PLIC 的可编程优先级寄存器按需动态调整。5.3 功耗与面积调优的实测体会Ibex 的配置参数会显著影响面积。选择RV32EMC且不使能乘除法单元、关闭分支预测综合面积比默认的RV32IMC小不少。具体数据取决于工艺库我在一个 55nm 工艺下的项目里最小配置只用了十几万门级别而默认配置明显更重。所以做低功耗 MCU 时不要默认开满特性按需求裁剪很重要。功耗方面Ibex 的 clock gating 比较完善但 SRAM 的功耗往往比 core 本身更突出。如果 SoC 里有多个 SRAM bank用ram_cfg_i可以控制 bank 使能在 low power 模式下关掉不访问的 bank。我试过把非活跃 bank 关掉后整体功耗能降低不少。面积和功耗优化最忌讳的是拍脑袋。每次修改配置后我都建议跑一遍综合报告对比 LUT/FF、时序 slack、功耗估算。RISC-V 配置项多但真正影响结果的就是乘法器、压缩指令、分支预测、PMP 这几个大项其余参数影响有限。6. 常见问题与排查经验速查6.1 仿真阶段的高频报错现象可能原因解决办法Verilator 编译报出一堆 SystemVerilog 语法错误版本太旧换 Verilator 5.x 或更新版本FuseSoC 找不到 lowrisc:ibex:ibex_simcore library 没注册在仓库根目录执行fusesoc library add ibex .仿真加载 ELF 后 PC 一直停在 0链接脚本基地址和 reset_addr 不一致检查 link.ld 和测试平台的 memory map程序跑完但仿真不退出程序里没有触发 testbench 的 terminate 条件增加退出地址写入或让 testbench 等待特定 CSR仿真报FAILED或者“Core failed”测试向量没通过别慌看 log 里哪条指令比对失败通常是你的 C 程序逻辑问题有人一看到 “Running Core Failed请查看提示信息” 就懵其实这就是 testbench 输出的通用错误信息意思是让去翻详细 log。真正的错误往往藏在前面几百行的比对记录里并不是核心本身坏了。6.2 上板阶段的高频问题现象可能原因解决办法bitstream 下载后 LED 全灭复位信号极性接反检查rst_ni是否低有效程序执行到一半乱跳BRAM 读延迟不匹配调整 memory 等待状态或去掉输出寄存器串口没有输出时钟频率宏没配对检查 UART demo 里的clk_freq_hz定义时序无法收敛主频过高或约束缺失降频到 50MHz补全所有时钟/复位约束下载后程序跑飞boot ROM 拷贝程序不对用 ILA 抓 PC确认复位后第一条指令6.3 一条很实用的 debug 思路我自己调试单个 RISC-V core 的流程几乎固化先跑官方 smoke test证明 RTL 和工具链是好的再把自己编译的 ELF 用仿真跑通排除软件问题最后才上板上板时先用 ILA 抓 PC 和instr_rvalid_i。如果某一步失败就把范围锁定在那一层。别一上来就改 RTL大概率是白费时间。如果你也正准备把一颗 Ibex 跑起来记住先仿真后上板把 Makefile 和 FuseSoC 流程吃透比急着调总线省太多时间。我个人习惯每次改动配置后先跑一遍自带的 smoke test再跑自定义程序。这条习惯让我至少少加了不知道多少小时的班也让我在“Core failed”面前不再手忙脚乱。