第 4 章 单周期CPU设计
通过前面的学习,我们介绍了相关的实验平台,也对开展CPU设计工作必须掌握的数字逻辑电路和Verilog编程的知识进行了回顾。从这一章开始,我们将进入本书的主体部分——基础CPU设计。我们将从设计实现一个只有5条指令的“迷你”单周期CPU开始,逐步添加指令和其他功能,最终设计出一个支持TLB MMU和Cache可以运行操作系统的流水线CPU。
这一章我们将关注于单周期CPU的设计,具体分为两个阶段,第一阶段设计一个5条指令的单周期CPU,第二阶段将单周期CPU支持的指令增加到20条。本章的编排重点照顾了初学者,进度安排比较缓慢,而且按照初学者惯常的学习习惯把设计方案讲解和实验环境介绍交织在一起介绍。然而,对于已经有一定CPU设计基础的读者,建议还是把本章的内容和实践任务快速过一遍,主要是为了熟悉本书的术语体系、设计风格以及配套的实验开发环境,以便更顺畅地切入到后续章节的实践任务中。
【本章学习目标】
- 建立设计方案与Verilog代码实现之间的认知联系。
- 形成良好的Verilog代码编写习惯。
- 掌握一些CPU功能仿真验证中调试错误的技术,具备初步的调试能力。
4.1 验证5条指令的单周期CPU
截止目前我们已经形成了一个5条指令的单周期CPU的设计方案。按照前面第1章中所介绍的芯片设计的工作阶段,接下来我们需要用Verilog语言描述出这个设计方案,然后展开功能和性能验证。如何用Verilog描述设计方案中呈现的电路,前面第3章已经做了介绍。这里我们将对如何验证CPU的功能正确性做更具体的介绍。
4.1.1 5条指令单周期CPU实验开发环境快速上手
俗话说“好马配好鞍”,要顺利完成CPU设计实验,离开不合适的实验开发环境。为此本书配套了系列化的CPU实验开发环境。这些实验开发环境会随着所验证CPU功能的增加而集成更多新的特性。不过它们的构建思路是一脉相承的,读者只要按照我们安排的实验进度正常推进,就不会在学习实验环境上花费太多精力。接下来我们就来接触最简单的5条指令单周期CPU实验开发环境,学习使用它的快速上手步骤。
4.1.1.1 获取实验开发环境
5条指令单周期CPU实验开发环境的获取方式与前面的实践任务相同,若仍不熟悉请回顾第2.3.1节中的介绍。再次强调要确保开发环境位于一个路径中没有中文字符的位置上。
5条指令单周期CPU的实验开发环境位于项目的一级子目录 minicpu_env 下。
4.1.1.2 开发CPU代码
用你习惯使用的文本编辑器1将所设计的CPU的Verilog代码描述出来。重点注意顶层模块的模块名和接口信号必须按照规定要求定义。
4.1.1.3 集成自己的CPU
正常的实验步骤中,需要将写好的CPU的Verilog代码拷贝到实验开发环境的指定目录下。不过本书为最大限度降低初学阶段的难度,第一个5条指令单周期CPU的实验采用的是提供一套现成的Verilog代码让读者填空的方式。这套待填空的代码已经位于 minicpu_env/miniCPU/ 目录下。
4.1.1.4 打开Vivado工程
希望将Vivado工程建在 minicpu_env/soc_verify/run_vivado/ 目录下。如果该目录下尚未创建工程,请参照附录D.2节介绍的步骤,利用minicpu_env/soc_verify/run_vivado/ 目录下的 create_project.tcl 文件创建工程。如果该目录下已按照前述方式创建了工程,可以直接运行 minicpu_env/soc_verify/run_vivado/project/ 目录下的 loongson.xpr。
4.1.2 minicpu_env 实验开发环境组织结构介绍
整个 minicpu_env 实验开发环境的目录结构及各部分功能简介如下所示:
|--miniCPU/ 所实现CPU的RTL代码
| |--minicpu_top.v 5条指令单周期CPU顶层模块
| |--regfile.v CPU中寄存器堆模块
| |--tools.v CPU中基本功能模块
|
|--func/ 功能验证测试程序
| |--inst_ram.coe 测试程序对应上板用的二进制纯数据文件
| |--inst_ram.mif 测试程序对应功能仿真用的二进制纯数据文件
| |--inst_ram.txt 测试程序汇编代码说明
|
|--soc_verify/ 所现的CPU的验证环境
|--rtl/ 验证用SoC设计代码目录
| |--soc_mini_top.v SoC的顶层文件
| |--CONFREG/ confreg模块,用于访问实验板上的LED灯、拨码开关等外设
| |--xilinx_ip/ 定制的Xilinx IP,包含clk_pll、inst_ram
|
|--testbench/ 功能仿真验证平台
|
|--run_vivado/ Vivado工程的运行目录
|--constraints/ Vivado工程设计的约束
4.1.2.1 功能仿真验证
数字电路的功能验证是为了检查所设计的数字电路在功能上是否符合设计目标。简单来说,就是检查设计的电路功能对不对。读者应该都开发过C语言程序,都知道写完的程序要测试一下正确性。我们这里说的功能验证与软件开发里面的功能测试的意图是一样的。所谓数字电路的功能仿真验证,就是用(软件模拟)仿真的方式而非电路实测的方式进行电路的功能验证。图4.1给出了数字电路功能仿真验证的一个基本框架。
在这个基本框架中,我们给待验证电路(DUT)一些特定的输入激励,然后观察DUT的输出结果是否如我们预期。所谓“不管白猫、黑猫,只要抓到老鼠就是好猫”,这里“老鼠”就是激励,期望结果是“抓到老鼠”,只要被验证对象在这个激励下得到所期望的结果,哪怕它明明是只黄鼠狼,我们也认为它是一只好猫。
我们给CPU设计进行功能仿真验证时,沿用的依然是上面的思路,但是在输入激励和输出结果检查两方面的具体处理方式与简单的数字电路设计存在区别。简单数据电路的功能仿真验证,通常是产生一系列变化的激励信号,输入到被验证的电路的输入端口上,然后观察电路输出端口的信号判断结果是否符号预期。然而对于CPU来说,其输入输出端口只有时钟、复位和I/O,采用这种直接驱动和观察输入输出端口的方式,验证效率太低。
我们采用测试程序作为CPU功能验证的激励。即输入激励是一段测试指令序列,通常是用汇编语言或C语言编写,用编译器编译出来的机器代码。我们通过观察测试程序的执行结果是否符合预期,来判断CPU功能是否正确。这样做验证的效率是大幅度提高了,但是验证过程中出错后定位出错点的调试难度也相应提升了。好在第一个5条指令单周期CPU实验的调试难度相对较低,采用前面第3章??节介绍的调试技术基本就能完成实践任务。后面随着实验难度的提升,我们的实验环境还将提供一套基于trace比对的调试辅助手段,可以帮助在调试过程中更加快速的定位。
4.1.2.1.1 验证环境模拟的计算机硬件系统
单纯实现一个CPU没有什么实际使用意义,通常我们需要基于CPU搭建出一个计算机硬件系统。在对CPU进行功能验证时我们遵循同样的思路,即基于所实现的CPU搭建出一个计算机硬件系统,然后通过在这个计算机硬件系统上运行测试程序来完成CPU的功能验证。这个计算机硬件系统将通过FPGA开发板实现。其核心是在FPGA芯片实现的一个片上系统(System On Chip,简称SoC)。这个SoC芯片通过引脚连接到电路板上的时钟晶振、复位电路,以及LED灯、数码管、按键这些外设接口设备。SoC芯片内部也是一个小系统。在验证5条指令的单周期CPU时,我们采用了一个最简单的SoC——SoC_Mini,其内部结构示意图如图4.2所示,对应的RTL代码均位于minicpu_verify/rtl/目录下。SoC_Mini中的核心是我们将要实现的CPU——miniCPU。这个CPU与指令RAM(inst ram)进行交互完成取指,与confreg进行交互完成外设的访问。除此而外这个小系统中还包含一个PLL模块。
指令RAM和CPU之间的关系读者应该是清楚的。这里简单解释一下PLL和confreg的功能。
本地实验箱开发板上给FPGA芯片提供的时钟(来自于时钟晶振)主频是100MHz如果直接用这个时钟作为SoC_Mini中各个模块的时钟,则意味着miniCPU至少要能达到100MHz的主频。这对于初学者来说这可能是个比较严格的要求,因此我们添加了一个PLL IP,让其以100MHz输入时钟作为参考时钟,输出一个频率低一些的时钟作为miniCPU的时钟输入。
confreg是“configuration register”的简称,是SoC内部的一些配置寄存器。实验开发环境中所搭建的SoC系统中,所搭建的SoC系统中,CPU是通过访问confreg来驱动板上的LED灯、数码管,接收外部按键的输入。简要解释一下这个操控的机理:外部的LED灯、数码管以及按键都是导线直接连接到FPGA的引脚上的,通过控制FPGA输出引脚上的电平的高、低就可以控制LED灯和数码管,同样的一个按键是否按下也可以通过观察FPGA输入引脚上电平的变化来判断。而这些FPGA引脚又进一步连接到confreg中某些寄存器的某些位上。所以CPU可以通过写confreg寄存器来控制输出引脚的电平进而控制LED灯和数码管,也可以通过读confreg寄存器来知晓连接到按键的引脚是高电平还是低电平。
特别提醒一下读者,因为整个SoC_Mini的设计都要实现到FPGA芯片中,所以在进行综合实现的时候,你所选择的顶层应该是soc_mini_top,不是你自己写的minicpu_top。
4.1.2.1.2 miniCPU的顶层接口
为了让各位实现的CPU能够直接集成到上述实验开发环境中进行验证,要对CPU的顶层接口做出明确的规定。miniCPU顶层接口信号的详细定义如表4.1所示。
名称 | 宽度 | 方向 | 描述 |
---|---|---|---|
clk | 1 | input | 时钟信号,来自clk_pll的输出时钟 |
resetn | 1 | input | 复位信号,低电平同步复位 |
inst_sram_we | 1 | output | RAM写使能信号,高电平有效 |
inst_sram_addr | 32 | output | RAM读写地址,字节寻址 |
inst_sram_wdata | 32 | output | RAM写数据 |
inst_sram_rdata | 32 | input | RAM读数据 |
data_sram_we | 1 | output | RAM写使能信号,高电平有效 |
data_sram_addr | 32 | output | RAM读写地址,字节寻址 |
data_sram_wdata | 32 | output | RAM写数据 |
data_sram_rdata | 32 | input | RAM读数据 |
4.2 验证20条指令的单周期CPU
前面已经提到有了20条指令,我们就能开发出更为丰富的测试程序。从20条指令的单周期CPU的验证开始,我们将升级实验开发环境。可参照第2.3.1节中介绍的方式获取该实验开发环境。
4.2.1 mycpu_env实验开发环境组织结构介绍
升级后的实验开发环境位于一级子目录 mycpu_env 下,其目录结构及各部分功能简介如下所示:
|--gettrace/ 生成参考trace的部分。
| |--src/ 设计代码目录。
| | |--tb_top.v 仿真顶层,该模块会抓取debug信息生成到golden_trace.txt中。
| | |--soc_lite_top.v SoC_Lite的顶层文件。
| | |--refCPU/ 产生比对trace的参考处理器核设计。
| | |--CONFREG/ confreg模块,用于访问CPU与开发板上数码管、拨码开关等外设。
| | |--BRIDGE/ 1×2的桥接模块,CPU的data sram接口同时访问confreg和data_ram。
| |--gettrace.xpr Vivado工程文件。
| |--golden_trace.txt 运行func测试程序所生成的参考trace。需自行生成。
|
|--func/ 实验任务所用的功能验证测试程序。
| |--include/ 功能验证测试程序共享的头文件所在目录。
| | |--sysdep.h 一些GCC通用的宏定义的头文件。
| | |--asm.h LoongArch汇编需用到的一些宏定义的头文件,比如LEAF(x)。
| | |--regdef.h LoongArch32 ABI下,32个通用寄存器的汇编助记定义。
| | |--cpu_cde.h SoC_Lite相关参数的宏定义,如访问数码管的confreg的基址。
| | |--inst_test.h 各功能测试点的验证程序使用的宏定义头文件
| |--inst/ 各功能测试点的汇编程序文件。
| | |--Makefile 子目录里的Makefile,会被上一级目录中的Makefile调用。
| | |--n*.S 各功能测试点的验证程序,汇编语言编写。
| |--obj/ 功能验证测试程序编译结果存放目录
| | |--* 详见后面小节的说明。
| |--start.S 功能验证测试的引导代码及主函数。
| |--Makefile 编译功能验证测试程序的Makefile脚本
| |--bin.lds 编译bin.lds.S得到的结果,可被make reset命令清除
| |--convert.c 生成coe和mif文件的处理工具的C程序源码
|
|--myCPU/ 自己实现的CPU的RTL代码。
|
|--soc_verify/ 自己实现的CPU的SoC系统验证环境
|--soc_dram/ CPU对外连接distributed RAM接口时对应的验证环境。
| |--rtl/ SoC_Lite设计代码目录。
| | |--soc_lite_top.v SoC_Lite的顶层文件。
| | |--CONFREG/ confreg模块,用于访问CPU与开发板上数码管、拨码开关等外设。
| | |--BRIDGE/ 1×2的桥接模块, CPU的data sram接口同时访问confreg和data_ram。
| | |--xilinx_ip/ 定制的Xilinx IP,包含clk_pll、inst_ram、data_ram。
| |--testbench/ 功能仿真验证平台。
| | |--mycpu_tb.v 功能仿真顶层,该模块会抓取debug信息与golden_trace.txt进行比对。
| |--run_vivado/ Vivado工程的运行目录。
| |--constraints/ Vivado工程设计的约束。
| |--mycpu_dram_prj/ Vivado工程文件所在目录。
顺带说一下,从实践任务6开始,本书第二部分的所有实践任务都将使用 mycpu_env 实验开发环境,因此这个实验环境中包含了较多的内容。读者不用一上来把这些内容都掌握,只需要根据实验进度逐步掌握新的内容即可。读者可能会发现 mycpu_env 目录下还有一些内容没有在上面列出并说明。这些没有列出的部分与现阶段的实践任务没有关系,会在后面相关的实验任务中再介绍。
4.2.1.1 SoC_Lite片上系统结构介绍
mycpu_env实验开发环境中所采用的片上系统也升级为SoC_Lite,其内部结构示意如图4.3所示。
可以看到SoC_Lite与之前的SoC_Mini相比,主要是多了数据RAM(data ram)和myCPU与data ram、confreg之间的“一分二”部件。数据RAM和CPU之间的关系之前已经说过,这里简单解释一下myCPU与data ram、confreg之间的“一分二”部件。
mycpu和dram、confreg之间有一个“一分二”部件。这是因为在LoongArch指令系统架构下,所有I/O设备的寄存器都是采用memory mapped方式访问的。我们这里实现的confreg也不例外。Memory mapped的访问方式意味I/O设备中的寄存器各自都有一个唯一内存编址,所以CPU可以通过load、store指令对其进行访问。不过dram作为内存也是通过load、store指令进行访问的。那么对于一条load或store指令来说,如何知晓它访问的是confreg还是dram?我们在设计SoC的时候用地址对其进行区分。因此在设计SoC的数据通路时就需要在这里引入一个“一分二”部件,它的选择控制信号生成是通过对访存的地址范围进行判断而得到的。
4.2.2 基于trace比对的调试框架
在实践任务5中,我们采用的是比较“原始”的调试方式。随着所实现CPU的复杂度提升,仅用这种调试方式对初学者无异于是个“灾难”。为此我们提供了一套基于trace比对的调试辅助手段,用以帮助在调试过程中更加快速的定位。
4.2.2.1 基于trace比对的调试辅助手段
读者在调试C程序的时候应该都使用过单步调试这种调试手段。在“慢动作”运行程序的每一行代码的情况下,能够及时看到每一行代码的运行行为是否符合预期,从而能够及时定位到出错点。我们在实验开发环境中提供给读者的这套基于trace比对的调试辅助手段,借鉴的就是这种“单步调试”的策略。
其具体实现方式是:我们先用一个已知的功能上是正确的CPU运行一遍测试指令序列,将每条指令的PC和写寄存器的信息记录下,记为golden_trace;然后在验证myCPU的时候运行相同的指令序列,在myCPU每条指令写寄存器的时候,将myCPU中的PC和写寄存器的信息同之前的golden_trace进行比对,如果不一样,那么立刻报错并停止仿真。
对LoongArch指令熟悉的读者可能马上就会问:有些转移指令和store指令不写寄存器,上面的方式没法判断啊?这些读者提的问题相当对。不过一旦分支跳转的不对,那么错误路径上第一条会写寄存器的指令的PC就会和golden_trace中的不一致,就会报错并停下来。store指令执行错了,后续从这个位置读数的load指令写入寄存器的值就会与golden_trace中的不一致,也会报错并停下来。虽然报错的位置稍微有些靠后,但总体上还是有规律可循的。分支指令和store指令的及时报错不是不可以实现,只不过它会进一步增加myCPU上调试接口的复杂度,也会在一定程度上限制myCPU实现分支指令和store指令的自由度,权衡利弊后,我们采取了用少量投入解决大部分问题的设计思路。
上面我们介绍了利用trace进行功能仿真验证错误定位的基本思路,下面我们再具体介绍一下如何生成golden_trace,以及myCPU验证的时候是如何利用golden_trace进行比对的。
4.2.2.2 利用参考模型生成golden_trace
功能验证程序func编译完成后,就可以使用验证平台里的gettrace工程运行仿真生成参考trace了。gettrace这个工程中里所用的SoC_Lite和验证myCPU所用SoC_Lite架构几乎一样,主要区别就是里面集成了一个已验证过的功能完备的参考处理器核。
仿真顶层为gettrace/src/tb_top.v,与抓取golden_trace相关的重要代码如下:
......`define TRACE_REF_FILE "../../../../golden_trace.txt" //参考trace的存放目录
`define END_PC 32'h1c000100 //func测试完成后会32'h1c000100处死循环
......assign debug_wb_pc = soc_lite.debug_wb_pc;
assign debug_wb_rf_we = soc_lite.debug_wb_rf_we;
assign debug_wb_rf_wnum = soc_lite.debug_wb_rf_wnum;
assign debug_wb_rf_wdata = soc_lite.debug_wb_rf_wdata;
......// open the trace file;
integer trace_ref;
initial begin
$fopen(`TRACE_REF_FILE, "w"); //打开trace文件
trace_ref = end
// generate trace
always @(posedge soc_clk)
begin
if(|debug_wb_rf_we && debug_wb_rf_wnum!=5'd0) //trace采样时机
begin
$fdisplay(trace_ref, "%h %h %h %h" , `CONFREG_OPEN_TRACE ,
//trace采样信号
debug_wb_pc, debug_wb_rf_wnum, debug_wb_rf_wdata_v); end
end
......
Trace采样的信号包括:
- 写回(Write Back,WB)指令的PC。
- 写回指令的写使能。
- 写回指令的目的寄存器号。
- 写回指令的写回值。
显然并不是每时每刻CPU都有写回,因此Trace采样需要有一定的时机。写回指令的写使能有效是指:该指令对通用寄存器堆的写使能信号有效且写回的目的寄存器号非0。大家可以思考下,为什么此处判断写回的目的寄存器非0时才采样?
4.2.2.3 使用 golden_trace 监控 myCPU
myCPU功能验证所使用的 SoC_Lite 与 gettrace 工程中的架构一致,但 testbench 有所不同,见 mycpu_verify/soc_verify/soc_XXX/testbench/mycpu_tb.v,重点部分代码如下:
......`define TRACE_REF_FILE "../../../../../../../gettrace/golden_trace.txt"
//参考trace的存放目录
`define CONFREG_NUM_REG soc_lite.confreg.num_data //confreg中数码管寄存器的数据
`define END_PC 32'h1c000100 //func测试完成后会在32'h1c000100处死循环
......assign debug_wb_pc = soc_lite.debug_wb_pc;
assign debug_wb_rf_we = soc_lite.debug_wb_rf_we;
assign debug_wb_rf_wnum = soc_lite.debug_wb_rf_wnum;
assign debug_wb_rf_wdata = soc_lite.debug_wb_rf_wdata;
......//get reference result in falling edge
reg [31:0] ref_wb_pc;
reg [4 :0] ref_wb_rf_wnum;
reg [31:0] ref_wb_rf_wdata_v;
always @(negedge soc_clk) //下降沿读取参考trace
begin
if(|debug_wb_rf_we && debug_wb_rf_wnum!=5'd0 && !debug_end && `CONFREG_OPEN_TRACE)
//读取trace时机与采样时机相同
begin
$fscanf(trace_ref, "%h %h %h %h" , trace_cmp_flag ,
//读取参考trace信号
ref_wb_pc, ref_wb_rf_wnum, ref_wb_rf_wdata); end
end
//compare result in rsing edge
always @(posedge soc_clk) //上升沿将debug信号与trace信号对比
begin
if(!resetn)
begin
1'b0;
debug_wb_err <= end
else if(|debug_wb_rf_we && debug_wb_rf_wnum!=5'd0 && !debug_end && `CONFREG_OPEN_TRACE)
//对比时机与采样时机相同
begin
if ( (debug_wb_pc!==ref_wb_pc) || (debug_wb_rf_wnum!==ref_wb_rf_wnum)
//对比时机与采样时机相同
||(debug_wb_rf_wdata_v!==ref_wb_rf_wdata_v) ) begin
$display("--------------------------------------------------------------");
$display("[%t] Error!!!",$time);
$display(" reference: PC = 0x%8h, wb_rf_wnum = 0x%2h, wb_rf_wdata = 0x%8h",
ref_wb_pc, ref_wb_rf_wnum, ref_wb_rf_wdata_v);$display(" mycpu : PC = 0x%8h, wb_rf_wnum = 0x%2h, wb_rf_wdata = 0x%8h",
debug_wb_pc, debug_wb_rf_wnum, debug_wb_rf_wdata_v);$display("--------------------------------------------------------------");
1'b1; //标记出错
debug_wb_err <= #40;
$finish; //对比出错,则结束仿真
end
end
end
......//monitor test
initial
begin
$timeformat(-9,0," ns",10);
while(!resetn) #5;
$display("==============================================================");
$display("Test begin!");
while(`CONFREG_NUM_MONITOR)
begin
#10000; //每隔10000ns,打印一次写回级PC,帮助判断CPU是否死机或死循环
$display (" [%t] Test is running, debug_wb_pc = 0x%8h", debug_wb_pc);
end
end
//test end
wire global_err = debug_wb_err || (err_count!=8'd0);
always @(posedge soc_clk)
begin
if (!resetn)
begin
1'b0;
debug_end <= end
else if(debug_wb_pc==`END_PC && !debug_end)
begin
1'b1;
debug_end <= $display("==============================================================");
$display("Test end!");
$fclose(trace_ref);
#40;
if (global_err)
begin
$display("Fail!!!Total %d errors!",err_count); //全局出错,打印Fail
end
else
begin
$display("----PASS!!!"); //全局无错,打印PASS.
end
$finish;
end
end
......
4.2.3 func功能测试程序
除了新增的gettrace功能,读者还会发现 mycpu_env/func/ 目录下的内容比之前 minicpu_env/func/ 目录下的增加了许多。这里对新的func程序做一些简要说明。
4.2.3.1 func测试程序说明
func程序分为func/start.S和func/inst/*.S,都是LoongArch32汇编程序:
- func/start.S :主函数,执行必要的启动初始化后调用func/inst/下的各汇编程序。
- func/inst/*.S :针对每条指令或功能点有一个汇编测试程序。
- func/include/*.h :测试程序的配置信息和宏定义。
主函数func/start.S中主体部分代码如下,分为三大部分,具体查看注释。
......
#以下是设置程序开始的LED灯和数码管显示,单色LED全灭,双色LED灯一红一绿。
LI (a0, LED_RG1_ADDR)
LI (a1, LED_RG0_ADDR)
LI (a2, LED_ADDR)
LI (s1, NUM_ADDR)
LI (t1, 0x0002)
LI (t2, 0x0001)
LI (t3, 0x0000ffff)
lu12i.w s3, 0
NOP4
st.w t1, a0, 0
st.w t2, a1, 0
st.w t3, a2, 0
st.w s3, s1, 0
#以下是运行各功能点测试,每个测试完执行idle_1s等待一段时间,且数码管显示加1。
inst_test:
bl n1_lu12i_w_test #lu12i.w
bl idle_1s
bl n2_add_w_test #add.w
bl idle_1s
......
#以下是显示测试结果,PASS则双色LED灯亮两个绿色,单色LED不亮;
#Fail则双色LED灯亮两个红色,单色LED灯全亮。
test_end:
LI (s0, TEST_NUM)
NOP4
beq s0, s3, 1f
LI (a0, LED_ADDR)
LI (a1, LED_RG1_ADDR)
LI (a2, LED_RG0_ADDR)
LI (t1, 0x0002)
NOP4
st.w zero, a0, 0
st.w t1, a1, 0
st.w t1, a2, 0
......
inst/ 目录下每个功能点的测试代码程序名为n#_*_test.S,其中“#”为编号,如有15个功能点测试,则从n1编号到n15。每个功能点的测试,其测试代码大致如下。其中红色部分标出了关键的3处代码。
......
LEAF(n1_lu12i_w_test)
addi.w s0, s0 ,1 #加载功能点编号s0++
addi.w s2, zero, 0x0
lu12i.w t2, 0x1
###test inst
addi.w t1, zero, 0x0
TEST_LU12I_W(0x00000, 0x00000)
...... #测试程序,省略
TEST_LU12I_W(0xff0af, 0xff0a0)
###detect exception
bne s2, zero, inst_error
###score ++ #s3存放功能测试计分,每通过一个功能点测试,则+1
addi.w s3, s3, 1
###output (s0<<24)|s3
inst_error:
slli.w t1, s0, 24
NOP4
or t0, t1, s3 #s0高8位为功能点编号,s3低8位为通过功能点数,
#相或结果显示到数码管上。
NOP4
st.w t0, s1, 0 #s1存放数码管地址
jirl zero, ra, 0
END(n1_lu12i_w_test)
从以上可以看到,测试程序的行为是:当通过第一个功能测试后,数码管会显示0x0100_0001,随后执行idle_1s;执行第二个功能点测试,再次通过数码管会显示0x0200_0002,执行idle_1s……依次类推。显示,每个功能点测试通过,应当数码管高8位和低8位永远一样。如果中途数码管显示从0x0500_0005变成了0x0600_0005,则说明运行第六个功能点测试出错。
最后来看 start.S
文件中 idle_1s
函数的代码,其使用一个循环来暂停测试程序执行的。其主体部分代码如下:
idle_1s:
......
#initial t3 //读取confreg模块里的switch_interleave的值
ld.w t2, t0, 0 #switch_interleave: {switch[7],1'b0, switch[6],1'b0...switch[0],1'b0}
NOP4
xor t2, t2, t1 //拨码开关拨上为0,故要xor来取反
NOP4
slli.w t3, t2, 9 #t3 = switch interleave << 9
NOP4
sub1:
addi.w t3, t3, -1 //t3累减1
#select min{t3, switch_interleave} //获取t3和当前switch_interleave的最小值
ld.w t2, t0, 0 #switch_interleave:{switch[7],1'b0,switch[6],1'b0...switch[0],1'b0}
NOP4
xor t2, t2, t1
NOP4
slli.w t2, t2, 9 #switch interleave << 9
NOP4 //以上ld.w-xor-slli.w三条指令再次获取switch_interleave
sltu t4, t3, t2 //无符号比大小,如果t3比switch_interleave 小则置t4=1
NOP4
bne t4, zero, 1f //t4!=0,意味着t3比switch_interleave大,则跳1f
nop
addi.w t3, t2, 0 //否则,将t3赋值为更小的switch_interleave
NOP4
1:
bne t3,zero, sub1 //如果t3没有减到0,则返回循环开头
jirl zero, ra, 0 //结束idle_1s
从以上代码可以看到,idle_1s 会依据拨码开关的状态设定循环次数。在仿真环境下,我们会模拟拨码开关为全拨下的状态,以使 idle_1s 循环次数最小。之所以这样设置,是因为 FPGA 运行远远快于仿真的速度,假设CPU运行一个程序需要106个CPU周期,再假设CPU在FPGA上运行频率为10MHz,那其在FPGA上运行完一个程序只需要0.1s;同样,我们仿真运行这个程序,假设我们仿真设置的CPU运行频率也是10MHz,那我们仿真运行完这个程序也是只需要0.1s吗?显然这是不可能的,仿真是软件模拟CPU运行情况的,也就是它要模拟每个周期CPU内部的变化,运行完这一个程序,需要模拟106个CPU周期。我们在一台2016年产的主流X86台式机上进行实测发现,Vivado自带的Xsim仿真器运行SoC_Lite的仿真,每模拟一个周期大约需要600us,这意味着Xsim上模拟106个周期所花费的实际时间约10分钟。
同一程序,运行仿真测试大约需要10分钟,而在FPGA上运行只需要0.1秒(甚至更短,比如CPU运行在50MHz主频则运行完程序只需要0.02s)。所以我们如果不控制好仿真运行时的 idle_1s 函数,则我们可能会陷入到idle_1s长时间等待中;类似的,如果我们上板时设定 idle_1s 函数很短(比如拨码开关全拨下),则 idle_1s 时间太短导致我们无法看到数码管累加的效果
如果大家在自实现CPU上板运行过程中,发现数码管累加跳动太慢,请调小拨码开关代表的数值;如果发现数码管累加跳动太快,请调大拨码开关代表的数值。
4.2.3.2 func 功能点配置
mycpu_env/func/inst/ 目录下包含所有的功能点测试,读者在进行具体实验时,需要根据实践任务编号配置 mycpu_env/func/include/test_config.h 文件。该文件内容如下:
// =========================================================================
// exp6 : n1~n20 SHORT_TEST1 1 NOP_INSERT 0 TEST1 1 TEST2 0 TEST3 0
// TEST4 0 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp7 : n1~n20 SHORT_TEST1 0 NOP_INSERT 1 TEST1 1 TEST2 0 TEST3 0
// TEST4 0 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp8~9 : n1~n20 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 0 TEST3 0
// TEST4 0 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp10 : n1~n36 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 0
// TEST4 0 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp11 : n1~n46 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 0 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp12 : n1~n47 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 1 TEST5 0 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp13~16 : n1~n58 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 1 TEST5 1 TEST6 0 TEST7 0 TEST8 0 TEST9 0
// exp18 : n1~n70 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 1 TEST5 1 TEST6 1 TEST7 0 TEST8 0 TEST9 0
// exp19, 21~22 : n1~n72 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 1 TEST5 1 TEST6 1 TEST7 1 TEST8 0 TEST9 0
// exp23 : n1~n79 SHORT_TEST1 0 NOP_INSERT 0 TEST1 1 TEST2 1 TEST3 1
// TEST4 1 TEST5 1 TEST6 1 TEST7 1 TEST8 1 TEST9 0
// ==========================================================================================================================
//==================================================================
//SHORT_TEST1: less test case for n1~n20.
// Only set for exp6.
//==================================================================
#define SHORT_TEST1 0
//==================================================================
//NOP_INSERT: Insert 4 nop insts between every alu operation.
// Only set for exp7.
//==================================================================
#define NOP_INSERT 0
#define TEST1 0
#define TEST2 0
#define TEST3 0
#define TEST4 0
#define TEST5 0
#define TEST6 0
#define TEST7 0
#define TEST8 0
#define TEST9 0
开始一个新的实践任务前,请根据 test_config.h
头部注释信息中查明该实验对应的SHORT_TEST1
, NOP_INSERT
, TEST1~TEST9
这12个配置宏的数值,修改该文件下部各个宏变量的定义值。
4.2.3.3 LoongArch-GCC交叉编译工具的安装
自行编译func程序需要使用LoongArch32R的GCC交叉编译工具。该工具链的安装可以从 https://gitee.com/loongson-edu/la32r-toolchains 下载源码自行编译、安装,也可以直接从 https://gitee.com/loongson-edu/la32r-toolchains/releases 下安装包。我们这里主要介绍后一种方式的安装步骤。
下载安装包时请根据所用机器是X86还是LoongArch选择对应的版本。下载压缩包 loongarch32r-linux-gnusf-*.tar.gz
至Linux操作系统自身的文件系统中。需要特别提醒的是,目前X86版本LoongArch32R的GCC交叉编译工具只支持64位系统(在系统下运行uname -a
命令显示架构为x86_64
的)。接下来:
(1)打开一个terminal,进入压缩包所在目录,进行解压:
sudo tar zxvf loongarch32r-linux-gnusf-*.tar.gz -C /opt/ $
(2)确保目录/opt/loongarch32r-linux-gnusf-*/bin/
存在,随后执行:
echo “export PATH=/opt/loongarch32r-linux-gnusf-*/bin/:$PATH” >> ~/.bashrc $
(3)重新打开一个terminal,输入loongarch32然后敲击tab键,如果能够-linux-gnusf-之类的补全,就说明工具链已经安装成功。此时可以编写一个hello.c 然后用工具链进行编译看其是否可以工作。
loongarch32r-linux-gnusf-gcc hello.c $
4.2.3.4 func测试程序编译脚本说明
func测试程序的编译脚本为验证平台目录下的func/Makefile
,对Makefile了解的可以去看下该脚本。该脚本支持以下命令:
make help
:查看帮助信息。make
:编译得到仿真下使用的结果。make clean
:删除*.o,*.a和./obj/目录。
4.2.3.5 func测试程序编译结果说明
func测试程序编译结果位于func/obj/
下,本书第二部分实践任务相关的共有3个文件,各文件具体解释见表4.2。
文件名 | 解释 |
---|---|
inst_ram.coe | 重新定制inst ram所需的coe文件 |
inst_ram.mif | 仿真时inst ram读取的mif文件 |
test.s | 对main.elf反汇编得到的文件 |
4.2.3.6 func测试程序的装载
我们开发的测试程序用GCC工具编译之后形成一个ELF格式的可执行文件——main.elf。那么我们是要在自己设计的CPU上直接运行这个ELF格式的可执行文件吗?显然不是。我们测试的环境俗称“裸机”,它是一台没有运行任何操作系统或者监控环境的单纯的硬件系统,所以文件系统、可执行程序的加载器等软件统统都没有。
我们其实真正需要的其实是main.elf的代码和初始数据2。我们只要能把这些代码和初始数据提取出来,将代码放到指令RAM中,将初始数据放到数据RAM,那么就可以把CPU跑起来了。所以我们用工具链中的objcopy工具,将main.elf文件中的.text段提取出来生成二进制格式的纯数据文件main.bin,将main.elf文件中的.data段提取出来生成二进制格式的纯数据文件main.data。
接下来就是把这些信息怎么“装”到RAM中去了。我们利用的是 Xilinx FPGA 中 block RAM IP 的初始内容加载功能。该功能需要将加载的内容按照规定的格式生成文本文件。于是我们进一步将前面得到的main.bin和main.data转换为所需的文本文件。每个二进制纯数据文件都生成一个.coe后缀的文件和一个.mif后缀的文件。这两个文件的数据内容其实完全相同,只是文档的其它格式信息存在差异。coe文件是用于生成上板配置文件的,而mif文件是用于功能仿真的。我们建议读者在调试过程中不要通过直接修改coe文件或是mif文件的方式来调整测试激励,除非你真的搞清楚了你的修改会影响哪个环节的验证结果。
4.2.3.7 func测试仿真验证结果判断
仿真结果正确判断有两种方法。
第一种方法,也是最简单的,就是看Vivado控制台打印Error还是PASS。正确的控制台打印信息如图4.4。
第二种方法,是通过波形窗口观察程序执行结果func正确的执行行为,抓取confreg模块的信号led_data、led_rg0_data、led_rg1_data、num_data: 1. 开始,单色LED写全1表示全灭,双色LED写0x1和0x2表示一红一绿,数码写全0; 2. 执行过程中,单色LED全灭,双色LED灯一红一绿,数码管高8位和低8位同步累加; 3. 结束时,单色LED写全1表示全灭,双色LED均写0x1表示亮两绿,数码管高8位和低8位数值(十六进制)相同,对应测试功能点数目。
4.2.3.8 func测试FPGA上板验证结果判断
在FPGA上板验证时其结果正确与否的判断也只有一种方法,func正确的执行行为是:
- 开始,单色LED全灭,双色LED灯一红一绿,数码管显示全0;
- 执行过程中,单色LED全灭,双色LED灯一红一绿,数码管高8位和低8位同步累加;
- 结束时,单色LED全灭,双色LED灯亮两绿,数码管高8位和低8位数值相同,对应测试功能点数目。
如果func执行过程中出错了,则数码管高8位和低8位第一次不同处即为测试出错的功能点编号,且最后的结果是单色LED全亮,双色LED灯亮两红,数码管高8位和低8位数值不同。
最后FPGA验证通过的效果,类似图4.6,数码管高8位和低8位显示为运行的功能点数目。
4.2.4 基于mycpu_env实验开发环境的实验流程
mycpu_env 实验开发环境丰富了func测试程序并引入了trace比对调试机制,整个实验流程上也比基于 minicpu_env 实验开发环境的增加了编译测试程序、生成比对Trace两个步骤。我们来小结一下。
4.2.4.1 获取实验开发环境
mycpu_env 实验开发环境的获取方式与前面的实践任务相同,若仍不熟悉请回顾第2.3.1节中的介绍。该开发环境具体位置位于项目的一级子目录 mycpu_env 下。
4.2.4.4 编译测试程序
这是新增的步骤。如果你获取实验环境是采用直接下载并解压指定实验的压缩包 expXX.zip 这种方式且实验过程中不考虑调整测试程序,那么这个步骤直接直接跳过,因为所提供的压缩包中已经包含了事先编译好的结果。否则的话,需要进入func目录,根据将要进行的实验修改include目录下测试功能点配置文件test_config.h,然后先运行make clean,再运行make。
如果是在Windows操作系统下面运行Vivado:先确保你的虚拟机中运行着一个已经安装了LoongArch-GCC交叉编译工具的Linux操作系统,将func目录设置为虚拟机共享目录3。在虚拟机的Linux操作系统中进行上述编译操作,回到Windows操作系统下,确认func/obj/整个目录下的内容确实是最新编译更新的。
有关LoongArch-GCC交叉编译工具的安装,请参看4.2.3.3节内容。
4.2.4.5 生成比对Trace
进入gettrace/目录,打开Vivado工程gettrace.xpr,进行仿真,生成参考结果golden_trace.txt。重点关注此时inst_ram加载的确实是前一个步骤编译出的结果。要等仿真运行完成,golden_trace.txt才有完整的内容。
4.2.4.7 上板验证自己的CPU
该步骤的操作与基于mini_cpu实验开发环境的基本一致。若仿真结果正常即可进入上板检测环节。回到mycpu这个工程中,进行综合实现,成功后即上板进行检测,观察实验箱上数码管显示结果是否与要求的一致。若一致则此次实验成功;否则转到下面的调试阶段进行问题排查。
4.2.4.8 调试自己的CPU
该步骤在基于mini_cpu实验开发环境中也应有,不过因为最初的实验难度低,故未重点强调。调试时请按照下列步骤排查,然后重复仿真验证、上板验证、调试三个过程直至正确。
- 复核生成、下载的bit文件是否正确。
- 如果判断生成的bit文件不正确,则重新生成bit文件。
- 如果判断生成的bit文件正确,转步骤2
- 复核仿真结果是否正确。
- 如果仿真验证结果不正确4,则回到前面仿真验证步骤。
- 如果仿真验证结果正确,转步骤3。
- 检查实现(Implementation)后的时序报告(Vivado界面左侧“IMPLEMENTATION”\(\rightarrow\)“Open Implemented Design”\(\rightarrow\)“Report Timing Summary”)。
- 如果发现时序不满足,则在Verilog设计里调优不满足的路径,然后回到前面的仿真验证环节依序执行各项操作;或者降低SoC_Lite的运行频率,即降低clk_pll模块的输出端频率,然后回到前面上板验证环节依序执行各项操作。
- 如果实现时时序是满足的,转步骤4。
- 认真排查综合和实现时的Warning。
- Critical warning是强烈建议要修正的,warning是建议尽量修正的,然后回到前面上板验证环节依序执行各项操作。
- 如果没有可修正的Warning了,转步骤5。
- 人工检查RTL代码,避免多驱动、阻塞赋值乱用、模块端口乱接、时钟复位信号接错、模块调用处的输入输出接反,查看那些从别处模仿来的“酷炫”风格的代码,查找有没有仿真时被force住的值导致仿真和上板不一致……如果怎么看代码都看不出问题,转步骤6。
- 参考附录C第1节“使用Chipscope在线调试”进行板上在线调试;如果调试了半天仍然无法解决问题,转步骤7。
- 反思。真的,现在除了反思还能干什么?
根据我们的教学和培训经验,在此重点提醒读者,很多“仿真通过,上板不过”都是以下问题之一导致的:
- 多驱动。
- 模块的input/output端口接入的信号方向不对。
- 时钟复位信号接错。
- 代码不规范,阻塞赋值乱用,always语句随意使用。
- 仿真时控制信号有“X”。仿真时,有“X”调“X”,有“Z”调“Z”。特别是设计的顶层接口上不要出现“X”和“Z”。
- 时序违约。
- 模块里的控制路径上的信号未进行复位。
4.2.5 mycpu_env 实验开发环境使用进阶
4.2.5.1 重新生成golden_trace.txt
每当我们更新func程序并重新编译后,切记在gettrace里重新生成golden_trace.txt,具体步骤如下:
- 打开gettrace工程,确保soc_lite_top.v中INST_COE宏定义指向更新后func生成的mif文件。
- 运行仿真,仿真结束后gettrace目录下的golden_trace.txt会被更新。
4.2.5.2 重新定制inst_ram
每当我们更新func程序并重新编译后,需要重新定制inst_ram使其应用func/obj/目录下的最新内容。
注意,只有soc_verify下的工程需重新定制inst_ram,gettrace下的工程则是在soc_lite_top.v中INST_COE宏定义直接指向了func/obj/目录下的mif文件。重新定制inst_ram的流程如下:
- 在Sources窗口中找到inst_ram IP,双击或者右键后点击”Re-customize IP…”,进入定制界面。
- 在弹出的重新定制IP界面中(如图4.7所示)选择Other Options选项卡,勾选Load Init File,并且在Coe File中通过点击“Browse”选择新生成的inst_ram.coe,点击OK。
- 在弹出的Generate Output Product窗口中点击Generate(根据需要选择global或ooc模式),完成inst_ram的重新定制。
4.2.5.3 替换mif文件快速仿真
本小节内容仅供参考,如果未掌握本节内容,完全不影响实践任务的开展。
前面讲到的重新定制inst_ram比较耗时,如果只进行仿真的话可以选择直接替换Vivado项目中所用mif文件的方法来达到更改inst_ram初始值的目的。以单周期CPU的实验开发环境为例,在启动仿真界面后,会发现工程文件中出现下面的目录
soc_verify/soc_dram/run_vivado/mycpu_prj1/mycpu.sim/sim_1/behave/xsim/
,
将该目录中的inst_ram.mif替换成软件环境obj目录下的inst_ram.mif
(CPU_CDE/func/obj/inst_ram.mif
),再点击“Restart”(注意:不是“Relaunch Simulation”)回到零时刻,点击“run all”开始仿真,此时inst_ram的初始值就会变成更新后的inst_ram.mif中的数据。
注意:每当新打开仿真或者点击”Relaunch Simulation”时,inst_ram.mif的文件会根据先前定制好的inst_ram来重新生成。
4.3 任务与实践
完成本章的学习后,希望读者能够完成以下2个实践任务:
4.3.1 实践任务5:5条指令单周期CPU
本实践任务要求:
- 阅读并理解实验环境中提供的代码,补充代码中缺失的部分,使设计可以通过仿真和上板验证。
请参照第2.3.1节中介绍的方式获取本次实践任务所需的实验开发环境。具体的实验环境位于 mini_env/ 目录下,其组织结构和使用方式已在本章4.1节介绍过,此处不再重复。
实验环境准备就绪后,请参考下列步骤完成本实践任务:
- 在 minicpu_env/soc_verify/run_vivado/ 目录下打开miniCPU工程。如需要,请参考附录D.4节进行工程和IP核升级。
- 对miniCPU工程中的inst_ram重新定制,选择对应func的coe文件(minicpu_env/func/inst_ram.coe)。
- 运行miniCPU工程的仿真(进入仿真界面后,直接点击run all),开始调试。可以修改 minicpu_env/soc_verify/testbench/ 目录下的minicpu_tb.v文件中的switch值观察led输出值是否符合预期(每次修改switch值之后都要重新仿真)。(因为本实验的测试程序为斐波那契数程序,斐波那契数列是:0,1,1,2,3,5,……从第三项开始,每一项都等于前两项之和。规定数列第三项为f(1),即f(1)=1,f(2)=2,f(3)=3,f(4)=5, …… 。修改拨码开关switch值相当于修改n,led输出值对应f(n)。
- myCPU仿真通过后,综合实现后生成bit流文件,进行上板验证。(如果无硬件实验平台,请跳过该步骤。)
4.3.2 实践任务6:20条指令单周期CPU
本实践任务要求: 1. 结合本章中讲述的设计方案,阅读并理解mycpu_env/myCPU/目录下提供的代码。 2. 完成代码调试。我们提供的代码中加入了若干错误,请大家通过仿真波形的调试修复这些错误,使设计可以通过仿真和上板验证。
请参照第2.3.1节中介绍的方式获取本次实践任务所需的实验开发环境。具体的实验环境位于 mycpu_env/ 目录下,其组织结构和使用方式已在本章4.2节介绍过,此处不再重复。
实验环境准备就绪后,请参考下列步骤完成本实践任务:
- 修改func配置文件——mycpu_env/func/include/test_config.h,选择exp6的配置,编译。(如果是通过压缩包exp6.zip获取实验开发环境的,请跳过该步骤。)
- 打开gettrace工程——mycpu_env/gettrace/gettrace.xpr。(该Vivado工程中的IP核是使用Vivado2019.2创建的,如果使用更高版本的Vivado打开,请参考附录D.4节进行IP核升级。)运行gettrace工程的仿真(进入仿真界面后,直接点击run all等待仿真运行完成),生成新的参考trace文件golden_trace.txt(mycpu_env/gettrace/golden_trace.txt)。要等仿真运行完成,golden_trace.txt才有完整的内容。(如果是通过压缩包exp6.zip获取实验开发环境的,请跳过该步骤。)
- 进入 mycpu_env/soc_verify/soc_dram/run_vivado/ 目录下启动验证myCPU的工程。如果该目录下尚未创建工程,请参照附录D.2节介绍的步骤,利用该目录下的 create_project.tcl 文件创建工程。如需要,请参考附录D.4节进行IP核升级。
- 参考本章4.2.5.2小节,对工程中的inst_ram重新定制。(如果是通过压缩包exp6.zip获取实验开发环境的,请跳过该步骤。)
- 在验证myCPU的工程中运行仿真(进入仿真界面后,直接点击run all),进行功能验证与调试,直至仿真测试通过。
- 在验证myCPU的工程中综合实现后生成bit流文件,进行上板验证。(如果无硬件实验平台,请跳过该步骤。)
Vivado中集成的文本编辑器功能比较简单,强烈建议各位使用一个专门用于代码开发的文本编辑软件来编写代码。↩︎
后期的某些实验中,测试程序需要一定量的输入数据,如果采用立即数加载的方式产生将耗费较多的CPU执行时间,届时我们会将这些输入数据以赋了初值的全局静态变量的形式传給测试程序。这些全局变量的初值将记录在ELF文件的只读数据段中。↩︎
针对本书中实验所涉及的程序编译工作,Windows操作系统自带的Windows Subsystem for Linux 2 (WSL2) 就已经完全够用。WSL2无需进行此操作,即可通过访问/mnt/XXX完成对Windows系统下XXX目录的访问。↩︎
如果仿真验证都没有通过就上板,我们只能表扬你勇气可嘉。↩︎