cpu设计和实现(数据访问)
Posted 嵌入式-老费
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cpu设计和实现(数据访问)相关的知识,希望对你有一定的参考价值。
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱: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 <= 24mem_data_i[31],mem_data_i[31:24];
mem_sel_o <= 4'b1000;
end
2'b01: begin
wdata_o <= 24mem_data_i[23],mem_data_i[23:16];
mem_sel_o <= 4'b0100;
end
2'b10: begin
wdata_o <= 24mem_data_i[15],mem_data_i[15:8];
mem_sel_o <= 4'b0010;
end
2'b11: begin
wdata_o <= 24mem_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 + 16inst_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目录里面找到。
以上是关于cpu设计和实现(数据访问)的主要内容,如果未能解决你的问题,请参考以下文章