第 9 章 存储管理单元设计

存储管理是现代操作系统的重要功能之一,其需要CPU硬件提供一定的支持,以软硬件协同的方式完成。CPU硬件中参与这一过程的逻辑通常被称为存储管理单元(Memory Management Unit,简称MMU)。截止目前,本书实践任务中所设计的CPU只实现了最简单的直接地址翻译模式。从这一章开始,我们将完成MMU其余部分的设计。这一部分的设计难点在于涉及的技术细节较多,初学者不容易分出主次。因此,我们将整个设计分为三个阶段,以供大家通过模块化的、循序渐进的方式来完成。

  • 第一阶段:我们将专注于TLB模块自身的设计。
  • 第二阶段:我们将TLB模块集成至已有的CPU中,并实现MMU相关指令和控制状态寄存器。
  • 第三阶段:我们将MMU相关异常的支持添加完毕,进行全部功能的联合验证。

在开始这一阶段的设计之前,请先学习《计算机体系结构基础》(第3版)的3.3节或其他文献中的相关内容。学习的重点在于理解计算机系统中围绕MMU进行的软硬件交互过程。在掌握这个交互过程原理的基础之上,就比较容易把指令系统规范中关于MMU的相关定义串联成一个有机整体,进而进行CPU中相关功能的设计。

【本章学习目标】

  • 掌握TLB MMU的相关知识。
  • 理解LoongArch架构中MMU相关的控制状态寄存器和指令。
  • 理解CPU中的地址翻译机制,理解LoongArch架构TLB相关异常及其处理过程。
  • 掌握在流水线CPU中添加TLB支持的方法。

【本章实践目标】

本章有三个实践任务(见本章9.2节)。读者可以在学习本章内容的基础上完成这些任务,两者之间的对应关系如下:

  • ??节和9.1节的内容对应实践任务17(9.2.1节)。
  • ??节和??节的内容对应实践任务18(9.2.2节)。
  • ??节和??节的内容对应实践任务19(9.2.3节)。

9.1 TLB模块设计分析

通过梳理TLB模块相关知识,并结合CPU流水线的结构特点,我们对TLB模块的设计分析如下:

1)TLB模块内部的主体应是一个二维组织结构的查找表。查找表的每一项分为两个部分,第一部分存储的信息既参与读写又参与查找比较,包括E、VPPN、PS、ASID和G;第二部分仅参与读写,包括PPN0、PLV0、MAT0、D0、V0、PPN1、PLV1、MAT1、D1、V1。查找表的项数由实现者自行定义。

2)TLB模块要支持取指和访存两个部分的虚实地址转换需求,即两部分都需要对TLB模块进行查找,且两部分对应的查找功能一致。查找时,需要向TLB模块输入s_vppn、s_va_bit12和s_asid信息,TLB模块输出的信息包含s_found、s_ppn、s_ps、s_plv、s_mat、s_d和s_v。其中输入的s_vppn来自访存虚地址的31..13位,s_va_bit12来自访存虚地址的12位,s_asid来自CSR.ASID的ASID域。TLB输出的s_ppn和s_ps用于产生最终的物理地址,s_found的结果用于判定是否产生TLB重填异常,s_found和s_v结果用于判定是否产生页无效异常,s_found和s_plv用于判定是否产生页特权等级不合规异常,s_found、s_v和s_d结果用于判定是否产生页修改异常。

3)为了使流水线能够满负荷运转不断流,TLB模块要能够支持取指和访存同时进行查找,这意味着上面的查找端口应该有两套。

4)TLB模块需要支持TLBSRCH指令的查找操作。我们建议复用访存指令查找TLB的端口,即输入复用s_vppn和s_asid,输出复用已有的s_found。除此而外还需要一个额外的s_index输出,用于记录命中在第几项,其信息用于填入到CSR.TLBIDX中。

5)TLB模块需要支持TLBWR和TLBFILL指令的写操作。我们建议为此设计独立的端口。此时需要向TLB模块输入写地址w_index,写入的TLB表项信息w_e、w_vppn、w_ps、w_asid、w_g、w_ppn0、w_plv0、w_mat0、w_d0、w_v0、w_ppn1、w_mat1、w_mat1、w_d1、w_v1。因为是写操作,所以必须有一个写使能输入信号we。

6)TLB模块需要支持TLBRD指令的读操作。我们倾向于为此设计独立的端口。此时需要向TLB模块输入读地址r_index。TLB模块需要输出读的结果有r_e、r_vppn、r_ps、r_asid、r_g、r_ppn0、r_plv0、r_mat0、r_d0、r_v0、r_ppn1、r_plv1、r_mat1、r_d1、r_v1。

7)TLB模块需要支持INVTLB指令的查找、无效操作。其不同op对应的查找所用信息都可以复用访存指令查找TLB的端口,只是需要一个额外的invtlb_op输入,用于标识invtlb的具体操作类型。invtlb的无效操作都是在TLB模块内部根据查找结果直接将符合条件的TLB表项的E位置为0。

通过上述分析,我们得到TLB模块的接口与内部主要信号的定义如下:

module tlb
#(
    parameter TLBNUM = 16
)
(
    input  wire                      clk,

    // search port 0 (for fetch)
    input  wire [              18:0] s0_vppn,
    input  wire                      s0_va_bit12,
    input  wire [               9:0] s0_asid,
    output wire                      s0_found,
    output wire [$clog2(TLBNUM)-1:0] s0_index,
    output wire [              19:0] s0_ppn,
    output wire [               5:0] s0_ps,
    output wire [               1:0] s0_plv,
    output wire [               1:0] s0_mat,
    output wire                      s0_d,
    output wire                      s0_v,

    // search port 1 (for load/store)
    input  wire [              18:0] s1_vppn,
    input  wire                      s1_va_bit12,
    input  wire [               9:0] s1_asid,
    output wire                      s1_found,
    output wire [$clog2(TLBNUM)-1:0] s1_index,
    output wire [              19:0] s1_ppn,
    output wire [               5:0] s1_ps,
    output wire [               1:0] s1_plv,
    output wire [               1:0] s1_mat,
    output wire                      s1_d,
    output wire                      s1_v,

    // invtlb opcode
    input  wire                      invtlb_valid,
    input  wire [               4:0] invtlb_op,

    // write port
    input  wire                      we,     //w(rite) e(nable)
    input  wire [$clog2(TLBNUM)-1:0] w_index,
    input  wire                      w_e,
    input  wire [              18:0] w_vppn,
    input  wire [               5:0] w_ps,
    input  wire [               9:0] w_asid,
    input  wire                      w_g,
    input  wire [              19:0] w_ppn0,
    input  wire [               1:0] w_plv0,
    input  wire [               1:0] w_mat0,
    input  wire                      w_d0,
    input  wire                      w_v0,
    input  wire [              19:0] w_ppn1,
    input  wire [               1:0] w_plv1,
    input  wire [               1:0] w_mat1,
    input  wire                      w_d1,
    input  wire                      w_v1,

    // read port
    input  wire [$clog2(TLBNUM)-1:0] r_index,
    output wire                      r_e,
    output wire [              18:0] r_vppn,
    output wire [               5:0] r_ps,
    output wire [               9:0] r_asid,
    output wire                      r_g,
    output wire [              19:0] r_ppn0,
    output wire [               1:0] r_plv0,
    output wire [               1:0] r_mat0,
    output wire                      r_d0,
    output wire                      r_v0,
    output wire [              19:0] r_ppn1,
    output wire [               1:0] r_plv1,
    output wire [               1:0] r_mat1,
    output wire                      r_d1,
    output wire                      r_v1
);

reg  [TLBNUM-1:0] tlb_e;
reg  [TLBNUM-1:0] tlb_ps4MB; //pagesize 1:4MB, 0:4KB
reg  [      18:0] tlb_vppn     [TLBNUM-1:0];
reg  [       9:0] tlb_asid     [TLBNUM-1:0];
reg               tlb_g        [TLBNUM-1:0];
reg  [      19:0] tlb_ppn0     [TLBNUM-1:0];
reg  [       1:0] tlb_plv0     [TLBNUM-1:0];
reg  [       1:0] tlb_mat0     [TLBNUM-1:0];
reg               tlb_d0       [TLBNUM-1:0];
reg               tlb_v0       [TLBNUM-1:0];
reg  [      19:0] tlb_ppn1     [TLBNUM-1:0];
reg  [       1:0] tlb_plv1     [TLBNUM-1:0];
reg  [       1:0] tlb_mat1     [TLBNUM-1:0];
reg               tlb_d1       [TLBNUM-1:0];
reg               tlb_v1       [TLBNUM-1:0];

......

endmodule

接下来考虑TLB模块内部设计,其实就是查找、读、写三套操作的设计实现。读和写操作的实现不用再介绍了,大家可以参考CPU中regfile的逻辑设计以及Verilog代码实现。唯一一个可能需要注意的地方是,TLB模块读写接口上的PS域是6比特的,但是因为在LoongArch精简版中只支持4KB和4MB两种页大小,所以TLB模块内部只用1比特来存放这两种页大小信息,需要进行一个简单的转换。

对于查找操作实现,指令手册给出的TLB查找流程的伪算法描述是用串行化思维描述的。在实现电路的时候,我们并不是先比较第0项、再比较第1项……而是同时比较所有项。假设我们的TLB有16项,那么就需要通过组合逻辑产生一个16位宽的查找结果match[15:0]。这个结果的第0位对应第0项的比较结果,第1位对应第1项的比较结果……其Verilog代码示意如下:

assign match0[ 0] = (s0_vppn[18:10]==tlb_vppn[ 0][18:10])
                 && (tlb_ps4MB[ 0] || s0_vppn[9:0]==tlb_vppn[ 0][9:0])
                 && ((s0_asid==tlb_asid[ 0]) || tlb_g[ 0]);
assign match0[ 1] = (s0_vppn[18:10]==tlb_vppn[ 1][18:10])
                 && (tlb_ps4MB[ 1] || s0_vppn[9:0]==tlb_vppn[ 1][9:0])
                 && ((s0_asid==tlb_asid[ 1]) || tlb_g[ 1]);
...... 
assign match0[15] = (s0_vppn[18:10]==tlb_vppn[15][18:10])
                 && (tlb_ps4MB[15] || s0_vppn[9:0]==tlb_vppn[15][9:0])
                 && ((s0_asid==tlb_asid[15]) || tlb_g[15]);

assign match1[ 0] = (s1_vppn[18:10]==tlb_vppn[ 0][18:10])
                 && (tlb_ps4MB[ 0] || s1_vppn[9:0]==tlb_vppn[ 0][9:0])
                 && ((s1_asid==tlb_asid[ 0]) || tlb_g[ 0]);
assign match1[ 1] = (s1_vppn[18:10]==tlb_vppn[ 1][18:10])
                 && (tlb_ps4MB[ 1] || s1_vppn[9:0]==tlb_vppn[ 1][9:0])
                 && ((s1_asid==tlb_asid[ 1]) || tlb_g[ 1]);
......
assign match1[15] = (s1_vppn[18:10]==tlb_vppn[15][18:10])
                 && (tlb_ps4MB[15] || s1_vppn[9:0]==tlb_vppn[15][9:0])
                 && ((s1_asid==tlb_asid[15]) || tlb_g[15]);

只要把这个查询比较结果生成好,那么是否查找命中的found就是看match是否不等于全0。命中项的PFN等信息读出逻辑也很容易实现,请参考??节中select信号是译码后位向量信息的多路选择器的介绍。

我们再来分析INVTLB指令所需的各种查找操作如何支持。既然目前TLB实现虚实地址转换过程是采用并行查找机制,所以INVTLB指令的查找也将采用并行查找机制,即同时对TLB中各项进行匹配判断。设计要点转为每一项的匹配该如何处理。通过分析INVTLB指令各操作的定义,发现可以将各操作的匹配分解成若干“子匹配”的逻辑组合,具体来说,可得到4个“子匹配”的判断条件:(1)cond1——G域是否等于0;(2)cond2——G域是否等于1;(3)cond3——s1_asid是否等于ASID域;(4)cond4——s1_vppn是否匹配VPPN和PS域。那么invtlb op=0、1的匹配条件就可以表达为cond1||cond2,op=4的匹配条件可以表达为cond1&&cond3,op=5的匹配条件可以表达为cond1&&cond3&&cond4,op=6的匹配条件可以表达为(cond2||cond3)&&cond4。同时我们也很容易得知op=6的匹配条件就是取指和访存进行虚实地址转换时的查找匹配条件,这样两类操作所需的查找匹配功能就可以统一到一套逻辑中。而两类操作的区别仅是基于查找结果所做的进一步操作。对于INVTLB指令来说,将对应TLB表项无效的操作就是将inv_match[i]等于1对应的tlb_e[i]置为0。

9.2 任务与实践

完成本章的学习后,希望读者能够完成以下3个实践任务:

  1. 设计TLB模块,参见下面第9.2.1小节。
  2. 在CPU中集成TLB模块并添加TLB相关指令和CSR寄存器,参见下面第9.2.2小节。
  3. 在CPU中完善TLB MMU功能并添加TLB相关例外支持,参见下面第9.2.3小节。

9.2.1 实践任务17:TLB模块设计

本实践任务要求如下:

  1. 设计TLB模块。
  2. 利用TLB模块级验证环境对所设计的TLB进行验证,通过仿真和上板验证。

请参照第2.3.1节中介绍的方式获取本次实践任务所需的实验开发环境。具体的实验环境与之前的环境不同,是针对TLB模块的单独验证环境,位于mycpu_env/module_verify/tlb_verify/ 目录下。具体目录结构及各部分功能简介如下所示:

 |--tlb_verify/          目录,TLB模块级验证环境。
 |  |--rtl/              目录,包含TLB模块以及验证顶层的设计源码。
 |  |  |--tlb_top.v      TLB模块级验证的顶层文件。
 |  |--testbench/        目录,包含功能仿真验证源码。
 |  |  |--testbench.v    仿真顶层。
 |  |--run_vivado/       Vivado工程的运行目录。
 |  |  |--constraints/   Vivado工程的设计约束。
 |  |  |--tlb_prj/       Vivado工程文件所在目录。

实验环境准备就绪后,请参考下列步骤完成本实践任务:

  1. 完成TLB模块的设计和RTL编写,记为tlb.v,该模块名需要命名为“tlb”,输入/输出端口参见本章第9.1节。将tlb.v文件放入mycpu_env/myCPU/ 目录下。
  2. 进入 mycpu_env/module_verify/tlb_verify/run_vivado/tlb_prj/ 目录下启动验证tlb的工程。如果该目录下尚未创建工程,请参照附录D.2节介绍的步骤,利用该目录下的 create_project.tcl 文件创建工程。如需要,请参考附录D.4节进行IP核升级。
  3. 在验证tlb模块的工程中运行仿真(进入仿真界面后,直接点击run all),进行功能验证与调试,直至仿真测试通过。
  4. 在验证tlb模块的工程中综合实现后生成bit流文件,进行上板验证。(如果无硬件实验平台,请跳过该步骤。)

9.2.1.1 仿真验证结果判断

在仿真时,会有16次写,16次读以及26次查操作,所有操作都完成后会打印PASS,如下所示:

[   2705 ns] OK!!!write
…………
=========================================================
Test end!
----PASS!!!

如果仿真中发现错误,请进行调试。这时需要观察TLB接口的访问,了解该次请求的效果,然后查看TLB的读出数据是否与预期效果相同。

9.2.1.2 上板验证结果判断

正确的上板运行效果如图9.1所示。

TLB上板验证正确的效果图

图 9.1: TLB上板验证正确的效果图

第一阶段上板运行时,应看到数码管发生如下变化:

  1. 首先是写操作(W),最右侧的数码管会从0x00累加到0x0f,此后最右侧那个单色LED灯亮起,表示写操作完成。
  2. 之后是同时进行读操作和查找操作,相应的数码管也会开始累加:
    • 对于读操作(R),会进行16次读,次右侧的数码管会从0x00累加到0x0f。
    • 对于0号查找操作(S0),会进行13次查找(查偶数次请求),次左侧的数码管会以步长2从0x00加到0x18,也就是0、2、4、……、0x18。
    • 对于1号查找操作(S1),会进行13次查找(查奇数次请求),最左侧的数码管会以步长2从0x01加到0x19,也就是1、3、5、……、0x19。
  3. 第2步中的累加完成后,LED的右侧三个灯全部亮起,表明测试完成。此时正确的数码管显示是0x19180f0f。如果数码管停在其他数值上,表示上板失败。

9.2.2 实践任务18:添加TLB相关指令和CSR寄存器

本实践任务要求在实践任务16和实践任务17的基础上完成以下工作:

  1. 将实践任务17完成的TLB模块集成到实践任务16完成的CPU中。
  2. 在CPU中增加TLBSRCH、TLBRD、TLBWR、TLBFILL、INVTLB指令。
  3. 在CPU中增加TLBIDX、TLBEHI、TLBELO0、TLBELO1、ASID、TLBRENTRY CSR寄存器。
  4. 在采用AXI总线的SoC验证环境里完成exp18对应func的功能验证,要求成功通过仿真和上板验证。

请参照第2.3.1节中介绍的方式获取本次实践任务所需的实验开发环境。具体的实验环境仍位于 mycpu_env/ 目录下,且仍使用 soc_axi/ 子目录。

实验环境准备就绪后,请参考下列步骤完成本实践任务:

  1. 将所实现CPU的代码更新至mycpu_env/myCPU/目录中。
  2. 修改func配置文件——mycpu_env/func/include/test_config.h,选择exp18的配置,编译。(如果是通过压缩包exp18.zip获取实验开发环境的,请跳过该步骤。)
  3. 打开gettrace工程——mycpu_env/gettrace/gettrace.xpr。(该Vivado工程中的IP核是使用Vivado2019.2创建的,如果使用更高版本的Vivado打开,请参考附录D.4节进行IP核升级。)运行gettrace工程的仿真(进入仿真界面后,直接点击run all等待仿真运行完成),生成新的参考trace文件golden_trace.txt(mycpu_env/gettrace/golden_trace.txt)。要等仿真运行完成,golden_trace.txt才有完整的内容。(如果是通过压缩包exp18.zip获取实验开发环境的,请跳过该步骤。)
  4. 进入 mycpu_env/soc_verify/soc_axi/run_vivado/ 目录下启动验证myCPU的工程。如果该目录下尚未创建工程,请参照附录D.2节介绍的步骤,利用该目录下的 create_project.tcl 文件创建工程。如需要,请参考附录D.4节进行IP核升级。如果该目录下已有前一实践任务创建过的工程,可以在打开工程后,参照附录D.3节介绍的步骤,更新项目中CPU实现文件的列表。
  5. 参考第44.2.5.2小节,对工程中的axi_ram重新定制。(如果是通过压缩包exp18.zip获取实验开发环境的,请跳过该步骤。)
  6. 在验证myCPU的工程中运行仿真(进入仿真界面后,直接点击run all),进行功能验证与调试,直至仿真测试通过。
  7. 在验证myCPU的工程中综合实现后生成bit流文件,进行上板验证。(如果无硬件实验平台,请跳过该步骤。)

9.2.3 实践任务19:添加TLB相关例外支持

本实践任务要求在实践任务18所实现CPU的基础上完成以下工作:

  1. 为CPU增加TLB相关异常:TLB重填例外、load/store/取指操作页无效例外、页修改例外、页特权等级不合规例外。
  2. 在CPU中增加DMW CSR寄存器。
  3. 为CPU增加虚实地址映射的功能。
  4. 在采用AXI总线的SoC验证环境里完成exp19对应func的功能验证,要求成功通过仿真和上板验证。

请参照第2.3.1节中介绍的方式获取本次实践任务所需的实验开发环境。具体的实验环境仍位于 mycpu_env/ 目录下,且仍使用 soc_axi/ 子目录。

实验环境准备就绪后,请参考下列步骤完成本实践任务:

  1. 将所实现CPU的代码更新至mycpu_env/myCPU/目录中。
  2. 修改func配置文件——mycpu_env/func/include/test_config.h,选择exp19的配置,编译。(如果是通过压缩包exp19.zip获取实验开发环境的,请跳过该步骤。)
  3. 打开gettrace工程——mycpu_env/gettrace/gettrace.xpr。(该Vivado工程中的IP核是使用Vivado2019.2创建的,如果使用更高版本的Vivado打开,请参考附录D.4节进行IP核升级。)运行gettrace工程的仿真(进入仿真界面后,直接点击run all等待仿真运行完成),生成新的参考trace文件golden_trace.txt(mycpu_env/gettrace/golden_trace.txt)。要等仿真运行完成,golden_trace.txt才有完整的内容。(如果是通过压缩包exp19.zip获取实验开发环境的,请跳过该步骤。)
  4. 进入 mycpu_env/soc_verify/soc_axi/run_vivado/ 目录下启动验证myCPU的工程。如果该目录下尚未创建工程,请参照附录D.2节介绍的步骤,利用该目录下的 create_project.tcl 文件创建工程。如需要,请参考附录D.4节进行IP核升级。如果该目录下已有前一实践任务创建过的工程,可以在打开工程后,参照附录D.3节介绍的步骤,更新项目中CPU实现文件的列表。
  5. 参考第44.2.5.2小节,对工程中的axi_ram重新定制。(如果是通过压缩包exp19.zip获取实验开发环境的,请跳过该步骤。)
  6. 在验证myCPU的工程中运行仿真(进入仿真界面后,直接点击run all),进行功能验证与调试,直至仿真测试通过。
  7. 在验证myCPU的工程中综合实现后生成bit流文件,进行上板验证。(如果无硬件实验平台,请跳过该步骤。)