跳转至

热身:单周期指令模拟器

难度:⭐⭐

阅读时间:2h

预计代码时间:2h

在正式开始 Tomasulo 实验之前,为了让大家熟悉实验框架,我们首先来实现一个简单的“单周期”的 CPU 模拟器。

注意

本小节热身实验不计入分数。其目的仅在帮助大家实现一个对拍工具,供后续实验使用。同时让大家尽可能快地熟悉框架。

如果不需要以上帮助,可以直接跳到下一小节,开始正式的实验。

熟悉实验框架

指令译码和执行

在框架的 common/instructions.cpp 中,我们已经提供了 RV32IM 和 Zicsr 扩展的全部指令的译码,输出以及执行的逻辑。

将对应的指令的二进制表示传入到 Instruction 类的构造函数中,即可获得该指令的对应结构体,用于执行。

指令重载的 std::ostream<< 运算符,可以直接使用 std::cout 等直接输出。

最后,我们还实现了 Instruction::execute 函数,用于执行对应指令。为了在后面的 tomasulo 实验中确保指令在正确的单元中执行,execute 函数额外添加了第一个参数,用于确认指令所在的执行单元。实验中涉及到的执行单元有以下几种:

  • ALU,用于执行整数指令
  • BRU,用于执行分支和跳转指令
  • MUL,用于执行乘法指令
  • DIV,用于执行除法和取余指令
  • LSU,用于计算 Load/Store 指令的地址
  • 正式实验中的 LSU 还会进行预访存
  • 预访存是为了获得数据用于移位对齐和唤醒,只进行存储器读取,不会写入
  • 如果有 cache 的话,情况可能会不一样

如果指令在错误的执行单元中执行,则会直接报错。

elf 加载

在框架的 common/utils.cpp 中,我们提供了 readElf 函数,该函数的的定义如下:

unsigned readElf(const std::string &elfFileName,
                 std::vector<unsigned> &instruction,
                 std::vector<unsigned> &data)

其中 elfFileName 是要读取的 elf 文件的名称。instructiondata 参数是两个 输出std::vector,分别对应程序的指令段和数据段。这个函数还会返回一个 unsigned 类型的变量,表示 elf 执行的入口地址。

由于实验框架最后要求实现的 CPU 的指令内存和数据内存是分离的,基地址分别为 0x800000000x80400000,实验使用的 elf 需要由特殊的链接脚本生成。实验框架的 test 文件夹下提供了测试程序,链接脚本和生成 elf 使用的 CMakeLists.txt。同学们在实验时,也可以自行编写程序进行实验和 debug。

在框架的 program/elf_test.cpp 中已经提供了一个解析 elf 的样板逻辑。同学们在实现单周期指令模拟器时,可以进行参考。

热身实验内容

请使用上述提到的框架代码内容,实现一个能够通过测试的单周期 CPU 模拟器。我们希望大家可以通过这个过程熟悉框架,同时可以将实现的简单模拟器用于后续实验的对拍。

详细实验步骤

  1. program 文件夹中,创立新的 single_cycle.cpp,用于编写 单周期指令模拟器 的执行逻辑。

  2. 参考 program/elf_test.cpp 编写读入 elf 的逻辑,代码如下:

#include "defines.h"
#include <iostream>
#include <string>
int main() {
     std::string name;
     std::cout << "Elf file name: ";
     std::cin >> name;

     std::vector<unsigned> inst, data;

     unsigned entry = readElf(name, inst, data);

     return 0;
}

这样我们就可以获取到一个 elf 内的指令和数据信息了。

  1. 根据实验框架的要求,实现的 CPU 的数据内存和指令内存分离,指令内存的位置在 0x800000000x80400000 之间;数据内存为 0x804000000x80800000。因此,我们定义 PC,让其指向程序的入口地址,也就是 readElf 函数返回的 entry 位置,开始程序的执行。同时定义 RISC-V 规定的 32 个通用寄存器。注意,要根据文档对 sp 和 gp 寄存器进行特殊的初始化:
...
unsigned pc = entry;
unsigned regs[32] = {0};
regs[2] = 0x80800000;  // initialize sp
regs[3] = 0x80400000;  // initialize gp
...
  1. 现在我们就可以开始读取指令并且执行对应功能了。首先通过提供的 Instruction 类,读取当前 PC 对应的指令信息,并完成译码:
#include "instructions.h"
...
while(true) {
    unsigned addr = (pc - 0x80000000) >> 2;
    unsigned instData = ???? // read the data from the address
    Instruction inst(instData);
    inst.pc = pc;
    inst.predictBundle.predictJump = false;
    // 你可以使用我们事先编写好的输出函数,把指令打出来看一看:
    std::cout << inst << std::endl; 
}
  1. 在执行这条指令之前,我们要进行以下的步骤:

  2. 根据指令的寄存器字段,获取寄存器对应的函数值:

    unsigned a = regs[inst.getRs1()], b = regs[inst.getRs2()];
    
  3. 确认指令对应的执行单元:

    auto fuType = getFUType(inst);
    
  4. 根据不同的执行单元,分类进行处理:

    bool endFlag = false;
    ExecuteResultBundle result;
    switch (getFUType(inst)) {
    case FUType::NONE:
        // EXIT 指令,跳出循环
        endFlag = true;
        break;
    
    case FUType::ALU:
        result = inst.execute("ALU", a, b);
        break;
    
    case FUType::BRU:
        result = inst.execute("BRU", a, b);
        break;
    
    case FUType::LSU:
        result = inst.execute("LSU", a, b);
        break;
    
    case FUType::MUL:
        result = inst.execute("MUL", a, b);
        break;
    
    case FUType::DIV:
        result = inst.execute("DIV", a, b);
        break;
    
    default:
        throw std::runtime_error("Unknown FUType");
    }
    
    if(endFlag)
       break;
    
  5. 参考 backend/execute_pipeline.cpp 完成代码逻辑和访存逻辑的编写,需要时修改寄存器。

  6. 结合 result 内的信息,完成可能的跳转或者 pc + 4

    pc = result.actualTaken ? result.jumpTarget : pc + 4;
    
  7. 为了完成对拍,我们需要输出指令的执行 trace。trace 应该包含以下内容:

    • 写寄存器的编号和数据
    • 写内存的地址和数据
    • 跳转目标地址

    如果你额外实现了其他内容,向 CPU 添加了额外的状态信息,则最好也将其信息标准化,然后输出,便于进行比对。

  8. 正式实验中,你的 CPU 会接入一个延时可配置的简易内存模拟器上。如果你希望在该环境上进行测试,则请参考后续的实验文档进行接入。

  9. 除此之外,你还可以参考 program 文件夹下的其他程序,参考 cxxopts 的使用,给你编写的对拍器添加命令行参数。

  10. ./CMakeLists.txt 中,参考其他程序添加 CMake 指令,将你的程序加入编译项目中,重新 cmake 进行编译即可。

以上,我们完成了一个用于对拍的单周期 RISC-V 模拟器的编写。相信大家通过以上的实验步骤,已经对实验框架有了一个初步的了解。接下来,我们就要正式开始 tomasulo 实验了。


最后更新: 2024年5月13日
作者:cuibst