MIPS 处理器的 SystemC 实现 学号 :1060379012 孙伟斌 B0603791 前言 这篇文档讲述了如何使用 SystemC 来实现一个多周期的流水 MIPS 处理器 通过对 MIPS 处理器的五步进行描述, 本文详细阐述了作业中的 SystemC 实现如何来构建者五步流水中的每一个细节 对于流水中的一些问题, 如 Hazard 等的处理, 本文中也在讲述流水实现时作了具体的解释 作业中使用 SystemC2.1 来实现一个 MIPS 的逻辑仿真 第一节 SystemC 概述 SystemC 是一个 C++ 库, 里面定义了一系列的类, 用来对系统进行建模,SystemC 主要是对系统进行逻辑验证 工业中构建芯片或系统, 可以先使用 SystemC 来建立它的逻辑模型, 经过各方面验证正确后可以进行实际设计 SystemC 构建的系统是由 Module 构成的,Module 就是系统的各个模块, 模块可大可小, 而模块与外界交互的通道是 Port, 端口 端口负责输入输出, 就像我们的 I/O 端口一样 模块内部的处理流程被构建为一个 SC_METHOD, 输入的数据经过其加工作为输出 而模块之间通过 Signal, 信号来进行连接, 我们看线路图上的连线, 就可以认为是 SystemC 中的 Signal 我们使用 SystemC 来构建 MIPS, 就是用模块来模拟 CPU 的各个部件, 用端口来模拟部件的输入输出, 用信号来模拟部件之间的连线 第二节 MIPS 架构 这一节来介绍要模拟的 MIPS 的架构图 MIPS 是一个 RISC 处理器, 因此 David Patterson 的教材中提到的一个典型的 MIPS 的特性都体现在我们要实现的 CPU 中, 我们参考 李亚明的设计来实现 MIPS: 1 / 11
这个设计实现的是一个五级流水的 MIPS, 这五个步骤分为 IF ID/Reg EXE MEM 与 WB 阶段, 是一个简单的流水设计 我们在这个系统上不仅实现了流水, 还实现了解决 Data Hazard 的 Forwarding 以及解决 Control Hazard 的 Delayed Branch 技术 作业中用 SystemC 实现 MIPS 就是按照上图, 每个部件成为一个 SC_MODULE 来构建, 实现中的每个模块与图中的部件对应, 端口与图中端口对应, 而信号线也差不多是一一对应 在接下来的讲述中会不时地牵扯到实现中的代码, 我用这些代码来解释 MIPS 的执行过程 第三节所实现的 MIPS 指令 这一小节来介绍作业中实现的 MIPS 的指令, 总共有三种指令 : 运算 控制与 Memory 访问指令 具体细节参见下表 : 2 / 11
就像一个典型的 MIPS 一样, 上述指令定长 操作数位置固定 简单的内存访问 并且 控制指令的目的地址计算简单 作业中的 MIPS 实现与上面的二进制代码兼容, 因此可以运 行用上面的代码编写的指令程序 第四节五步流水的 SystemC 实现 在这一节中, 将结合作业中的 MIPS 实现来讲述 MIPS 的五步流水 一 IF 阶段 3 / 11
图中 IF 阶段用红色表示, 我们看到 PC 确定指令地址, 然后到指令存储器里取出指令, 放到 IF/ID 流水寄存器中, 此外, 通过多路复用开关 Mux 把 PC+4 的值, 也就是下一条指令的地址也存入 IF 流水寄存器 这个时候我们注意到, 上面的那个 Mux, 在实现中称为 MUX_Branch, 代码如下 : // mux for branch, PC+4 or PC+offset. SC_MODULE(MUX_Branch) sc_in< bool > Is_Branch; // branch or not, BRANCH signal sc_in< sc_uint<32> > PC_ADD_4; sc_in< sc_uint<32> > Branch_PC; sc_out< sc_uint<32> > Out_PC; // PC+4 // PC+offset. // output PC ; SC_CTOR(MUX_Branch) SC_METHOD(run); sensitive<<is_branch<<pc_add_4<<branch_pc; void run() Out_PC = (Is_Branch.read())?Branch_PC:PC_ADD_4; 这是我们遇到的第一个 Mux, 因此我在这里把它的代码列出让读者有个了解, 后面的 Mux 将大同小异 4 / 11
注意流水寄存器,IF 前的那个, 我们称为 Pre_IF, 以及 IF/ID 之间的, 称为 IF_2_ID, 它们被一根信号控制着 :WPCIR, 这个信号表示是不是可读, 也就是说是这两个流水寄存器的锁, 它们有什么用呢? 实现中通过这个信号来实现加气泡 通过锁住这两个流水寄存器, IF 的指令将不会变化,PC 也不会变化, 因此下一条指令不会再载入, 即产生了一个 Stall 我们这里产生 Stall 是用于处理 lw 指令产生的 RAW Data Hazard, 这个时候 Forwarding 依然不能解决问题, 必须要加一个 Stall 来处理, 我们会在 ID 中解释 二 ID 阶段 : 依然是红色表示 ID 阶段的部件 我们看到指令译码是在 IF_2_ID 流水寄存器中进行的, 其实译码很简单, 不需要考虑指令的意义, 由于指令定长且数据位置固定, 只需要把所有可能情况下用到的域都取出来, 用什么是控制器 Control Unit 的事情 因此可以看到我们的译码只是简单的取不同的字段 : // fetch instruction parts: ID_FUNC // func. part = IF_INST.read().range(5,0); ID_IMM // imm. number part = IF_INST.read().range(15,0); ID_OP // op. part = IF_INST.read().range(31,26); 5 / 11
ID_RS // No.1 reg. part = IF_INST.read().range(25,21); ID_RT // No.2 reg. part = IF_INST.read().range(20,16); ID_RD // dest. reg. part = IF_INST.read().range(15,11); 真正考虑指令意义的是最复杂的控制器 (CU),CU 的复杂在于要产生指令的控制信号, 但同时让它复杂的更主要的原因是由于 MIPS 中各阶段的信号与数据都是随着执行流程一起跑的, 并没有什么中心地方来存放这些信号与信息, 因此为了我们解决 RAW Data Hazard 时判断前面的指令结果是否与当前在 ID 阶段的指令源操作数冲突, 需要每个阶段都把自己的控制信号与信息还要返回给 CU, 也就是说,CU 并不是持续性的, 它没有什么状态, 服务完了一条指令, 就立刻清除干净, 根本不会保存任何信息 当需要前面指令信息时, 通过每个阶段的返回信号来获得所需信息 我们处理 Data Hazard, 就是要检测 ID 阶段的指令的原操作数是不是前面指令的目的寄存器, 因此需要前面指令的 Rd 与 xm2reg xwreg 信号, 这里 x 代表各个阶段 通过 Forwarding 来解决 RAW 问题, 就是判断寄存器是否冲突, 若冲突, 把冲突的前面那条指令的结果 forward 到当前指令作为 ALU 的输入 而有时候, 比如像 lw 指令后面的运算指令造成 RAW, 比如 : lw r1, r0, 100 add r2, r1, r3 这个时候必须要加 Stall 才能解决问题, 因此倘若不能 Forwarding, 则上面提到的 WPCIR 信号将锁住前面两个流水寄存器来产生气泡 作业中实现的 RAW 检测代码如下, 基本是自解释的, 可以了解一下相应的处理逻辑 : bool write_reg_enable = // reg. write enable (!is_sw)&&(!is_beq)&&(!is_bne)&&(!is_jump); bool write_mem_enable = // mem. write enable is_sw; bool write_pc_ir_enable = // PC. IR. write enable true; sc_uint<2> forward_to_a = 0; sc_uint<2> forward_to_b = 0; // forward result to ALU A // forward result to ALU B if (in_cu_mwreg.read() &&!is_jump) if (in_cu_mdesr.read() == in_cu_rs.read()) forward_to_a = (in_cu_mm2reg.read() == true)?3:2; if (in_cu_mdesr.read() == in_cu_rt.read()) forward_to_b = (in_cu_mm2reg.read() == true)?3:2; if (in_cu_ewreg.read() &&!is_jump) if (in_cu_edesr.read() == in_cu_rs.read()) 6 / 11
// forward to ALU A if (in_cu_em2reg.read()) // stall here write_reg_enable = false; write_pc_ir_enable = false; write_mem_enable = false; else // flow forward_to_a = 1; if (in_cu_edesr.read() == in_cu_rt.read()) // forward to ALU B if (in_cu_em2reg.read()) // stall here write_reg_enable = false; write_pc_ir_enable = false; write_mem_enable = false; else // flow forward_to_b = 1; out_cu_fwda out_cu_fwdb out_cu_wreg out_cu_wmem out_cu_wpcir = forward_to_a; = forward_to_b; = write_reg_enable; = write_mem_enable; = write_pc_ir_enable; 除了解决 Data Hazard,ID 阶段还有为了加速流水而采取的将 Branch 部件向前移的方案 我们看到图中有一个 Adder, 实现中称为 ADD, 专门用来算跳转地址 ; 而在 rs 与 rt 的输出后, 有一个相等比较器, 实现中称为 EQU,Equal Unit EQU 用来判断是否跳转, 从而产生转向 CU 的 RSRTEQU 信号, 继而产生 JUMP 信号, 从而跳转或不跳转, 相应的 Mux 来选择下一个 PC 需要注意, 即使这样, 仍然会有一个周期的浪费发生在 Branch 指令后面, 我们采用了 Delayed Branch, 即依靠编译器来为我们填充一些必要的指令来减少浪费, 因此在实现中, Branch 指令的下一条指令将永远都会执行 我们的控制指令还包括 Jump, 这种指令的目的地址计算很特殊, 是用当前的 PC+4 高位作为高位来计算地址, 因此注意到在图中 imm 形成需要一个移位器 ID 阶段包括 Reg 的访问, 寄存器的模拟比较简单, 只是一个可读写的数组而已 三 EXE 阶段 7 / 11
该阶段主要是进行运算 只有运算指令与内存访问指令会使用到这个阶段, 主要的部件是 ALU ALU 根据 CU 产生的 ALUC 来确定执行的操作, 我们定义了六种操作 :add, sub, and, or, sll, srl 以及 sra 由于进入 ALU 的 A 与 B 的可能是来自寄存器, 也有可能来自立即数 Imm, 更有可能是 Shift 操作时的 Shift Amount, 因此在 A 与 B 之前各有一个 Mux 来选择输入 对于 Shift 操作, 一个部件 SA 来把从指令当中取出的 Imm 换算成 Shift Amount, 即移位的位数 注意到 EWREG 与 EM2REG 都返回 CU 来供 ID 阶段的指令进行 Forwarding 判断 在实现中,ALU 通过对 ALUC 的一系列 switch 与 case 来进行操作运算 下面是详细的执行代码 : sc_uint<32> result; switch(in_alu_ealuc.read()) case 0: // 0000 for add result = in_alu_a.read() + in_alu_b.read(); case 1: // 0001 for sub result = in_alu_a.read() - in_alu_b.read(); case 2: // 0010 for and result = in_alu_a.read() & in_alu_b.read(); case 3: // 0011 for or result = in_alu_a.read() in_alu_b.read(); 8 / 11
case 4: // 0100 for sll result = 0; // should be [A.read()-1, 0] result.range(31, in_alu_a.read()) = in_alu_b.read().range(31-in_alu_a.read(), 0); case 5: // 0101 for srl result = 0; result.range(31-in_alu_a.read(), 0) = in_alu_b.read().range(31, in_alu_a.read()); case 6: // 0110 for sra result = (in_alu_b.read()[31] == 0)? 0:(-1); result.range(31-in_alu_a.read(), 0) = in_alu_b.read().range(31, in_alu_a.read()); default: // not supported result = 0; 四 MEM 阶段 图中红色标出 MEM 阶段的部件 我们知道, 只有 lw( 读内存 ) 和 sw( 写内存 ) 要用到该 MEM 部件 其他的在该阶段什么都不做 不过同理, 也有信号返回到 CU 来协助进行 Forwarding, 有 MWREG MM2REG 该阶段的主要部件是 Data Memory, 为了解决 Structure Hazard, 我们的 Memory 分为 9 / 11
Instruction Memory 与 Data Memory 在实现中称为 Cache, 因为本来这里就应该是 Data Cache, 实现中只提供了两个字,8Bytes 的 Data Cache 供使用 当然, 增多是没问题的, 但我们这里用不到那么多内存 也许 Cache 这个名字会造成误解, 认为实现中的这个部件真的跟 Cache 一样, 有映射 替换等机制, 其实不是, 说白了只是简单的数组访问 实现中的数据访问从 0x100 开始, 只有 8Bytes, 因此只有 0x100 与 0x104 两个地址 五 WB 阶段 该阶段是写回寄存器阶段 这个阶段部件很少, 只有两个控制信号管事 WREG 告诉寄存器 WE 端口要写寄存器了, 然后由 WM2REG 来控制该阶段唯一个部件, 多路选择器, 表示接受是否是从 Memory 来的数据还是从上上周期的运算结果的数据, 送往 REGFILE 的 D 端口 而一直跟随着指令的 xdesr 信号终于有了用武之地, 用来选择要写的寄存器 还要注意的应该有一个控制信号来控制数据何时才能写寄存器, 为了避免 WB 阶段的指令与 ID 阶段取 Reg 的指令冲突, 我们规定 Reg 在时钟的上跳沿写, 而在下讲沿读, 因此我们会看到在 REGFILE 那里下面还有个接受时钟取反后的 CK 端口, 它就是来控制写 REG 操作何时进行的了 对于 Reg 的读写, 实现中使用两个不同的函数来构成 SC_METHOD 以及 SC_CTHREAD: 10 / 11
SC_CTOR(REG_FILE) for (int i = 0; i < 32; ++i) Registers[i] = 0; SC_METHOD(read_reg); // Read sensitive<<n1<<n2; SC_CTHREAD(write_reg, CLK.neg()); // Write void read_reg() Q1 = Registers[N1.read()]; Q2 = Registers[N2.read()]; void write_reg() while(true) if (WE.read() && N.read()!= 0) Registers[N.read()] = D; Q1 = Registers[N1.read()]; Q2 = Registers[N2.read()]; wait(); 注意到上面代码中的红色斜体行, 我们用两个函数来处理读与写, 而写在上升沿执行, 以避免与读冲突, 从而确保没有 Data Hazard 的发生 第五节总述 上述文字讲述了使用 SystemC 来实现一个五步流水的 MIPS 的具体细节, 在实现 MIPS 过程中, 我们解决了 Data Hazard 与 Control Hazard 问题, 当然, 还有 MIPS 的设计, 确保了 Structure Hazard 的解决 我们采用了最简单的 Forwarding 与 Delayed Branch 来解决上述 Hazard 问题, 虽然只是一个 MIPS 的简单的模拟实现, 但在这过程中, 从开始搭建到最终完成, 所遇到的问题与解决都为我们提供了宝贵的实践经验 这是教科书上所学不到的, 通过真正的去思考 去做一个 CPU, 我们加深了对体系结构的理解, 并真实的触摸到了 CPU 的最细微的地方, 是一个很有意义的作业 11 / 11