OpenRisc-46-or1200的MA模块分析

引言

“茶壶里倒饺子--有口说不出“,说的是肚子里很多东西,却无法说出。其实在计算机体系结构里,也存在这个问题,这就是memory wall(存储墙)的问题。说到底,计算机的作用就是根据人们输入一堆东西,然后输出处理结果,所以,输入输出部分,就显得格外重要。CPU本身的计算能力再强,一直得不到足够的输入,或者无法将大量的结算结果输出出来,也是无济于事。

流水线中的MA阶段,就是CPU和外部交流的主要窗口,所以MA阶段的设计的优劣,对CPU的性能起着至关重要的作用。

本小节就分析一下or1200的五级流水的MA阶段的相关模块。


1,概述

流水线的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等控制通路的模块。


2,or1200的MA模块分析

1>整体结构

了解了MA阶段的功能之后,我们就可以分析一下or1200的具体实现了,其整体结构如下所示

需要说明的是,or1200的MA阶段对应的是lsu(load store unit)模块。这个模块还例化了两个子模块,分别是or1200_mem2reg和or1200_reg2mem,分别实现load和stor指令。

此外,lsu模块和cache之间是wishbone总线,遵循wishbone总线协议,这个和rf模块和指令cache之间是相同的,都是wishbone总线。

OpenRisc-46-or1200的MA模块分析_第1张图片


2>模块分析

lsu模块对应的RTL文件是or1200_lsu.v。

1》异常信号的产生

在前面我们介绍过了,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);


2》数据的搬运

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



3》load操作

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

使用:or1200_mem2reg.v


//
// 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

两外一种方式是将4个字节分开处理,稍复杂一些。

简单的这种实现方式中第二个always块要用到第一个always块的结果(aligned),存在一个先后顺序。

比较复杂的实现方式,直接使用memdata,没有先后顺序。

所以比较简单的方式的电路延迟会大一些。可见,上帝总是公平的,有得必有失啊。

关于叫复杂的实现方式,这里不再介绍,感兴趣可以参考相关代码。


4》store操作

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


3,小结

一般来说,CPU的LSU模块会精心设计,因为这是整个CPU与外部进行数据交换的唯一通道,对性能的影响可想而知。但是呢,我们并没有看大很复杂的电路逻辑,无论是reg2mem还是mem2reg,实现方式都非常简单。可能这正好验证了那句话:”简约而不简单“。

enjoy!






你可能感兴趣的:(OpenRisc-46-or1200的MA模块分析)