以下内容摘自《步步惊芯——软核处理器内部设计分析》一书
在上一篇博文中使用GNU工具链可以得到可执行文件,然后在模拟器中运行这个可执行文件,并记录指令执行的信息到文件中,通过分析这个文件可以判断程序是否是按照预期那样执行。但这只是一个软件的模拟过程,用于前期的验证,对于剖析OR1200内部结构的作用并不大。为了剖析OR1200内部结构,我们还需要借助硬件仿真工具ModelSim,本节设计了一个OR1200可以运行的最小系统,并通过ModelSim仿真,观察OR1200内部执行细节。
图2.18 OR1200可以运行的最小系统结构
在第1章中已经介绍了下载地址,使用SVN从http://opencores.org/ocsvn/openrisc/openrisc这个地址CheckOut最新的代码。本书将以OR1200的rel3这个版本为例进行分析,所以进入/branches/or1200_rel3/rtl/verilog目录,可以找到所有的verilog设计文件。
1、新建工程mim_or1200
打开ModelSim,本书使用的是Windows环境下ASE(Altera Starter Edition)6.6d版。选择“File->New->Project”,出现如图2.19所示对话框。
图2.19 ModelSim新建Project
给新的工程起名为mim_or1200,选择存储路径,注意不要包含中文,将Default Library Name也改为min_or1200,点击OK,出现如图2.20所示界面。
图2.20 ModelSim新建或添加已有文件对话框
选择“Add Existing File”表示添加已存在的文件,在出现的对话框中点击“Browse”按钮将/branches/or1200_rel3/rtl/verilog目录下的所有文件都选中添加,同时选择下面的“Copy to project directory”,点击OK,这样OR1200所有的verilog文件都添加到工程min_or1200中了。
图2.21 ModelSim中为min_or1200工程添加文件对话框
2、新建测试平台(Test Bench)
新建一个Verilog文件添加到工程中,文件名为or1200_tb.v,这是一个简单的测试平台(Test Bench)文件,内容如下:
`timescale 1ns/100ps module or1200_tb(); reg CLOCK_50; reg rst; initial begin CLOCK_50 = 1‘b0; //时钟20ns一个周期,所以时钟频率是50MHz forever #10 CLOCK_50 = ~CLOCK_50; end initial begin rst = 1‘b1; //复位信号 #200 rst= 1‘b0; //在200ns处复位结束 #1000 $stop; //仿真过程持续1000ns end or1200_top or1200_top_inst //因为是最小系统,所以除了时钟、复位信号外,其余全都为0 ( .clk_i(CLOCK_50), .rst_i(rst), .pic_ints_i(20‘b0), .clmode_i(2‘b00), // 指令Wishbone总线接口 .iwb_clk_i(clk_i), .iwb_rst_i(rst), .iwb_dat_i(32‘b0), .iwb_ack_i(1‘b0), .iwb_err_i(1‘b0), .iwb_rty_i(1‘b0), .iwb_cyc_o(), .iwb_adr_o(), .iwb_dat_o(), .iwb_stb_o(), .iwb_we_o(), .iwb_sel_o(), `ifdef OR1200_WB_CAB .iwb_cab_o(), `endif // 数据Wishbone总线接口 .dwb_clk_i(clk_i), .dwb_rst_i(rst), .dwb_dat_i(32‘b0), .dwb_ack_i(1‘b0), .dwb_err_i(1‘b0), .dwb_rty_i(1‘b0), .dwb_cyc_o(), .dwb_adr_o(), .dwb_dat_o(), .dwb_stb_o(), .dwb_we_o(), .dwb_sel_o(), `ifdef OR1200_WB_CAB .dwb_cab_o(), `endif // 外部调试接口 .dbg_stall_i(1‘b0), .dbg_ewt_i(1‘b0), .dbg_lss_o(), .dbg_is_o(), .dbg_wp_o(), .dbg_bp_o(), .dbg_stb_i(1‘b0), .dbg_we_i(1‘b0), .dbg_adr_i(0), .dbg_dat_i(0), .dbg_dat_o(), .dbg_ack_o(), // 电源管理接口 .pm_cpustall_i(0), .pm_clksd_o(), .pm_dc_gate_o(), .pm_ic_gate_o(), .pm_dmmu_gate_o(), .pm_immu_gate_o(), .pm_tt_gate_o(), .pm_cpu_gate_o(), .pm_wakeup_o(), .pm_lvolt_o() ); endmodule
OR1200有很多外部接口,从上面可以知道有指令Wishbone总线接口、数据Wishbone总线接口、外部调试接口、电源管理接口等,但是由于我们是最小系统,所以整个OR1200只有时钟、复位信号有效,其余接口的输入信号都直接设置为0。
3、修改OR1200配置
打开文件or1200_defines.v,这是OR1200的配置文件,其中定义了很多宏定义,可以通过注释、取消注释来修改OR1200的配置。因为最小系统没有数据缓存(DCache)、指令缓存(ICache)、数据MMU(DMMU)、指令MMU(IMMU),所以需要下面的宏定义,这些宏定义默认是被注释掉的,取消宏定义前的注释符:
`define OR1200_NO_DC //表示不需要数据Cache `define OR1200_NO_IC //表示不需要指令Cache `define OR1200_NO_DMMU //表示不需要数据MMU `define OR1200_NO_IMMU //表示不需要指令MMU
//`define OR1200_DU_IMPLEMENTED //注释掉这个宏定义表示不使用调试单元 //`define OR1200_PIC_IMPLEMENTED //注释掉这个宏定义表示不使用可编程中断控制器 //`define OR1200_TT_IMPLEMENTED //注释掉这个宏定义表示不使用定时器单元
`define OR1200_QMEM_IMPLEMENTED //默认是没有Implement QMEM,此处需要取消掉注释符
4、修改or1200_qmem.v文件
最小系统设计将指令存放在QMEM中,为了使用QMEM存储指令,还需要修改QMEM的代码,打开or1200_qmem_top.v,找到如下代码:
`ifdef OR1200_QMEM_IADDR assign iaddr_qmem_hit = (qmemimmu_adr_i & `OR1200_QMEM_IMASK) == `OR1200_QMEM_IADDR; `else assign iaddr_qmem_hit = 1‘b0; `endif `ifdef OR1200_QMEM_DADDR assign daddr_qmem_hit = (qmemdmmu_adr_i & `OR1200_QMEM_DMASK) == `OR1200_QMEM_DADDR; `else assign daddr_qmem_hit = 1‘b0; `endif
`ifdef OR1200_QMEM_IADDR assign iaddr_qmem_hit = 1‘b1; `else assign iaddr_qmem_hit = 1‘b0; `endif `ifdef OR1200_QMEM_DADDR assign daddr_qmem_hit = 1‘b1; `else assign daddr_qmem_hit = 1‘b0; `endif
至于为什么作上述修改,笔者会在QMEM分析的时候再解释,此处只需要明白经过上面的修改,OR1200将从QMEM中读取指令、加载存储数据。
这样我们的最小系统就创建结束了,在ModelSim中选择“Compile ALL”会编译整个工程。
initial $readmemh ( "mem.data", mem );
表示从mem.data中读取数据初始化mem,而这个mem正是QMEM的存储空间,mem.data是一个文本文件,里面存储的是指令,其每行存储一个32位的数据(使用十六进制表示),readmemh函数会将mem.dada中的数据依次填写到mem中。
读者朋友可能有的会认为直接使用在上一篇博文得到的Example.or32作为mem.data的内容,就可以使用上述语句初始化QMEM了,这个想法是不对的,Example.or32是ELF格式的文件,需要一个操作系统,或者一个Loader来解释该文件,并按照文件的要求将其代码存放到合适的内存地址,然后CPU跳转到该地址执行。但此时我们的最小系统是一个裸机,也就是说当我们加电复位的时候CPU只知道从0x100处读入指令开始执行,并不会理解什么ELF格式,所以我们需要自己初始化内存,也就是将ELF文件中的可执行代码从0x100处开始存放。使用2.2.1节中程序初始化QMEM,那么mem.data的内容应该如下:00000000 00000000 …… ///这里一共有0x40个00000000,因为0x100是字节地址,这里每一行是4个字节, ///所以一共有(0x100/4)=0x40行00000000 00000000 a4000000 ///在第0x41行存储的是第一条指令 e020004d e040004d 9c21000a e0420800 15000001
图2.22 Simulate选择对话框
在出现的sim选项卡中,分别在下面四个信号上点击右键,选择“Add”->“Add To Wave”,
/or1200_tb/CLOCK_50 /or1200_tb/or1200_top_inst/or1200_cpu/or1200_ctrl/ex_insn /or1200_tb/or1200_top_inst/or1200_cpu/or1200_rf/rf_b/mem[1] /or1200_tb/or1200_top_inst/or1200_cpu/or1200_rf/rf_b/mem[2]
图2.23 ModelSim仿真结果
仿真结果显示,当指令“0x9c21000a”执行结束后r1变为0xa,这条指令正是“l.addi r1,r1,0xa”,当指令“0xe0420800”执行结束后r2变为0xa,这条指令正是“l.add r2,r2,r1”,与模拟器执行的效果是一样的。读者朋友们可以添加其余感兴趣的信号,观察其在执行过程中的变化。本书光盘的Chapter2目录下包括ModelSim仿真工程,Chapter2/Code目录下包括示例程序源代码。
上一节中我们手工制作了一个mem.data文件来初始化QMEM,这种做法在代码量很大或者ELF文件稍微复杂一点的情况下都会带来问题,因此需要一个更好的办法。
在GNU工具链中提供了一个工具or32-elf-objcopy,用于将一种格式的目标文件复制成另外一种格式。因此可以先使用or32-elf-objcopy得到Example.or32的二进制(Binary)形式,然后编写一个小程序将二进制形式的代码按照ModelSim中存储器初始化文件的格式保存。这个小程序很简单,此处不再列出代码,在本书附带的光盘中可以找到源程序,程序名为Bin2Mem.exe,其使用方法为:
./Bin2Mem.exe –f mem.bin –o mem.data
(1)生成Binary文件:or32-elf-objcopy –O binary Example.or32 mem.bin (2)格式转化: ./Bin2Mem.exe –f mem.bin –o mem.data
ifndef CROSS_COMPILE CROSS_COMPILE = or32-elf- endif CC = $(CROSS_COMPILE)as LD = $(CROSS_COMPILE)ld OBJCOPY = $(CROSS_COMPILE)objcopy OBJECTS = Example.o export CROSS_COMPILE # ******************** # Rules of Compilation # ******************** all: Example.or32 Example.trace mem.data %.o: %.S $(CC) $< -o $@ Example.or32: ram.ld $(OBJECTS) $(LD) -T ram.ld $(OBJECTS) -o $@ mem.bin: Example.or32 $(OBJCOPY) -O binary $< $@ mem.data:mem.bin ./Bin2Mem.exe -f $< -o $@ Example.trace: Example.or32 sim -t $< -m1M > $@ clean: rm -f *.o *.or32 *.bin *.data *.trace
有了前面的基础相信这个Makefile很好理解,笔者就不再解释了,需要注意的是这里将模拟器执行也加入到Makefile中了,这样使用Makefile可以同时得到模拟器执行结果。
好了,现在是万事俱备啊,简单总结一下我们的实验步骤:
(1)编写源代码,当然是汇编代码,文件名为Example.S。
(2)复制我们上面的Makefile、Bin2Mem.exe、ram.ld到源代码所在目录。
(3)打开终端,路径调整到源代码所在目录,输入“make all”。
就这么容易,我们得到了OR1KSim模拟器的执行结果Example.trace、得到了可以在ModelSim中使用的存储器初始化文件mem.data。使用前者可以查看程序的执行是否有预期效果,使用后者可以在ModelSim中仿真硬件执行效果,查看需要观察的信号在每个时钟周期的变化情况。
上文提及之所以要使用ModelSim就是为了深入探究OR1200内部运行原理,下面我们就来做一个实验,程序代码还是不变,所以mem.data也是不变的,只是在ModelSim仿真的时候多观察几个信号,我们观察如下信号:
/or1200_tb/CLOCK_50 /or1200_tb/or1200_top_inst/or1200_cpu/or1200_ctrl/if_insn /or1200_tb/or1200_top_inst/or1200_cpu/or1200_ctrl/id_insn /or1200_tb/or1200_top_inst/or1200_cpu/or1200_ctrl/ex_insn
图2.24 点击“Restart”重新开始仿真
图2.25 点击“Run-All”开始仿真
会出现如图2.26所示仿真波形。
图2.26 观察流水线之一
各位读者朋友请定睛一看,看到了什么?
没错,这就是传说中的流水线。or1200_ctrl这个模块中的三个变量if_insn、id_insn、ex_insn分别表示当前取到的指令、正在译码的指令、正在执行的指令。图2.27可能会更加清楚。
图2.27 观察流水线之二
从上图可以直观的观察到随着时钟的前进,每一条指令都依次成为取到的指令、正在译码的指令、正在执行的指令,换句话说每一条指令都依次经过取指阶段、译码阶段、执行阶段,并且译码的同时,上一条指令在执行,下一条指令被取到。这就是流水线!
通过这个小实验我们将计算机教科书上抽象的流水线变得具体,也给我们带了很大的欢乐,随着后续分析工作的展开,我们会看到更多处理器内部的工作原理,相信也会给我们带来更大的欢乐。不过在此之前先给大家介绍一下流水线的有关知识,熟悉流水线的读者朋友可以直接跳过下一节。
本书讲的是计算机中的流水线,首先听一听维基百科中对计算机流水线的定义:流水线是指将计算机指令处理过程拆分为多个步骤,并通过多个硬件处理单元并行执行来加快指令执行速度。此处有两个关键词:(1)拆分;(2)并行。指令的处理从直观上分析至少可以拆分为三步:从存储器取出指令、解释指令、按照解释的结果执行,简单的说就是:取指、译码、执行。如果我们只有一个硬件处理单元,这个单元既要取指,又要译码,还要执行,假设上述三种操作都可以在时间T完成,那么一条指令的处理时间为3T,n条指令的处理时间就为3nT,但是如果我们设计有三个硬件单元,分别做这三项工作的一项,那么就可以在执行的同时对下一条指令译码,在对下一条指令译码的同时还可以再取一条指令,这就是经典的三级流水线,如图2.28所示。
图2.28 三级流水线示意图
从图中可知在三级流水线上执行3条指令所需时间为5T,而如果没有使用流水线则需要9T,流水线确实加快了指令执行。ARM7采用的就是三级流水线。但世间事没有这么简单完美的,上面假设取指、译码、执行需要的时间都是T,实际并非如此,比如取指的时间就可能很长,假设取指需要2T个时间,那么如图2.29所示。
图2.29 取指时间为2T时的流水线工作情况
可见在3T-4T的时间段、5T-6T的时间段流水线在等待取指结束,此时译码阶段、执行阶段都停滞,这样一来自然就慢下来,最后执行3条指令所需时间为8T。解决取指时间过长的措施是引入缓存(Cache),处理器从缓存读取指令的时间只需要1个时钟周期,读者朋友可能注意到在OR1200运行的最小系统中并没有使用缓存,但从图2.27中观察发现流水线是一个时钟前进一步,取指也只需要1个时钟周期,是的,最小系统中确实没有使用缓存,但是使用了QMEM,读者目前可以认为QMEM是OR1200的片上存储器,不经过输入输出总线,所以读取速度很快,可以在1个时钟周期中读取到指令,最小系统使用QMEM的目的也是为了尽量简化问题,不引入过多需要考虑的因素,从而关注流水线本身的运行情况。
还有一种情况是执行阶段时间过长,比如指令为加载存储指令(Load/Store)时,由于涉及到访问存储器,所需执行阶段的时间就可能大于T,此时也会导致流水线停滞。为了解决这种情况下的流水线停滞问题,引入了五级流水线,分别是:取指、译码、执行、访存、回写。
图2.30 五级流水线示意图
其中访存(Memory Access)的作用是从存储器装载数据到寄存器或者将寄存器数据存储到存储器,当然如果不是Load/Store指令则不需要这一步,此时在访存阶段就只是将执行阶段的运算结果送到下一级回写阶段。回写阶段(Write Back)的作用是将数据写入目的寄存器。ARM9就采用了这种五级流水线,OR1200也声称是五级流水线,但是通过第9章对加载存储类指令的剖析,可以明白OR1200实际只是三级流水线,本书也是按照三级流水线来分析OR1200中指令处理过程的。
从没有流水线到三级流水线再到五级流水线,我们可以发现流水线实际就是将一项工作分为若干个子工作,达到并行执行这些子工作的目的,那么流水线是不是越多越好呢?当然不是,首先是成本问题,其次,会加大“流水线相关”这一问题的发生概率,流水线相关的问题我们会在第4章结合指令分析讲解,还有一个原因,如果执行转移指令,但是要转移目的地址不是已经在流水线中的指令,那么此时就会清空流水线,然后读取目的地址的指令进入流水线,这就会带来延迟时间,在第7章分析指令l.sys时会有更加深刻的体会。最后会增加编译程序的复杂度。所以流水线并不是越多越好。创建or1200最小SOPC,并进行仿真,布布扣,bubuko.com
原文:http://blog.csdn.net/leishangwen/article/details/21953749