支持27条指令的单周期RISC-V

在实现前面23条指令的基础上,实现以下4条指令。

(1)U型格式的LUI指令;

(2)U型格式的AUIPC指令;

(3)J型格式的JAL指令;

(4)I型格式的JALR指令。

本实验首先在远程虚拟实验平台调试验证设计,成功后再移植到线下实验板实速运行。

任务一、微架构设计与验证

1. 数据通路设计

通过前面实验的逐步扩展数据通路,已经理解了数据通路与指令之间的关系。本实验不再给定数据通路,而是将设计数据通路作为一个主要的实验任务。

通过查阅RISC-V手册了解指令功能,综合分析后确定最优的数据通路,使其简洁、高效。修改后的数据通路不仅能执行新增加的4条指令,也应能执行之前已经完成的23条指令。

设计步骤如下。

(1)设计数据通路;

(2)列出控制信号的输入输出真值表;

(3)编写代码实现。

2. 虚拟面板设计

根据自己设计的数据通路,绘图作为虚拟面板的背景;添加必要的观察数据和信号,同时在设计代码中添加相应的观察变量。

建议在之前的23条指令虚拟面板基础上替换数据通路背景图,减少添加观察数据和观察信号的工作量。替换背景图通常会引起观察变量以及信息流显示(绿线)的错位;观察变量可拖动到新的位置,但是实验平台目前还没有交互式定义信息流绿线的功能。为避免错位后影响外观,附件材料中提供了不包含绿线的23条指令的虚拟面板文件,可在此虚拟面板的基础上替换自己修改的背景图。如果希望自己增加的数据通路也能够显示信息流绿线,需要手工修改jvp文件,可在硬件设计调试成功之后向老师咨询。

为了减少绘制背景图的工作量,附件材料还提供了前面23条指令的单周期数据通路图,可使用开源软件diagrams.net(又名draw.io)修改。diagrams.net有Web版和桌面版, 可从https://www.diagrams.net/ 进入Web版或下载桌面版。背景图需导出为PNG格式,导出菜单中选择最后一项“高级”,可以设置DPI,原虚拟面板背景图的DPI是150dpi。背景图的文件大小不宜过大,否则会影响虚拟面板的加载速度,超过1MB实验平台无法加载。

3. 验证

阅读下面的汇编语言源程序,为每一条语句写上注释,在实验平台运行这段程序,分析运行结果是否正确。

    lui x3, 0x10000
L1: addi x10, x0, 0x555
    sw x10, 12(x3)
    auipc x5, 0x10000
    lw x6, 0(x5)
    bne x6,x10, L1
L2: jal x1, L3
    beq x1, x2, L2
L3: auipc x2,0
    addi x2, x2, 12
    jalr x1, x1, 0

任务二、线下实验板实速运行

实速运行是指脱离远程虚拟实验平台的调试环境,在线下实验板运行测试程序,通过板载的拨动开关、LED指示灯、七段数码管等输入程序运行需要的数据、输出运行结果。

本课程支持的线下实验板有基于Intel/Altera FPGA的DE2-115和基于AMD/Xilinx FPGA的Nexys4DDR,其他实验板需学员自己适配。下面以DE2-115为例说明移植过程。

1. 创建工程

Quartus支持在同一个工程中创建多个“修订”(Revision),并且可以复制一个现有的Revision设置,在此基础上创建新的Revision。实验材料中提供了一个脚本文件“create_revision_rv_DE2115.tcl”用于自动创建面向DE2-115的“修订”,使用方法见实验材料中的文档说明,这里不再赘述。除了用脚本自动创建“修订”,也可以手动创建“修订”,下面介绍手动创建Revision的步骤。

(1)在Quartus Prime中打开之前的RV_Project工程,点击菜单Project ➤ Revision,弹出的Revisions窗口中列出了现有的Revision;双击最后一行的[new revision],输入Revision name “RV_DE2115”,创建新的修订。

(2)点击菜单Assignments ➤ Remove Assignments,勾选“All”点击OK按钮移除所有的assignments。

(3)点击菜单View ➤ Tcl Console,打开 “Tcl Console” 子窗口。点击菜单Tools ➤ Tcl Scripts,在弹出的Tcl Scripts窗口中点击“Add to project”按钮,选择DE2-115.tcl(该文件在实验材料中提供,应事先复制到工程所在文件夹)。点击“Run”按钮执行该tcl脚本文件,因为DE2-115设置较多,脚本执行会花比较长时间,等待执行完成之后会弹出一个信息框提示“Tcl Script File …​ executed”;如果出错,会在 “Tcl Console” 子窗口显示红色的错误信息,这也是要打开“Tcl Console” 子窗口的原因。

(4)点击Project ➤ Add/Remove Files in Project,移除RemotePocket.sdc,添加DE2-115.sdc;移除RV_Pocket.v,添加RV_DE2_115.v并将其设为顶层模块。

2. 设计代码移植

实速运行脱离调试环境独立运行,设计代码的移植主要是屏蔽片上调试器(On-chip debuger,OCD)模块,将原来经过OCD连接的信号改为直接连接;由于指令存储器原来需要OCD写入,所以实际是RAM,屏蔽OCD之后需要改为ROM,建议用IP核实现指令存储器的ROM模块;此外,存储器的时钟也是一个需要考虑的问题。

(1)存储器的类型和时钟

通过前面的实验我们已经知道,FPGA内部的存储器有两种类型:同步读和异步读。“同步读”使用FPGA内部的专用RAM资源,不占用逻辑资源,但是锁存地址占用了一个周期,所以需要两个时钟周期才能读出数据。而“异步读”不锁存地址,可以在给出地址的同时读出数据。理论上,单周期CPU应该采用“异步读”存储器与其配合,才能一个周期执行一条指令。但是在调试环境下,OCD采用“同步读”存储器与其配合,为了能够在“单周期”内执行完一条指令,存储器的时钟采用了连续的10MHz时钟,而CPU时钟由OCD提供,仅在用户通过实验软件执行指令时产生一个脉冲;简单来说,存储器采用了比CPU更高频率的时钟,在一个CPU时钟周期内有若干次存储器读出,从而保证了“单周期”执行完一条指令。

实速运行时,CPU时钟不再由OCD提供,而是采用10MHz时钟,这时存储器模块就有两种选择。如果仍然采用“同步读”存储器,存储器时钟就要采用更高频率的时钟,以满足“单周期”的要求;或者采用“异步读”存储器,存储器时钟采用与CPU相同的10MHz时钟。

前面存储器实验中已经学习了同步读和异步读RAM的Verilog描述方法,而ROM用Verilog描述只能生成异步ROM,同步ROM必须使用IP核[1]。IP核的生成方法见附录。

(2)SoC模块的移植

首先替换OCD。将JuTAG_CPU模块的实例化注释掉,添加以下代码,替换原来由JuTAG模块产生的信号,以及经过JuTAG_CPU内部连接的信号。

    assign cpuReset = RESET;
    assign cpuClk = CLK;
    assign memWR = cpuWR;
    assign memRD = cpuRD;  // 如果CPU没有产生cpuRD,改为memRD = 1'b1;
    assign memAB = cpuAB;
    assign memWriteData = cpuWriteData;
    assign bsc_sw = vSWITCH;
    assign bsc_btn = vBUTTON;

然后添加指令存储器模块。注意时钟的连接,如果采用同步ROM,连接SoC模块CLOCK端口提供的50MHz时钟;如果采用异步ROM,连接和CPU模块相同的10MHz CLK时钟。指令存储器的容量设计为16K×32(64KB),因为指令存储器按字编址,而CPU输出的是字节地址,所以要将CPU输出的地址右移两位送给指令存储器。

最后还要修改数据存储器的实例化。前面实验使用的数据存储器是同步读RAM,时钟连接的是10MHz CLK时钟,现在要将它连接到CLOCK端口提供的50MHz时钟。另外,数据存储器的容量也需要加大,以满足后面编写的应用程序和测试程序的需要;建议数据存储器的容量也设置为16K×32(64KB),至少设置为1K×32(4KB),可通过实例化时的参数设置。目前设计的CPU尚未支持存储器的字节访问,数据存储器仍然是按字编址,所以地址也要右移两位。

3. 应用程序设计和实速运行测试

遵循从简单到复杂的原则,编写一些包含输入输出的RISC-V汇编语言程序,用来测试移植是否成功以及所设计的CPU能否正确运行。下面通过一个例子说明过程。

(1)编写汇编语言程序并用汇编器翻译为机器指令

程序清单如下。

0x00000000:	  80000413            addi s0, x0, -2048 #IO基地址0xFFFFF800
0x00000004:	  00042903    LOOP:   lw   s2, 0x00(s0)  #读开关
0x00000008:	  01242823            sw   s2, 0x10(s0)  #写LED输出寄存器
0x0000000C:	  01042983            lw   s3, 0x10(s0)  #读LED输出寄存器
0x00000010:	  03342023            sw   s3, 0x20(s0)  #写数码管输出寄存器
0x00000014:	  FF1FF0EF            jal  LOOP

(2)将机器指令转换为ROM初始化文件

如果指令存储器采用的是IP核,初始化文件格式有两种:Memory Initialization File(.mif)格式和Hexadecimal Intel-Format File(.hex)格式,详见附录介绍。上述程序的mif文件内容如下。

WIDTH = 32;
DEPTH = 16384;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT BEGIN
00000000:80000413;
00000001:00042903;
00000002:01242823;
00000003:01042983;
00000004:03342023;
00000005:FF1FF0EF;
END;

注意,mif文件中的地址是字地址,是程序清单中的指令地址除4。

当程序比较长时,从反汇编清单中逐个复制机器指令到mif文件,不但耗时还容易出错。很多优秀的代码编辑器(如VS CODE)都有列选择和复制功能,能够极大地提高效率。

(3)编译并运行

在Quartus的存储器IP核设置中,指定上述mif文件,编译工程,将电路文件加载到DE2-115开发板的FPGA中。有关Quartus软件为DE2-115加载电路的操作说明,见前面“认识虚拟实验”的实验指导《FPGA验证流程(本地版)》。

从汇编语言程序可以看出,该程序的功能是读出拨动开关的值,输出到LED指示灯和数码管进行显示。拨动开关,检查指示灯和数码管的显示是否正确。

上面这个简单的例子涉及的指令很少,仅仅做了最基本的输入输出测试,通过以后,还要再编写一些更复杂的程序,更全面地测试所设计的CPU能否正常工作。可以对前面汇编语言实验做过的程序稍加修改,如斐波那契数列计算程序,可以通过开关输入循环次数,数码管显示最终的结果;再如位操作的程序,可以改用LED显示输出结果。

4. 实速运行的调试

实速运行出现问题,如何排查呢?假如上面举例的简单程序,加载到FPGA运行时拨动开关没反应,数码管显示全0,可能会让人一筹莫展。其实这类看起来很严重的问题,往往是一些小的疏忽造成的,比如在移植过程中,某些信号连接错了。通过仔细检查代码,有些错误可以被发现,但还有些错误可能隐藏的比较深,没那么容易看出来。即使解决了小疏忽,基本输入输出正常了,但是更复杂的测试程序出现问题,就难以通过看代码找出问题。

在计数器实验中曾经介绍过仿真工具,对于像计算机这样的复杂电路来说,仿真是一种比较高效的调试方法,也是工业界通行的验证方法。实验材料中提供了一个testbench代码,可以用它对自己的设计进行仿真。仿真工具的使用方法,可参阅计数器实验中的实验指导《ModelSim仿真入门》,更多资料可阅读《Intel Quartus Prime用户指南: 第三方仿真》

利用仿真通常可以查出大部分设计中存在的问题,但是仿真毕竟不是在真实FPGA上运行的结果,如果仿真没有发现错误,实际运行又不正确,有没有其他调试工具呢?Quartus 提供了Signal Tap片上逻辑分析仪,可以观察FPGA芯片上的实际运行结果。Signal Tap的具体用法请参阅《Intel Quartus Prime用户指南: 调试工具》


1. 作者用同步方法描述的ROM,在Quartus环境下编译的结果并未使用专用的RAM块。如果读者找到了描述方法,欢迎交流。