darkriscv:基于 Verilog 的开源 RISC-V 软核项目

darkriscv不仅仅是一个riscv-cpu-core,而是一个包含了总线桥片(Bridge)和内存(Memory+Flash)及外设(UART)的完整的SoC,还包含了可以启动并进行交互式操作的软件系统,这正是ChipCamp所喜欢的全套芯片系统。

分支1Tags0

DarkRISCV

构建状态

从零开始在一夜之间实现的开源RISC-V——详见2018年8月19日的初始提交。

该项目持续更新,最新的官方提交ID为61bd172 @2025.7.16。

Chipcamp的复刻项目率先更新,提交ID为9cb5d43 @2025.12.12。

darkriscv

Chipcamp博客自20251216起!

  1. git clone https://gitcode.com/ChipCamp/darkriscv.git

    以下将darkriscv称为DARKHOME。

    注意修改{DARKHOME/rtl/config.vh}以开启__INTERACTIVE__。

    注意修改{DARKHOME/sim/cosim.mk}以添加链接库-lstb。

  2. 在DARKHOME目录下:执行make!

    使用iVerilog将.v文件编译为IR文件{DARKHOME/sim/darksocv} 使用iverilog VPP以交互模式运行/仿真程序{DARKHOME/sim/darksocv}(太酷了!)

    感谢{DARKHOME/src/darksocv.mem}已存在,因此无需枯燥地使用riscv-tool-chain。

  3. 在DARKHOME/sim目录下:执行make!

    与上述操作相同!

    感谢{DARKHOME/src/darksocv.mem}已存在,因此无需枯燥地使用riscv-tool-chain。

  4. 在DARKHOME/sim目录下:执行make APPLICATION=spidemo

    使用Verilator将.v文件编译为Cpp文件{HOME/sim/tmp_gui/.cpp} 使用gcc将{HOME/sim/tmp_gui/.cpp}编译为ELF文件{HOME/sim/tmp_gui/Vdarksocv}。 执行ELF文件{HOME/sim/tmp_gui/Varksocv}以SPI GUI模式加载程序(太酷了)!!

    感谢{HOME/src/darksocv.mem}已存在,因此无需枯燥地使用riscv-tool-chain。

  5. 在DARKHOME/rtl目录下:

    启动yosys read_verilog darksocv.v darkbridge.v darkuart.v darkriscv.v darkpll.v darkram.v darkio.v darkcache.v darkmac.v hierarchy -check -top darksocv proc show darksocv

    这将生成{$HOME/.yosys_show.dot}文件,并使用dot显示.v文件的原理图!!

快速开始!

如果您已安装 Icarus Verilog,只需克隆代码并输入 make 即可!

git clone git@github.com:darklife/darkriscv.git cd darkriscv make

这将运行带有默认固件的 DarkRISCV,它会打印出许多来自内核本身的有趣消息,转储一些流水线信息并生成一个 VCD 文件!

可以使用 GTKWave 查看 VCD 文件:

gtkwave sim/darksocv.vcd

这样,您就可以添加每个模块的信号并探索波形了!😃

目录

简介

DarkRISCV 软核诞生于 2018 年 8 月 19 日凌晨 2 点至 8 点的一个奇妙夜晚,最初是作为开源 RISC-V 指令集的概念验证。

尽管与其他 RISC-V 实现相比,其代码规模较小且略显粗糙,但 DarkRISCV 拥有许多令人印象深刻的特性:

  • 实现了 UCB RISC-V RV32E 和 RV32I 用户空间指令集
  • 在 Ultrascale ku040 上可工作至 250MHz(超频可达 400MHz!)
  • 在廉价的 Spartan-6 上可达 100MHz,可适配小型 Spartan-3E 如 XC3S100E!
  • 大多数情况下可维持每指令 1 个时钟周期(通常为 70% 的时间)
  • 灵活的哈佛架构(易于集成缓存控制器、总线桥等)
  • 类 DSP 流水线:流水线级之间无互锁、停顿或转发!
  • 在真实的 Xilinx FPGA(Spartan-3、Spartan-6、Spartan-7、Artix-7、Kintex-7 和 Kintex Ultrascale)上运行良好
  • 在一些真实的 Altera 和 Lattice FPGA 上也能良好运行!
  • 可与 RISC-V 的 gcc 9.0 及以上版本(如需大端模式支持则需 gcc 12.0)配合使用
  • 占用 850-1500 个 LUT(仅内核,采用 LUT6 技术,具体数量取决于启用的特性和优化)
  • 处理密度:233 DMIPS/1kLUT(UK040@400MHz,IPC ~ 0.7)
  • 处理效率:0.6 IPC/1kLUT(1200LUT,IPC~0.7)
  • 用于中断和调试的可选 CSR
  • 可选的 RV32E 支持(更小更快,在 LUT4 FPGA 上表现更佳)
  • 可选的 16x16 位 MAC 指令(用于数字信号处理)
  • 可选的 DBNZ(非零递减跳转),带有延迟槽!
  • 可选的粗粒度多线程(MT)
  • 可选的机器级中断处理
  • 可选的 supervisor 级断点处理
  • 可选的指令和数据缓存(DarkBridge)
  • 可选的哈佛架构到冯·诺依曼架构桥接器(DarkBridge)
  • 可选的 SDRAM 控制器(来自 kianRiscV 项目)
  • 可选的大端模式支持(在构建时定义)
  • 可选的 M 扩展(仅单周期乘法,无除法,使用 -mno-div)
  • BSD 许可证:可在任何地方无限制使用!(=

未来计划添加、正在开发或已由部分客户测试的一些额外特性:

  • 以太网控制器(GbE)
  • 多处理(SMP)
  • 片上网络(NoC)
  • RV64I 支持(并不像看起来那么容易……)
  • 动态总线宽度和大端模式支持
  • 用户/超级用户模式
  • 非对齐内存访问
  • 8/16/32 位总线桥接

以及更多其他特性!

下图展示了 DarkRISCV 内核的框图:

darkriscv core

图中添加了缓存只是为了便于理解,但它们通常是外部的,位于 DarkSoCV 或 DarkBridge 中。不难看出,指令路径经过了大量优化,实际上它有 3 个阶段:PF(预取)、IF(指令取指)和 ID(指令解码)。在 EX(执行)阶段,只有一个阶段,这解释了为什么 DarkRISCV 不需要转发且在执行时不会停顿。此外,与 PF/IF/ID 不同,EX 阶段有四个 ALU:一个完整的 ALU 用于寄存器/寄存器和寄存器/立即数操作,一个专用 ALU 用于分支测试,一个专用 ALU 用于 PC 更新,一个专用 ALU 用于内存地址计算,所有这些都并行工作。最后是寄存器组,它在写入时是时钟控制的单路径,但在读取时是组合逻辑且多路径,因此可以无需转发或停顿地向 ALU 提供数据。

当然,DarkRISCV 需要外部模块才能工作,因此下图展示了混合哈佛和冯·诺依曼模式下的 DarkSoCV,此时内核围绕哈佛架构的指令和数据并行缓存工作,而 SoC 的其余部分则围绕冯·诺依曼架构工作,指令和数据在同一总线上顺序传输,因此可以共享主存储器(BRAM 和 SDRAM):

darkriscv SoC

得益于 BSD 许可证,该项目完全开放,因此欢迎提出建议,祝您开发愉快!o/

历史

数年前,最初的构想源于我早期设计的其他16位RISC处理器,例如uDarkRISCV(https://github.com/darklife/udarkrisc)。它采用简化的两级流水线结构:第一时钟周期从指令存储器中取指,第二时钟周期对指令进行解码和执行。流水线采用无互锁的重叠方式,使得*DarkRISCV*在大多数情况下能够达到每时钟周期执行一条指令的性能。不过,当发生分支跳转时,流水线刷新会损失一个时钟周期。当然,为了在单个时钟周期内完成块RAM的读操作,需要采用带组合逻辑存储器的单相时钟,或者带块RAM存储器的两相时钟,以确保在这些情况下无需等待状态。

因此,早期代码非常精简,仅用约三百行经过混淆处理但结构优美的Verilog代码编写而成。经过无数个充满激情的不眠之夜以及众多同事的鼎力相助,DarkRISCV的质量达到了相当高的水平,能够完美运行标准GCC为RV32I编译的代码。

经过两年的开发,我们又实现了一个采用单相时钟的三级流水线版本,这使得解码和执行阶段的任务分配更加均衡。在该版本中,指令在第一个时钟周期从块RAM中取出,第二个时钟周期进行解码,第三个时钟周期执行。

由于加载指令无法在单个时钟周期内从块RAM中加载数据,外部逻辑会在IO操作中插入一个额外的时钟周期。此外,在发生分支跳转时,流水线刷新也需要额外两个时钟周期。流水线刷新带来的影响取决于编译器的优化程度,但根据最新的测量结果,三级流水线版本的每时钟周期指令数(IPC)约为0.7,低于两级流水线版本测得的0.8。

尽管如此,凭借三级流水线以及其他一些代价不菲的优化,DarkRISCV在低成本的Spartan-6 FPGA上可以达到100MHz的工作频率,相比两级流水线版本(通常为50MHz)能提供更高的性能。

为了庆祝该项目诞生六周年,我们投入了一些精力对SoC进行了更好的组织,将其拆分为独立的模块,并引入了新的总线概念以支持更大规模的系统。因此,DarkRISCV不仅能继续很好地支持小型、高性能的类DSP哈佛架构系统,还能支持大型、类计算机的冯·诺依曼架构系统。

项目背景

开发 DarkRISCV 的主要动机是为 680x0/Coldfire 系列相关项目提供一条迁移路径。

尽管市面上有许多 680x0 内核,但它们都是围绕不同的概念和需求设计的。就我个人需求而言(超过 50MIPS 性能,同时占用约 1000 个 LUT),可选方案并不多。目前来看,最佳选择是 TG68 内核,即使移除了 MUL/DIV 指令,它也至少需要 2400 个 LUT,并且在 Spartan-6 芯片上最高工作频率为 40MHz。此外,TG68 内核每条指令至少需要 2 个时钟周期,这意味着其峰值性能为 20MIPS。考虑到 680x0 指令本身较为复杂,这样的结果其实已经相当不错,目前它可能是替代 68000 的最佳开源选择。

然而,它在空间占用和性能方面仍不符合我的要求。在调研过程中,我也测试了其他内核,但没有发现比 TG68 更好的选择,于是我甚至开始设计一个“RISC 化”的 68000 内核,试图找到解决方案。

遗憾的是,由于编译器的要求(标准 GCC),我发现很难在减少空间占用的同时提高性能,因此开始研究非 680x0 架构的内核。

在对不同内核进行了大量测试后,我发现了 picorv32 内核以及 RISC-V 周围的整个生态系统。picorv32 是一个非常出色的项目,在低成本的 Spartan-6 芯片上可以达到 150MHz 的峰值频率。尽管大多数指令需要 3 或 4 个时钟周期,但 picorv32 在某些方面类似于 68020,却能以 150MHz 运行并提供 50MIPS 的峰值性能,这令人印象深刻。

虽然 picorv32 是直接替代 680x0 系列的绝佳选择,但它的性能还不足以替代某些 Coldfire 处理器(如 80MHz 下超过 70MIPS)。

由于我在用于类 DSP 应用的实验性 16 位 RISC 内核方面有一些经验,我开始编写 DarkRISCV,只是为了评估其复杂程度并与我那个“RISC 化”的 68000 进行比较。令我惊讶的是,在第一个晚上,我就映射了 rv32i 规范中的几乎所有指令,DarkRISCV 开始能在 75MHz 下正确执行首批指令,并且每条指令仅需一个时钟周期。这不仅类似于一款快速出色的 68040,甚至可以超越某些 Coldfire 处理器!哇!😃

在第一个晚上的工作取得成功后,我便开始着手修复硬件和软件实现中的一些细节问题。

目录说明

尽管 DarkRISCV 只是一个小型处理器内核,但为了测试该内核,仍需要一个小型生态系统,包括 RISCV 兼容软件、仿真支持以及外设支持,以确保处理器内核能够产生可观测的结果。每个元素都与同类元素一起存储在目录中,顶层目录组织如下:

  • README.md:顶层 README 文件(指向本文档)
  • LICENSE:无限自由!o/
  • Makefile:一切从这里开始!
  • src:测试固件的源代码(C 语言编写的 boot.c、main.c 等)
  • rtl:DarkRISCV 内核及支持逻辑的源代码(Verilog)
  • sim:用于测试 RTL 文件的仿真源代码(目前通过 Icarus)
  • boards:不同开发板的支持和示例(目前通过 Xilinx ISE)
  • tmp:空目录,但 ISE 会在此创建大量文件)

设置说明:

步骤 1:使用以下代码将 DarkRISC 仓库克隆到本地。 git clone https://github.com/darklife/darkriscv.git

MacOS 预设置指南:

本文档包含在 MacOS 上成功使用 Darkriscv 生态系统所需的所有依赖项以及安装这些依赖项的步骤。

实际上,由于依赖项之一 Xilinx ISE 14.7 Design Suite 目前不支持 MacOS,因此该生态系统无法在 MacOS 中直接使用。

为了克服此问题,我们需要通过以下两种方法在 MacOS 上安装 Linux/Windows:

a) WineSkin,这是一种 Windows 模拟器,它可以原生运行 Windows 应用程序,但会拦截并模拟 Windows 调用,直接映射到 macOS 中。

b) VirtualBox(或 VMware、Parallels 等),用于运行完整的 Windows 操作系统或 Linux,这似乎比 WineSkin 选项好得多。

我使用了第二种方法,安装了 VMware Fusion 来运行 Linux Mint。以下是我用于获取下载文件的链接。

依赖项:

  1. Icarus Verilog a. Bison b. GNU c. G++ d. FLEX

  2. Xilinx 14.7 ISE

Icarus Verilog 设置:

以下步骤针对 Linux 操作系统进行了精简。所有其他操作系统平台的完整步骤可在 https://iverilog.fandom.com/wiki/Installation_Guide 上找到。

步骤 1:从 ftp://ftp.icarus.com/pub/eda/verilog/ 下载 Verilog 下载 tar 文件。始终安装最新版本。目前最新版本是 Verilog-10.3。

步骤 2:使用“% tar -zxvf verilog-version.tar.gz”解压 tar 文件。

步骤 3:使用“cd Verilog-version”进入 Verilog 文件夹。这里是 cd Verilog-10.3。

步骤 4:检查是否已安装以下库:Flex、Bison、g++ 和 gcc。如果没有,请在终端中使用“sudo apt-get install flex bison g++ gcc”进行安装。重启系统以确保更改生效。

步骤 5:在 Verilog-10.3 目录中运行以下命令

  1. ./configure
  2. make
  3. sudo make install

步骤 6:使用“sudo apt-get install verilog”安装 Verilog。

可选步骤:sudo apt-get install gtkwave

Xilinx 设置:

请观看以下 YouTube 视频以获取完整安装步骤。

https://www.youtube.com/watch?v=meO-b6Ib17Y

注意:确保在 Linux 中已安装 libncurses 库。

如果没有,请使用以下代码:

  1. 对于 64 位架构 a. sudo apt-get install libncurses5 libncursesw-dev
  2. 对于 32 位架构 a. sudo apt-get install libncurses5:i386

安装完所有先决条件后,进入根目录并运行以下代码:

cd darkriscv make(必要时使用 sudo)

顶层 Makefile 负责构建所有内容,但必须先对其进行编辑,用户至少必须选择编译器路径和目标开发板。

默认情况下,顶层 Makefile 使用:

CROSS = riscv32-embedded-elf CCPATH = /usr/local/share/gcc-$(CROSS)/bin/ ICARUS = /usr/local/bin/iverilog BOARD = avnet_microboard_lx9

只需根据您的系统配置更新配置,输入 make,并希望所有内容都在正确的位置!您可能需要修复一些路径并在 PATH 环境变量中设置其他路径,但最终它会正常工作。

当一切配置正确后,结果将如下所示:

# make
make -C src all             CROSS=riscv32-embedded-elf CCPATH=/usr/local/share/gcc-riscv32-embedded-elf/bin/ ARCH=rv32e HARVARD=1
make[1]: Entering directory `/home/marcelo/Documents/Verilog/darkriscv/v38/src'
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-gcc -Wall -I./include -Os -march=rv32e -mabi=ilp32e -D__RISCV__ -DBUILD="\"Sat, 30 May 2020 00:55:20 -0300\"" -DARCH="\"rv32e\"" -S boot.c -o boot.s
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-as -march=rv32e -c boot.s -o boot.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-gcc -Wall -I./include -Os -march=rv32e -mabi=ilp32e -D__RISCV__ -DBUILD="\"Sat, 30 May 2020 00:55:20 -0300\"" -DARCH="\"rv32e\"" -S stdio.c -o stdio.s
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-as -march=rv32e -c stdio.s -o stdio.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-gcc -Wall -I./include -Os -march=rv32e -mabi=ilp32e -D__RISCV__ -DBUILD="\"Sat, 30 May 2020 00:55:21 -0300\"" -DARCH="\"rv32e\"" -S main.c -o main.s
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-as -march=rv32e -c main.s -o main.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-gcc -Wall -I./include -Os -march=rv32e -mabi=ilp32e -D__RISCV__ -DBUILD="\"Sat, 30 May 2020 00:55:21 -0300\"" -DARCH="\"rv32e\"" -S io.c -o io.s
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-as -march=rv32e -c io.s -o io.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-gcc -Wall -I./include -Os -march=rv32e -mabi=ilp32e -D__RISCV__ -DBUILD="\"Sat, 30 May 2020 00:55:21 -0300\"" -DARCH="\"rv32e\"" -S banner.c -o banner.s
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-as -march=rv32e -c banner.s -o banner.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-cpp -P  -DHARVARD=1 darksocv.ld.src darksocv.ld
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-ld -Tdarksocv.ld -Map=darksocv.map -m elf32lriscv  boot.o stdio.o main.o io.o banner.o -o darksocv.o
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-ld: warning: section `.data' type changed to PROGBITS
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-objdump -d darksocv.o > darksocv.lst
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-objcopy -O binary  darksocv.o darksocv.text --only-section .text* 
hexdump -ve '1/4 "%08x\n"' darksocv.text > darksocv.rom.mem
#xxd -p -c 4 -g 4 darksocv.o > darksocv.rom.mem
rm darksocv.text
wc -l darksocv.rom.mem
1016 darksocv.rom.mem
echo rom ok.
rom ok.
/usr/local/share/gcc-riscv32-embedded-elf/bin//riscv32-embedded-elf-objcopy -O binary  darksocv.o darksocv.data --only-section .*data*
hexdump -ve '1/4 "%08x\n"' darksocv.data > darksocv.ram.mem
#xxd -p -c 4 -g 4 darksocv.o > darksocv.ram.mem
rm darksocv.data
wc -l darksocv.ram.mem
317 darksocv.ram.mem
echo ram ok.
ram ok.
echo sources ok.
sources ok.
make[1]: Leaving directory `/home/marcelo/Documents/Verilog/darkriscv/v38/src'
make -C sim all             ICARUS=/usr/local/bin/iverilog HARVARD=1
make[1]: Entering directory `/home/marcelo/Documents/Verilog/darkriscv/v38/sim'
/usr/local/bin/iverilog -I ../rtl -o darksocv darksimv.v ../rtl/darksocv.v ../rtl/darkuart.v ../rtl/darkriscv.v
./darksocv
WARNING: ../rtl/darksocv.v:280: $readmemh(../src/darksocv.rom.mem): Not enough words in the file for the requested range [0:1023].
WARNING: ../rtl/darksocv.v:281: $readmemh(../src/darksocv.ram.mem): Not enough words in the file for the requested range [0:1023].
VCD info: dumpfile darksocv.vcd opened for output.
reset (startup)

              vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
                  vvvvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvv  
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvv    
rr                vvvvvvvvvvvvvvvvvvvvvv      
rr            vvvvvvvvvvvvvvvvvvvvvvvv      rr
rrrr      vvvvvvvvvvvvvvvvvvvvvvvvvv      rrrr
rrrrrr      vvvvvvvvvvvvvvvvvvvvvv      rrrrrr
rrrrrrrr      vvvvvvvvvvvvvvvvvv      rrrrrrrr
rrrrrrrrrr      vvvvvvvvvvvvvv      rrrrrrrrrr
rrrrrrrrrrrr      vvvvvvvvvv      rrrrrrrrrrrr
rrrrrrrrrrrrrr      vvvvvv      rrrrrrrrrrrrrr
rrrrrrrrrrrrrrrr      vv      rrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrr          rrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrr      rrrrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrrrr  rrrrrrrrrrrrrrrrrrrrrr

       INSTRUCTION SETS WANT TO BE FREE

boot0: text@0 data@4096 stack@8192
board: simulation only (id=0)
build: darkriscv fw build Sat, 30 May 2020 00:55:21 -0300
core0: darkriscv@100.0MHz with rv32e+MT+MAC
uart0: 115200 bps (div=868)
timr0: periodic timer=1000000Hz (io.timer=99)

Welcome to DarkRISCV!
> no UART input, finishing simulation...
echo simulation ok.
simulation ok.
make[1]: Leaving directory `/home/marcelo/Documents/Verilog/darkriscv/v38/sim'
make -C boards all          BOARD=piswords_rs485_lx9 HARVARD=1
make[1]: Entering directory `/home/marcelo/Documents/Verilog/darkriscv/v38/boards'
cd ../tmp && xst -intstyle ise -ifn ../boards/piswords_rs485_lx9/darksocv.xst -ofn ../tmp/darksocv.syr
Reading design: ../boards/piswords_rs485_lx9/darksocv.prj

*** lots of weird FPGA related messages here *** 

cd ../tmp && bitgen -intstyle ise -f ../boards/avnet_microboard_lx9/darksocv.ut ../tmp/darksocv.ncd
echo done.
done.

这意味着软件已正确编译和链接,仿真运行正常,FPGA 构建也生成了可通过 make install 加载到您 FPGA 板上的镜像(前提是您拥有 FPGA 板,并且板卡目录中当然有 JTAG 支持脚本)。

如果 FPGA 已正确编程,且 UART 已连接到终端仿真器,FPGA 将配置为 DarkRISCV,它将运行测试软件并产生以下结果:

              vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
                  vvvvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvv  
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvv    
rr                vvvvvvvvvvvvvvvvvvvvvv      
rr            vvvvvvvvvvvvvvvvvvvvvvvv      rr
rrrr      vvvvvvvvvvvvvvvvvvvvvvvvvv      rrrr
rrrrrr      vvvvvvvvvvvvvvvvvvvvvv      rrrrrr
rrrrrrrr      vvvvvvvvvvvvvvvvvv      rrrrrrrr
rrrrrrrrrr      vvvvvvvvvvvvvv      rrrrrrrrrr
rrrrrrrrrrrr      vvvvvvvvvv      rrrrrrrrrrrr
rrrrrrrrrrrrrr      vvvvvv      rrrrrrrrrrrrrr
rrrrrrrrrrrrrrrr      vv      rrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrr          rrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrr      rrrrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrrrr  rrrrrrrrrrrrrrrrrrrrrr

       INSTRUCTION SETS WANT TO BE FREE

boot0: text@0 data@4096 stack@8192
board: piswords rs485 lx9 (id=6)
build: darkriscv fw build Fri, 29 May 2020 23:56:39 -0300
core0: darkriscv@100.0MHz with rv32e+MT+MAC
uart0: 115200 bps (div=868)
timr0: periodic timer=1000000Hz (io.timer=99)

Welcome to DarkRISCV!
> 

这个精美的 ASCII RISCV 标志是由 Andrew Waterman 制作的![6]

只要构建能够正常工作,就可以开始进行修改了。但我对于使用软处理器的建议是:不要同时对硬件和软件进行操作!这意味着,最好是冻结硬件,只专注于软件;或者冻结软件,只专注于硬件。你完全可以对两者都进行研究,但不要同时进行。否则,在同时修改了软件和硬件之后,你可能会发现 DarkRISCV 处于无法工作的状态,而且无法确定问题究竟出在哪里。

“src” 目录

src 目录包含测试固件的源代码,其中包括启动代码、主进程和辅助库。这些代码通过 gcc 进行编译,并生成一些辅助文件,例如:

  • boot.c:启动过程的原始 C 代码
  • boot.s:C 代码的汇编版本,由 gcc 自动生成
  • boot.o:C 代码的编译版本,由 gcc 自动生成

当所有 .o 文件都生成后,链接的结果是一个 darksocv.o ELF 文件。该文件用于生成 darksocv.bin 文件,随后 darksocv.bin 文件会被转换为十六进制格式,并分离为 ROM 和 RAM 文件(这些文件会被 Verilog 代码加载到 blockRAM 中)。链接器还会生成一个包含所有生成代码完整列表的 darksocv.lst 文件,以及一个显示所生成代码中所有函数和变量映射的 darsocv.map 文件。

固件的结构非常简单:

  • boot.c 包含启动代码
  • main.c 包含主应用程序代码(shell)
  • banner.c 包含 riscv 标语
  • stdio.c 包含一个小型的 stdio 实现
  • io.c 包含 IO 接口

通过编辑 src/Makefile,可以轻松地将额外的代码添加到编译过程中。

例如,为了添加一个 Lempel-Ziv 压缩算法代码 lz.c,需要让 Makefile 知道我们需要 lz.s 和 lz.o:

OBJS = boot.o stdio.o main.o io.o banner.o lz.o ASMS = boot.s stdio.s main.s io.s banner.s lz.s SRCS = boot.c stdio.c main.c io.c banner.c lz.c

然后在 main.c 中添加一个 “lz” 命令,以便能够通过命令提示符调用该函数。或者,你也可以完全替换提供的固件,使用你自己的固件。

“sim”目录

另一方面,仿真会显示一些波形,并且可以在运行示例代码时检查DarkRISCV的运行情况。

DarkRISCV的主要仿真工具是Xilinx ISE 14.7中的iSIM,但通过“sim”目录下的Makefile也支持Icarus仿真器(当检测到__ICARUS__符号时,与Icarus相关的更改将生效)。我还为ModelSim包含了一个变通方法,正如我们的朋友HYF所指出的(当检测到MODEL_TECH符号时,与ModelSim相关的更改将生效)。

仿真运行的固件与实际FPGA中的相同,但为了提高仿真性能,UART代码未被仿真,因为115200 bps的波特率会占用大量无效的仿真时间。

“rtl”目录

RTL目录包含DarkRISCV内核和一些辅助文件,例如DarkSoCV(一个带有ROM、RAM和IO的小型片上系统)、DarkUART(一个用于调试的小型UART)以及配置文件,在配置文件中可以启用和禁用“实现说明”部分中描述的某些功能。

有关更多详细信息,请查看rtl目录中的README.md文件。

“board”目录

当前支持的开发板如下:

  • id==0 仅仿真
  • id==1 avnet_microboard_lx9
  • id==2 xilinx_ac701_a200
  • id==3 qmtech_sdram _lx16
  • id==4 qmtech_spartan7_s15
  • id==5 lattice_brevia2_lxp2
  • id==6 piswords_rs485_lx9
  • id==7 digilent_spartan3_s200
  • id==8 aliexpress_hpc40gbe k420
  • id==9 qmtech_artix7_a35
  • id==10 aliexpress_hpc40gbe_ku040
  • id==11 papilio_duo_logicstart
  • id==12 qmtech_kintex7_k325
  • id==13 scarab_minispartan6-plus_lx9
  • id==14 colorlighti9_ecp5-45f
  • id==15 colorlighti5_ecp5-25f
  • id==16 ulx3s_ecp5-85f

目录的组织方式不言自明,目录名称中包含了供应商、开发板和FPGA型号。每个“board”目录都包含可在Xilinx ISE 14.x中打开的项目文件,以及用于构建该开发板型号的FPGA镜像的Makefile。尽管提供了“ucf”文件以生成包含UART和一些LED的完整构建,但FPGA并未按任何特定配置完全接线,您必须添加将在FPGA开发板上使用的引脚。

无论如何,尽管未完全接线,构建过程总能为您提供有关FPGA利用率和时序的良好估计(因为UART输出确保必须综合完整的处理器)。

由于支持的开发板数量众多,无法每次都测试所有开发板,这意味着有时针对一个开发板的更改可能会以错误的方式影响其他开发板。

实现说明*

[本节内容仅供参考,其描述可能与当前代码并不完全匹配]

由于我的目标是超低成本的 Xilinx Spartan-6 系列 FPGA,因此该项目目前基于适用于 Linux 的 Xilinx ISE 14.7,这是最新可用的 ISE 版本。然而,代码中并未明确引用 Xilinx 元件,所有逻辑均直接通过 Verilog 推断得出,这意味着该项目可轻松移植到其他 FPGA 系列和其他环境中,正如 Lattice XP2 支持所展示的那样。无论如何,请记住某些 Verilog 结构在某些 FPGA 中可能无法良好工作。

在最近的更新中,我加入了一种在 x86 主机上测试固件的方法,这非常有帮助,因为可以与固件进行交互并快速修复一些明显的错误。当然,x86 代码不会运行 boot.c 代码,因为在 x86 上运行 RISCV 引导代码没有意义(?)。

无论如何,在使用软核时,主要建议是永远不要同时进行硬件和软件的开发!从尽可能简单的软件配置开始,并冻结软件。当实施新的软件更新时,使用尽可能简单的硬件配置,并冻结硬件。

RV32I 规范本身令人印象深刻且易于实现(参见 [1],第 16 页)。当然,它也有一些缺点,例如有趣的小端序总线(与 680x0 系列中面向网络的大端序总线相反),但经过一些实验测试后,很容易使其正常工作。

有趣的是,在研究为 DarkRISCV 添加大端序支持后,我发现无法让 GCC 正确生成代码和数据。

该规范的另一个缺点是缺乏延迟分支。虽然我理解从概念角度看延迟分支并不理想,但它们是提升性能的有效技巧。作为参考,DarkRISCV 中缺乏延迟分支或分支预测器可能会降低 20% 到 30% 的性能,实际测量的性能可能在每指令 1.25 到 1.66 个时钟周期之间。

尽管分支预测的实现并不复杂,但我发现实验性的多线程支持更加有趣,因为它可以利用分支的空闲时间来切换处理器线程。无论如何,我将尝试调试分支预测代码以提高单线程性能。

该内核支持 2 级或 3 级流水线,尽管主要逻辑几乎相同,但它们的工作方式存在巨大差异。仅供参考,以下部分反映了内核的历史演变,可能并不反映当前的内核代码。

最初的 2 级流水线设计在 ROM 和 RAM 时序方面存在一个小问题:为了在两个时钟周期内预取并执行指令,并保持预取以每时钟周期 1 条指令的速度持续工作(执行阶段也是如此),ROM 和 RAM 必须在下一个时钟之前做出响应。这意味着存储器必须是组合逻辑的,或者至少使用两相时钟。

针对 2 级流水线版本的第一个解决方案是使用两相时钟,这也是默认解决方案,它使 DarkRISCV 作为伪 4 级流水线工作:

  • 1/2 阶段:指令预取(rom)
  • 1/2 阶段:静态指令解码(core)
  • 1/2 阶段:地址生成、寄存器读取和数据读/写(ram)
  • 1/2 阶段:数据写入(寄存器写入)

从处理器的角度来看,只有 2 个阶段;从存储器的角度来看,也有 2 个阶段。但它们处于不同的时钟相位。在正常情况下,不建议这样做,因为它会使性能降低 2 倍,但在 DarkRISCV 的情况下,性能始终受限于指令执行的组合逻辑。

2 级流水线的第二个解决方案是使用组合逻辑,以便在下一个时钟沿之前提供所需结果,从而可以使用单相时钟。该解决方案由指令缓存和数据缓存组成,当操作数存储在小型基于 LUT 的组合缓存中时,处理器可以执行内存操作而无需额外的等待状态。然而,当操作数未存储在缓存中时,会插入额外的等待状态以从块 RAM 或外部存储器中获取操作数。根据一些初步测试,具有 64 个直接映射指令的指令缓存可以达到 91% 的命中率。数据缓存性能虽然不是很好(命中率仅为 68%),但对于访问外部存储器并减少慢速 SDRAM 和 FLASH 的影响而言是必需的。

从组合时序的角度来看,使用缓存和两相时钟的解决方案都表现不佳。因此,提供了 3 级流水线版本,以便在块 RAM 上使用单相时钟。

在这种情况下,其概念是将预取和解码分离,以便预取可以完全在指令总线的块 RAM 端完成。在不同阶段进行解码可提供额外的性能,执行阶段几乎所有时间都工作在一个时钟周期内,除了执行加载指令时。在这种情况下,外部存储器逻辑会插入一个等待状态。然而,写操作在单个时钟周期内执行。

带等待状态的解决方案可以用于 2 级流水线版本,但会极大地降低性能。如果可以用相同的时钟运行所有版本,那么理论性能(以每指令时钟周期数 CPI、分支跳转时的流水线刷新时钟数 FLUSH 和存储器等待状态数 WSMEM 表示)将为:

  • 带两相时钟的 2 级流水线:CPI=1,FLUSH=1,WSMEM=0:实际 CPI≈1.25
  • 带单相时钟的 3 级流水线:CPI=1,FLUSH=2,WSMEM=1:实际 CPI≈1.66
  • 带单相时钟的 2 级流水线:CPI=2,FLUSH=1,WSMEM=1,实际 CPI≈2.00

根据经验,2 级流水线中 FLUSH 的影响约为 20%,3 级流水线中约为 30%。当然,实际影响取决于代码本身。就加载指令的内存访问中等待状态的影响而言,影响范围在 5% 到 10% 之间,同样取决于代码。

然而,3 级流水线的时钟频率远优于 2 级流水线,特别是因为逻辑在解码和执行阶段之间的分布更好。

目前,Spartan-6 中最关键的路径是内核数据端(连接到 RAM 和外设)的地址总线。问题在于必须在单个时钟周期内完成以下操作:

  • 生成 DADDR[31:0] = REG[SPTR][31:0] + EXTSIG(IMM[11:0])
  • 根据操作数大小和 DADDR[1:0] 生成 BE[3:0]

在读取操作的情况下,DATAI 路径还包括一个小型多路复用器,用于分离 RAM 和外设总线,以及分离不同的外设,这意味着随着外设数量的增加和复杂性的提高,路径会变长。

当然,最佳性能设置是使用 3 级流水线和整个逻辑中的单时钟相位(上升沿),因此 2 级流水线和双时钟相位将仅作为参考保留。

3 级流水线的唯一缺点是加载操作中多一个等待状态,以及分支跳转时更长的两个时钟周期的流水线刷新。

仅供参考,我记录了一些有关性能测量的详细信息:

当前的固件示例在 100MHz 时钟的 3 级流水线版本上运行,经验证性能为 62 MIPS。由于加载指令中的额外等待状态导致 5% 的损失,以及分支跳转后的流水线刷新导致 32% 的损失,因此未达到理论上的 100MIPS 性能。另一方面,2 级流水线版本在相同时钟下的经验证性能为 79MIPS。唯一的损失是分支跳转后的流水线刷新导致 20% 的损失。

当然,流水线刷新的影响也取决于软件,并且由于软件目前是针对代码大小进行优化的。当使用 -O2 而非 -Os 编译时,3 级流水线的性能提升至 68MIPS,损失变为加载指令占 6%,流水线刷新占 25%。-O3 选项导致 67MIPS,最佳结果是 -O1 选项,在 3 级版本中产生 70MIPS,在 2 级版本中产生 85MIPS。

因此,如果性能是一个要求,则必须修改 src/Makefile 以使用 -O1 优化代替默认的 -Os。

尽管 2 级版本比 3 级版本快 15%,但 3 级版本可以达到更高的时钟频率,从而提供更好的性能。

关于流水线刷新,在分支跳转后是必需的,因为 RISCV 不支持延迟分支。解决此问题的方案是实现分支缓存(分支预测器),以便内核用最近的分支填充缓存,并能够预测未来的分支。在一些初始测试中,具有 4 个条目的分支预测器似乎达到了 60% 的命中率。

另一种可能性是利用刷新时间来处理其他任务,例如处理中断。由于中断处理以及一般而言的线程切换都需要刷新当前流水线以更改上下文,因此将中断/线程切换与流水线刷新匹配是有一定意义的!

通过 THREADING 选项可以测试此功能。

该实现处于非常早期的开发阶段,不能正确处理初始 SP 和 PC。无论如何,它可以工作,并使 main() 代码在 gets() 处停止,同时中断处理以每秒超过 100 万次的中断速率更改 OPORT,而不会影响执行,并且对性能的影响很小! 😃

中断支持可以扩展为更完整的线程支持,但需要在硬件和软件中使用一些技巧,以便为不同的线程填充正确的 SP 和 PC。

中断处理使用围绕线程的概念,并且通过一些额外的努力,可能支持 4、8 甚至 16 个线程。在这种情况下,缺点是寄存器组的大小会增加,这解释了为什么 rv32e 是线程化的一个有趣选项:寄存器数量减半,可以在核心中存储两个以上的线程。

目前,darkriscv 中的上下文切换时间在 3 级流水线中为两个时钟周期,这与流水线刷新本身相匹配。在 100MHz 时,经验性的每秒最大上下文切换次数约为 294 万次。

关于新的 MAC 指令,它以非常初步的方式实现,操作码为 7'b0001011(custom-0 操作码)。我正在研究使用 p.mac 指令的可能性,但目前该指令是在 stdio.c(即 darkriscv libc)中可用的 mac() 函数中手动编码的。有关包含新指令并使其与 GCC 一起工作的详细信息,请参见参考文献 [5]。

初步测试表明,正如预期的那样,性能降至 90MHz,尽管可以在非零时序分数下以 100MHz 运行并达到 100MMAC/s 的峰值性能,但小型 32 位累加器饱和速度太快,需要额外的技巧来避免溢出。

乘法操作使用两个 16 位整数,结果与一个单独的 32 位寄存器相加,该寄存器用作累加器。由于操作始终是有符号的,并且符号始终使用最高位 (MSB),这意味着 15x15 乘法会产生 30 位结果,该结果将添加到 31 位值中,这意味着仅在两次 MAC 操作后就会达到溢出。

为了避免溢出,可以对输入操作数进行移位。例如,在采用 u-law 编码的 G711 情况下,有效分辨率为 14 位(13 位整数和 1 位符号),这意味着将使用 13x13 位乘法,并产生 26 位结果以添加到 31 位整数中,足以在溢出前运行 32 次 MAC 操作(在这种情况下,当 ACC 达到负值时):

awk 'BEGIN { ACC=231-1; A=213-1; B=-A; for(i=0;ACC>=0;i++) print i,A,B,AB,ACC+=AB }'

0 8191 -8191 -67092481 2080391166
1 8191 -8191 -67092481 2013298685
2 8191 -8191 -67092481 1946206204
...
30 8191 -8191 -67092481 67616736
31 8191 -8191 -67092481 524255
32 8191 -8191 -67092481 -66568226

这个理论正确吗?我不确定,但看起来不错!😃

作为补充,我在 stdio.c 中包含了对 GCC 函数的支持,这些函数涉及 32 位有符号和无符号整数的原生 *、/ 和 %(乘法、除法和取模)操作,这意味着真正的 32x32 位操作产生 32 位结果。该代码源自一个与 68000 相关的旧项目(就像 stdio.c 中的大多数代码一样),虽然不是很快,但我猜它是有效的。随着 MAC 指令在语法和功能上的更好定义,我认为可以优化乘法/除法/取模以尝试使用它并提高性能。

以下是其他 Xilinx 器件在 ISE 中针对速度等级 2 的一些额外性能结果(仅综合,3 级版本):

  • Spartan-6: 100MHz(使用 gcc -O1 测得 70MIPS)
  • Artix-7: 178MHz
  • Kintex-7: 225MHz

对于速度等级 3:

  • Spartan-6: 117MHz
  • Artix-7: 202MHz
  • Kintex-7: 266MHz

Kintex-7 在理论上使用 gcc -O1 可以达到 186MIPS。

此性能是在未激活 MAC 和 THREADING 的情况下实现的。多亏了 RV32E 选项,现在可以为 Spartan-3E 进行综合,在低成本的 100E 型号和 70MHz 时钟(仅综合和速度等级 5)的情况下,LUT 占用率为 95%:

  • Spartan-3E: 70MHz

对于 2 级版本和速度等级 2,流水线刷新的影响较小(20%),加载操作无影响,并且由于使用两相时钟,时钟受到一些影响:

  • Spartan-6: 56MHz(使用 -O1 测得 47MIPS)

关于编译器性能,从启动到提示,在 100MHz 的 3 级流水线内核且无中断的情况下测试,rom 和 ram 以 32 位字为单位测量:

  • gcc 使用 -O3: t=289us rom=876 ram=211
  • gcc 使用 -O2: t=291us rom=799 ram=211
  • gcc 使用 -O1: t=324us rom=660 ram=211
  • gcc 使用 -O0: t=569us rom=886 ram=211
  • gcc 使用 -Os: t=398us rom=555 ram=211

由于 FPGA 中的 ROM 空间有限,-Os 是默认选项。

另一方面,关于对 Vivado 的支持,可以将 Artix-7(ise/boards 目录中提供的 Xilinx AC701)项目转换为 Vivado 并进行一些有趣的测试。转换中的唯一问题是 UCF 文件未被转换,这意味着必须创建一个包含引脚描述的新 XDC 文件。

Vivado 与 ISE 相比非常慢,需要大量时间来综合并提供关于性能的最少反馈……但经过几周的等待和大量的经验计算,我得到了速度等级 2 器件的一些数字:

  • Artix7: 147MHz
  • Spartan-7: 146MHz

以及速度等级 3 器件的一个数字:

  • Kintex-7: 221MHz

尽管与 ISE 相比,Vivado 速度慢得多,并且对于相同的 FPGA 显示出悲观的数字,但我认为 Vivado 更现实,至少它支持新的 Spartan-7,后者显示出非常好的数字(几乎与 Artix-7 相同!)。

这些值仅供参考。实际值取决于内核中的一些选项,例如流水线级数、存储器的连接方式等。基本上,3 级流水线版本可达到最佳时钟频率(在 Spartan-6 中高达 100MHz),但它在加载指令中至少需要 1 个等待状态,在分支跳转时需要 2 个额外的时钟周期来刷新流水线。2 级流水线不需要额外的等待状态,分支跳转时只需 1 个额外的时钟周期,但运行性能较低(56MHz)。

嗯,经过几年的研究,我的结论是分支预测解决了很多关于性能的问题,但也带来了很多其他问题,因此最好的解决方案可能不是实现它,而是在可能的情况下尝试手动优化,例如展开循环。

测试的另一个可能增强是 DBNZ 指令,在 Z80 和 68000 上广为人知,基本上是一条循环指令,它递减计数器,测试是否为零并分支,重复循环直到计数器不为零……

在 RISC-V 的情况下,DBNZ 指令的影响非常小,基本上是替换 SUBI+BNE,但对分支上的流水线刷新这一实际问题没有影响。因此,在 DarkRISCV 中测试了一种带有延迟分支的特殊变体“DBNZD”,以便在 3 级流水线版本上可以在 DBNZ 之后运行 2 条额外的指令。

可以通过 config.vh 中的 DBNZ 启用 DBNZ 指令,memcpy32(又名 MOVE16)的典型语法如下:

memcpy32:
        srli    a2, a2, 4          # a2 = len >> 4   (number of 16-byte blocks)
        beqz    a2, .done

.loop:
        lw      t0,  0(a0)
        lw      t1,  4(a0)
        lw      t2,  8(a0)
        lw      t3, 12(a0)

        sw      t0,  0(a1)
        sw      t1,  4(a1)
        sw      t2,  8(a1)
        sw      t3, 12(a1)

        dbnz    a2, .loop          # Decrement and branch (2 delay slots follow)

        addi    a0, a0, 16         # Delay slot 1
        addi    a1, a1, 16         # Delay slot 2

.done:
        ret

开发工具

关于 gcc 编译器,我目前使用的是针对 RISC-V 的实验性 gcc 9.0.0 版本。对于 DarkRISCV,除了指定 -march=rv32i 外,不需要任何补丁或更新。尽管 fence*、e* 和 crg* 指令尚未实现,但 gcc 似乎不会使用这些指令,而且核心中也没有这些指令。

虽然可以使用 RISC-V 官方网站提供的编译器套件,但 lowRISC 项目的同事们指出了一种更巧妙的工具链构建方法:

https://www.lowrisc.org/blog/2017/09/building-upstream-risc-v-gccbinutilsnewlib-the-quick-and-dirty-way/

基本步骤如下:

git clone --depth=1 git://gcc.gnu.org/git/gcc.git gcc git clone --depth=1 git://sourceware.org/git/binutils-gdb.git git clone --depth=1 git://sourceware.org/git/newlib-cygwin.git mkdir combined cd combined ln -s ../newlib-cygwin/* . ln -sf ../binutils-gdb/* . ln -sf ../gcc/* . mkdir build cd build ../configure --target=riscv32-unknown-elf --enable-languages=c --disable-shared --disable-threads --disable-multilib --disable-gdb --disable-libssp --with-newlib --with-arch=rv32ima --with-abi=ilp32 --prefix=/usr/local/share/gcc-riscv32-unknown-elf make -j4 make make install export PATH=$PATH:/usr/local/share/gcc-riscv32-unknown-elf/bin/ riscv32-unknown-elf-gcc -v

这样一切就会神奇地工作起来!(:

如果您在构建编译器时遇到困难,或者没有兴趣修改固件,只是想了解 darkriscv 在 FPGA 上的运行情况,该项目包含了已编译的 ROM 和 RAM,您可以直接查看编译器生成的所有派生对象、源代码及相关文件,无需进行任何编译。

最后,由于 DarkRISCV 尚未经过全面测试,有时将代码执行结果与另一个稳定的参考进行比较是个好主意!

在这方面,我目前使用的是 picorv32 项目:

https://github.com/cliffordwolf/picorv32

等我有时间,我会尝试创建一个组织更完善的支持环境,以便在相同的缓存、内存和 IO 子系统中轻松测试 DarkRISCV 和 picorv32,从而可以根据所需功能选择核心,例如,追求更高性能时使用 DarkRISCV,需要更多功能时使用 picorv32。

关于软件,最复杂的问题是使内存设计与链接器布局相匹配。当然,这是 gcc 的问题,甚至不能算是一个“问题”,实际上,这正是软件人员在链接代码和数据时的工作方式!

在最简化的版本中,DarkRISCV 直接连接到 blockRAM,是一个纯哈佛架构处理器,需要将指令块和数据块分开!

当缓存控制器被激活时,它会为指令和数据提供独立的存储器,但对外提供一个更传统的冯·诺依曼内存架构接口。

在这两种情况下,一个设计合理的链接器脚本(darksocv.ld)或许能解决问题!

链接器脚本中当前的内存映射如下:

  • 0x00000000:4KB ROM
  • 0x00001000:4KB RAM

此外,链接器将 IO 映射到以下位置:

  • 0x80000000:UART 状态
  • 0x80000004:UART 发送/接收缓冲区
  • 0x80000008:LED 缓冲区

RAM 内存包含 .data 区域、.bss 区域(位于 .data 之后并初始化为零)、.rodada 以及 RAM 末端的栈区域。

尽管 RISC-V 定义为小端模式,但似乎可以在 GCC 中轻松更改配置。在这种情况下,假设所有变量都以大端格式存储。当然,这种更改需要在核心本身进行类似的修改,这并不复杂,因为它只影响加载和存储指令。将来,我会尝试测试 GCC 和 darkriscv 的大端版本,以评估在面向网络的应用中可能的性能提升!😃

最后,关于软件的最新更新包括一个新选项,可以构建 x86 版本,以便通过在 x86 上测试完全相同的固件来辅助开发。

初步来看,可以通过以下配置构建针对 RV32E 的 gcc:

git clone --depth=1 git://gcc.gnu.org/git/gcc.git gcc git clone --depth=1 git://sourceware.org/git/binutils-gdb.git git clone --depth=1 git://sourceware.org/git/newlib-cygwin.git mkdir combined cd combined ln -s ../newlib-cygwin/* . ln -sf ../binutils-gdb/* . ln -sf ../gcc/* . mkdir build cd build ../configure --target=riscv32-embedded-elf --enable-languages=c --disable-shared --disable-threads --disable-multilib --disable-gdb --disable-libssp --with-newlib --with-arch-rv32e --with-abi=ilp32e --prefix=/usr/local/share/gcc-riscv32-embedded-elf make -j4 make make install export PATH=$PATH:/usr/local/share/gcc-riscv32-embedded-elf/bin/ riscv32-embedded-elf-gcc -v

目前,我发现没有简单的方法让 GCC 为 RISC-V 生成大端代码。相反,更简单的方法是直接在 IO 设备或内存区域中进行端序转换。

由于在某些机器上构建 GCC 并不容易,我在公共共享文件夹中放置了 RV32E 的 GCC 工具源代码和预编译二进制集:

https://drive.google.com/drive/folders/1GYkqDg5JBVeocUIG2ljguNUNX0TZ-ic6?usp=sharing

据我回忆,它是在 Slackware Linux 或类似系统上编译的,无论如何,它在 Windows 10 的 WSL 以及其他类 Linux 环境中都能正常工作。

更新:经过测试,更现代的 GCC 12+ 版本与 DarkRISCV 配合使用没有任何问题!

开发板

目前,支持以下开发板:

  • Avnet Microboard LX9:配备 Xilinx Spartan-6 LX9,运行频率为 100MHz
  • XilinX AC701 A200:配备 Xilinx Artix-7 A200,运行频率为 90MHz
  • QMTech SDRAM LX16:配备 Xilinx Spartan-6 LX16,运行频率为 100MHz
  • QMTech NORAM S15:配备 Xilinx Spartan-7 S15,运行频率为 100MHz
  • Lattice Brevia2 XP2:配备 Lattice XP2-6,运行频率为 50MHz
  • Piswords RS485 LX9:配备 Xilinx Spartan-6 LX9,运行频率为 100MHz
  • Digilent S3 Starter Board:配备 Xilinx Spartan-3 S200,运行频率为 50MHz

上述速度与开发板上的可用时钟相关,通过对时钟发生器进行编程可以产生不同的时钟。大多数开发板都采用 Spartan-6,其内核在约 100MHz 时运行良好,与主振荡器的频率(通常为 50MHz)无关。

所有基于 Xilinx 的开发板通常支持 115200 bps 的 UART 用于控制台,一些 LED 用于调试,以及片上 4KB ROM 和 4KB RAM(还有用于重启内核的 RESET 按钮和用于示波器的 DEBUG 信号)。

对于 QMTECH 开发板,由于其不包含 JTAG 以及 UART/USB 端口,因此可以通过外接 USB/UART 转换器和低成本 JTAG 适配器轻松解决此问题!

Lattice Brevia 由板载 50MHz 振荡器提供时钟,UART 工作在 115200bps,LED 和 DEBUG 端口连接到板载 LED。

尽管是 Digilent Spartan-3 Starter Board,但它是用作 LUT4 技术参考的非常实用的开发板,这使得未来有可能改进对其他低成本 LUT4 FPGA 的支持。

在软件方面,提供了一个小型 shell,其中包含一些基本命令:

  • clear:清屏
  • dump :转储 RAM 的某个区域
  • led :更改 LED 寄存器(用于控制 LED 的亮灭)
  • timer :更改定时器预分频器,这会影响中断频率
  • oport :更改 OPORT 寄存器(用于改变 DEBUG 线路)
  • iport:打印 OPORT 寄存器

该 shell 的目的是提供一些基本的测试功能,以提供有关当前硬件状态的通过/不通过状态。

有用的内存区域:

  • 4096:RAM 起始地址(数据)
  • 4608:RAM 起始地址(数据)
  • 5120:空白区域
  • 5632:空白区域
  • 6144:空白区域
  • 6656:空白区域
  • 7168:空白区域
  • 7680:RAM 结束地址(堆栈)

由于 DarkRISCV 使用分离的指令总线和数据总线,因此无法转储 ROM 区域。但是,即使激活了 HARVARD 选项,也不存在此限制,因为内核的构造方式是将 ROM 总线连接到双端口存储器的一个总线,而 RAM 总线连接到同一双端口存储器的另一个不同总线。从 DarkRISCV 的角度来看,它们是完全分离且独立的总线,但实际上它们位于同一内存区域,这使得数据总线可以更改代码存储的区域。借助此功能,未来将有可能从 FLASH 存储器创建可加载代码! 😃

FuseSoC 支持

去年圣诞节(2022 年),我们的同事 Lucas Teske 为 darkriscv 添加了 FuseSoC 支持……我不太清楚它的工作原理和使用方法,但它应该能自动处理构建工具!这与 SERV 使用的工具相同,我过去曾用它来添加一些千核记录……要在 darkriscv 中使用它,请尝试:

  • fusesoc run --target=qmtech_artix7_a35 darklife:darkriscv:darksocv

目前,并非所有开发板都得到了真正的支持。支持的开发板包括:

  • Colorlight i9
  • Colorlight i5
  • Lattice iCE40 Devkit
  • QMtech Artix 7 (Vivado)

Yosys 支持

我们的同事 Hirosh Dabui(来自 KianRiscV 项目)通过 Yosys 添加了对 Lattice FPGA 的支持,这样就可以使用 makefile 直接从 Linux 构建和编程 FPGA 了!

从零开始创建 RISC-V

我发现有些人对在一夜之间设计出 RISC-V 处理器的可能性持怀疑态度。当然,这并不像看起来那么容易,实际上,它需要大量的经验、规划和运气。而且,处理器能正确运行一些指令并在串口输出一些信息,并不真正意味着设计是完美的,相反,你需要大量的调试时间来修复所有隐藏的问题。

作为参考,我前段时间发布了我在设计 DarkRISCV 之前设计的 16 位“microrisc”内核代码:

虽然它简单得多,但它非常接近 DarkRISCV 的一些原始概念,并且与同一时期针对小型 Xilinx Spartan-3 FPGA 的其他内核情况类似。哦,因为有很多“micro”-什么的,我把它重命名为 micro-DarkRISC——不是 RISCV——所以它现在是 Dark 家族的一部分了!😃

还有其他一些很好的项目可以作为参考:

我找到了我的朋友(Lucas Teske)的一系列在线视频,展示了从零开始设计 RISC-V 处理器的过程(包含 9 个视频的播放列表):

或者,Twitch 上有原始视频:

遗憾的是,该视频集目前只有葡萄牙语版本,并且有很多关于技术的并行讨论,包括在线修复 Teske 的笔记本电脑!我希望将来能够编辑这个视频集,并可能制作英文字幕。

关于处理器本身,它是一个面向微码的概念,采用经典的冯·诺依曼架构,旨在更轻松地支持不同的 ISA。它与我们周围常见的传统 RISC 内核确实非常不同!此外,它还包含了围绕开源工具(如 Icarus、Yosys 和 gtkWave)的良好生态系统!

虽然尚未完成(已完成 95%!),但我认为它对 RISC-V 设计非常有启发性:

  • rv32e 指令集:非常精简(37 条)且具有非常正交的位模式(6 位)
  • rv32e 寄存器集:16 个 32 位寄存器组和一个 32 位程序计数器
  • 具有基本操作的 rv32e ALU,支持寄存器/立即数和寄存器/寄存器指令
  • rv32e 指令解码:非常易于理解,实现起来非常直接
  • rv32e 软件支持:GCC 支持提供了一种生成代码和测试代码的简便方法!

Teske 的目标不是设计出史上最快的 RISC-V 内核(我们已经有很多 CPI 约为 1 的更快内核,如 darkriscv、vexriscv 等),而是创建一个干净、可靠且全面的 RISC-V 内核。

你可以在以下仓库中查看代码:

另一个很好的参考是我的朋友 Hirosh 的 KivanRiscV:

学术论文与应用场景

有趣的是,DarkRISCV 出现在了一些学术论文中,有时作为对比对象,有时则充当实验品。

在实际应用方面,标准的嵌入式 C 代码在 DarkRISCV 上通常运行良好。以下是一些目前正在使用的应用示例:

  • 微控制器编程器(JTAG 及其他复杂协议)
  • 数据压缩/解压缩(LZ 流)
  • 加密(RSA 和 SHA256,需要硬件加速器)
  • 数字信号处理(需要 MAC 指令)

以 RSA 为例,通过 IO 映射(即不使用 M 扩展,而是通过加载/存储指令访问 IO 映射寄存器)简单地集成一个流水线 32x32 位乘法器,与单纯的 RV32E 指令集相比,RSA 性能提升了 20 倍。在 SHA256 方面,通过 IO 映射的硬件实现完整的 SHA256 加速器,可将原本需要数秒的处理时间缩短至几毫秒。在这两种情况下,都无需在核心中集成复杂指令,也无需等待——核心和加速器可以并行工作。

性能对比

我尝试对DarkRISCV与不同FPGA进行公平的性能对比,但这并不像看起来那么简单!第一个问题是找到每个内核的HDL版本,因为工具需要Verilog或VHDL文件才能构建。第二个问题是确定编译内容:顶层模块有很多组合,包含不同的外设和设计理念。在本次对比中,我只包含了内核部分。第三个问题与内核配置有关:要将它们配置为最小面积或最大速度并非易事,也不明确,因此我对所有内核都使用了默认配置。简而言之,我用一种简单直接的方式解决了这个问题(这在95%的情况下反映了现实)。

当我为每个内核进行单独构建时,还存在最后一个问题:如何分析结果并对内核进行排名。我的选择是计算在固定的FPGA(本例中为Kintex-7 K420,具有260600个6输入LUT)上可能达到的MIPS数量。不同的内核需要不同数量的LUT,只需将总LUT数除以所需数量,我们就能得到理论上每块FPGA可容纳的内核数量(峰值内核数/FPGA)。此外,不同的内核具有不同的理论峰值IPC(每时钟周期指令数)。根据综合工具在默认设置下的推测,它们将以不同的最大频率运行。只要我们知道每时钟周期的指令数和每秒的最大时钟数(最大频率),就能得到每个内核的最大每秒指令数(峰值MIPS)。只要我们还知道最大内核数量,就可以计算出每块FPGA的最大峰值MIPS。

以下列表远非完整,但我建议用它来比较不同的内核:

内核 LUT FF DSP BRAM IPC MFreq Ncores MIPS/k7 REPO地址
DarkRISCV 1177 246 0 0 1 150 221 33232 https://github.com/darklife/darkriscv
VexRISCV 1993 1345 4 5 1 214 130 28013 https://github.com/m-labs/VexRiscv-verilog
PicoRV32 1291 568 0 1 1/4 309 201 15630 https://github.com/cliffordwolf/picorv32
SERV 217 174 0 0 1/32 367 1200 13788 https://github.com/olofk/serv
RPU 2943 1103 12 1 1 111 88 9905 https://github.com/Domipheus/RPU
UE RISC-V 3676 2289 4 0 1 124 70 8811 https://github.com/ultraembedded/riscv
UE BiRISC-V 15667 6324 4 0 2 87 16 2917 https://github.com/ultraembedded/biriscv

这些结果足够公平吗?在我看来,只要条件相同,是的。这些结果足够真实吗?在我看来,可能吧。至少有两种情况不符:当构建中包含顶层模块时,DarkRISCV在同一FPGA上可达到240MHz。不确定为什么仅内核的结果会导致更低的最大频率,但重点是尝试找到一种比较不同内核的方法,所以这没关系。第二种情况是SERV,它显示的最大频率为367MHz,每FPGA最多1200个内核。事实上,我在该FPGA上测试了多达1000个SERV内核,并且由于多核层次结构的优化,似乎可以容纳1100到1200个内核。坏消息是,每个内核的最大时钟勉强达到128MHz。同样,重点是尝试找到一种比较不同内核的方法,所以这也没关系。

可以肯定的是,这些数字对未来的设计者非常有用,其传达的信息很明确:保持简单!每个内核面积越小,意味着在相同面积内可以容纳更多内核,在某些情况下,还意味着更好的性能。在大多数情况下,更小的面积也意味着更好的时钟性能,但在这种情况下,更好的时钟性能只有在IPC接近1时才有用,而保持IPC接近1并不容易。举个例子,DarkRISCV确实可以达到IPC≈1,但代码必须手动优化,并且顶层模块必须进行修改,以消除BRAM的延迟。在更一般的情况下,只需较少的努力,编译器和更标准的顶层模块可以将IPC保持在0.7左右,这对于大多数应用来说已经足够了。所以,请把你的期望放得非常非常低!: )

为了说明DarkRISCV的速度,src目录中提供了一些基准测试:

  • coremark:RV32I和RV32IM的更新结果显示,RV32I为0.96 coremark/MHz,RV32IM为2.28 coremark/MHz!可以测量绝对coremark速率以及相对coremark/MHz值。就相对coremark速率而言,3级流水线的峰值为0.9 coremark/MHz,而2级流水线的峰值为1.1 coremark/MHz,这几乎与旧的486SX内核(针对RV32E测量,无乘/除指令,否则结果会增加2倍!)相同。就绝对coremark速率而言,最佳结果是在KU040上以400MHz运行3级流水线,结果为360 loops/s。虽然2级流水线效率更高,但它的时钟速度只有一半,结果为220 loops/s。当启用MAC指令时,DarkRISC-v的峰值为1.02 coremark/MHz,这表明coremark高度依赖于32x32位乘法。
  • dhrystone:可以测量dhrystone值以获得绝对dhrystone/s值(又名DMIPS)以及相对值(DMIPS/MHz)。目前,3级流水线在100MHz时为66 DMIPS,即0.66 DMIPS/MHz;2级流水线在66MHz时为50 DMIPS,即0.75 DMIPS/MHz。启用MAC指令时,结果没有变化。
  • primes:由于素数源代码中包含一个庞大的列表,因此可以将DarkRISCV的性能与其他RISC-V内核和其他架构进行比较。在基本RV32E设置下,DarkRISCV的性能与VexRISCV(RV32IM,5级流水线)非常相似,并且当启用MAC指令时,DarkRISCV可以超越VexRISCV,因为32x32位乘法由3条16x16位MAC指令执行。
  • MB/s:我们通常会大量讨论DarkRISCV上的总线带宽,由于它采用哈佛架构,指令总线和数据总线可以并行工作,在100MHz时钟下每条总线的带宽可达400MB/s。当然,虽然指令总线确实能全天保持该带宽,但数据总线会受到加载/存储流的限制,因此在先进的Xilinx FPGA(6系列及以上)上,更实际的数值是加载时高达200MB/s,存储时高达400MB/s。在不太先进的FPGA上,加载和存储可能都限制在200MB/s,因为需要读-修改-写周期……作为参考,一个经过优化的memcpy128()函数,每个循环移动16字节,在先进FPGA上可能需要18个时钟周期,峰值仅为88MB/s(https://godbolt.org/z/75a531zn6),部分原因是我们需要加载和存储数据(因此带宽减半),部分原因是BRAM读取需要2个时钟周期。
  • 在Xilinx FPGAs上的综合:另一个很好的基准测试是在Xilinx FPGAs上的综合,结果通常是每个内核约1000个LUT,在Kintex-7上约240MHz……通过扩展上述相对基准测试,可以计算特定FPGA上的coremark或dhrystone,并且通过将总LUT数除以内核LUT数,可以计算该FPGA上可容纳多少个内核,并按比例放大基准测试结果,以计算每芯片的总性能!

致谢

特别感谢我在 Verilog/VHDL/IT 领域的老同事们:

  • Paulo Matias(导师,Verilog/Bluespec/RISC-V 专家)
  • Paulo Bernardi(同事,Verilog 专家)
  • Evandro Hauenstein(同事,Git 专家)
  • Lucas Mendes(技术专家)
  • Marcelo Toledo(技术专家)
  • Fabiano Silos(技术专家)

同时,特别感谢那些在互联网上发现此项目并以各种方式为其改进做出贡献的“darkriscv 之友”:

  • Guilherme Barile(技术专家,首位发表有关 darkriscv 内容的人![2])
  • Alasdair Allan(技术专家,发表了一篇关于 darkriscv 的文章 [3])
  • Gareth Halfacree(技术专家,发表了一篇关于 DarkRISCV 的文章 [4])
  • Ivan Vasilev(将 DarkRISCV 移植到 Lattice Brevia XP2!)
  • GitHub 用户 timdudu(修复了 LDATA 并发现了 BCC 指令中的一个 bug)
  • GitHub 用户 hyf6661669(大量贡献,包括针对 AUIPC 和 S{B,W,L} 指令的修复、ModelSIM 仿真、存储/加载指令使用的内存字节选择等诸多方面!)
  • GitHub 用户 zmeiresearch(支持 Lattice XP2 Brevia 开发板)
  • Hirosh Dubai(提供动力并进行了大量关于 RISC-V 的讨论!)
  • 所有其他在 GitHub 上通过修复、更正和建议做出贡献的同事们。

最后,感谢所有直接或间接为这个项目做出贡献的人,包括我所在的公司以及所有测试过 DarkRISCV 的同事们。

参考文献

[1] https://www.amazon.com/RISC-V-Reader-Open-Architecture-Atlas/dp/099924910X [2] https://news.ycombinator.com/item?id=17852876 [3] https://blog.hackster.io/the-rise-of-the-dark-risc-v-ddb49764f392 [4] https://abopen.com/news/darkriscv-an-overnight-bsd-licensed-risc-v-implementation/ [5] http://quasilyte.dev/blog/post/riscv32-custom-instruction-and-its-simulation/ [6] https://github.com/riscv/riscv-pk/blob/master/bbl/riscv_logo.txt

项目介绍

darkriscv不仅仅是一个riscv-cpu-core,而是一个包含了总线桥片(Bridge)和内存(Memory+Flash)及外设(UART)的完整的SoC,还包含了可以启动并进行交互式操作的软件系统,这正是ChipCamp所喜欢的全套芯片系统。

定制我的领域