跳转至

Lab5 片上存储总线

欲速则不达,见小利则大事不成。 ——《论语·子路》

随着时代的发展,当今的程序已经逐步倾向于存储密集型,即程序的运行过程中需要大量的数据,因此片上存储系统已经成为了整个计算系统中越来越重要的一部分。在存储访问过程中,如何使用处理器可靠地、高效地进行访问是我们重要的研究方向。在本次实验中,我们将会基于 AXI4 总线协议设计处理器的存储接口,从而实现对片上存储的访问。

本次实验的主要任务有:

  • 了解 AXI4 总线时序,理解 AXI4 总线协议;
  • 根据指令高速缓存和数据高速缓存的特性,设计 AXI4 转接-仲裁桥。
请获取 Lab5 所需的代码框架

请将工作目录切换到 Lab5,运行以下命令:

shell
$ ./zinit.sh

在获取源码之后,请将 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_readpmem_write 函数。这两个函数的实现已经完成,你需要阅读这两段代码,了解 AXI4 协议在存储器端的实现细节。

从本次实验起,我们不再使用 DPI-C 机制完成访存

在之前的实验中,我们一直在使用 DPI-C 机制进行访存,这种方式与真实的系统有很大差距:真实的系统中,CPU 的访存信号应当被拉到顶层模块接口中,并连接到存储系统。从本次实验起,我们将 CPU 的访存信号接到顶层,并使用 pmem_readpmem_write 函数模拟存储系统直接与 CPU 交互,实现访存操作。

Task 1.1

请补全 memory/paddr.cpp 中 pmem_read 函数。

在补全代码之前,请阅读 memory/paddr.cpp 中的 pmem_write 函数,了解 AXI4 总线协议的仿真实现。你需要特别关注的是:

  • 一次读调用和写调用在仿真环境中如何实现多次握手;
  • 仿真环境中的 AXI4 总线信号如何与 AXI4 总线协议中的信号对应。

在理解握手的具体实现后,请参考 pmem_write 函数和代码框架,实现 pmem_read 函数。

完成这一步后,请关注套在这两个函数外的预处理语句:

C++
#ifdef AXI
// ...
#endif

我们通过定义 AXI 宏来启用 AXI4 总线协议。你可以在编译时定义这个宏,只需要修改 script/rewrite.mk 中的 CXXFLAGS

makefile
CXXFLAGS += -DAXI

完成以上工作之后,请你修改 sim.cpp 中的 single_cycle 函数:

C++
void single_cycle() {
    dut->clk = 1;
    dut->eval();
    dut->clk = 0;
#ifdef AXI
    pmem_write();
    pmem_read();
#endif
    dut->eval();
    // m_trace->dump(sim_time++);
    if(dut->commit_wb == 1) set_state();
}

2 AXI4转接-仲裁桥的设计

请对照代码框架中的信号命名约定阅读下文

在代码框架中的 axi_arbiter.sv 文件中,我们给出了各个输入输出信号的命名约定。大家在阅读下文的过程中可以对照代码来看,这样会有利于大家对于逻辑设计的理解。

2.1 转接-仲裁桥简介

通过前一个任务,我们可以看出 AXI4 相对于我们熟悉的 BRAM 接口有以下两个“劣势”:

  • 单次访问时间要更多,访问存储器时流水线需要更多停顿;
  • 握手信号更复杂,处理器需要更复杂的控制逻辑。

为了解决这个问题,我们必须要使用两个高速缓存(指令高速缓存、数据高速缓存)来辅助进行访存:

  • 如果访问的数据已经在高速缓存中,那么根据 BRAM 的特性,只需要一个周期的访存即可,流水线不需要停顿;
  • 如果访问的数据不在高速缓存中,那么高速缓存会向存储器发起读写请求,流水线需要停顿。

高速缓存并不会向存储器发出 AXI4 协议的信号,因为 AXI4 协议的信号会引入更多控制逻辑,这对于本来就在时序关键路径上的高速缓存无疑是雪上加霜。因此,我们需要在高速缓存和存储器之间再加入一个转接-仲裁桥:

image-20230824081752155

这个转接-仲裁桥的主要功能是:

  • 将 Cache 发出的 valid-ready 握手信号转换为 AXI4 总线信号,将 AXI4 的响应信号转换为简单的 valid-ready 握手信号;
  • 仲裁 ICache 和 DCache 同时发出的读请求,使主存先处理 DCache 请求再处理 ICache 请求。

由于 AXI4 总线支持读写并行,因此我们的转接-仲裁桥需要两个独立的状态机分别处理读写请求,下面我们将逐一实现。

2.2 转接-仲裁桥的写状态机

在转接-仲裁桥的设计中,写状态机是简单的,因为只有 DCache 会向主存发起“写回”操作,所以写状态机只负责转接,不需要仲裁。

在设计前,我们先来了解一下 DCache 是如何配合 AXI4 总线信号进行数据写回的:

  1. DCache 会向转接-仲裁桥发出一个 d_wvalid 信号,这个信号代表 DCache 当前有一个 32 位的数据需要写回主存。转接-仲裁桥需要响应一个 d_wready 信号告知 DCache 当前数据已经被写回主存;
  2. 第 1 步会被不断重复,直到当前数据是最后一个数据时,DCache 会在发出 d_wvalid 的同时发出一个 d_wlast 信号,用来表示当前是最后一个写数据;
  3. DCache 的全部数据发出后,DCache 会发出 d_bready 信号,并等待转接-仲裁桥的 d_bvalid 信号完成写回握手。

我们可以看到,DCache 并不会发出 awvalid 信号,也就是说转接-仲裁桥对 DCache 隐藏了地址握手,从而将写操作的三类握手简化为两类握手:重复的写数据握手和一次写回确认握手。我们可以设计转接-仲裁桥的写状态机如下:

image-20230824084210686

这个状态机需要调控六个信号:

  • d_wready:Dcache 的写数据回应信号;
  • d_bvalid:DCache 的写回确认请求信号;
  • awvalid:主存的写地址请求信号;
  • wvalid:主存的写数据请求信号;
  • wlast:主存的写数据结束信号;
  • bready:主存的写回确认回应信号。

对于每个状态的含义和输出如下所示:

  • IDLE:表示当前没有任何写请求,所有信号都应该置为 0;
  • AW:表示当前正在和主存进行写地址握手,此时应当将 awvalid 置为 1;
  • W:表示当前正在不停和主存进行写数据握手,此时应当将 d_wready 置为 wreadywvalid 置为 1,wlast 置为 d_wlast
  • B:表示当前正在和主存进行写回确认握手,此时应当将 d_bvalid 置为 bvalidbready 置为 d_bready
Task 2.1

请按照上述描述,在 axi_arbiter 模块中的相应位置完成写状态机的补充。该写状态机是第三段为组合逻辑的三段式状态机。

避免产生锁存器!

在状态机组合逻辑的 always 块中,很容易由于列举情况不完备导致锁存器。

好在,为了避免出现这种问题,助教已经在 always 块中写出了每个信号的默认赋值。你只需要在默认赋值之后使用 case 语句对你需要改变的信号进行赋值即可。

转接的含义

转接是在握手的基础上衍生出来的一种操作。但是,如果两个设备采用不同的协议和接口,那握手也就无从谈起。因此,我们需要一个中间的逻辑单元来调配握手信号,这也就是我们所说的转接。

2.3 转接-仲裁桥的读状态机

ICache 和 DCache 都可能对主存发起读操作,因此转接-仲裁桥的读状态机不仅需要对 Cache 和 AXI4 的信号进行转接,还需要对双 Cache 的请求进行仲裁。

以 ICache 为例,我们先来了解 Cache 如何发起读请求并进行处理:

  1. ICache 会向转接-仲裁桥发出一个 i_rvalid 请求,这个信号代表 ICache 在请求一个数据。同时,在第一个 i_rvalid 发出时,请求读取的地址也会被送到转接-仲裁桥;
  2. 第一步会被不断重复,直到 ICache 从转接-仲裁桥同时得到了 i_rreadyi_rlast 信号,表明本次读事务全部处理完毕。

读请求的处理比写请求要更加简单,转接-仲裁桥隐去了地址握手的部分,使得读操作握手只包括若干次读数据握手。不过,由于 DCache 和 ICache 可能同时发出读请求,因此转接-仲裁桥需要对二者进行“仲裁”。我们采用固定的优先级,即优先响应 DCache 的读请求。

我们可以设计状态机如下:

image-20230824103905460

这个状态机需要转接-仲裁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 置为 rlastd_rready 置为 rvalid
  • I_AR:表示当前正在和主存进行读地址握手,此时应当控制信号 arvalid 置为 1,数据信号 arlen, arsize, araddr 分别置为 i_rlen, i_rsize, i_raddr
  • I_R:表示当前正在进行重复多次的读数据握手,此时应当将控制信号 rready 置为 1,i_rlast 置为 rlasti_rready 置为 rvalid
为什么要将 rvalid 接到 d_rready 上?

Valid 是主方发起的请求信号,而 ready 是从方发出的回应信号。在 Cache 和转接-仲裁桥这一侧,读操作是由 Cache 发起,由转接-仲裁桥回应。而在存储器和转接-仲裁桥这一侧,读数据由存储器给出,转接-仲裁桥回应。因此在这里,存储器的 rvalid 恰好就是 DCache 的 d_rready

从这里也可以看出,转接-仲裁桥不仅仅可以转接单向的请求,还可以将双向请求的多次握手进行简化。

Task 2.2

请按照上述描述,在 axi_arbiter 模块中的相应位置完成读状态机的补充。该读状态机是第三段为组合逻辑的三段式状态机。 在补充完成后,你就可以运行 functest 和 picotest 中的测试用例了。如果你的实现完全正确,那么这些测试用例都可以通过。

3 实验报告

在本次实验的报告中,你需要回答或阐述以下问题:

  1. 在 AXI4 协议中,写操作分为哪三类握手,它们在一次写事务中的进行顺序是怎样的?
  2. 你认为为什么要先响应 DCache 的读请求?(提示:请你阅读流水线代码,观察 ICache 请求是否会阻塞 ID 之后的流水线?)
  3. 你对本次实验有什么意见或建议?