支持27条指令的单周期架构设计

实验目的

(1)理解lui、auipc、jal和jalr指令的功能;

(2)深入理解数据通路与指令功能的关联。

实验任务

在前面实现的23条指令单周期数据通路的基础上,扩充以下4条指令,实现单周期架构的27条指令。

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

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

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

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

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

本项目实现的是 单周期架构。 代码基础是上上个项目“实现分支指令”,不是上一个“初步实现流水线”项目。

从开源项目托管网站下载实验材料,下载方法见下载实验材料。 实验材料的文件组织如下。

📂 riscv
  📂 RV-00
    📂 project_de2-115 (1)
  📂 RV-27
    📁 diagram    (2)
    📂 real-speed (3)
    📁 testbench  (4)
1 用于线下实验板的文件
2 用于设计虚拟面板的文件
3 用于实速运行测试的文件
4 用于仿真的testbench文件

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

1. 数据通路设计

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

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

设计步骤如下。

(1)设计数据通路;

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

(3)编写代码实现。

2. 虚拟面板设计

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

📂 riscv
  📂 RV-27
    📁 diagram
      📄 RV-23.drawio
      📄 RV-27_golden.jvp

(1)绘制虚拟面板背景图

根据自己设计的数据通路,绘图作为虚拟面板的背景。可使用远程实验验证流程中推荐的开源软件diagrams.net(又名draw.io)。

为了减少绘制背景图的工作量,实验材料diagram文件夹下提供了前面23条指令的单周期数据通路图RV-23.drawio,可在此基础上修改。

绘制完成的背景图需导出为PNG格式,具体方法见远程实验验证流程中的相关说明。 如果PNG图片在虚拟面板上的显示尺寸过大或过小,可以改变dpi重新导出。

(2)打开虚拟面板

和逻辑电路实验不同,CPU实验目前尚不支持从空白面板交互式地设计虚拟面板,可以在一个已有面板的基础上进行交互式修改。 实验材料提供了一个基本的虚拟面板文件,位于diagram文件夹下,在实验系统中打开其中的RV-27_golden.jvp

(3)导入背景图

用上面自己绘制的背景图替换虚拟面板的背景图。操作方法可参考远程实验验证流程中的相关说明。

替换背景图通常会引起信息流显示(绿线)的错位,实验平台目前还没有交互式定义信息流绿线的功能。为避免错位后影响外观,实验材料中的RV-27_golden.jvp未包含信息流绿线的定义。如果希望自己的虚拟面板也能够显示信息流绿线,需要手工修改jvp文件,可在硬件设计调试成功之后向老师咨询。

(4)添加数据框和信号框

添加必要的观察数据和信号,同时在设计代码中添加相应的观察变量。关于代码与面板元件的对应方法,可参阅实现ADDI指令

RV-27_golden.jvp面板中已经包含了一些数据框和信号框,与其对应的ws和wd结构体定义如下。

    //送给调试器的观察信号,需要与虚拟面板的信号框相对应
    struct packed{
        logic       WS1 ;   //MemWrite
        logic       WS0 ;   //RegWrite
    }ws;

    //送入扫描链的观察数据,需要与虚拟面板的数据框相对应
    struct packed{
        logic [31:0] WD3;   //regReadData1
        logic [4:0]  WD2;   //ra1
        logic [31:0] WD1;   //instruction
        logic [31:0] WD0;   //pc
    }wd;
设计代码中必须包含上面的观察变量,它们将用于电路测试。除此之外,可追加需要的观察变量;提交时无需删除增加的观察变量,它们不会影响电路测试。

3. 验证

编写测试程序,在实验平台运行,分析运行结果是否正确。测试程序要求如下。

(1)包含本实验新增的4条指令。

(2)转移指令需测试向下转移和向上转移两种情况。

(3)jalr指令需测试rd和rs1的寄存器号相同的情况,以及计算出的转移地址不为偶数的情况。

(4)测试新修改的数据通路能否正确执行前面实验完成的分支指令、访存指令和运算指令。

访存指令的测试程序中,使用了如下两条指令设置数据存储器的基地址。

    addi x3, x0, 0x100
    slli x3, x3, 20

现在实现了lui指令,用一条指令就可以完成。如下。

    lui x3, 0x10000

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

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

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

1. 创建工程

Quartus支持在同一个工程中创建多个“修订”(Revision),并且可以复制一个现有的Revision设置,在此基础上创建新的Revision。 所需实验材料如下。

📂 riscv
  📂 RV-00
    📁 project_de2-115
      📄 create_revision_rv_DE2115.tcl
      📄 DE2-115.sdc
      📄 DE2-115.tcl
      📄 RV_DE2_115.v
      📄 用TCL文件自动创建revision.md

创建Revision的具体方法见线上线下混合模式的Quartus工程设置,不同之处如下。

(1)基础工程不同

本实验已有工程是RISC-V工程,并且已有的Rivision名称应为 RV_Pocket,顶层文件为 RV_Pocket.v

(2)顶层文件不同

用于DE2-115开发板的CPU实验的顶层文件为 RV_DE2_115.v

(3)脚本文件不同

本实验所用脚本文件为 create_revision_rv_DE2115.tcl

也可参阅实验材料中包含的文档,该文档具体说明了用TCL脚本自动创建Revision的步骤。

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;
    assign memAB = cpuAB;
    assign memWriteData = cpuWriteData;
    assign bsc_sw = vSWITCH;
    assign bsc_btn = vBUTTON;
    assign JTDO = JTDI;

然后添加指令存储器模块。注意时钟的连接,如果采用同步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)编写汇编语言程序并用汇编器翻译为机器指令

例 1是一个简单的测试程序,读取开关状态输出到LED指示灯和数码管显示。

例 1. 基本IO测试程序清单
00000000:	  80000413            addi s0, x0, -2048 (1)
00000004:	  00042903    LOOP:   lw   s2, 0x00(s0)  (2)
00000008:	  01242823            sw   s2, 0x10(s0)  (3)
0000000C:	  01042983            lw   s3, 0x10(s0)  (4)
00000010:	  03342023            sw   s3, 0x20(s0)  (5)
00000014:	  FF1FF0EF            jal  LOOP  
1 IO基地址0xFFFFF800
2 读开关
3 写LED输出寄存器
4 读LED输出寄存器
5 写数码管输出寄存器

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

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

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文件中的地址是字地址,是例 1程序清单中的指令地址除4。

如果指令存储器是写代码实现的异步ROM,将指令编码写入一文本文件作为存储器的初始化文件,文件格式在存储器实验学习过。

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

上述文件在实验材料中提供,如下所示。

📂 riscv
  📂 RV-27
    📂 real-speed
      📄 IM.txt
      📄 test-io_instr.mif
      📄 test-io.dump

(3)编译并运行

在Quartus的存储器IP核设置中,指定上述mif文件作为初始内容文件,编译工程,将电路文件加载到DE2-115开发板的FPGA中。有关Quartus软件为DE2-115加载电路的操作说明,见“实验工具和环境”中的“本地实验验证流程-JULAB版”。

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

指示灯的连接顺序和逻辑电路实验不同。 在逻辑电路实验的DE2_115_TOP.v中,LED[0]连接到DE2-115开发板的 LEDR[0],即开关 SW[0] 正上方的红色指示灯;CPU实验框架的RV_DE2_115.v中,LED[0]连接到DE2-115开发板的 LEDG[0],即最右侧的绿色指示灯。

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

4. 实速运行的调试

实速运行出现问题,如何排查呢?假如上面举例的简单程序,加载到FPGA运行时拨动开关没反应,数码管显示全0,可能会让人一筹莫展。其实这类看起来很严重的问题,往往是一些小的疏忽造成的,比如在移植过程中,某些信号连接错了。

如果加载FPGA后没反应,首先检查SW[17]的位置。由于DE2-115开发板没有专用的复位按键,CPU实验框架临时将SW[17](最左边一个开关)用作复位;当SW[17]拨向上方时复位,拨向下方时正常运行。之所以没有用按键作为复位按钮,是考虑以后可以将按键扩展为外设。如果在以后的设计中,SW[17]也要作为外设,需要修改RV_DE2_115.v中的下面这行代码。

    wire rst_btn = SW[17];

改为

    wire rst_btn = 1'b0;

通过仔细检查代码,有些错误可以被发现,但还有些错误可能隐藏的比较深,没那么容易看出来。即使解决了小疏忽,基本输入输出正常了,但是更复杂的测试程序出现问题,就难以通过看代码找出问题。

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

例 2. testbench
`timescale 1ns / 100ps
module tb_soc();

reg rst;
reg fpga_clk;
reg sys_clk;

SoC uut(
    .RESET(rst),
    .CLOCK(fpga_clk),
    .CLK(sys_clk)
);

initial begin 
	fpga_clk = 0;
    sys_clk = 0;
    rst = 1;
    #10 rst = 0;
end

initial begin
	forever #10 fpga_clk = ~fpga_clk;	//50Mhz
end

initial begin
	forever #50 sys_clk = ~sys_clk;	//10Mhz
end

endmodule

注:本文件在实验材料的testbench文件夹中提供。

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


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