开场先聊个背景。做开源项目系列到现在,第三十二期总算是轮到RISC-V这个话题了。标题里写了“新增俩”,意思是这次我同时收录了两个新项目进库,一次性介绍完:一个叫PicoRV32,一个叫NEORV32。前者是纯Verilog写成的极简单文件软核,后者是用VHDL搭建的完整片上系统。两个项目解决的是同一类问题——在FPGA里放一个能直接跑C程序的处理器,但实现思路完全不同。如果你在做FPGA、平时写Verilog、或者一直想在板子上跑通一个属于自己的处理器,这期内容可以直接当入门参考资料用。
为什么拖到第三十二期才系统聊RISC-V?因为现在的开源处理器选项已经不是“有没有”的问题,而是“怎么选”的问题。和x86、ARM那种需要授权、看不到内部实现不同,RISC-V把指令集架构本身开源了,任何人可以基于它设计处理器、改处理器、在FPGA上验证处理器。这对FPGA开发者来说是个天然契合点:HDL生态本身就讲究开放与共享,RISC-V给了大家一个标准化的指令集底座,于是近些年冒出了一大批优秀的软核项目。PicoRV32和NEORV32就是其中两个很有代表性的,一个极致简单,一个功能齐全。
这篇内容适合三类人:刚入门FPGA、想给工程加一个处理器的开发者;做SoC设计、需要一套可裁剪软核做原型验证的工程师;还有纯硬件爱好者,想在板子上跑通一个完整计算机系统。看完之后,你不光能了解这两个项目的核心设计思路,还能照着实操部分,在真实开发板上把它们跑起来。
1. RISC-V开源软核:为什么成了FPGA圈的必选项
1.1 指令集开源带来的连锁反应
在很多人的印象里,处理器设计是巨头才能干的事。x86被两家公司把持,ARM走授权模式,普通工程师想接触真实的处理器内部结构,难度不小。RISC-V把这个局面彻底打破了。它采用的BSD/类BSD开源许可,意味着你可以自由使用、修改、商用,不需要向任何人报备。指令集本身变成了一套公开的、稳定的接口规范,谁都可以照着它去实现一个CPU。
这对FPGA开发者的意义特别直接:FPGA的“可重构”特性,一直需要一个能被反复修改、反复实验的处理器形态。x86/ARM的处理器核不会以RTL源码形式给你,商业软核(比如NIOS II、MicroBlaze)虽然是现成的,但被厂商绑定得很死,内部逻辑也不透明。RISC-V软核则完全在另一个维度上:源码就放在GitHub仓库里,用Verilog或VHDL写成,你可以打开看每一行代码,可以删掉不需要的模块,可以加自定义指令,然后跑到自己的板子上。这种“看得见、改得动”的处理器,才是FPGA工程师真正需要的。
我用一个生活化的类比来说这件事:FPGA本身像一盒万能积木,你可以拼出任何数字电路;而RISC-V软核是已经有人拼好、验证过的“通用大脑模块”。以前你想用这个大脑,得买厂商的特定积木包,现在有人把图纸开源了,你可以自由地拿过来改一版,拼进自己的作品里。
1.2 FPGA上软核处理器的三个典型用途
了解完背景,还得说清楚一个问题:FPGA里已经有强大的并行逻辑能力,为什么还要塞一个串行执行的处理器进去?
第一个典型用途是替代复杂状态机。通信协议解析、命令解释、外部芯片的初始化序列、传感器数据的数学运算,这些逻辑用纯状态机写会非常痛苦,尤其当功能需求频繁变更时,每改一次状态机都要重新综合一次。放一个软核进去,代码层面用C写,编译生成二进制,下载到RAM里就能改功能。FPGA只保留高速接口、信号处理等必须用硬件实现的部分,整体开发效率会高很多。
第二个用途是SoC原型验证。现在很多芯片公司设计SoC时,会在流片前用FPGA做验证。开源RISC-V软核还能作为参考设计,配合自定义外设或加速器,在FPGA上搭建整个系统,验证架构是否合理、软件能否跑通。
第三个用途是教学和科研。计算机体系结构课程需要学生理解流水线、中断、缓存这些概念,与其看PPT,不如直接在FPGA上运行一个真实的RISC-V核,用逻辑分析仪抓内部信号。科研人员则可以在软核上做指令集扩展实验,测试新的加速器对性能的影响。这也是为什么PicoRV32和NEORV32这类项目在学校里非常受欢迎。
2. 本期新增项目一:PicoRV32,单文件Verilog的极简软核
2.1 项目定位与核心设计思路
PicoRV32出自Clifford Wolf之手,如果对开源EDA生态比较熟,这个名字一定不陌生——Yosys这个开源综合工具就是他主导开发的。目前项目托管在YosysHQ组织下,地址是github.com/YosysHQ/picorv32。它的核心卖点用一个词就能概括:极简。
极简到什么程度?整个处理器的核心代码只有一个文件,picorv32.v。对,你没看错,不是一整个IP仓库,不是一堆模块文件,就是一个Verilog文件。使用时不需要创建IP核、不需要配置向导、不需要引入一堆依赖,直接把这一个文件拖进你的工程,例化一下,一个可以跑程序、能响应中断的RISC-V处理器就出现了。
这个设计选择背后是有考量的。软核处理器在FPGA工程里往往只是一个子系统,使用者通常希望它简单、透明、容易控制。PicoRV32放弃了花哨的架构和复杂的分支预测,换来的是极低的集成成本和极高的可读性。我在自己的工程里用过一次,当时大概只花了半天时间就把它跑通了,这体验比在Vivado里折腾MicroBlaze舒畅太多。
2.2 指令集支持与硬件配置项
别以为“极简”就等于“残缺”。PicoRV32完整支持RISC-V的整数指令集RV32I,可以通过配置扩展支持压缩指令C扩展和乘除法M扩展,组合起来就是RV32IC、RV32IMC等常见形态。压缩指令扩展对FPGA场景很有意义,因为FPGA的片上BRAM很宝贵,指令变短意味着同样的内存容量能装下更多程序。
硬件层面有几个关键的配置参数,我列一下最常用的:
ENABLE_MUL:开启整数乘法,默认关,开启后很多C程序的运算速度会有明显提升。ENABLE_DIV:开启整数除法,同样默认关。ENABLE_IRQ:开启内置中断控制器,这个必须配合它自家的PIC寄存器使用。ENABLE_FAST_MUL:用DSP资源实现快速乘法器,代价是占用更多硬件资源,性能更好。
资源占用方面,最简配置的RV32I内核大约只需要750个LUT左右,即使加上M、C扩展和中断控制器,一般也就1500到2000个LUT。这是什么概念呢?一块入门级的Cyclone IV或Artix-7开发板,逻辑资源有几万到十几万个LUT,所以PicoRV32完全不会成为资源瓶颈。运行频率方面,在Artix-7这类现代FPGA上跑100MHz以上轻轻松松,对大多数控制类任务够用了。
2.3 在FPGA工程里集成的最小步骤
我直接用仓库自带的simple_soc示例来演示。这个示例把PicoRV32和一些基础外设打包成了一个可运行的SoC,里面包含UART、SPI、GPIO等,甚至有专门的firmware数组,用来保存编译好的程序。整体流程分五步:
第一步,克隆仓库、安装RISC-V工具链。工具链命令通常是riscv64-unknown-elf-gcc或riscv64-none-elf-gcc,Ubuntu上可以直接用apt装,也可以去SiFive官网下载预编译版本。
第二步,编写一个最简单的C程序,比如往某个地址写一个字符:
int main(void) { volatile unsigned int *uart = (unsigned int *)0x10000000; *uart = 'H'; while (1) {} return 0; }第三步,用工具链交叉编译并生成Verilog内存初始化文件:
riscv64-unknown-elf-gcc -march=rv32imc -mabi=ilp32 -nostdlib -Ttext=0 -o hello.elf hello.c riscv64-unknown-elf-objcopy -O verilog hello.elf firmware.hex这里用objcopy -O verilog生成的文件,是带地址标注的Verilog内存加载格式,可以直接嵌入到RTL里作为BRAM初始化数据。第四步,把生成的hex数据替换到simple_soc的firmware数组里,然后综合整个工程。第五步,烧录到FPGA,用串口工具观察输出或者看LED变化。
整个流程看起来简单,但每个环节背后都有值得注意的细节,我放到第六部分一起讲。
2.4 使用中的几个关键注意点
PicoRV32用起来很顺,但它不是万能的,有几个坑提前说清楚。
首先,它没有Cache,没有MMU,设计定位就是裸机和轻量RTOS场景,想跑完整版Linux基本别指望。如果你要做带操作系统的大型系统,请绕道Rocket或BOOM这类更重量级的项目。
其次,它的中断控制器是自定义的CSR风格,和标准RISC-V的PLIC/CLINT不一样。写中断处理程序的时候,要按照PicoRV32自带的寄存器规范来写,不能直接套用标准RISC-V的驱动代码。这不算缺陷,但对习惯了标准外设的人需要一个适应过程。
第三,程序大小受FPGA片上BRAM限制。如果目标工程要跑比较大的程序,建议考虑使用AXI4接口的picorv32_axi变体,外接一片SRAM或DDR,内存空间就不再是瓶颈了。
3. 本期新增项目二:NEORV32,从CPU到完整SoC的跨越
3.1 VHDL写成的完整系统模板
如果说PicoRV32是把“处理器最小化”做到了极致,那NEORV32就是走了完全相反的路线——它不做裸核,而是给你一整套可配置的片上系统。这个项目由Stephan Nolting维护,地址是github.com/stnolting/neorv32。必须提前说明的一点是,虽然这个系列标题一直挂着Verilog相关,但NEORV32本体是用VHDL写的。这不是什么缺陷,FPGA工程里Verilog和VHDL混用太常见了,很多工具链都支持混合语言编译。我反而觉得这是一个接触VHDL代码风格的好机会,毕竟读懂别人用另一种HDL写的模块,也是FPGA工程师的基本功。
NEORV32对标的对象,其实是MicroBlaze和NIOS II这类“软核处理器+外设总线+外围IP”的完整模式。它默认就是一套SoC:CPU核心在中间,周围挂着一圈外设,通过内部总线连接。你可以通过修改顶层参数,按需裁剪外设和功能模块,塑造出适合自己应用的处理器系统。它的设计和文档都非常规范,打开源码能明显感觉到作者有很强的工程洁癖,注释清楚、命名规整,作为学习材料质量很高。
3.2 丰富到可以直接当MCU用的外设矩阵
NEORV32最吸引人的地方,就是它的外设阵容。常规的UART、SPI、TWI(兼容I2C)、GPIO、PWM、定时器、看门狗、RTC全都有,还额外提供了一些别家软核不常带的东西,比如真随机数发生器TRNG、硬件CRC引擎、支持WS2812类灯带的NeoLED控制器、1-Wire接口、七段数码管驱动,甚至还有一个叫CFU的自定义功能单元,允许你往CPU里插入自定义指令逻辑。
我整理了一个常用外设表,方便快速查阅:
| 外设模块 | 功能 | 适用场景 |
|---|---|---|
| UART0/UART1 | 串口收发 | 调试输出、外接串口设备 |
| SPI | 主机模式串行外设接口 | 连接Flash、ADC、屏幕 |
| TWI | I2C兼容两线接口 | 连接传感器、EEPROM |
| PWM | 脉宽调制输出 | 电机控制、调光调声 |
| Timer/Counter | 定时与计数 | 周期任务、信号测量 |
| WDT | 看门狗 | 系统异常恢复 |
| RTC | 实时时钟 | 时间戳、定时唤醒 |
| TRNG | 真随机数发生器 | 加密、签名、防预测 |
| CRC | 循环冗余校验引擎 | 通信校验、存储校验 |
| CFU | 自定义功能单元 | 自定义指令扩展加速 |
实际体验下来,这外设列表已经非常接近一颗低端MCU的规格了。也就是说,你可以在FPGA里搭出一颗属于自己的“单片机”,然后像写STM32程序一样去操作这些外设寄存器。
3.3 内置Bootloader与软硬件协同
NEORV32还有一个我非常喜欢的特性:内置Bootloader。FPGA上电后,核心先从片上ROM执行Bootloader程序,然后通过UART等待用户程序的下载。你在电脑上把编译好的固件用仓库自带的串口下载工具发过去,Bootloader接收完,把它写入RAM,然后跳转执行。
这个流程听起来简单,但用过其他软核的人会明白这有多香。PicoRV32这类裸核,每次改程序都要重新综合FPGA工程,把新的初始化数据烧进配置比特流,烧录一次少则几分钟多则十几分钟。而NEORV32把程序下载变成了“往串口发一个文件”的操作,迭代周期缩短到几秒钟。对于一些需要频繁调试软件开发逻辑的项目,这种体验上的提升是决定性的。
3.4 系统裁剪与移植要点
NEORV32的灵活性体现在可配置性上。顶层模块提供了一堆参数,你可以控制CPU核心是否支持压缩指令、是否开启乘除法扩展,也可以独立开关每一个外设模块。最小系统可以裁到两千个LUT左右,而完整外设全开也就几千到上万LUT量级,在不同规模的FPGA上都能找到合适的位置。
移植到自己的开发板时,有几件必须做的事:核对系统时钟频率,NEORV32默认的设计频率通常在100MHz以下,要确认PLL配置;检查复位信号极性,FPGA厂商的复位IP有时默认是高有效,和NEORV32的默认低有效不一致;最后就是引脚约束,尤其是UART的TX/RX引脚,必须确认连接到了板子上的USB转串口芯片。做完这几步,Bootloader启动后就能在串口终端看到提示信息了。
4. 两个项目的横向对比与选型建议
4.1 核心参数对比表
为了让选型更直观,我直接把两个项目放到同一张表里对比:
| 维度 | PicoRV32 | NEORV32 |
|---|---|---|
| 实现语言 | Verilog | VHDL |
| 定位 | 极简CPU核心 | 完整SoC平台 |
| 指令集 | RV32I/IC/IM/IMC,可选RV32E | RV32I/E,支持M/A/C/Zicsr/Zifencei及部分Zb*扩展 |
| 外设 | 默认无,需自行添加 | GPIO/UART/SPI/TWI/PWM/Timer/WDT/RTC/TRNG/CRC等 |
| Bootloader | 无 | 内置,支持UART下载程序 |
| 典型资源占用 | 约750~2000 LUT | 约2000~5000 LUT(按配置浮动) |
| 学习成本 | 低,单文件即可看清原理 | 中高,设计层次较完整 |
| 适合场景 | 集成进自定义SoC、教学演示 | 独立嵌入式系统、快速原型开发 |
4.2 什么场景选谁
我的建议很简单:如果只是想在现有工程里加一个“能跑C程序的控制核心”,PicoRV32会是不错的选择。它的单体文件模式让你几乎不用改动现有工程结构,随便抓一块FPGA都能塞进去。如果目标是搭一个完整的嵌入式系统,希望像用MCU一样操作各种外设、快速迭代软件,那么NEORV32会更合适。尤其是当项目需要UART下载、定时器、PWM、I2C这些外设时,NEORV32让你不需要从零开始写外设控制器。
4.3 其他值得关注的RISC-V开源项目
顺便也列一下这个领域里其他几个绕不开的项目,方便后续自己按图索骥:Rocket Core是伯克利大学出品的经典RISC-V实现,用Chisel编写,适合做学术研究和中等复杂度SoC;BOOM是乱序多发射的高性能核,也出自伯克利体系,适合研究超标量架构;VexRiscv基于SpinalHDL,有优秀的流水线设计,社区非常活跃;SERV是一个位串行的极简核,资源占用低到离谱,适合极端资源受限场景;Ibex是lowRISC维护的低功耗核,代码质量很高,常被用在安全IoT领域。每个项目都有自己独特的定位,按需选取就好。
5. 实操记录:在FPGA开发板上从零跑通RISC-V软核
5.1 软硬件准备工作
建议准备一块逻辑资源不低于5K LUT的FPGA开发板,我用的是Xilinx Artix-7系列的板子,Altera Cyclone系列也可以,原理完全一样。工具准备分两条线:软件工具链用RISC-V交叉编译器,Windows下建议直接使用SiFive预编译的Windows工具链,Linux下可以apt安装gcc-riscv64-unknown-elf,然后确认版本支持-march=rv32imc参数;FPGA开发环境就用你熟悉的Vivado或Quartus,仿真阶段可以用Icarus Verilog加GTKWave,非常轻量。
5.2 用PicoRV32构建最小可运行系统
先实际操作一下PicoRV32的最小跑通流程。克隆仓库后,写一个最简单的C程序,这次为了能在板子上直接看到效果,控制GPIO让LED闪烁:
#define REG_GPIO 0x10000000 void delay(volatile int n) { while (n--) {} } int main(void) { volatile unsigned int *gpio = (unsigned int *)REG_GPIO; while (1) { *gpio = 0x01; delay(100000); *gpio = 0x00; delay(100000); } return 0; }编译时使用-march=rv32imc对齐当前内核配置,如果没有使能乘除法扩展,就直接用rv32i,以免出现非法指令:
riscv64-unknown-elf-gcc -march=rv32imc -mabi=ilp32 -nostdlib -Ttext=0 -o led.elf led.c riscv64-unknown-elf-objcopy -O verilog led.elf led.hex生成的led.hex里会是类似@00000000开头、紧接着十六进制指令数据的文本。把它替换到examples/simple_soc.v文件的固件数组里,或者在测试平台中通过$readmemh加载:
initial begin $readmemh("led.hex", ram); end然后综合、布局布线、烧录。如果你使用的是通用开发板,需要检查simple_soc里的GPIO引脚有没有映射到实际LED引脚,没有的话要在约束文件里重新分配。这一步踩坑最多,具体细节我放在问题章节里讲。
5.3 用NEORV32体验Bootloader的秒级下载
换上NEORV32之后,流程要舒适很多。克隆仓库后,先编译Bootloader和应用程序,仓库顶层Makefile已经封装好了常见操作,执行后会生成两个镜像文件,一个是Bootloader镜像,用于固化到FPGA ROM;另一个是应用镜像,稍后通过串口下载。
RTL端需要做的是把整个rtl目录里的源码添加到FPGA工程,设置顶层为neorv32_top,根据板子实际情况配置时钟引脚和复位引脚,并加上UART的引脚约束。NEORV32的顶层参数非常多,但Default配置对应的是一个可用度很高的通用系统,可以直接用于First Run。第一次综合成功后,把Bootloader固化到FPGA,打开串口终端,波特率默认通常是115200,上电复位后就能看到Bootloader的版本提示。再执行对应的下载命令,把应用镜像发送过去,内存加载完成后,系统就跳转到你的程序里运行了。
整个流程比PicoRV32的反复综合舒服太多,我用NEORV32做原型的时候,经常是启动板子、改代码、编译、串口下载、看效果,整个过程三分钟以内搞定。
5.4 仿真验证的两个小技巧
上板之前,强烈建议先做仿真。PicoRV32仓库自带测试平台,用Icarus Verilog可以直接跑:
make test这个测试会运行经典的Dhrystone基准等程序,并能用GTKWave打开波形查看CPU内部信号。如果你想观察自己写的程序执行情况,也可以写一个简单的顶层测试平台,使用$readmemh加载自己的hex文件,然后观察PC指针、指令信号、寄存器写信号等关键波形。我习惯先仿真看到串口有输出或者GPIO翻转波形正常,再动手综合上板,这样可以把逻辑问题隔离在硬件环境之外。
6. 常见问题与排查技巧实录
6.1 工具链与编译阶段的坑
RISC-V工具链的坑主要集中在版本和架构参数上。最常遇到的问题是GCC默认生成64位指令,结果软核不支持,程序一执行就卡死。解决办法是编译时显式指定-march=rv32imc -mabi=ilp32,并且用riscv64-unknown-elf-objdump反汇编验证生成的是32位指令。
另一个问题是工具链版本太老,不支持部分新扩展。PicoRV32还好,NEORV32对新指令扩展的支持可能要求GCC版本较新。如果apt源里的版本不够新,建议下载SiFive的预编译工具链,或者手动编译riscv-gnu-toolchain,后者虽然耗时,但最稳妥。
6.2 内存初始化与综合阶段的坑
使用objcopy -O verilog生成的hex文件,地址默认从程序的链接地址开始。如果链接时用了-Ttext=0,文件就是@00000000开头,可以直接用来初始化起始地址为0的BRAM。如果程序链接到了其他地址,比如0x80000000,那么BRAM初始化数据需要在对应地址处填充,处理方法是调整链接脚本或者使用自定义的存储控制器,让CPU取指地址与BRAM地址映射一致。
综合阶段经常遇到的问题是综合工具把BRAM推断成大量分布式RAM,浪费LUT资源。正常情况下$readmemh加载的大块数组会被识别为BRAM,但如果数组被复位逻辑额外引用,就可能被推断成LUTRAM。解决办法是去掉对RAM的无谓复位,确保数据写入只通过地址和数据端口完成。
6.3 上板运行问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 程序完全不执行 | 复位信号极性不对、时钟没起振 | 核对复位引脚和时钟约束,用ILA抓内部复位和时钟 |
| 串口无输出 | 波特率配置错误、UART引脚约束错 | 检查软件配置的波特率与Bootloader是否一致,核对引脚位置 |
| LED不亮但程序在跑 | GPIO引脚映射错误或输出寄存器地址不符 | 反查simple_soc地址映射,确认物理引脚约束 |
| 综合资源超限 | 外设模块开太多 | 关闭不需要的外设参数,尤其是个别大型模块 |
| 程序跳飞或者死循环 | 内存初始化数据地址偏移 | 对比hex文件地址与RAM起始地址 |
6.4 独家避坑技巧
最后分享两个我从实操中总结的细节。第一个和仿真有关:做PicoRV32仿真时,如果程序没有正常结束,testbench会一直空转,看起来像死机。这时候不要怀疑CPU坏了,先看PC指针是不是在循环执行,很多情况下是C程序的启动代码没有正确初始化栈指针导致的。解决方法是在链接脚本里设置好_sp符号,或者在C启动文件里手动初始化sp寄存器。第二个和NEORV32的下载有关:Windows平台用串口工具下载时,要小心驱动自动把CTS/RTS流控打开,NEORV32的Bootloader默认不使用硬件流控,一旦开了流控,下载会一直卡在等待阶段,把串口工具的流控关掉就能解决。
这个领域后续还有很多可以继续深入的内容,比如给PicoRV32添加自定义指令、把NEORV32接到自定义硬件加速器上、或者把两个软核跑起来后移植一个RTOS。就我个人体会来说,开源软核最大的价值不只是省授权费,而是它被设计成可以被你读懂、改坏再修好。选型的时候先想清楚自己要的到底是什么:PicoRV32让你快速拥有一个可掌控的内核,NEORV32让你快速拥有一个完整的系统。先把仿真跑起来,再动手上板,把每一个环节拆开验证,这条路走通之后,你再看其他RISC-V项目,都会觉得通透很多。