“茶壶里倒饺子--有口说不出“,说的是肚子里很多东西,却无法说出。其实在计算机体系结构里,也存在这个问题,这就是memory wall(存储墙)的问题。说到底,计算机的作用就是根据人们输入一堆东西,然后输出处理结果,所以,输入输出部分,就显得格外重要。CPU本身的计算能力再强,一直得不到足够的输入,或者无法将大量的结算结果输出出来,也是无济于事。
流水线中的MA阶段,就是CPU和外部交流的主要窗口,所以MA阶段的设计的优劣,对CPU的性能起着至关重要的作用。
本小节就分析一下or1200的五级流水的MA阶段的相关模块。
流水线的MA阶段,其功能是负责CPU内部寄存器堆和外部RAM的数据搬运,这不外乎两个方向:从register到memory(store),从memory到register(load)。
由于MA模块和memory之间一般会隔着cache,并且在cache的前面还有mmu存在,所以实际上是register和mmu,cache之间的数据搬运。
此外,在数据搬运过程中会有一些特殊情况出现,比如地址不对齐,mmu的TLB miss,mmu的pagefault。
由于要进行数据搬运,就需要靠bus,所以还有bus error,出现bus error异常的情况大体可分为两类,load/store的地址不存在,地址存在但slave的ack超时。
这样看来,MA阶段的任务可分为两个,第一,也是主要任务就是实现数据搬运,第二,就是产生相关的一些异常信号给expect和freeze等控制通路的模块。
了解了MA阶段的功能之后,我们就可以分析一下or1200的具体实现了,其整体结构如下所示
需要说明的是,or1200的MA阶段对应的是lsu(load store unit)模块。这个模块还例化了两个子模块,分别是or1200_mem2reg和or1200_reg2mem,分别实现load和stor指令。
此外,lsu模块和cache之间是wishbone总线,遵循wishbone总线协议,这个和rf模块和指令cache之间是相同的,都是wishbone总线。
lsu模块对应的RTL文件是or1200_lsu.v。
在前面我们介绍过了,lsu部分有两个任务,其中之一是产生一些异常信号。下面是这些信号的产生代码。
其中包括,对齐异常,tlb miss异常,page fault异常,和总线错误异常。
关于这些异常,我们在介绍mmu和cache模块的时候,已经说过,这里就不再赘述了。
关于对齐异常,为什么要进行对齐访问,这是两外一个专门的话题,这里只说一下关键点。
对内存来说,对齐的地址访问时,需要的时间短,为什么时间短呢?原因是由内存的内部组织有关的,
内存的读:内存在进行地址译码时会将地址总线的低两位(4字节对齐),或者低1位(两字节对齐)屏蔽,然后同时选通四个字节或者两个字节,这样数据总线上就会并发的输出读出的数据。
内存的写:和读的机制是一样的,同样会屏蔽地址总线的低位,并发的将数据总线的数据写入内存。
如果不是对齐访问,那么CPU需要多个周期才能完成,所以会比对齐的访问慢。
为了提高CPU的访存效率,规定load/store指令的地址必须是对齐的。
其实数据对齐访问是编译器做的事情,编译器在编译程序时,确保所有的变量等数据的存放都是地址对齐的。
总之,一句话,必须对齐访问,CPU也是被逼无奈,是内存的组织结构决定了内存只能对齐访问,如果CPU要实现非对齐访问的话,CPU就需要将非对齐访问拆分成多个对齐访问,显然会浪费时间,所以CPU也规定,程序在访问内存时,也必须是对齐的。省的出现程序执行缓慢的情况,让人误认为是CPU性能不行。
需要说明的是,id_precalc_sum[1:0]前面的‘|’符号是缩减运算符。
always @(posedge clk or posedge rst) begin if (rst) except_align <= 1'b0; else if (!ex_freeze & id_freeze | flushpipe) except_align <= 1'b0; else if (!ex_freeze) except_align <= ((id_lsu_op == `OR1200_LSUOP_SH) | (id_lsu_op == `OR1200_LSUOP_LHZ) | (id_lsu_op == `OR1200_LSUOP_LHS)) & id_precalc_sum[0] | ((id_lsu_op == `OR1200_LSUOP_SW) | (id_lsu_op == `OR1200_LSUOP_LWZ) | (id_lsu_op == `OR1200_LSUOP_LWS)) & |id_precalc_sum[1:0]; end assign except_dtlbmiss = dcpu_err_i & (dcpu_tag_i == `OR1200_DTAG_TE); assign except_dmmufault = dcpu_err_i & (dcpu_tag_i == `OR1200_DTAG_PE); assign except_dbuserr = dcpu_err_i & (dcpu_tag_i == `OR1200_DTAG_BE);
lsu,顾名思义,其只要作用就是实现load和store指令的。
load,就是将内存的数据搬到寄存器里,store,就是将寄存器的数据搬到内存。
lsu模块例化了两个子模块,来分别实现这个两个功能。代码如下:
// // Instantiation of Memory-to-regfile aligner // or1200_mem2reg or1200_mem2reg( .addr(dcpu_adr_o[1:0]), .lsu_op(ex_lsu_op), .memdata(dcpu_dat_i), .regdata(lsu_dataout) ); // // Instantiation of Regfile-to-memory aligner // or1200_reg2mem or1200_reg2mem( .addr(dcpu_adr_o[1:0]), .lsu_op(ex_lsu_op), .regdata(lsu_datain), .memdata(dcpu_dat_o) );
lsu模块,有两头儿,一头是寄存器,一头是cache,要实现数据搬运就要有对应的地址和数据信号线。
对于寄存器这边,是流水线内部,所以是内部信号,就是寄存器的地址(id_precalc_sum)和寄存器的值(lsu_datain)。
对于cache这边,是流水线外部,采用wishbone总线,地址信号是dcpu_adr_o,数据信号是dcpu_dat_o。
下面是这几个信号的产生逻辑。
这里还需要说明的是,除了这几个总线信号,还有两个开关信号,用来指示load store操作是否完成了。就是lsu_stall和lsu_unstall两个信号。这两个信号都输出给了freeze模块,用来暂停和使能流水线。
// // Precalculate part of load/store EA in ID stage // assign id_precalc_sum = id_addrbase[2-1:0] + id_addrofs[2-1:0]; always @(posedge clk or posedge rst) begin if (rst) dcpu_adr_r <= {2+1{1'b0}}; else if (!ex_freeze) dcpu_adr_r <= id_precalc_sum; end // // External I/F assignments // // carry from precalc, pad to 30-bits assign dcpu_adr_o[31:2] = ex_addrbase[31:2] + (ex_addrofs[31:2] +{{(32-2)-1{1'b0}},dcpu_adr_r[2]}); assign dcpu_adr_o[2-1:0] = dcpu_adr_r[2-1:0]; assign dcpu_cycstb_o = du_stall | lsu_unstall | except_align ? 1'b0 : |ex_lsu_op; assign dcpu_we_o = ex_lsu_op[3]; assign dcpu_tag_o = dcpu_cycstb_o ? `OR1200_DTAG_ND : `OR1200_DTAG_IDLE; always @(ex_lsu_op or dcpu_adr_o) casez({ex_lsu_op, dcpu_adr_o[1:0]}) {`OR1200_LSUOP_SB, 2'b00} : dcpu_sel_o = 4'b1000; {`OR1200_LSUOP_SB, 2'b01} : dcpu_sel_o = 4'b0100; {`OR1200_LSUOP_SB, 2'b10} : dcpu_sel_o = 4'b0010; {`OR1200_LSUOP_SB, 2'b11} : dcpu_sel_o = 4'b0001; {`OR1200_LSUOP_SH, 2'b00} : dcpu_sel_o = 4'b1100; {`OR1200_LSUOP_SH, 2'b10} : dcpu_sel_o = 4'b0011; {`OR1200_LSUOP_SW, 2'b00} : dcpu_sel_o = 4'b1111; {`OR1200_LSUOP_LBZ, 2'b00}, {`OR1200_LSUOP_LBS, 2'b00} : dcpu_sel_o = 4'b1000; {`OR1200_LSUOP_LBZ, 2'b01}, {`OR1200_LSUOP_LBS, 2'b01} : dcpu_sel_o = 4'b0100; {`OR1200_LSUOP_LBZ, 2'b10}, {`OR1200_LSUOP_LBS, 2'b10} : dcpu_sel_o = 4'b0010; {`OR1200_LSUOP_LBZ, 2'b11}, {`OR1200_LSUOP_LBS, 2'b11} : dcpu_sel_o = 4'b0001; {`OR1200_LSUOP_LHZ, 2'b00}, {`OR1200_LSUOP_LHS, 2'b00} : dcpu_sel_o = 4'b1100; {`OR1200_LSUOP_LHZ, 2'b10}, {`OR1200_LSUOP_LHS, 2'b10} : dcpu_sel_o = 4'b0011; {`OR1200_LSUOP_LWZ, 2'b00}, {`OR1200_LSUOP_LWS, 2'b00} : dcpu_sel_o = 4'b1111; default : dcpu_sel_o = 4'b0000; endcase
load操作,负责将内存的数据读到寄存器里。这只要有or1200_mem2reg模块完成,对应的RTL文件是or1200_mem2reg.v。
这个模块有两种实现方式,具体采用哪一种有编译开关OR1200_IMPL_MEM2REG2来管理。这个开关的定义在or1200_define.v。具体定义和使用代码如下所示。
定义:or1200_define.v
可见OR1200_IMPL_MEM2REG2是禁止的,所以具体的实现是采用的方式1。
// // Type of mem2reg aligner to implement. // // Once OR1200_IMPL_MEM2REG2 yielded faster // circuit, however with today tools it will // most probably give you slower circuit. // `define OR1200_IMPL_MEM2REG1 //`define OR1200_IMPL_MEM2REG2
// // In the past faster implementation of mem2reg (today probably slower) // `ifdef OR1200_IMPL_MEM2REG2
那采用方式1是如何实现的呢?代码如下。
从中可以看出,这种方式比较简单,就是直接将wishbone总线中数据总线的值根据具体的load操作的具体指令将对应的数据赋给寄存器。
// // Straightforward implementation of mem2reg // reg [width-1:0] regdata; reg [width-1:0] aligned; // // Alignment // always @(addr or memdata) begin case(addr) 2'b00: aligned = memdata; 2'b01: aligned = {memdata[23:0], 8'b0}; 2'b10: aligned = {memdata[15:0], 16'b0}; 2'b11: aligned = {memdata[7:0], 24'b0}; endcase end // // Bytes // always @(lsu_op or aligned) begin case(lsu_op) `OR1200_LSUOP_LBZ: begin regdata[7:0] = aligned[31:24]; regdata[31:8] = 24'b0; end `OR1200_LSUOP_LBS: begin regdata[7:0] = aligned[31:24]; regdata[31:8] = {24{aligned[31]}}; end `OR1200_LSUOP_LHZ: begin regdata[15:0] = aligned[31:16]; regdata[31:16] = 16'b0; end `OR1200_LSUOP_LHS: begin regdata[15:0] = aligned[31:16]; regdata[31:16] = {16{aligned[31]}}; end default: regdata = aligned; endcase end
简单的这种实现方式中第二个always块要用到第一个always块的结果(aligned),存在一个先后顺序。
比较复杂的实现方式,直接使用memdata,没有先后顺序。
所以比较简单的方式的电路延迟会大一些。可见,上帝总是公平的,有得必有失啊。
关于叫复杂的实现方式,这里不再介绍,感兴趣可以参考相关代码。
store操作,负责将寄存器中的数据写到cache。这个由or1200_reg2mem模块完成,对应的文件是or1200_reg2mem.v。
这部分实现也比较简单,根据具体的store的大小(几个字节),将reg的对应部分赋给wishbone的数据线上。
代码如下:
module or1200_reg2mem(addr, lsu_op, regdata, memdata); parameter width = `OR1200_OPERAND_WIDTH; // // I/O // input [1:0] addr; input [`OR1200_LSUOP_WIDTH-1:0] lsu_op; input [width-1:0] regdata; output [width-1:0] memdata; // // Internal regs and wires // reg [7:0] memdata_hh; reg [7:0] memdata_hl; reg [7:0] memdata_lh; reg [7:0] memdata_ll; assign memdata = {memdata_hh, memdata_hl, memdata_lh, memdata_ll}; // // Mux to memdata[31:24] // always @(lsu_op or addr or regdata) begin casez({lsu_op, addr[1:0]}) // synopsys parallel_case {`OR1200_LSUOP_SB, 2'b00} : memdata_hh = regdata[7:0]; {`OR1200_LSUOP_SH, 2'b00} : memdata_hh = regdata[15:8]; default : memdata_hh = regdata[31:24]; endcase end // // Mux to memdata[23:16] // always @(lsu_op or addr or regdata) begin casez({lsu_op, addr[1:0]}) // synopsys parallel_case {`OR1200_LSUOP_SW, 2'b00} : memdata_hl = regdata[23:16]; default : memdata_hl = regdata[7:0]; endcase end // // Mux to memdata[15:8] // always @(lsu_op or addr or regdata) begin casez({lsu_op, addr[1:0]}) // synopsys parallel_case {`OR1200_LSUOP_SB, 2'b10} : memdata_lh = regdata[7:0]; default : memdata_lh = regdata[15:8]; endcase end // // Mux to memdata[7:0] // always @(regdata) memdata_ll = regdata[7:0]; endmodule
一般来说,CPU的LSU模块会精心设计,因为这是整个CPU与外部进行数据交换的唯一通道,对性能的影响可想而知。但是呢,我们并没有看大很复杂的电路逻辑,无论是reg2mem还是mem2reg,实现方式都非常简单。可能这正好验证了那句话:”简约而不简单“。
enjoy!