自己动手写CPU之第四阶段(2)验证第一条指令ori的实现效果
将陆续上传本人写的新书《自己动手写CPU》(尚未出版),今天是第12篇,我尽量每周四篇 书名又之前的《自己动手写处理器》改为《自己动手写CPU》 4.3 验证OpenMIPS实现效果 4.3.1指令存储器ROM的实现 本节将验证我们的OpenMIPS是否实现正确,包含:流水线是
将陆续上传本人写的新书《自己动手写CPU》(尚未出版),今天是第12篇,我尽量每周四篇
书名又之前的《自己动手写处理器》改为《自己动手写CPU》
4.3 验证OpenMIPS实现效果
4.3.1指令存储器ROM的实现
本节将验证我们的OpenMIPS是否实现正确,包含:流水线是否正确、ori指令是否实现正确。在验证之前,需要首先实现指令存储器,以便OpenMIPS从中读取指令。
指令存储器模块是只读的,其接口如图4-7所示,还是采用左边是输入接口,右边是输出接口的方式绘制,这样便于理解。接口含义如表4-12所示。
指令存储器ROM模块在文件inst_rom.v中实现,代码如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module inst_rom( input wire ce, input wire[`InstAddrBus] addr, output reg[`InstBus] inst ); // 定义一个数组,大小是InstMemNum,元素宽度是InstBus reg[`InstBus] inst_mem[0:`InstMemNum-1]; // 使用文件inst_rom.data初始化指令存储器 initial $readmemh ( "inst_rom.data", inst_mem ); // 当复位信号无效时,依据输入的地址,给出指令存储器ROM中对应的元素 always @ (*) begin if (ce == `ChipDisable) begin inst代码很好理解,有以下几点说明。
(1)在初始化指令存储器时,使用了initial过程语句。initial过程语句只执行一次,通常用于仿真模块中对激励向量的描述,或用于给变量赋初值,是面向模拟仿真的过程语句,通常不能被综合工具支持。所以如果要将本章实现的OpenMIPS处理器使用综合工具进行综合,那么需要修改这里初始化指令存储器的方法。
(2)在初始化指令存储器时,使用了系统函数$readmemh,表示从inst_rom.data文件中读取数据以初始化inst_mem,而inst_mem正是之前定义的数组。inst_rom.data是一个文本文件,里面存储的是指令,其每行存储一条32位宽度的指令(使用十六进制表示),系统函数$readmemh会将inst_rom.data中的数据依次填写到inst_mem数组中。
(3)OpenMIPS是按照字节寻址的,而此处定义的指令存储器的每个地址是一个32bit的字,所以要将OpenMIPS给出的指令地址除以4再使用,比如:要读取地址0xC处的指令,那么实际就是对应ROM的inst_mem[3],如图4-8所示。
除以4也就是将指令地址右移2位,所以在读取的时候给出的地址是addr[`InstMemNumLog2+1:2],其中InstMemNumLog2是指令存储器的实际地址宽度,比如:如果inst_mem有1024个元素,那么InstMemNum等于1024,InstMemNumLog2等于10,表示实际地址宽度为10。
4.3.2 最小SOPC的实现
为了验证,需要建立一个SOPC,其中仅包含OpenMIPS、指令存储器ROM,所以是一个最小SOPC。OpenMIPS从指令存储器中读取指令,指令进入OpenMIPS开始执行。最小SOPC的结构如图4-9所示。
最小SOPC对应的模块是openmips_min_sopc,位于文件openmips_min_sopc.v中,读者可以在本书附带光盘的Code\Chapter4\目录下找到该文件,主要内容如下。在其中例化了处理器OpenMIPS、指令存储器ROM,并将两者按照图4-9的方式连接。
module openmips_min_sopc( input wire clk, input wire rst ); // 连接指令存储器 wire[`InstAddrBus] inst_addr; wire[`InstBus] inst; wire rom_ce; // 例化处理器OpenMIPS openmips openmips0( .clk(clk), .rst(rst), .rom_addr_o(inst_addr), .rom_data_i(inst), .rom_ce(rom_ce) ); // 例化指令存储器ROM inst_rom inst_rom0( .ce(rom_ce), .addr(inst_addr), .inst(inst) ); endmodule4.3.3 编写测试程序
我们需要写一段测试程序,并将其存储到指令存储器ROM,这样当上一节建立的最小SOPC开始运行的时候,就会从ROM中取出我们的程序,送入OpenMIPS处理器执行。由于目前的OpenMIPS只实现了一条ori指令,所以测试程序很简单,如下,对应本书附带光盘Code\Chapter4\TestAsm目录下的inst_rom.S文件。
ori $1,$0,0x1100 # $1 = $0 | 0x1100 = 0x1100 ori $2,$0,0x0020 # $2 = $0 | 0x0020 = 0x0020 ori $3,$0,0xff00 # $3 = $0 | 0xff00 = 0xff00 ori $4,$0,0xffff # $4 = $0 | 0xffff = 0xffff共有4条指令,都是ori指令。
第1条指令将0x1100进行零扩展后与寄存器$0进行逻辑“或”运算,结果保存在寄存器$1中。
第2条指令将0x0020进行零扩展后与寄存器$0进行逻辑“或”运算,结果保存在寄存器$2中。
第3条指令将0xff00进行零扩展后与寄存器$0进行逻辑“或”运算,结果保存在寄存器$3中。
第4条指令将0xffff进行零扩展后与寄存器$0进行逻辑“或”运算,结果保存在寄存器$4中。
指令的注释说明了指令的执行结果。接下来,按照正常的顺序应该是使用编译器编译我们的测试程序,但由于GCC编译器的安装、使用、Makefile文件的制作等内容还需要不少篇幅讲解,而想必各位读者和笔者一样,急切地想知道OpenMIPS是否实现正确,所以本节采用手工编译的方式编译测试程序,4.4节将专题介绍GCC编译器的使用。
手工编译只需按照指令内容填充进图4-1所示的ori指令格式中,即可得到对应的二进制字,比如:对于指令ori $1,$0,0x1100,对应的二进制字如图4-10所示。
转化为十六进制即0x34011100,其余3条指令按照同样的方式可以得到对应的二进制字,按照$readmemh函数的要求,一行放一条指令,得到测试程序对应的isnt_rom.data文件如下,可在本书附带光盘的Code\Chapter4\TestAsm目录下找到同名文件。
34011100 34020020 3403ff00 3404ffff4.3.4 建立Test Bench文件
本小节将建立Test Bench文件,其中给出最小SOPC运行所需的时钟信号、复位信号。代码如下,对应本书附带光盘Code\Chapter4\目录下的openmips_min_sopc_tb.v文件。
// 时间单位是1ns,精度是1ps `timescale 1ns/1ps module openmips_min_sopc_tb(); reg CLOCK_50; reg rst; // 每隔10ns,CLOCK_50信号翻转一次,所以一个周期是20ns,对应50MHz initial begin CLOCK_50 = 1'b0; forever #10 CLOCK_50 = ~CLOCK_50; end // 最初时刻,复位信号有效,在第195ns,复位信号无效,最小SOPC开始运行 // 运行1000ns后,暂停仿真 initial begin rst = `RstEnable; #195 rst= `RstDisable; #1000 $stop; end // 例化最小SOPC openmips_min_sopc openmips_min_sopc0( .clk(CLOCK_50), .rst(rst) ); endmodule4.3.5使用ModelSim检验OpenMIPS实现效果
万事俱备,只欠东风了,本节是验证前的最后一步——建立ModelSim工程,进行仿真。参考第2章的介绍,新建一个ModelSim工程,工程名可以为openmips_min_sopc,将上文创建的OpenMIPS所有源文件、Test Bench文件、指令存储器的源文件等(也就是本书附带光盘Code\Chapter4目录下所有.v文件)添加到工程中,然后编译。
注意:还需要将上一小节制作的inst_rom.data文件复制到工程目录下。
编译通过后,将workspace切换到Library选项卡,打开work这个library,选中openmips_min_sopc_tb,右键点击,选择Simulate,如图4-11所示。
在出现的波形显示界面中,添加要观察的信号,即可开始仿真。此处我们选择寄存器$1-$4作为观察对象,如图4-12所示,通过观察寄存器$1-$4的最终值,可知OpenMIPS正确执行了测试程序,也就是正确实现了ori指令。
添加更多要观察的信号,可以了解流水线执行情况,如图4-13所示。为了使流水线情况显示的更加直观,此处以第一条指令在流水线中的执行过程为例,并且图中去掉了其它指令执行时引起的信号变化。
(1)在复位结束后的第一个时钟周期上升沿,rom_ce_o变为ChipEnable,表示指令存储器使能,开始取指,进入取指阶段,从指令存储器中取出第一条指令0x34011100,赋给IF/ID模块的输入端口if_inst。下一个时钟周期,第一条指令进入译码阶段。
(2)观察译码阶段。
- 此时译码阶段的指令id_inst正是第一条指令0x34011100
- 指令地址id_pc是0x00000000
- 在ID模块对指令进行译码,得到指令运算类型alusel_o是3'b001,查询defines.h文件中的宏定义可知,对应宏EXE_RES_LOGIC,表示是逻辑运算
- 得到运算子类型aluop_o是8'b00100101,查询defines.h文件中的宏定义可知,对应宏EXE_OR_OP,表示逻辑“或”运算
- 译码得到参与运算的源操作数1是0x00000000,正是$0寄存器的值
- 译码得到参与运算的源操作数2是0x00001100,正是指令中立即数零扩展后的值
- 译码得到wreg_o的值为1,表示要写目的寄存器
- 译码得到要写入的目的寄存器wd_o是5'b00001,正是$1寄存器
(3)观察执行阶段。
- 进行指定的运算,得到wdata_o为0x00001100,就是要写到目的寄存器的数据
- 传递译码阶段wreg_o的值,为1,表示要写目的寄存器
- 传递译码阶段wd_o的值,为5'b00001,表示要写入的目的寄存器是$1寄存器
(4)观察访存阶段
- 传递执行阶段wdata_o的值,为0x00001100,表示要写到目的寄存器的数据
- 传递执行阶段wreg_o的值,为1,表示要写目的寄存器
- 传递执行阶段wd_o的值,为5'b00001,表示要写入的目的寄存器是$1寄存器
(5)观察回写阶段
- 得到访存阶段wdata_o的值,为0x00001100,表示要写到目的寄存器的数据
- 得到访存阶段wreg_o的值,为1,表示要写目的寄存器
- 得到访存阶段wd_o的值,为5'b00001,表示要写入的目的寄存器是$1寄存器
在回写阶段的最后,将按照要求写目的寄存器$1,使得$1的值为0x00001100。通过上面的观察,可知原始的OpenMIPS五级流水线实现正确。接下来,我们就可以以此为基础,不断充实,添加实现更多的MIPS指令,不过,在此之前,我们要先学习使用GNU工具链,本节的例子只有4条指令,可以手工编译,以后会遇到比较复杂,拥有较多指令的程序,届时,手工编译就显得效率低下了,所以要使用GNU工具链。
未完待续!
上一篇: PHP怎么调出“打开”或“另存为”窗口
下一篇: 关于冷知识的10篇文章推荐