Lab5 片上存储总线
欲速则不达,见小利则大事不成。 ——《论语·子路》
随着时代的发展,当今的程序已经逐步倾向于存储密集型,即程序的运行过程中需要大量的数据,因此片上存储系统已经成为了整个计算系统中越来越重要的一部分。在存储访问过程中,如何使用处理器可靠地、高效地进行访问是我们重要的研究方向。在本次实验中,我们将会基于 AXI4 总线协议设计处理器的存储接口,从而实现对片上存储的访问。
本次实验的主要任务有:
- 了解 AXI4 总线时序,理解 AXI4 总线协议;
- 根据指令高速缓存和数据高速缓存的特性,设计 AXI4 转接-仲裁桥。
请获取 Lab5 所需的代码框架
请将工作目录切换到 Lab5,运行以下命令:
在获取源码之后,请将 simulator/IP/pipeline-AXI 目录重新命名为 mycpu。
1 AXI4 总线与仿真环境的适配
请先阅读 AXI4 总线简介
如果你对 AXI4 总线协议并不了解,请参考 AXI4 总线简介,了解 AXI4 总线协议的基本内容后再来进行本次实验。
在之前的实验中,我们一直使用片内的 BRAM 作为存储器,但在实际的开发板上,BRAM 无法提供很大的容量。因此,我们转而采用 DRAM 作为我们的主存储器。然而,DRAM 的访问速度比较慢,我们需要使用特殊的协议来约定访问的时序。在我们的实验中,将通过 AXI4 总线来实现 CPU 对 DRAM 的访问,为此,我们需要重新设计 CPU 的访存接口,使其能够适配 AXI4 总线协议
同样地,我们使用仿真环境来模拟 DRAM。在 memory/paddr.cpp 中,我们定义了支持 AXI4 协议的 pmem_read
和 pmem_write
函数。这两个函数的实现已经完成,你需要阅读这两段代码,了解 AXI4 协议在存储器端的实现细节。
从本次实验起,我们不再使用 DPI-C 机制完成访存
在之前的实验中,我们一直在使用 DPI-C 机制进行访存,这种方式与真实的系统有很大差距:真实的系统中,CPU 的访存信号应当被拉到顶层模块接口中,并连接到存储系统。从本次实验起,我们将 CPU 的访存信号接到顶层,并使用 pmem_read
和 pmem_write
函数模拟存储系统直接与 CPU 交互,实现访存操作。
Task 1.1
请补全 memory/paddr.cpp 中 pmem_read
函数。
在补全代码之前,请阅读 memory/paddr.cpp 中的 pmem_write
函数,了解 AXI4 总线协议的仿真实现。你需要特别关注的是:
- 一次读调用和写调用在仿真环境中如何实现多次握手;
- 仿真环境中的 AXI4 总线信号如何与 AXI4 总线协议中的信号对应。
在理解握手的具体实现后,请参考 pmem_write
函数和代码框架,实现 pmem_read
函数。
完成这一步后,请关注套在这两个函数外的预处理语句:
我们通过定义 AXI
宏来启用 AXI4 总线协议。你可以在编译时定义这个宏,只需要修改 script/rewrite.mk 中的 CXXFLAGS
:
完成以上工作之后,请你修改 sim.cpp 中的 single_cycle
函数:
2 AXI4转接-仲裁桥的设计
请对照代码框架中的信号命名约定阅读下文
在代码框架中的 axi_arbiter.sv 文件中,我们给出了各个输入输出信号的命名约定。大家在阅读下文的过程中可以对照代码来看,这样会有利于大家对于逻辑设计的理解。
2.1 转接-仲裁桥简介
通过前一个任务,我们可以看出 AXI4 相对于我们熟悉的 BRAM 接口有以下两个“劣势”:
- 单次访问时间要更多,访问存储器时流水线需要更多停顿;
- 握手信号更复杂,处理器需要更复杂的控制逻辑。
为了解决这个问题,我们必须要使用两个高速缓存(指令高速缓存、数据高速缓存)来辅助进行访存:
- 如果访问的数据已经在高速缓存中,那么根据 BRAM 的特性,只需要一个周期的访存即可,流水线不需要停顿;
- 如果访问的数据不在高速缓存中,那么高速缓存会向存储器发起读写请求,流水线需要停顿。
高速缓存并不会向存储器发出 AXI4 协议的信号,因为 AXI4 协议的信号会引入更多控制逻辑,这对于本来就在时序关键路径上的高速缓存无疑是雪上加霜。因此,我们需要在高速缓存和存储器之间再加入一个转接-仲裁桥:
这个转接-仲裁桥的主要功能是:
- 将 Cache 发出的 valid-ready 握手信号转换为 AXI4 总线信号,将 AXI4 的响应信号转换为简单的 valid-ready 握手信号;
- 仲裁 ICache 和 DCache 同时发出的读请求,使主存先处理 DCache 请求再处理 ICache 请求。
由于 AXI4 总线支持读写并行,因此我们的转接-仲裁桥需要两个独立的状态机分别处理读写请求,下面我们将逐一实现。
2.2 转接-仲裁桥的写状态机
在转接-仲裁桥的设计中,写状态机是简单的,因为只有 DCache 会向主存发起“写回”操作,所以写状态机只负责转接,不需要仲裁。
在设计前,我们先来了解一下 DCache 是如何配合 AXI4 总线信号进行数据写回的:
- DCache 会向转接-仲裁桥发出一个
d_wvalid
信号,这个信号代表 DCache 当前有一个 32 位的数据需要写回主存。转接-仲裁桥需要响应一个d_wready
信号告知 DCache 当前数据已经被写回主存; - 第 1 步会被不断重复,直到当前数据是最后一个数据时,DCache 会在发出
d_wvalid
的同时发出一个d_wlast
信号,用来表示当前是最后一个写数据; - DCache 的全部数据发出后,DCache 会发出
d_bready
信号,并等待转接-仲裁桥的d_bvalid
信号完成写回握手。
我们可以看到,DCache 并不会发出 awvalid
信号,也就是说转接-仲裁桥对 DCache 隐藏了地址握手,从而将写操作的三类握手简化为两类握手:重复的写数据握手和一次写回确认握手。我们可以设计转接-仲裁桥的写状态机如下:
这个状态机需要调控六个信号:
d_wready
:Dcache 的写数据回应信号;d_bvalid
:DCache 的写回确认请求信号;awvalid
:主存的写地址请求信号;wvalid
:主存的写数据请求信号;wlast
:主存的写数据结束信号;bready
:主存的写回确认回应信号。
对于每个状态的含义和输出如下所示:
- IDLE:表示当前没有任何写请求,所有信号都应该置为 0;
- AW:表示当前正在和主存进行写地址握手,此时应当将
awvalid
置为 1; - W:表示当前正在不停和主存进行写数据握手,此时应当将
d_wready
置为wready
,wvalid
置为 1,wlast
置为d_wlast
; - B:表示当前正在和主存进行写回确认握手,此时应当将
d_bvalid
置为bvalid
,bready
置为d_bready
。
Task 2.1
请按照上述描述,在 axi_arbiter
模块中的相应位置完成写状态机的补充。该写状态机是第三段为组合逻辑的三段式状态机。
避免产生锁存器!
在状态机组合逻辑的 always
块中,很容易由于列举情况不完备导致锁存器。
好在,为了避免出现这种问题,助教已经在 always
块中写出了每个信号的默认赋值。你只需要在默认赋值之后使用 case 语句对你需要改变的信号进行赋值即可。
转接的含义
转接是在握手的基础上衍生出来的一种操作。但是,如果两个设备采用不同的协议和接口,那握手也就无从谈起。因此,我们需要一个中间的逻辑单元来调配握手信号,这也就是我们所说的转接。
2.3 转接-仲裁桥的读状态机
ICache 和 DCache 都可能对主存发起读操作,因此转接-仲裁桥的读状态机不仅需要对 Cache 和 AXI4 的信号进行转接,还需要对双 Cache 的请求进行仲裁。
以 ICache 为例,我们先来了解 Cache 如何发起读请求并进行处理:
- ICache 会向转接-仲裁桥发出一个
i_rvalid
请求,这个信号代表 ICache 在请求一个数据。同时,在第一个i_rvalid
发出时,请求读取的地址也会被送到转接-仲裁桥; - 第一步会被不断重复,直到 ICache 从转接-仲裁桥同时得到了
i_rready
和i_rlast
信号,表明本次读事务全部处理完毕。
读请求的处理比写请求要更加简单,转接-仲裁桥隐去了地址握手的部分,使得读操作握手只包括若干次读数据握手。不过,由于 DCache 和 ICache 可能同时发出读请求,因此转接-仲裁桥需要对二者进行“仲裁”。我们采用固定的优先级,即优先响应 DCache 的读请求。
我们可以设计状态机如下:
这个状态机需要转接-仲裁9个信号:
- 控制信号:
i_rready
:ICache 的读数据回应信号;i_rlast
:ICache 的读数据结束信号;d_rready
:DCache 的读数据回应信号;d_rlast
:DCache 的读数据结束信号;rready
:主存的读数据回应信号。
- 数据信号:
arlen
:主存的读数据个数信号(ICache 和 DCache 可能会有不同的读个数需求);arsize
:主存的单个读数据长度信号;arvalid
:主存的读地址请求信号;araddr
:主存的读地址信号。
仲裁的含义
从上面的信号中我们可以看到,比起转接的只调配控制信号,仲裁还需要调配一些数据信号。
对于每个状态的含义和输出如下所示:
- IDLE:表示当前无任何读请求,所有控制信号都应该置为 0;
- D_AR:表示当前正在和主存进行读地址握手,此时应当控制信号
arvalid
置为 1,数据信号arlen, arsize, araddr
分别置为d_rlen, d_rsize, d_raddr
; - D_R:表示当前正在进行重复多次的读数据握手,此时应当将控制信号
rready
置为 1,d_rlast
置为rlast
,d_rready
置为rvalid
; - I_AR:表示当前正在和主存进行读地址握手,此时应当控制信号
arvalid
置为 1,数据信号arlen, arsize, araddr
分别置为i_rlen, i_rsize, i_raddr
; - I_R:表示当前正在进行重复多次的读数据握手,此时应当将控制信号
rready
置为 1,i_rlast
置为rlast
,i_rready
置为rvalid
。
为什么要将 rvalid
接到 d_rready
上?
Valid 是主方发起的请求信号,而 ready 是从方发出的回应信号。在 Cache 和转接-仲裁桥这一侧,读操作是由 Cache 发起,由转接-仲裁桥回应。而在存储器和转接-仲裁桥这一侧,读数据由存储器给出,转接-仲裁桥回应。因此在这里,存储器的 rvalid
恰好就是 DCache 的 d_rready
。
从这里也可以看出,转接-仲裁桥不仅仅可以转接单向的请求,还可以将双向请求的多次握手进行简化。
Task 2.2
请按照上述描述,在 axi_arbiter
模块中的相应位置完成读状态机的补充。该读状态机是第三段为组合逻辑的三段式状态机。
在补充完成后,你就可以运行 functest 和 picotest 中的测试用例了。如果你的实现完全正确,那么这些测试用例都可以通过。
3 实验报告
在本次实验的报告中,你需要回答或阐述以下问题:
- 在 AXI4 协议中,写操作分为哪三类握手,它们在一次写事务中的进行顺序是怎样的?
- 你认为为什么要先响应 DCache 的读请求?(提示:请你阅读流水线代码,观察 ICache 请求是否会阻塞 ID 之后的流水线?)
- 你对本次实验有什么意见或建议?