您的位置:首页 > 其它

自己动手写CPU之第四阶段(2)——验证第一条指令ori的实现效果

2014-07-25 12:58 477 查看
将陆续上传本人写的新书《自己动手写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 <= `ZeroWord;
end else begin
inst <= inst_mem[addr[`InstMemNumLog2+1:2]];
end
end

endmodule

代码很好理解,有以下几点说明。

(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)
);

endmodule

4.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
3404ffff


4.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)
);

endmodule


4.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工具链。

未完待续!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐