zl程序教程

您现在的位置是:首页 >  大数据

当前栏目

cpu设计和实现(数据访问)

数据CPU 实现 设计 访问
2023-09-27 14:27:10 时间

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】

        在cpu设计当中,数据访问是比较重要的一个环节。一般认为,数据访问就是内存访问。其实不然。我们都知道,cpu访问一般有指令访问和数据访问两种。指令访问就是rom访问,这个比较纯粹。但是数据访问就比较复杂一点。因为,除了单纯的ram读取访问之外,数据访问还担负着io访问的功能。一般的外设访问都是通过ip来完成的,这些io都有自己的设备地址空间,那么cpu如何通过这些设备地址空间来控制这些外设呢?答案就是数据访问。

1、一般数据访问

        对于cpu自身来说,其实并不关心外面访问的数据是ram数据、还是io数据,对它来说都是一样的。所以,首先cpu一般需要先定义一个外部空间,不妨称之为data_ram.v,

`include "defines.v"

module data_ram(

	input	wire										clk,
	input wire										ce,
	input wire										we,
	input wire[`DataAddrBus]			addr,
	input wire[3:0]								sel,
	input wire[`DataBus]						data_i,
	output reg[`DataBus]					data_o
	
);

	reg[`ByteWidth]  data_mem0[0:`DataMemNum-1];
	reg[`ByteWidth]  data_mem1[0:`DataMemNum-1];
	reg[`ByteWidth]  data_mem2[0:`DataMemNum-1];
	reg[`ByteWidth]  data_mem3[0:`DataMemNum-1];

	always @ (posedge clk) begin
		if (ce == `ChipDisable) begin
			//data_o <= ZeroWord;
		end else if(we == `WriteEnable) begin
			  if (sel[3] == 1'b1) begin
		      data_mem3[addr[`DataMemNumLog2+1:2]] <= data_i[31:24];
		    end
			  if (sel[2] == 1'b1) begin
		      data_mem2[addr[`DataMemNumLog2+1:2]] <= data_i[23:16];
		    end
		    if (sel[1] == 1'b1) begin
		      data_mem1[addr[`DataMemNumLog2+1:2]] <= data_i[15:8];
		    end
			  if (sel[0] == 1'b1) begin
		      data_mem0[addr[`DataMemNumLog2+1:2]] <= data_i[7:0];
		    end			   	    
		end
	end
	
	always @ (*) begin
		if (ce == `ChipDisable) begin
			data_o <= `ZeroWord;
	  end else if(we == `WriteDisable) begin
		    data_o <= {data_mem3[addr[`DataMemNumLog2+1:2]],
		               data_mem2[addr[`DataMemNumLog2+1:2]],
		               data_mem1[addr[`DataMemNumLog2+1:2]],
		               data_mem0[addr[`DataMemNumLog2+1:2]]};
		end else begin
				data_o <= `ZeroWord;
		end
	end		

endmodule

        有了这个data_ram.v,那么mem.v就有了访问的空间了,可以输出地址和数据了,

				`EXE_LB_OP:		begin
					mem_addr_o <= mem_addr_i;
					mem_we <= `WriteDisable;
					mem_ce_o <= `ChipEnable;
					case (mem_addr_i[1:0])
						2'b00:	begin
							wdata_o <= {{24{mem_data_i[31]}},mem_data_i[31:24]};
							mem_sel_o <= 4'b1000;
						end
						2'b01:	begin
							wdata_o <= {{24{mem_data_i[23]}},mem_data_i[23:16]};
							mem_sel_o <= 4'b0100;
						end
						2'b10:	begin
							wdata_o <= {{24{mem_data_i[15]}},mem_data_i[15:8]};
							mem_sel_o <= 4'b0010;
						end
						2'b11:	begin
							wdata_o <= {{24{mem_data_i[7]}},mem_data_i[7:0]};
							mem_sel_o <= 4'b0001;
						end
						default:	begin
							wdata_o <= `ZeroWord;
						end
					endcase
				end

        涉及到的命令有load和store两种,我们可以挑选lb这一个命令进行解析,其他命令都是一个道理。首先,从代码上看,很明显mem.v传递给data_ram.v的地址是mem_addr_o。有了这个地址之后,mem.v就可以从data_ram.v那里获取mem_data_i的数据了。并且,还可以根据mem_addr_i末两位,决定最终写回的数据w_data_o取哪一部分。看到这里,大家可能会有一个疑问,那mem_addr_i又是哪里来的,通过查看ex_mem.v文件,发现了这么一句,

    mem_mem_addr <= ex_mem_addr;

        这说明,在exe阶段,地址其实就已经准备好了。这样,我们继续查看ex.v文件,发现了很有趣的这三条语句,

  //aluop_o传递到访存阶段,用于加载、存储指令
  assign aluop_o = aluop_i;
  
  //mem_addr传递到访存阶段,是加载、存储指令对应的存储器地址
  assign mem_addr_o = reg1_i + {{16{inst_i[15]}},inst_i[15:0]};

  //将两个操作数也传递到访存阶段,也是为记载、存储指令准备的
  assign reg2_o = reg2_i;

        这三条语句告诉我们,它们就是为mem访存做准备的。其中,mem_addr_o就是访存的最终地址。在exe阶段,地址就已经准备好了。而且,aluop_o需要继续递延到mem访存阶段,因为到时候还需要根据它继续判断执行的访存指令是哪一个。

        有了这些内容,我们才感觉到mem.v有了点实际的意义,不再像以前一样,只是负责数据的透传,没有什么具体的用途。当然,在整个测试的过程中,我们也发现2个问题,

        1)在defines.v中,既要把InstMemNum调整为64,还要把DataMemNum调整为64,不然iverilog编译不了;

        2)原来openmips.v Line375有编译错误,需要修改下,把ram_ce_o修改成1位wire即可,

C:\Users\feixiaoxing\Desktop\design_mips_cpu\Examples-in-book-write-your-own-cpu-master\Code\Chapter9_1>C:\iverilog\bin\iverilog.exe -o tb *.v
openmips.v:375: warning: Port 22 (mem_ce_o) of mem expects 1 bits, got 4.
openmips.v:375:        : Padding 3 high bits of the expression.
openmips_min_sopc.v:59: warning: Port 11 (ram_ce_o) of openmips expects 4 bits, got 1.
openmips_min_sopc.v:59:        : Padding 3 high bits of the port.

        有了上面这些内容做铺垫,就可以通过dumpfile、dumpvars的方法生成vcd文件调试波形了。

2、ll和sc命令

        在cpu里面完成原子操作有很多办法。最最常见的方法,就是关中断、开中断。这个方法非常容易想到,毕竟如果把中断都关掉了,那么基本上cpu就不会受到任何外来的干扰了。但是,mips cpu在这个基础上又想到了另外一个方法,那就是ll和sc。

        ll和sc通过影子寄存器llbit的方法,同样可以实现原子访问。首先ll访问的时候,cpu会从ram中读取数据,并且将llbit置为1。接着sc保存的时候,cpu会检测llbit是否为1。如果是1,则成功写入数据,llbit置为0,返回的寄存器置为1;如果是0,则取消写入过程,llbit不变,返回的寄存器置为0。

        整个过程最为精妙的地方在于两点,

        1)ll和sc一定要配对使用,单独使用是不成立的;

        2)影子寄存器对用户来说是不可见的,并且sc中写入数据的那个寄存器,同时担当了返回值的作用。

`include "defines.v"

module LLbit_reg(

	input	wire										clk,
	input wire										rst,
	
	input wire                    flush,
	//写端口
	input wire										LLbit_i,
	input wire                    we,
	
	//读端口1
	output reg                    LLbit_o
	
);


	always @ (posedge clk) begin
		if (rst == `RstEnable) begin
					LLbit_o <= 1'b0;
		end else if((flush == 1'b1)) begin
					LLbit_o <= 1'b0;
		end else if((we == `WriteEnable)) begin
					LLbit_o <= LLbit_i;
		end
	end

endmodule

        这就是llbit寄存器的读写代码。注意,llbit的读取是在mem访问的阶段进行的,它与reg访问在id阶段完成、mfhi&mflo在exe阶段完成都不一样。写入则和所有寄存器一样,都是wb阶段完成的。

        最后,我们还得回到一个老生常谈的话题。那就是llbit有可能读取不正确的问题。因为llbit是mem阶段被读取的,那么完全有可能llbit处于wb写回阶段,但是还没有赋值给reg,那么这个时候数据预取的工作就又要做一遍了,这从mem.v代码也可以完全看得出来,

	always @ (*) begin
		if(rst == `RstEnable) begin
			LLbit <= 1'b0;
		end else begin
			if(wb_LLbit_we_i == 1'b1) begin
				LLbit <= wb_LLbit_value_i;
			end else begin
				LLbit <= LLbit_i;
			end
		end
	end

3、load指令导致的流水线暂停

        前面我们讨论过,exe中出现madd这样指令的时候会出现流水线暂停。其实大家思考下,如果出现这样两条指令的时候,也会出现流水线暂停,


...... 
lw   $1, 0x0($0)    
beq  $1, $2, Label  
...... 

        前者刚从memory取出一个数据,保存到寄存器1。紧接着寄存器1和寄存器2马上就要进行比较判断了,判断的结果决定了pc后续的跳转地址是哪里。但是这个时候,lw才刚刚到exe阶段,还没有到达mem阶段。应该怎么处理呢?其实,也没啥好办法,就是让流水线暂停一会,

	always @ (*) begin
			stallreq_for_reg1_loadrelate <= `NoStop;	
		if(rst == `RstEnable) begin
			reg1_o <= `ZeroWord;	
		end else if(pre_inst_is_load == 1'b1 && ex_wd_i == reg1_addr_o 
								&& reg1_read_o == 1'b1 ) begin
			stallreq_for_reg1_loadrelate <= `Stop;							
		end else if((reg1_read_o == 1'b1) && (ex_wreg_i == 1'b1) 
								&& (ex_wd_i == reg1_addr_o)) begin
			reg1_o <= ex_wdata_i; 
		end else if((reg1_read_o == 1'b1) && (mem_wreg_i == 1'b1) 
								&& (mem_wd_i == reg1_addr_o)) begin
			reg1_o <= mem_wdata_i; 			
		end else if(reg1_read_o == 1'b1) begin
			reg1_o <= reg1_data_i;
		end else if(reg1_read_o == 1'b0) begin
			reg1_o <= imm;
		end else begin
			reg1_o <= `ZeroWord;
	  end
	end
	

        直接查看第二个判断条件。如果前面一条指令是load指令,并且需要写入某一个寄存器,除此之外,写入的寄存器还是当前要读入的寄存器,那么stallreq_for_reg1_loadrelate直接设置为1。当然,不仅仅是stallreq_for_reg1_loadrelate,还有可能stallreq_for_reg2_loadrelate也可能设置为1。两者共同决定了stallreq的最终结果。

  assign stallreq = stallreq_for_reg1_loadrelate | stallreq_for_reg2_loadrelate;
  assign pre_inst_is_load = ((ex_aluop_i == `EXE_LB_OP) || 
  													(ex_aluop_i == `EXE_LBU_OP)||
  													(ex_aluop_i == `EXE_LH_OP) ||
  													(ex_aluop_i == `EXE_LHU_OP)||
  													(ex_aluop_i == `EXE_LW_OP) ||
  													(ex_aluop_i == `EXE_LWR_OP)||
  													(ex_aluop_i == `EXE_LWL_OP)||
  													(ex_aluop_i == `EXE_LL_OP) ||
  													(ex_aluop_i == `EXE_SC_OP)) ? 1'b1 : 1'b0;

其他:

        今天的知识点有点多,大家可以多多思考,多多体会,多多练习。最后还是感谢《自己动手写cpu》这本书,今天谈到的这些代码都可以在Chapter9_1、Chapter9_2、Chapter9_3目录里面找到。