实验5:AXI总线与Cache
推荐兄弟文档,AXI总线与Cache
前景引入
Cache存在的目的是为了提高存储器的访问速度,其解决了CPU和主存之间速度不匹配的问题
到目前为止,我们已经明白,我们需要一个大小合适的BRAM作为与CPU直接交互的存储器。
现在还有一个问题,这个存储器不能够存下我们需要的所有内容,怎么办?
这里就是体现了Cache的核心思想。存下最有可能被访问的数据,每次访问的时候,先访问Cache,如果Cache中有,直接返回,如果没有,再去主存中取。
综上所述,在实现Cache的时候,我们需要解决的问题就是:
- 访问Cache,什么情况下访问中
- 访问Cache,发现没有访问中,怎么从主存中取数据
- 如果Cache里面的数据都是有效的,现在需要写入数据,替换掉哪一个数据块
工作原理
L1 Cache的工作原理
L1缓存(Level 1 Cache)是CPU中最接近核心处理单元(如ALU,算术逻辑单元)的高速存储区域。它的主要作用是加速CPU对数据的访问,尤其是那些CPU频繁访问的数据。L1缓存的工作原理可以从以下几个方面进行详细阐述:
1. 缓存的基本概念
缓存(Cache)是计算机系统中的一种小型、高速存储器,用来存储频繁访问的数据。L1缓存是CPU内部的一级缓存,直接连接到CPU核心,它通常分为两个部分: - 指令缓存(L1 I-cache):存储指令,帮助CPU快速获取程序指令,减少访问内存的延迟。 - 数据缓存(L1 D-cache):存储数据,帮助CPU快速读取和写入数据。
2. 缓存的访问流程
CPU执行指令时,通常会首先访问L1缓存。以下是L1缓存的典型工作流程: 1. 查找数据:CPU首先在L1缓存中查找需要的数据或指令。 2. 命中(Cache Hit):如果数据在L1缓存中找到了,称为“命中”(Cache Hit)。此时,CPU可以直接从L1缓存中获取数据,避免了访问慢速主存(DRAM)的延迟。 3. 未命中(Cache Miss):如果数据在L1缓存中没有找到,称为“未命中”(Cache Miss)。此时,CPU会向L2缓存、L3缓存或者主存(RAM)请求数据。如果L2或L3缓存命中,数据将从那里获取;否则,最终从主存中获取数据。 4. 数据更新:如果从L2缓存、L3缓存或主存获取数据时,CPU会将数据加载到L1缓存中,以便下次使用。
助教的提示
Cache的状态机怎么设计,核心就是在上面的这段话。如果实验的时候,不知道怎么设计,请反复阅读,并且理解其中的逻辑。心里有数后,再去实现。
3. 缓存的局部性原理
L1缓存的高效工作依赖于局部性原则,主要包括: - 时间局部性(Temporal Locality):如果某个数据被访问过,那么它在短时间内很可能会再次被访问。例如,CPU在执行循环时,访问相同的数据多次,L1缓存可以存储这些数据,以减少重复访问主存的开销。 - 空间局部性(Spatial Locality):如果某个数据被访问,附近的数据也很可能被访问。例如,程序访问数组时,通常是访问连续的元素。L1缓存会将数据按块(block)加载到缓存中,这样相邻的数据也能在缓存中找到。
4. 缓存一致性和替换策略
由于L1缓存的容量较小,存储的数据量有限,因此需要使用一定的缓存替换策略来决定哪些数据应该被保留,哪些应该被丢弃。常见的缓存替换策略有: - LRU(Least Recently Used):替换最久没有被访问的数据。 - FIFO(First In First Out):按照数据进入缓存的顺序进行替换。 - 随机替换:随机选择一个缓存行进行替换。
5. L1缓存的大小和结构
L1缓存的大小通常比较小,通常在32KB到128KB之间,具体大小取决于CPU的架构。L1缓存通常是分离式(Separate)结构,即指令缓存和数据缓存是分开存储的,也可以是统一式(Unified)结构,即指令和数据缓存共享同一块存储区域。 - 分离式缓存:L1 I-cache和L1 D-cache各自独立管理指令和数据的缓存,适用于指令和数据访问模式不同的场景。 - 统一式缓存:L1缓存作为一个整体来管理指令和数据,适用于指令和数据访问模式比较相似的场景。
6. L1缓存的读写过程
- 读取数据:当CPU需要读取数据时,它首先会查找L1数据缓存。如果数据存在于缓存中(命中),CPU直接使用。如果没有(未命中),则需要从L2缓存或主存读取,并将数据加载到L1缓存中。
- 写入数据:L1缓存的数据写入通常有两种方式:
- 写回(Write-back):数据写入L1缓存后,只有在该数据被替换出去时,才会写回主存。
- 写直达(Write-through):数据在写入L1缓存的同时,也直接写入主存,确保缓存和主存的同步。
- 更多请自行上网查询
基本结构
Cache的地址映射方式
指的是,Cache怎么"知道"(准确的说是维护),其某一个位置的数据与主存的哪一个位置对应。
常见的有三种方式
下面已两路组相连的方式给出一个设计框架
ICache的状态机与原理图
-
状态机:
这里注意i_前缀的信号是BRAM发给Cache的,是AXI读数据通道的信号;而控制idle向lookup跳转的rvalid是外部(排序模块)传给Cache的,和BRAM的AXI信号没有关系
-
这是一个两路组相联的原理图:
其中TagV Memory是Tag和Valid的存储区域。
为什么需要valid?valid代表对应的cache块的内容是否有效,本次实验中,初始cache内部所有块都无效,但是它们都有00000的tag,如何分辨一个块内的数据是无效的还是00000tag对应的有效数据呢,这时候就需要一位valid指示,初始valid全为0,每次调入块后,在记录tag的同时将对应valid置1
-
相关的交互信号及其含义
- 输入信号
信号名 意义 raddr [31: 0] 需要访存的主存的地址 rvalid [ 0: 0] 该地址访存是否有效 extra 自己决定的 - 输出信号
信号名 意义 rdata [31: 0] 上一个周期,给点地址对应的数据 extra 自己决定的
DCache的状态机与原理图
-
状态机:
-
这是一个两路组相联的原理图:
AXI接口的BRAM使用教程
手册/文档查询
计算机体系结构基础 第六章中对AXI的简介 有清晰的示例时序图(先看这个)
AXI协议技术文档: AXI Protocol v1.0.pdf
AXI参考指导:ug761_axi_reference_guide.pdf
中文版简介: AXI 总线简介.pdf (再看这个,不需要看Intercontact部分)
IP核BRAM生成器的技术手册 pg058-blk-mem-gen.pdf
简单补充AXI协议的5种通道:
以下是信息传输的主要方向,每个通道有相反方向的ready信号代表是否可以接收。a为address的缩写,对应的通道会提供地址,并发起请求。
Cache ---读请求ar---> BRAM
Cache <---读数据r--- BRAM
Cache ---写请求aw---> BRAM
Cache ---写数据w---> BRAM
Cache <---写响应b--- BRAM
以读请求通道设置arlen为8'h3(每次读传输4拍)为例,先经过一个周期的读请求握手,然后需要读数据通道的四个周期的握手,其中最后一次握手有rlast为1指示这是最后一拍。
下图T1没有完成握手,T2读请求通道握手成功,之后T6 T9 T10 T13完成四次读数据握手,最后一次有RLAST有效信号。这是一次完整的总线事务,包含5次总线传输。
以下介绍IP核通用的文档查询方法,但你不必在本次实验中完成它。
在下载了docnav的情况下,你可以在IP核中打开对应的手册。本课程的vivado安装建议中没有勾选docnav,你可以通过HELP/Add Design Tools or Devices来安装docnav。这里的常见踩坑细节见课程群。
IP核参数选择
接口类型选择AXI4,本次不需要错误收集(ECC)
AXI类型建议选择完整的AXI4。
你也可以选择简化版的AXI4 Lite,只是AXI4 Lite每次读写都只能读写一次,而不能用arlen信号来指定单次读请求连续读取的拍数。
具体而言,本次实验要求规格为1024×32的BRAM,Cache的一块有四个字,对应四个字地址;Cache读取BRAM时,在AXI4下可以指定arlen=8'h3,单次读传输进行4拍(每一拍,是指一次读数据通道成功握手的时钟上升沿),每拍传一个字,只需要一次读请求握手,4次读数据握手;而在AXI4 Lite下由于单次传输只能传一个数据,需要进行四次读请求握手,每次读请求握手后分别有一次读数据握手。
深度1024:BRAM有1024个单元
宽度32:每个单元32位
这样生成的BRAM的接口中,地址要求输入32位,而且是字节地址,单位为字节,最低两位可以视作在字中选择哪个字节,最高20位无效,中间10位为我们所使用的字地址,对应1024的深度。
IP核例化方法
在Sources中选择IP Sources,你可以在.veo文件中找到IP核例化的示例,复制使用节约时间,并通过注释了解其位宽和「是输入还是输出信号」。
以下给出一个参考的例化示例
blk_mem_gen_1 bram_axi (
.rsta_busy(rsta_busy), // output wire rsta_busy
.rstb_busy(rstb_busy), // output wire rstb_busy
.s_aclk(clk), // input wire s_aclk
.s_aresetn(rstn), // input wire s_aresetn
.s_axi_awid(4'b0001), // input wire [3 : 0] s_axi_awid
.s_axi_awaddr({20'h0,addr,2'h0}), // input wire [31 : 0] s_axi_awaddr
//根据你的需要,也可以让读写地址不同
//注意:这里的地址是字节地址,一个地址对应一个字节
.s_axi_awlen(8'h0), // input wire [7 : 0] s_axi_awlen
//写每次传输的拍数 cache可采用写直达策略,每次cache内容更改后,立即将这一字写入BRAM
.s_axi_awsize(3'b010), // input wire [2 : 0] s_axi_awsize
//数据传输每拍传输的字节数,这里一拍传输4字节(1字32位)
.s_axi_awburst(2'b01), // input wire [1 : 0] s_axi_awburst
//突发类型为incrementing burst,即地址递增,对应常规的顺序存储
.s_axi_awvalid(awvalid), // input wire s_axi_awvalid
//写请求有效信号,表示当前写请求通道的数据有效,cache希望开始写数据
.s_axi_awready(awready), // output wire s_axi_awready
//写请求准备好信号,表示当前BRAM准备好开始接收数据
.s_axi_wdata(wdata), // input wire [31 : 0] s_axi_wdata
//要写入的数据
.s_axi_wstrb(4'b1111), // input wire [3 : 0] s_axi_wstrb
//写数据字节选通位,表示当前写数据通道的数据哪些字节有效要写入,这里一字的每个字节都有效
.s_axi_wlast(wlast), // input wire s_axi_wlast
//写数据最后一个有效信号,表示当前写数据通道的数据是否是最后一个有效数据
//如果采用每次写只写一拍,那么这个信号可以固定为1
.s_axi_wvalid(wvalid), // input wire s_axi_wvalid
//写数据有效信号,表示当前cache向写数据通道提供的数据有效,要写入BRAM
.s_axi_wready(wready), // output wire s_axi_wready
//写数据准备好信号,表示BRAM准备好接收数据
.s_axi_bid(bid), // output wire [3 : 0] s_axi_bid
//写响应ID,BRAM告知Cache,对应ID的写请求已经完成,本次可忽略该信号
.s_axi_bresp(bresp), // output wire [1 : 0] s_axi_bresp
//写响应response,BRAM告知Cache,对应ID的写请求是否成功完成,本次可忽略该信号
.s_axi_bvalid(bvalid), // output wire s_axi_bvalid
//写响应握手信号,BRAM表示当前写响应通道的数据是有效的
.s_axi_bready(bready), // input wire s_axi_bready
//写请求响应准备好信号,cache表示当前写响应通道准备好接收数据
.s_axi_arid(4'b0001), // input wire [3 : 0] s_axi_arid
.s_axi_araddr({20'h0,addr,2'h0}), // input wire [31 : 0] s_axi_araddr
//注意:这里的地址是字节地址,一个地址对应一个字节
//读取的地址将从此地址开始连续4个字,越界则从0开始
.s_axi_arlen(8'h3), // input wire [7 : 0] s_axi_arlen
//读每次传输的拍数 cache一行四字 一拍传输一字 一次传输4拍
.s_axi_arsize(3'b010), // input wire [2 : 0] s_axi_arsize
//数据传输每拍传输的字节数,这里一拍传输4字节(1字32位)
.s_axi_arburst(2'b01), // input wire [1 : 0] s_axi_arburst
//突发类型为incrementing burst,即地址递增,对应常规的顺序存储
.s_axi_arvalid(arvalid), // input wire s_axi_arvalid
//读请求有效信号,表示当前读请求通道的数据有效,cache希望开始读数据
.s_axi_arready(arready), // output wire s_axi_arready
//读请求准备好信号,表示当前BRAM准备好开始提供数据
.s_axi_rid(rid), // output wire [3 : 0] s_axi_rid
//读数据ID,BRAM告知Cache,本次读出的数据对应读请求的id,本次可忽略该信号
.s_axi_rdata(rdata), // output wire [31 : 0] s_axi_rdata
//读出的数据
.s_axi_rresp(rresp), // output wire [1 : 0] s_axi_rresp
//读数据response,BRAM告知Cache,本次读请求是否成功完成
.s_axi_rlast(rlast), // output wire s_axi_rlast
//读数据最后一拍指示,4拍的传输中,第四拍的rlast为1,其余为0
.s_axi_rvalid(rvalid), // output wire s_axi_rvalid
//读数据有效信号,BRAM表示当前读数据通道的数据是有效的
.s_axi_rready(rready) // input wire s_axi_rready
//读请求准备好信号,cache表示当前读数据通道准备好接收数据
);