我们会继续上传新书《自己写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指令。所以測试程序非常easy,例如以下,相应本书附带光盘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实现效果
万事俱备,仅仅欠东风了,本节是验证前的最后一步——建立ModelSimproject,进行仿真。參考第2章的介绍,新建一个ModelSimproject,project名能够为openmips_min_sopc。将上文创建的OpenMIPS全部源文件、Test Bench文件、指令存储器的源文件等(也就是本书附带光盘Code\Chapter4文件夹下全部.v文件)加入到project中。然后编译。
注意:还须要将上一小克制作的inst_rom.data文件拷贝到project文件夹下。
编译通过后。将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模块的输入portif_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工具链。
待续!
版权声明:本文博客原创文章。博客,未经同意,不得转载。