以下内容摘自《步步惊芯——软核处理器内部设计分析》一书
上一章剖析了ICache模块。本章将剖析DCache模块,首先指出DCache模块相比ICache的特别之处,因为这些不同,所以DCache的分析相对复杂。接着分析了OR1200中DCache的结构,给出了构成DCache的四个模块的关系,将这四个模块分为数据部分、控制部分,介绍了数据部分的工作过程。13.3节说明了DCache中特殊寄存器的作用与格式。13.4节指出使用到DCache的7种情景,13.5节给出一个示例程序,其中涉及了DCache的大部分使用情景,之后,重点分析了在其中3种情景下DCache的工作过程。
13.1 DCache的特别之处
DCache中缓存的是数据,而ICache中缓存的是指令,这就是DCache与ICache的根本区别,由此也决定了DCache的特别之处。指令存储在指令存储器中,且指令存储器是只读的,对应只有一个取指操作,但是数据存储在数据存储器中,且数据存储器是可读、可写的,对应加载、存储两个操作。一般情况下,指令存储器、数据存储器都属于内存的一部分,有时就是内存中同样的地址空间。
当向数据存储器中存储数据且DCache命中时,称之为写命中,可以采用两种策略:
- 通写法(Write through):不仅把数据写入DCache中相应的块,而且也写入数据存储器中相应的块。
- 回写法(Write back):只把数据写入DCache中相应的块,并不会立即更新数据存储器中的内容,而是等到DCache中相应的块因为某种原因需要从DCache中移出时,才更新数据存储器中的内容。
通写法与回写法各有特点,回写法的优点是速度快,写操作能以DCache的速度进行,而且对于同一个内存块的多次写操作只需最后进行一次内存写操作,通写法的优点是易于实现,而且内存与DCache的数据总是保持一致。
当向数据存储器中存储数据且DCache失靶时,称之为写不命中,也可以采用两种策略:
- 按写分配法(Write allocate):先把要写数据所在的存储块调入DCache,然后再进行写入。
- 不按写分配法(no-Write allocate):直接写数据存储器中的目的地址,而不将要写数据所在的存储块调入DCache。
一般采用通写法时使用不按写分配法,采用回写法时使用按写分配法。在OR1200中可以配置DCache使用通写法还是回写法,采用通写法时就与不按写分配法结合使用,采用回写法时就与按写分配法结合使用,所以后面在分析的时候只是说明通写法、回写法,而不再说明按写分配法、不按写分配法。
or1200_defines.v
//不注释下面的宏定义,就是采用“通写法+不按写分配法”,
//注释掉下面的宏定义,就是采用“回写法+按写分配法”
`define OR1200_DC_WRITETHROUGH
OR1200中关于DCache的宏定义如下,从中可知,DCache能够配置为512B、4KB、8KB、16KB、32KB,默认是8KB,本章采用默认配置,后面分析时不再重复说明。此时内存块的大小是16字节,采用直接映射,DCache目录表有512 line,因此使用地址的4-12bit作为DCache目录表的查找索引。
or1200_defines.v
//`define OR1200_NO_DC //是否有DCache,默认是注释掉,也就是有DCache
//`define OR1200_DC_1W_4KB
`define OR1200_DC_1W_8KB
//`define OR1200_DC_1W_16KB
//`define OR1200_DC_1W_32KB
`ifdef OR1200_DC_1W_32KB
`define OR1200_DCLS 5 //如果DCache配置为32KB,则内存块的大小是32字节
`else
`define OR1200_DCLS 4 //否则都是16字节
`endif
`define OR1200_SPRGRP_DC_ADR_WIDTH 3 //特殊寄存器对应索引的宽度
//特殊寄存器DCBFR在第3组特殊寄存器中的索引是2
`define OR1200_SPRGRP_DC_DCBFR 3'd2
//特殊寄存器DCBIR在第3组特殊寄存器中的索引是3
`define OR1200_SPRGRP_DC_DCBIR 3'd3
//特殊寄存器DCBWR在第3组特殊寄存器中的索引是4
`define OR1200_SPRGRP_DC_DCBWR 3'd4
`ifdef OR1200_DC_1W_8KB //如果配置DCache为8KB,那么一些宏定义如下
`define OR1200_DCSIZE 13 //DCache是8KB,所以地址宽度是13
`define OR1200_DCINDX `OR1200_DCSIZE-2 // 11
`define OR1200_DCINDXH `OR1200_DCSIZE-1 // 12
`define OR1200_DCTAGL `OR1200_DCINDXH+1 // 13
//13位地址中的高9位是DCache目录表的索引
`define OR1200_DCTAG `OR1200_DCSIZE-`OR1200_DCLS
//标识的宽度,包括地址的高19位、有效标志位V
`define OR1200_DCTAG_W 20
`endif
13.2 DCache结构
与ICache对应,OR1200中实现DCache的文件有or1200_dc_top.v、or1200_dc_fsm.v、or1200_dc_tag.v、or1200_dc_ram.v、or1200_spram.v、or1200_spram_32_bw.v,分别实现了DCache模块、DC_FSM模块、DC_TAG模块、DC_RAM模块、单口RAM、可按字节写的单口RAM模块。在DCache中例化了DC_FSM、DC_TAG、DC_RAM模块,在DC_TAG模块中例化了单口RAM,在DC_RAM模块中例化了可按字节写的单口RAM。如图13.1所示。其中DC_TAG、DC_RAM可以称为数据部分,DC_FSM可以称为控制部分,在数据部分进行查找操作,将查找结果(DCache命中或失靶)送到控制部分,由控制部分依据查找结果进行下一步的操作,如果是写操作,那么下一步的操作还需参考当前配置的写策略。本节将介绍DCache中的数据部分、控制部分。
13.2.1 DCache模块与其余模块的连接关系
在介绍DCache数据部分、控制部分之前,先给出DCache模块与其余模块之间的连接关系,有助于后面的分析。在第3章介绍QMEM时,已经给出了DCache与QMEM、SB之间的连接关系,从中可知DCache与两者之间都是通过Wishbone总线接口连接的,参考本书光盘中的or1200_top.vsd可以得到DCache模块完整的对外连接关系,如图13.2所示。
有以下几点说明:
(1)从图中可知DCache除了具有Wishbone总线接口外还具有特殊寄存器访问接口:spr_cs、spr_write、spr_dat_i,与ICache一样,这说明DCache中有特殊寄存器,但是该特殊寄存器不可以读(没有spr_dat_o接口),只能写(有spr_dat_i接口),与ICache不同之处在于DCache还有spr_addr接口,说明DCache中的特殊寄存器是有地址要求的,也就是有索引,而在ICache中只有一个特殊寄存器,所以其索引可以任意,参考表12.1。
(2)相比CPU与ICache之间的接口,CPU与DCache之间还增加了dc_no_writethrough、mtspr_dc_done两个接口,其作用会在后面的分析中介绍。
(3)QMEM的输出中有接口dcqmem_ci_o,该信号实际直接来自DMMU,回忆一下DTLB中每个表项都有一个标志位CI,表示对应的页是否可以被缓存,通过QMEM的dcqmem_ci_o输出到DCache中,如果要访问一个内存块的内容,但是该内存块所在页的dcqmem_ci_o值为1,那么该内存块是不可能在DCache中,无需查找DCache,直接从内存中访问,反之,如果dcqmem_ci_o的值为0,还要首先在DCache中进行查找。
(4)相比QMEM与ICache之间的Wishbone总线接口,QMEM与DCache之间的Wishbone总线接口多了两个输入dcqmem_dat_i、dcqmem_we_i,这是与存储操作有关的接口信号。
(5)DCache并没有与WB_BIU模块直接连接,而是在DCache与WB_BIU之间放置了一个SB(Store Buffer)模块,后者与DCache之间的接口也符合Wishbone总线规范,该模块的作用将在下一章分析。
(6)DCache与SB之间的接口名称都是dcsb_xxx_x的形式,DCache与QMEM之间的接口名称都是dcqmem_xxx_x的形式,因此通过名称就可以知道这个接口的位置。
13.2.2 DCache中数据部分
DCache的数据部分包括DC_TAG、DC_RAM,其主体分别是单口RAM、可按字节写的单口RAM。两者共同组成了图12.2中的目录表。查找方法如图13.3所示。
此处采用的是OR1200默认的DCache设置,即总容量是8KB,内存块大小为16字节,目录表有512 line。图13.3的解释请参考对ICache中数据部分的解释,需要注意一点:DC_TAG每个表项包含标识、V、Dirty,比IC_TAG多了一个Dirty标志位,该位为1表示当前line对应的缓存数据被修改了但是对应内存块没有修改,也就是缓存与内存不一致,反之表示line对应的缓存与内存一致。参考通写法、回写法的定义可知:只有在使用回写法策略时,才可能存在Dirty为1的情况,在使用通写法策略时,Dirty标志位始终为0。
DC_TAG是通过单口RAM实现的,其主要代码如下,注意一点:其数据宽度是21,比ICache中的IC_TAG多了一位用来保存Dirty的值。
or1200_dc_tag.v
module or1200_dc_tag( //DC_TAG
clk, rst, addr, en, we, datain, tag_v, tag, dirty
);
……
//OR1200_DCTAG_W为20,所以dw为21,增加的1位是Dirty位
parameter dw = `OR1200_DCTAG_W + 1;
//地址宽度为9
parameter aw = `OR1200_DCTAG;
or1200_spram # //例化单口RAM
( .aw(`OR1200_DCTAG), .dw(`OR1200_DCTAG_W + 1) )
//从单口RAM中读出的数据doq包括标识、有效位V、是否修改位Dirty
dc_tag0
( .clk(clk), .ce(en), .we(we), .addr(addr),
.di(datain), .doq({tag, tag_v, dirty}) );
endmodule
DC_RAM也是通过单口RAM实现的,但是可按字节写的单口RAM,回忆一下OR1200中的存储指令l.sb、l.sh,都是只修改一个字中的一部分,所以需要这样设计。DC_RAM的主要代码如下:
or1200_dc_ram.v
module or1200_dc_ram( //DC_RAM
clk, rst, addr, en, we, datain, dataout
);
parameter dw = `OR1200_OPERAND_WIDTH; //数据宽度是32bit
parameter aw = `OR1200_DCINDX; //地址宽度是11
……
or1200_spram_32_bw # //例化单口RAM,此处的单口RAM可以按字节写
( .aw(`OR1200_DCINDX), .dw(dw) )
dc_ram //注意变量we的宽度为4
( .clk(clk), .ce(en), .we(we), .addr(addr),
.di(datain), .doq(dataout) );
endmodule
其中可以按字节写的单口RAM是使用4个8bit宽度的数组实现的,主要代码如下:
or1200_spram_32_bw.v
module or1200_spram_32_bw ( clk, ce, we, addr, di, doq );
parameter aw = 10;
parameter dw = 32; //数据宽度是32
……
input [3:0] we; // 注意此处的写使能信号we的宽度是4
reg [7:0] mem0 [(1<<aw)-1:0]; //定义了4个8位数组
reg [7:0] mem1 [(1<<aw)-1:0];
reg [7:0] mem2 [(1<<aw)-1:0];
reg [7:0] mem3 [(1<<aw)-1:0];
……
//分别从4个8位数组中读出对应的数据组成一个//32位的数作为要读取的数
assign doq = {mem0[addr_reg], mem1[addr_reg], mem2[addr_reg], mem3[addr_reg]};
always @(posedge clk)
if (ce)
addr_reg <= addr; //寄存地址信号
always @(posedge clk)
if (ce) begin
if (we[3]) //如果we[3]为1表示要写目的地址的最高位字节
mem0[addr] <= di[31:24];
if (we[2]) //如果we[2]为1表示要写目的地址的次高位字节
mem1[addr] <= di[23:16];
if (we[1]) //如果we[1]为1表示要写目的地址的次低位字节
mem2[addr] <= di[15:08];
if (we[0]) //如果we[0]为1表示要写目的地址的最低位字节
mem3[addr] <= di[07:00];
end
endmodule // or1200_spram
13.2.3 DCache中控制部分
DCache会依据DC_RAM、DC_TAG的查找结果,以及当前的写策略决定下一步的操作。DCache中的控制部分主要在DC_FSM模块中实现,其主体是一个状态机,有IDLE、CLOADSTORE、LOOP2、LOOP3、LOOP4、FLUSH5、INV6、WAITSPRCS7等8个状态,定义如下:
or1200_fsm.v
`define OR1200_DCFSM_IDLE 3'd0
`define OR1200_DCFSM_CLOADSTORE 3'd1
`define OR1200_DCFSM_LOOP2 3'd2
`define OR1200_DCFSM_LOOP3 3'd3
`define OR1200_DCFSM_LOOP4 3'd4
`define OR1200_DCFSM_FLUSH5 3'd5
`define OR1200_DCFSM_INV6 3'd6
`define OR1200_DCFSM_WAITSPRCS7 3'd7
由于有两种写策略,在不同的写策略下,DCache有不同的工作过程,状态转换也不同,比较复杂,所以本章将在分析DCache过程中逐步给出状态之间转换关系。
13.2.4 DCache数据部分与控制部分的对外接口
DC_FSM、DC_TAG、DC_RAM模块都在DCache中例化,例化语句如下:
or1200_dc_top.v
……
or1200_dc_fsm or1200_dc_fsm( //例化DC_FSM
.clk(clk),
.rst(rst),
.dc_en(dc_en),
.dcqmem_cycstb_i(dcqmem_cycstb_i),
.dirty(dirty),
.tag(tag),
.tag_v(tag_v),
.dcqmem_ci_i(dcqmem_ci_i),
.burst(dcfsm_burst),
.tag_we(dcfsm_tag_we),
.dc_addr(dc_addr),
.dcqmem_we_i(dcqmem_we_i),
.dcqmem_sel_i(dcqmem_sel_i),
.tagcomp_miss(tagcomp_miss),
.biudata_valid(dcsb_ack_i),
.biudata_error(dcsb_err_i),
.lsu_addr(dcqmem_adr_i),
.dcram_we(dcram_we),
.biu_read(dcfsm_biu_read),
.biu_write(dcfsm_biu_write),
.dcram_di_sel(dcfsm_dcram_di_sel),
.biu_do_sel(dcfsm_biu_do_sel),
.first_hit_ack(dcfsm_first_hit_ack),
.first_miss_ack(dcfsm_first_miss_ack),
.first_miss_err(dcfsm_first_miss_err),
.dc_no_writethrough(dc_no_writethrough),
.tag_valid(dcfsm_tag_valid),
.tag_dirty(dcfsm_tag_dirty),
.dc_block_flush(dc_block_flush),
.mtspr_dc_done(mtspr_dc_done),
.dc_block_writeback(dc_block_writeback),
.spr_dat_i(spr_dat_i),
.spr_cswe(spr_cs & spr_write)
);
or1200_dc_ram or1200_dc_ram( //例化DC_RAM
.clk(clk),
.rst(rst),
.addr(dc_addr[`OR1200_DCINDXH:2]),
.en(dc_en),
.we(dcram_we),
.datain(to_dcram),
.dataout(from_dcram)
);
or1200_dc_tag or1200_dc_tag( //例化DC_TAG
.clk(clk),
.rst(rst),
.addr(dctag_addr),
.en(dctag_en),
.we(dctag_we),
.tag_v(tag_v),
.tag(tag),
.dirty(dirty)
.datain({dc_addr[31:`OR1200_DCTAGL], dctag_v, dctag_dirty}),
);
……
参考上述例化语句得到图13.4,其中给出了DC_FSM、DC_TAG、DC_RAM模块的接口,以及各个接口连接到DCache的对应变量,每个模块的左边是输入接口,右边是输出接口,每个模块内部是接口名,外部引脚上的名称代表DCache中的对应变量。本章在后面分析DCache时需要经常参考该图,各个接口的含义也留在使用到该接口的时候再作说明。