「BUAA-CO」P6_流水线cpu(Plus)


流水线CPU设计方案

设计概述

​ 本文所设计的CPU为Verilog实现的流水线MIPS架构CPU,该CPU支持43条MIPS汇编指令,为了实现该功能,笔者设计了IFU,GRF,NPC,CMP,EXT,ALU,D_Reg,E_Reg,M_Reg,W_Reg,MCU, HCU,MDU,BE,DE 等关键模块。整个搭建过程通过自下而上的方式完成——先根据应实现的指令对功能部件进行设计与搭建,然后对各个功能部件进行连接,形成完整的数据通路。

实现指令说明

我们将本CPU实现的指令分为以下几类:

  • calc_R: add,sub,addu, subu, and, or, nor, xor, slt, sltu

  • calc_I: addi,addiu, andi, ori, xori, slti, sltiu

  • shift: sll, sra, srl

  • shiftv: sllv, srav, srlv

  • load: lw, lh, lhu, lb, lbu

  • store: sw, sh, sb

  • B类:beq,bne,bgtz,blez,bgez,bltz

  • J类:j, jal, jr, jalr

  • 特殊:lui

  • md类:mult, multu, div, divu

  • mf类:mfhi, mflo

  • mt类:mthi, mtlo

工程模块定义

在P6实验中,我们将IM(16KB(32bit/word×4096word))和DM(16KB(32bit/word×4096word))外置,因此在P5的基础上,我们在cpu中删去了这两个部分,同时增加相应的信号与接口与外置的IM和DM进行信息交换。同时,为了评测GRF的读写行为,我们还需要将GRF的读写信息通过相应接口和信号传入外部(test bench)。相关接口如下(在顶层模块mips中定义)

信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 同步复位信号
i_inst_rdata I 32 i_inst_addr队形的32位指令
m_data_addr I 32 m_data_addr 对应的 32 位数据
i_inst_addr O 32 需要进行取指操作的流水级 PC(一般为 F 级)
m_data_addr O 32 数据存储器待写入地址
m_data_wdata O 32 数据存储器待写入数据
m_data_byteen O 4 字节使能信号(相当于之前的MenWrite信号)
m_inst_addr O 32 M级PC
w_grf_we O 1 grf 写使能信号
w_grf_addr O 5 grf 中待写入寄存器编号
w_grf_wdata O 32 grf 中待写入数据
w_inst_addr O 32 W 级 PC

功能模块定义

IFU(取指令单元)

该模块相当于程序计数器,在时钟上升沿时将npc写入pc,并具有同步复位的功能。同时,为了配合jal,jalr的链接操作,我们同时输出pc+8的值,在后面的流水寄存器中传递。

  • 端口定义

    信号名 方向 位宽 描述
    clk I 1 时钟信号
    reset I 1 异步复位信号
    en I 1 使能信号
    npc I 32 下一条要被执行的指令的地址
    pc O 32 输出当前正在执行的指令的地址
    pc8 O 32 pc+8
  • 功能定义

    序号 功能名称 功能描述
    1 复位 当reset信号有效时,将PC寄存器中的值置为0x00003000
    2 停止 当en信号失效时,PC寄存器忽略时钟输入,PC当前值保持不变
    3 写PC寄存器 当en信号失效且时钟上升沿来临时,将下一条指令的地址(next PC)写入PC寄存器

GRF(通用寄存器组)

该模块内部包含32个具有写使能32位寄存器,分别对应MIPS架构中$0 ~ $31通用寄存器(其中0号寄存器中的值恒为0,即不具备写使能,因此为了实现该机制,我们不再设置0号寄存器,每次对0进行特判)。GRF可以实现同步复位,同时可以根据输入的5位地址(0~31)向寄存器堆存取数据,实现定向访存寄存器。

为了实现内部转发,在GRF写入地址与读取地址相同时,我们将当前WD中输入的数据(但没有写入)实时反映到RD1或RD2端口上,用来解决数据冲突。

  • 端口定义

    信号名 方向 位宽 描述
    clk I 1 时钟信号
    pc I 32 输出当前正在执行的
    reset I 1 同步复位信号
    1:复位信号有效
    0:复位信号无效
    A1 I 5 地址输入信号,指定32个寄存器中的一个,将其中的数据读出到RD1
    A2 I 5 地址输入信号,指定32个寄存器中的一个,将其中的数据读出到RD2
    A3 I 5 地址输入信号,指定32个寄存器中的一个,将其作为写入目标
    WD I 32 数据输入信号
    WE I 1 写使能信号
    1:写入有效
    0:写入失效
    RD1 O 32 输出A1指定的寄存器中的32位数据
    RD2 O 32 输出A2指定的寄存器中的32位数据
  • 功能定义

    序号 功能名称 功能描述
    1 复位 reset信号有效时,所有寄存器中储存的值均被清零
    2 读数据 读出A1,A2地址对应的寄存器中储存的数据,将其加载到RD1和RD2
    3 写数据 当WE信号有效且时钟上升沿来临时,将WD中的数据写入到A3地址对应的寄存器

NPC(下一指令计算单元)

​ 该模块根据当前pc(包括D级和F级)和其他控制信号(NPCOp,CMP输出信息),计算出下一指令所在的地址npc,传入IFU模块。

  • 端口定义

    信号名 方向 位宽 描述
    F_pc I 32 F级指令地址
    D_pc I 32 D级指令地址
    offset I 32 地址偏移量,用于计算B类指令所要跳转的地址
    imm26 I 26 当前指令数据的前26位(0~25),用于计算jal和j指令所要跳转的地址
    ra I 32 储存在寄存器($ra或是jalr指令中存储“PC+4”的寄存器)中的地址数据,用于实现jr和jalr指令
    judge I 1 B类指令判断结果
    1:说明当前B类指令的判断结果为真
    0:说明判断结果为假
    NPCOp I 3 NPC功能选择
    0x000:顺序执行
    0x001:B类指令跳转
    0x010: jal/j跳转
    0x011: jr/jalr跳转
    npc O 32 输出下一指令地址

EXT(扩展单元)

​ 该模块对16位立即数进行扩展,可以实现符号扩展,0扩展和加载高位(lui)操作。

  • 端口定义

    信号名 方向 位宽 描述
    in I 16 16位立即数
    EXTOp I 2 EXT功能选择信号
    0x000: 0扩展
    0x001: 符号扩展
    0x010: 加载到高位(lui指令使用)
    out O 32 扩展结果

CMP(B类指令比较单元)

​ 该单元根据输入的CMPOp信号对当前B指令的类型进行判断,进而对当前输入的数值进行相应比较,最后输出结果。

  • 端口定义

    信号名 方向 位宽 描述
    D1 I 32 输入CMP单元的第一个数据
    D1 I 32 输入CMP单元的第二个数据
    CMPOp I 3 CMPOp功能选择信号
    0x000:beq判断
    0x001:bne判断
    0x010:blez判断
    0x011: bgtz判断
    out O 1 判断结果输出
    1: 判断结果为真
    0:判断结果为假

ALU(逻辑运算单元)

  该模块可实现加,减,按位与,按位或等11种运算,并根据ALUOP信号的值在这些功能中进行选择。除此之外,该模块还可以实现溢出判断。

  • 端口定义

    信号名 方向 位宽 描述
    ALUOp I 4 ALU功能选择信号
    src_A I 32 参与ALU计算的第一个值
    src_B I 32 参与ALU计算的第二个值S
    shamt I 5 移位数输入
    out O 32 输出ALU计算结果
  • 功能定义

    序号 功能名称 ALU_Op 功能描述
    1 0b0000 ALU_Result = Src_A + Src_B
    2 0b0001 ALU_Result = Src_A - Src_B
    3 按位与 0b0010 ALU_Result = Src_A & Src_B
    4 按位或 0b0011 ALU_Result = Src_A | Src_B
    5 按位异或 0b0100 ALU_Result = Src_A ⊕ Src_B
    6 按位或非 0b0101 ALU_Result = ~(Src_A | Src_B)
    7 逻辑左移 0b0110 ALU_Result = Src_B << Shift
    8 逻辑右移 0b0111 ALU_Result = Src_B >> Shift
    9 算术右移 0b1000 ALU_Result = Src_B >>> Shift
    10 带符号比较 0b1001 ALU_Result = (Src_A > Src_B) ? 1 : 0(带符号比较)
    11 无符号比较 0b1010 ALU_Result = (Src_A > Src_B) ? 1 : 0(无符号比较)

MDU (乘除模块)

​ 该模块对数据进行访存,容量为容量为 12KB(32bit/word×3072word),不仅可以实现对字的访问和存储,还可以实现对半字和字节的操作。

  • 端口定义

    信号名 方向 位宽 描述
    clk I 1 时钟信号
    reset I 1 复位信号
    start I 1 乘除运算启动信号
    MDUOp I 4 乘除模块功能选择
    0x0000:空指令
    0x0001:mult指令
    0x0010:multu指令
    0x0011:div指令
    0x0100:divu指令
    0x0101:mfhi指令
    0x0110:mflo指令
    0x0111:mthi指令
    0x1000:mtlo指令
    A I 32 运算数据
    B I 32 运算数据
    HI O 32 HI寄存器输出值
    LO O 32 LO寄存器输出值
    out O 32 MDU输出值(HI、LO中选择)
    busy O 1 乘除运算进行信号

BE(字节使能模块)

为了实现外置DM的按字节访存,我们需要对字中的每一个字节设置使能信号(4位)。在本模块中,我们根据当前执行的访存指令(由LSOp传递信息)和DM写入地址(addr),计算并输出相应的4位字节使能信号。同时,为配合”官方td“”对应字节写入“的设置,我们还需要在该模块对写入值进行处理。

  • 端口定义

    信号名 方向 位宽 描述
    A I 2 DM写入地址的低2位
    LSOp I 2 访存功能信号
    WD_in I 32 未经处理的DM写入数据
    byteen O 4 字节使能信号
    WD_out O 32 处理后的DM写入数据

DE(数据扩展模块)

外置DM根据”读取地址“的高30位将整个字读出,但是为了能够实现字节和半字的读取,我们在该模块中对读出的整个字进行处理,并对需要写入GRF的字节或半字进行扩展。

  • 端口定义

    信号名 方向 位宽 描述
    A I 2 DM写入地址的低2位
    RD_in I 32 未经处理的DM读出数据
    LSOp I 2 访存功能信号
    RD_out O 32 处理后的DM读出数据

流水寄存器模块定义

D_Reg(IF/ID流水寄存器)

  • 端口定义

    方向 信号名 位宽 描述 输入来源
    I clk 1 时钟信号 mips.v中的clk
    I reset 1 同步复位信号 mips.v中的reset
    I en 1 D级寄存器使能信号 HCU中stall信号取反
    I clr 1 D级寄存器清空信号 默认为1‘b0
    I F_instr 32 F级instr输入 IFU_instr
    I F_pc 32 F级pc输入 IFU_pc
    I F_pc8 32 F级pc8输入 IFU_pc + 8
    O D_instr 32 D级instr输出
    O D_pc 32 D级pc输出
    O D_pc8 32 D级pc8输出

E_Reg(ID/EX流水寄存器)

  • 端口定义

    方向 信号名 位宽 描述 输入来源
    I clk 1 时钟信号 mips.v中的clk
    I reset 1 同步复位信号 mips.v中的reset
    I clr 1 E级寄存器清空信号 HCU中stall信号
    I D_instr_s 5 移位指令的位移数 D_instr的s域数据
    I D_V1_f 32 D级V1输入(转发值) 通过MUX_CMP_D1选择的数据
    I D_V2_f 32 D级V2输入(转发值) 通过MUX_CMP_D1选择的数据
    I D_A1 5 D级A1输入 D_instr的rs域数据
    I D_A2 5 D级A2输入 D_instr的rt域数据
    I D_A3 5 D级A3输入 通过MUX_A3选择出的数据
    I D_E32 32 D级E32输入 通过EXT模块扩展出的数据
    I D_pc 32 D级pc输入 前一级相同信号
    I D_pc8 32 D级pc8输入 前一级相同信号
    I Tnew_D 2 D级指令的Tnew输入 前一级相同信号
    I RFWrite_D 1 D级控制信号输入 前一级相同信号
    I MemWrite_D 1 D级控制信号输入 前一级相同信号
    I SelEMOut_D 1 D级控制信号输入 前一级相同信号
    I SelWOut_D 1 D级控制信号输入 前一级相同信号
    I SelALUS_D 1 D级控制信号输入 前一级相同信号
    I SelALUB_D 1 D级控制信号输入 前一级相同信号
    I LSOp_D 3 D级控制信号输入 前一级相同信号
    I ALUOp_D 4 D级控制信号输入 前一级相同信号
    I MUDOp_D 4 D级控制信号输入 前一级相同信号
    O E_instr_s 5 移位指令的位移数
    O E_V1 32 E级V1输出
    O E_V2 32 E级V2输出
    O E_A1 5 E级A1输出
    O E_A2 5 E级A2输出
    O E_A3 5 E级A3输出
    O E_E32 32 E级E32输出
    O E_pc 32 E级pc输出
    O E_pc8 32 E级pc输出
    O Tew_E 2 E级指令的Tnew输出
    O RFWrite_E 1 E级控制信号输出
    O MemWrite_E 1 E级控制信号输出
    O SelEMOut_E 1 E级控制信号输出(使用)
    O SelWOut_E 1 E级控制信号输出
    O SelALUS_E 1 E级控制信号输出(使用)
    O SelALUB_E 1 E级控制信号输出(使用)
    O LSOp_E 3 E级控制信号输出(使用)
    O ALUOp_E 4 E级控制信号输出
    O MUDOp_D 4 E级控制信号输出
  • 运算功能

    \(Tnew\_E = (Tnew\_D > 0) ? Tnew\_D - 1: 0\)

M_Reg(EX/MEM流水寄存器)

  • 端口定义

    方向 信号名 位宽 描述 输入来源
    I clk 1 时钟信号 mips.v中的clk
    I reset 1 同步复位信号 mips.v中的reset
    I E_AO 32 E级AO输入 ALU_out数据
    I E_V2_f 32 E级V2输入(转发值) MUX_ALU选择出来的数据
    I E_A2 32 E级A2输入 前一级相同信号
    I E_A3 5 E级A3输入 前一级相同信号
    I E_pc 32 E级pc输入 前一级相同信号
    I E_pc8 32 E级pc8输入 前一级相同信号
    I Tnew_E 2 E级Tnew输入 前一级相同信号
    I SelEMOut_E 1 D级控制信号输入 前一级相同信号
    I SelWOut_E 1 D级控制信号输入 前一级相同信号
    I RFWrite_E 1 D级控制信号输入 前一级相同信号
    I MemWrite_E 1 D级控制信号输入 前一级相同信号
    I LSOp_E 3 D级控制信号输入 前一级相同信号
    O M_AO 32 M级AO输出
    O M_V2 32 M级V2输出
    O M_A2 32 M级A2输出
    O M_A3 5 M级A3输出
    O M_pc 32 M级pc输出
    O M_pc8 32 M级pc8输出
    O Tnew_M 2 M级Tnew输出
    O SelEMOut_M 1 D级控制信号输出(使用)
    O SelWOut_M 1 D级控制信号输出
    O RFWrite_M 1 D级控制信号输出
    O MemWrite_M 1 D级控制信号输出(使用)
    O LSOp_M 3 D级控制信号输出(使用)
  • 运算功能

    \(Tnew\_M = (Tnew\_E > 0) ? Tnew\_E - 1: 0\)

W_Reg(MEM/WB流水寄存器)

  • 接口定义

    方向 信号名 位宽 描述 输入来源
    I clk 1 时钟信号 mips.v中的clk
    I reset 1 同步复位信号 mips.v中的reset
    I M_AO 32 M级AO输入 前一级相同信号
    I M_DR 32 M级DR输入 前一级相同信号
    I M_A3 5 M级A3输入 前一级相同信号
    I M_pc 32 M级pc输入 前一级相同信号
    I M_pc8 32 M级pc8输入 前一级相同信号
    I RFWrite_M 1 M级控制信号输入 前一级相同信号
    I SelWOut_M 2 M级控制信号输入 前一级相同信号
    O W_AO 32 W级AO输出
    O W_DR 32 W级DR输出
    O W_A3 5 W级A3输出
    O W_pc 32 W级pc输出
    O W_pc8 32 W级pc8输出
    O RFWrite_W 1 W级控制信号输出(使用)
    O SelWOut_W 2 W级控制信号输出(使用)

控制模块定义

MCU(主控制器模块)

在主控制模块中,我们对指令中Opcode域和Funct域中的数据进行解码,输出ALUOp,MemtoReg等19条控制指令,从而对数据通路进行调整,满足不同指令的需求。为实现该模块,我们又在内部设计了两个子模块——和逻辑(AND Logic)和或逻辑(OR Logic)。前者的功能是识别,将输入的Opcode和Funct数据识别为对应的指令,后者的功能是生成,根据输入指令的不同产生不同的控制信号。

  • 输入端口定义

    信号名 方向 位宽 描述
    opcode I 6 输入D_instr_opcode域数据
    funt I 6 输入D_instr_funct域数据
  • 输出端口(控制信号)定义

    信号名 位宽 作用级 描述 相关指令
    RFWrite 1 W GRF写使能信号
    MemWrite 1 M DM写入使能信号 store型:sw, sb, sh
    SelA3 2 D 对MUX_A3的输出进行选择
    SelEMOut 1 E、M 对MUX_E_out和MUX_M_out的输出进行选择 jr、jalr
    SelWOut 2 W 对MUX_W_out的输出进行选择
    SelALUB 1 E 对MUX_ALU_B的输出进行选择
    SelALUS 1 E 对MUX_ALU_S的输出进行选择 shift型、shiftv型
    NPCOp 3 D NPC模块功能选择信号
    CMPOp 3 D CMP模块功能选择信号 B型
    EXTOp 2 D EXT模块功能选择信号
    LSOp 3 M DM模块功能选择信号 load型、store型
    ALUOp 4 E ALU模块功能选择信号 calc_R型、calc_I型
    MDUOp 4 E MDU模块功能选择信号 md型、mf型、mt型
  • 注:该模块中的“Sel”型信号均作用于功能MUX

HCU(冒险控制器模块)

在冒险控制模块中,我们通过对传入的“A”(A1,A2,A3)和“T”(Tnew,Tuse)进行分析,判断当前需要进行转发(forward)还是暂停(stall),并通过组合逻辑生成相应控制信号。

  • 输入端口定义

    信号名 方向 位宽 描述
    D_A1 I 5 D级A1输入
    D_A2 I 5 D级A2输入
    E_A1 I 5 E级A1输入
    E_A2 I 5 E级A2输入
    M_A2 I 5 M级A2输入
    E_A3 I 5 E级A3输入
    M_A3 I 5 M级A3输入
    W_A3 I 5 W级A3输入
    Tuse_rs I 2 D级MCU中输出的Tuse_rs信号
    Tuse_rt I 2 D级MCU中输出的Tuse_rt信号
    Tnew_E I 2 E级Tnew_E信号输入
    Tnew_M I 2 M级Tnew_M信号输入
    Tnew_W I 2 W级Tnew_W信号输入
  • 输出端口(控制信号)定义

    信号名 位宽 作用级 描述
    FwdCMPD1 2 D 对HMUX_CMP_D1的输出进行选择
    FwdCMPD2 2 D 对HMUX_CMP_D2的输出进行选择
    FwdALUA 2 E 对HMUX_ALU_A的输出进行选择
    FwdALUB 2 E 对HMUX_ALU_B的输出进行选择
    FwdDM 1 M 对HMUX_DM的输出进行选择
    stall 1 F、D、M 暂停信号
  • 注:该模块中的“Fwd”型信号均作用于转发MUX

  • 注:该模块中的stall信号同时作用与IFU,D_Reg,E_Reg

选择器模块

功能MUX

MUX名 选择数量 描述 输出信号名 控制信号
MUX_A3 3 D级中A3输入信号进行选择
0:D_instr_rt
1:D_instr_rd
2:0x1f
D_A3 SelA3
MUX_ALU_B 2 对E级ALU模块src_B接口的信号进行选择
0:E_V2_f
1:E_E32
ALU_B SelALUB
MUX_ALU_S 2 对E级ALU模块src_S接口的信号进行选择
0:E_instr_s
1:E_V1_f
ALU_S SelALUS
MUX_E_out 2 对E级储存的计算结果进行选择
0:E_32
1:E_pc8
E_out SelEMOut
MUX_M_out 2 对M级储存的计算结果进行选择
0:M_AO
1:M_pc8
M_out SelEMOut
MUX_W_out 3 对W级储存的计算结果进行选择
0:W_AO
1:W_DR
2:W_pc8
W_out SelWOut

转发MUX

MUX名 选择数量 描述 输出信号名 控制信号
HMUX_CMP_D1 3 将数据转发到CMP_D1接口
0:GRF_RD1
1:M_out
2:E_out
D_V1_f FwdCMPD1
HMUX_CMP_D2 3 将数据转发到CMP_D2接口
0:GRF_RD2
1:M_out
2:E_out
D_V2_f FwdCMPD2
HMUX_ALU_A 3 将数据转发到ALU_A接口
0:E_V1
1:W_out
2:M_out
E_V1_f FwdALUA
HMUX_ALU_B 3 将数据转发到ALU_B接口
0:E_V2
1:W_out
2:M_out
E_V2_f FwdALUB
HMUX_DM 2 将数据转发到DM_WD接口
0:M_V2
1:W_out
M_V1_f FwdDM

重要机制实现方法

分支转移实现

B类指令

为了减少因控制冲突导致的暂停(stall),我们将B类指令的判断进行前置,单独使用CMP模块进行判断。当B类指令进入D级后(此时F级的指令为编译优化调度的指令),CMP模块的判断结果进入NPC,如过CMP结果为真(CMP_out = 1)而且NPCOp信号为0x001(说明当前指令为B类指令),NPC输出转移的地址npc并进入IFU的输入端,在下一时钟沿上升时进入F级,实现转移。

j和jal

当j或jal进入D级后(此时F级的指令为编译优化调度的指令),D_instr中imm26域的数据进入NPC进行处理,如果当前NPCOp信号为0x010(说明当前指令为jal或j指令),NPC输出转移的地址npc,并进入IFU的输入端,在下一时钟沿上升时进入F级,实现转移。

jal指令在实现跳转的同时,还需要将下一条指令的地址存入31号寄存器中,因此我们需要在IFU中计算出改地址,并随着jal指令进行流水,最终在W级写入GRF的31号寄存器。由于存在延迟槽,pc+4地址中的指令是编译优化机制调度过来的,因此我们要保存的地址应该为pc+8。

jr和jalr

当jr进入D级后(此时F级的指令为编译优化调度的指令),D_V1_f(经过转发后的D_V1值)进入NPC,如果当前NPCOp信号为0x011(说明当前指令为jr指令),NPC输出转移的地址npc,并进入IFU的输入端,在下一时钟沿上升时进入F级,实现转移。

jalr指令在实现跳转的同时,还需要将下一条指令的地址存入31号寄存器中,因此我们需要在IFU中计算出改地址,并随着jal指令进行流水,最终在W级写入GRF的31号寄存器。由于存在延迟槽,pc+4地址中的指令是编译优化机制调度过来的,因此我们要保存的地址应该为pc+8。

乘除槽实现

为了支持 mult、multu、div、divu、mfhi、mflo、mthi mtlo 这些乘除法相关指令,需要设计独立的乘 / 除功能部件。该部件位于在流水线的 EX 阶段,如图 1 所示。

  • 若当前指令为mfhimflo,即读取HILO中的数据,我们直接根据MDUOp的值将相应数据从out接口输出,进入下一级

    assign out = (MDUOp == `MDU_MFHI) ? HI : 
                 (MDUOp == `MDU_MFLO) ? LO :
                                        0;
  • 若当前指令为mthimtlo,即向HILO寄存器中写入数据,可以直接在一个周期内完成,我们直接根据MDUOp的值进行写值操作即可。

    case(MDUOp)
    	//…………………………………………………
        `MDU_MTHI:   HI <= A;  
        `MDU_MTLO:   LO <= A;
    endcase
  • 若当前指令为mult、multu、div、divu,即乘除运算指令,即使我们可以通过Verilog中内置的乘除运算实现,但是我们需要模拟实际电路的延迟,因此编程时不能直接将结果写入到HILO寄存器,而是先存入临时寄存器HI_tempLO_temp。在写入的同时,还需将延迟的周期数记录下来,写入max寄存器。以上操作当乘除指令进入E级时的第一个周期就执行,此时start信号为1。

     case(MDUOp)
    	`MDU_MULT: begin 
    		{HI_temp, LO_temp} <= $signed(A) * $signed(B);
    		max <= 5;
    	 end
    	`MDU_MULTU: begin 
    		{HI_temp, LO_temp} <= $unsigned(A) * $unsigned(B);
    		max <= 5;
    	 end
    	`MDU_DIV: begin 
    		{HI_temp, LO_temp} <= {$signed(A) % $signed(B), $signed(A) / $signed(B)};
    		max <= 10;
    	 end
    	`MDU_DIVU: begin 
    		{HI_temp, LO_temp} <= {$unsigned(A) % $unsigned(B), $unsigned(A) / $unsigned(B)};
    		max <= 10;
    	end
    endcase

    当模拟延迟结束之后,才能将值写入HILO寄存器。因此我们设置一个计数器cnt,当busy信号有效时进行计数,当达到max时停止计数并清空,同时将HI_tempLO_temp中的值写入HI

    LO。完整代码如下

    //HI、LO、HI_temp、LO_temp
    always @(posedge clk) begin
            if(reset) begin
                HI <= 0;
                LO <= 0;
                HI_temp <= 0;
                LO_temp <= 0;
            end
            else if(busy == 0) begin//the first circle 
                case(MDUOp)
    				//………………………………
                endcase
            end
            else begin//busy is 1, meaning the instruction is mult or div
                if(cnt == max - 1) {HI, LO} <= {HI_temp, LO_temp};
                else {HI, LO} <= {HI, LO};
            end
        end
    // cnt and busy 
    always @(posedge clk) begin
        if(reset) begin
            cnt <= 0; busy <= 0;
        end
        else if(start) begin
            busy <= 1'b1;
        end
        else if(~start && busy)begin
            if(cnt == max - 1) begin
                cnt <= 0; busy <= 0;
            end
            else cnt <= cnt + 1;
        end
    end
  • 由于乘除运算独立于ALU,因此在乘除槽处于运算延迟时,其他无关指令均可继续进行。但是,如果后面的指令和乘除槽相关(md、mf、mt三类),则需要暂停(stall),延迟模拟结束后才可继续进行。

冒险处理

冒险处理我们均通过“A_T”法实现——

转发(forward)

当前面的指令要写寄存器但还未写入,而后面的指令需要用到没有被写入的值时,这时候会产生数据冒险,我们首先考虑进行转发。我们假设所有的数据冒险均可通过转发解决。也就是说,当某一指令前进到必须使用某一寄存器的值的流水阶段时,这个寄存器的值一定已经产生,并存储于后续某个流水线寄存器中

在这一阶段,我们不管需要的值有没由计算出,都要进行转发,即暴力转发。为实现这一机制,我们要清楚哪些模块需要转发后的数据(需求者)和保存着写入值的流水寄存器(供应者

  • 供应者及其产生的数据

    流水级 产生数据 MUX名&选择信号名 MUX输出名
    E E_E32,E_pc8 MUX_E_out & SelEMOut E_out
    M M_AO,M_pc8 MUX_M_out & SelEMOut M_out
    W W_AO,W_RD,W_pc8 MUX_W_out & SelWOut W_out

    注:当M级指令为读hi和lo的指令时, M_AO中的结果是从上一周期在乘除槽中读取的hi或lo的值;如果是其他指令,M_AO是上一周期ALU的计算结果。

  • 需求者及其产生的数据

    接收端口 选择数据 HMUX名&选择信号名 MUX输出名
    CMP_D1/NPC_ra D_V1,M_out,E_out HMUX_CMP_D1 & FwdCMPD1 D_V1_f
    CMP_D2 D_v1,M_out,E_out HMUX_CMP_D2 & FwdCMPD2 D_V2_f
    ALU_A/MDU_A E_V1, W_out,M_out HMUX_ALU_A & FwdALUA E_V1_f
    ALU_B/MDU_B E_V2,W_out,M_out HMUX_ALU_B & FwdALUB E_V1_f
    DM_WD M_V2, W_out HMUX_DM & FwdDM M_V2_f

从上表可以看出,W级中的数据没有转发到D级,原因是我们在GRF内实现了内部转发机制,将GRF输入端的数据(还未写入)及时反映到RD1或这RD2,判断条件为A3 == A2或者A3 == A1

此时为了生成HMUX的选择信号,我们需要向HCU(冒险控制器)输入”A”数据,然后进行选择信号的计算,执行转发的条件为——

  • 前位点的读取寄存器地址和某转发输入来源的写入寄存器地址相等且不为 0
  • 写使能信号有效

根据以上条件我们可以生成上面的5个HMUX选择信号,选择信号的输出值应遵循“就近原则”,及最先产生的数据最先被转发。代码如下——

assign FwdCMPD1 = ((D_A1 != 5'd0) && (D_A1 == E_A3) && (RFWrite_E)) ? 2'd2 :
                  ((D_A1 != 5'd0) && (D_A1 == M_A3) && (RFWrite_M)) ? 2'd1 : 
                                                                      2'd0;
 
assign FwdCMPD2 = ((D_A2 != 5'd0) && (D_A2 == E_A3) && (RFWrite_E)) ? 2'd2 :
                  ((D_A2 != 5'd0) && (D_A2 == M_A3) && (RFWrite_M)) ? 2'd1 :
                                                                      2'd0;

assign FwdALUA  = ((E_A1 != 5'd0) && (E_A1 == M_A3) && (RFWrite_M)) ? 2'd2 :
                  ((E_A1 != 5'd0) && (E_A1 == W_A3) && (RFWrite_W)) ? 2'd1 :
                                                                      2'd0;

assign FwdALUB  = ((E_A2 != 5'd0) && (E_A2 == M_A3) && (RFWrite_M)) ? 2'd2 :
                  ((E_A2 != 5'd0) && (E_A2 == W_A3) && (RFWrite_W)) ? 2'd1 :
                                                                      2'd0;

assign FwdDM    = ((M_A2 != 5'd0) && (M_A2 == W_A3) && (RFWrite_W)) ? 1'd1 : 
                                                                      1'd0;

暂停(stall)

接下来,我们来处理通过转发不能处理的数据冒险。在这种情况下,新的数据还未来得及产生。我们只能暂停流水线,等待新的数据产生。为了方便处理,我们仅仅为D级的指令进行暂停处理。

我们把Tuse和Tnew作为暂停的判断依据——

  • Tuse:指令进入 D 级后,其后的某个功能部件经过多少时钟周期就必须要使用寄存器值。对于有两个操作数的指令,其每个操作数的 Tuse 值可能不等(如 store 型指令 rs、rt 的 Tuse 分别为 1 和 2 )。
  • Tnew:位于 E 级及其后各级的指令,再经过多少周期就能够产生要写入寄存器的结果。在我们目前的 CPU 中,W 级的指令Tnew 恒为 0;对于同一条指令,Tnew@M = max(Tnew@E - 1, 0)、

在这一阶段,我们找到D级生成的Tuse_rs和Tuse_rt和在E,M,W级寄存器中流水的Tnew_D,Tnew_M,Tnew_W,如下表所示

  • Tuse表和计算表达式

    指令类型 Tuse_rs Tuse_rt
    calc_R 1 1
    calc_I 1 X
    shift X 1
    shiftv 1 1
    load 1 X
    store 1 2
    md 1 1
    mt 1 X
    mf X X
    branch 0 0
    j / jr X X
    jal / jalr 0 X
    lui X X
    //X表示不需要使用GRF中的值,此时Tuse可取得最大值
    assign Tuse_rs = (branch | _jr | _jalr)                               ? 2'd0 :
                     (md | mt | calc_R | calc_I | shiftv | load | store)  ? 2'd1 : 
                                                                            2'd3 ;
        
    assign Tuse_rt = (branch)                                   ? 2'd0 :
                     (md | calc_R | shift | shiftv)             ? 2'd1 :
                     (store)                                    ? 2'd2 : 
                                                                  2'd3;
  • Tnew表和计算表达式

    指令类型 Tnew_D Tnew_E Tnew_M Tnew_W
    calc_R 2 1 0 0
    calc_I 2 1 0 0
    shift 2 1 0 0
    shiftv 2 1 0 0
    load 3 2 1 0
    store X X X X
    md X X X X
    mt X X X X
    mf 2 1 0 0
    branch X X X X
    jal / jalr 0 0 0 0
    j / jr X X X X
    lui 1 0 0 0
    //X表示不需要向GRF写值,此时Tnew可取得0(最小值)
    //在控制器中生成的是D级Tnew,在流水过程中递减
    assign Tnew    = (load)                                     ? 2'd3 :
                     (mf | calc_R | calc_I | shift | shiftv)    ? 2'd2 : 
                     (_lui)                                     ? 2'd1 :
                     (_jal | _jalr)                             ? 2'd0 : 
                                                                  2'd0;     

然后我们Tnew和Tuse传入HCU(冒险控制器中),然后进行stall信号的计算。如果满足以下条件则stall有效——

  • Tnew > Tuse

  • 前位点的读取寄存器地址和某转发输入来源的写入寄存器地址相等且不为 0

  • 写使能信号有效

  • 当E级延迟槽在进行运算(start | busy)时,D级为md、mt、mf指令

代码如下——

 assign   stall_rs_E = (D_A1 != 5'd0) & (D_A1 == E_A3) & (RFWrite_E) & (Tuse_rs < Tnew_E);
  
assign   stall_rs_M = (D_A1 != 5'd0) & (D_A1 == M_A3) & (RFWrite_M) & (Tuse_rs < Tnew_M);

assign   stall_rt_E = (D_A2 != 5'd0) & (D_A2 == E_A3) & (RFWrite_E) & (Tuse_rt < Tnew_E);

assign   stall_rt_M = (D_A2 != 5'd0) & (D_A2 == M_A3) & (RFWrite_M) & (Tuse_rt < Tnew_M);

assign   stall_rs   = stall_rs_E | stall_rs_M;

assign   stall_rt   = stall_rt_E | stall_rt_M; 

assign   stall_md   = (MDUOp_D != `MDU_NONE) & (E_start | E_busy);

assign   stall      = stall_rs | stall_rt | stall_md;

stall为1时执行以下操作——

  • 冻结PC寄存器(IFU_en = ~stall = 0)
  • 冻结D级寄存器(D_en = ~stall = 0)
  • 清空E级寄存器(E_clr = stall = 1)

测试方案

典型测试样例

计算、访存指令测试

我们先通过对除了分支、跳转之外的计算访存指令进行测试。该部分测试由python代码自动生成,生成思路是,连续枚举4条连续指令,将calc_R(包含shift_v), calc_I, LS, shift, md, mt, mf七种类型的指令进行排列组合(每一个类型随机取出一条指令),共有2401种排列。为了增加冲突概率,我们将被读写的寄存器范围调整到有限的5个,当测试样例足够多时,基本可以覆盖所有的冲突情况。

  • 生成代码

    list_R = ["addu", "subu", "and", "or", "nor", "xor", "sltu", "slt", "sllv", "srlv", "srav"]
    list_I = ["andi", "addiu", "ori", "xori", "lui", "slti", "sltiu"]
    list_LS = ["lw", "sw", "lh", "lhu", "sh", "lb", "lbu", "sb"]
    list_shift = ["sll", "srl", "sra"]
    list_B = ["bne", "beq","bgtz", "blez", "bltz", "bgez"]
    list_MD = ["mult", "multu", "div", "divu"]
    list_MT = ["mthi", "mtlo"]
    list_MF = ["mfhi", "mflo"]
    
    length = 8
    
    def R_test(file, n):
       #………………………………
    def I_test(file, n):
       #………………………………
    def LS_test(file, n):
       #………………………………
    def shift_test(file, n):
       #………………………………
    def MD_test(file, n):
       #………………………………
    def MT_test(file, n):
       #………………………………
    def MF_test(file, n):
       #………………………………
        
    instr = [R_test, I_test, LS_test, shift_test, MD_test, MT_test, MF_test]
    
    for i in range(7):
        f = open("mips_code_{}.asm".format(i), "w")
        #寄存器随机赋值
        for x in range(length):
            temp = random.randint(-2147483648, 2147483648) 
            s = "li ${} {}\n".format(x, temp)
            f.write(s)
        f.write("li $8, {}\n\n".format(random.randint(-2147483648, 2147483648) % 10000 + 1))
        #排列生成指令
        for j in range(7):
            for k in range(7):
                for m in range(7):
                    instr[j](f, 1)
                    instr[k](f, 1)
                    instr[m](f, 1)
                    instr[i](f, 1)
                    f.write('\n')
    
  • 测试样例

    li $0 -1590414783
    li $1 -1387657999
    li $2 97336612
    li $3 -1971889419
    li $4 1148790734
    li $5 -289210629
    li $6 1026640559
    li $7 1852052372
    li $8, 7860
    
    sltu $4, $0, $1
    srav $4, $1, $6
    or $6, $0, $4
    xor $0, $6, $6
    
    nor $3, $5, $2
    slt $0, $5, $3
    ori $5, $1, -31050
    or $5, $2, $6
    
    subu $1, $2, $6
    sltu $4, $4, $3
    lbu $0, 878($0)
    slt $2, $2, $2
    
    or $6, $2, $4
    srav $1, $7, $0
    sll $0, $7, 7
    srlv $6, $3, $6
    
    nor $3, $3, $7
    or $5, $7, $2
    div $4, $8
    sltu $7, $3, $2
    
    sllv $6, $2, $0
    srlv $5, $5, $5
    mtlo $0
    addu $0, $5, $4
    
    slt $6, $7, $6
    addu $5, $1, $1
    mfhi $1
    nor $4, $5, $5
    
    srav $3, $5, $4
    sltiu $3, $4, 7080
    and $3, $0, $4
    or $7, $5, $5
    
    xor $5, $3, $3
    andi $2, $6, 25900
    xori $1, $5, 18289
    srlv $0, $6, $5
    
    slt $4, $6, $2
    ori $7, $3, -32729
    lh $7, 1250($0)
    sllv $7, $4, $4
    
    addu $4, $1, $2
    xori $7, $0, 11378
    srl $4, $4, 5
    addu $6, $5, $6
    
    srav $7, $3, $4
    slti $1, $5, -24158
    multu $6, $8
    sltu $6, $0, $1
    

转移指令(B类和J类)功能测试

main:	
li	$s0, 0
li	$s1, -1000
li	$s2, 1000
lui	$s3, 0x8000	#s3 is  -2147483648
lui	$s4, 0x8000
ori	$s4, 0x0001	#s4 is  -2147483647
lui	$s5, 0x7fff
ori	$s5, 0xffff	#s5 is  2147483647
lui	$s6, 0x7fff
ori	$s6, 0xfffe	#s6 is 2147483646


beq_1:	beq	$s0, $s0, beq_1_test
		nop
beq_2:	beq	$s1, $s1, beq_2_test
		nop
……
beq_10:  beq	$s5, $s6, beq_10_test
		nop
beq_end:

bne_1:	bne	$s0, $s1, bne_1_test
		nop
……
bne_10:	bne	$s3, $s3, bne_10_test
		nop
bne_end:

blez_1: blez	$s0, blez_1_test
		nop
……
blez_10: blez	$s6, blez_10_test
		nop
blez_end:

bgtz_1:	 bgtz	$s2,bgtz_1_test
		nop
……
bgtz_10: bgtz	$s1,bgtz_10_test
		nop
bgtz_end:

jal_1:	jal	jal_1_test
		nop
……
jal_5:	jal	jal_5_test
		nop
jal_end:

sw	$t0, 0($s0)
sw	$t1, 0($s0)
sw	$t2, 0($s0)
sw	$t3, 0($s0)
sw	$t4, 0($s0)
sw	$t5, 0($s0)
li	$v0, 10
syscall
	
beq_1_test: addi	$t0,$t0, 1
	   j	beq_2
		nop
……
beq_10_test: addi	$t0,$t0, 1
	   j	beq_end
		nop

bne_1_test: addi	$t1,$t1, 1
	   j	bne_2
		nop
……
bne_10_test:addi	$t1,$t1, 1
	   j	bne_end
		nop

blez_1_test:	addi	$t2,$t2, 1
		j	blez_2
		nop
……
blez_10_test:	addi	$t2,$t2, 1
		j	blez_end
		nop

bgtz_1_test:	addi	$t3,$t3 1
		j	bgtz_2
		nop
……
bgtz_10_test:	addi	$t3,$t3 1
		j	bgtz_end
		nop
		
jal_1_test:	addi	$t4, $4, 1
		jr	$ra
		nop
……
jal_5_test:	addi	$t4, $4, 1
		jr	$ra
		nop

jalr_1_test:	addi	$t5, $t5, 1
		jr	$v1
		nop
……
jalr_5_test:	addi	$t5, $t5, 1
		jr	$v1
		nop

转移指令冲突测试

连续枚举4条指令,最后一条时b类指令,在前三条设置和B类相冲突的指令(计算、访存)
li	$s1, 6355
li	$s2, 12
li	$s3, -55
li	$s4, 0
li	$s5, 0xffff
li	$s6, 0x7fffffff
li	$s7, -12333


or	$s3, $s2, $s4
lw	$s5, 12($0)
add     $s1, $s3, $s2###
beq	$s1, $s2, next_1
nop
slt	$s3, $s4, $s5
next_1:

or	$s3, $s2, $s4
sll    $s1, $s3, $s2###
lw	$s5, 12($0)
bne	$s1, $s2, next_2
nop
slt	$s3, $s4, $s5
next_2:

ori     $s1, $s3, 12345###
or	$s3, $s2, $s4
lw	$s5, 12($0)
bne	$s1, $s2, next_3
nop
slt	$s3, $s4, $s5
next_3:


or	$s3, $s2, $s4
sw	$s3, 0($0)
lw      $s1, 0($0)###
bne	$s1, $s2, next_4
nop
slt	$s3, $s4, $s5
next_4:


sw	$s4, 0($0)
lw      $s1, 0($0)###
or	$s3, $s2, $s4
bne	$s1, $s2, next_5
nop
slt	$s3, $s4, $s5
next_5:

sw	$s5, 0($0)
lw      $s1, 0($0)###
and	$s6, $7, $s3
or	$s3, $s2, $s4
bne	$s1, $s2, next_6
nop
slt	$s3, $s4, $s5
next_6:


jal	jal_1###
or	$s3, $s2, $s4
jal_1:
beq	$31, $31, next_7
nop
slt	$s3, $s4, $s5
next_7:


jal	jal_2###
or	$s3, $s2, $s4
jal_2:
and	$s6, $7, $s3
beq	$31, $31, next_8
nop
slt	$s3, $s4, $s5
next_8:


la	$30, jalr_1
jalr	$30###
or	$s3, $s2, $s4
jalr_1:
beq	$31, $31, next_9
nop
slt	$s3, $s4, $s5
next_9:

la	$30, jalr_2
jalr	$30###
or	$s3, $s2, $s4
jalr_2:
and	$s6, $7, $s3
beq	$31, $31, next_10
nop
slt	$s3, $s4, $s5
next_10:

mthi	$s5
mfhi    $s1###
beq	$s1, $s5, next_11
nop
slt	$s3, $s4, $s5
next_11:

mthi	$s5
mfhi    $s1###
add	$s2, $s6, $s7
beq	$s1, $s5, next_12
nop
slt	$s3, $s4, $s5
next_12:

mthi	$s5
mfhi    $s1###
add	$s2, $s6, $s7
lui	$s2, 4455
beq	$s1, $s5, next_13
nop
slt	$s3, $s4, $s5
next_13:
连续四条指令中存在2个跳转(设置模板,由代码自动生成)
  • 生成代码

    def B_test(file, lable):
        k = random.randint(0,10000000) % len(list_B)
        if(k == 0 or k == 1):
            rs = random.randint(0, 10000000) % length
            rt = random.randint(0, 10000000) % length
            s = "{} ${}, ${}, {}\n".format(list_B[k], rs, rt, lable)
            file.write(s)
        else:
            rs = random.randint(0, 10000000) % length
            s = "{} ${}, {}\n".format(list_B[k], rs, str(lable))
            file.write(s)
    
    
    def b_begin(file, n):
        file.write("\nb_test_{}_one:\n".format(n))
        B_test(file, "b_test_{}_one_then".format(n))
        R_test(file,1)
    
        file.write("b_test_{}_two:\n".format(n))
        B_test(file, "b_test_{}_two_then".format(n))
        I_test(file,1)
    
        file.write("jal_test_{}:\n".format(n))
        file.write("jal jal_test_{}_then\n".format(n))
        I_test(file,1)
    
        file.write("end_{}:\n\n".format(n))
    
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
    
    def b_end(file, n):
        file.write("\nb_test_{}_one_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MT_test(file, 1)
        file.write("j b_test_{}_two\n".format(n))
        R_test(file, 1)
        
        file.write("\nb_test_{}_two_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MT_test(file, 1)
        file.write("jal jal_test_{}\n".format(n))
        file.write("addu $1, $ra, $0\n")
    
        file.write("\njal_test_{}_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MT_test(file, 1)
        file.write("addiu $ra,$ra, 8\n".format(n))
        B_test(file, "end_{}".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MT_test(file, 1)
        file.write("jr $ra\n".format(n))
  • 测试样例

    b_test_1_one:
    bgtz $4, b_test_1_one_then
    addu $4, $6, $7
    b_test_1_two:
    beq $5, $1, b_test_1_two_then
    ori $0, $7, 23724
    jal_test_1:
    jal jal_test_1_then
    ori $3, $5, -28895
    end_1:
    
    sllv $2, $1, $7
    andi $0, $2, 6835
    sb $2, 2366($0)
    sll $2, $1, 2
    
    ……………………
    
    b_test_1_one_then:
    addu $1, $0, $7
    lui $4,39476
    lw $3, 100($0)
    srl $1, $0, 0
    multu $3, $8
    mtlo $7
    j b_test_1_two
    xor $4, $1, $1
    
    b_test_1_two_then:
    srav $6, $4, $2
    andi $6, $4, 31313
    sb $4, 3770($0)
    srl $5, $1, 1
    divu $2, $8
    mtlo $3
    jal jal_test_1
    addu $1, $ra, $0
    
    jal_test_1_then:
    slt $6, $7, $0
    sltiu $5, $4, 1372
    lh $6, 1034($0)
    sll $0, $7, 1
    multu $5, $8
    mtlo $6
    addiu $ra,$ra, 8
    bgez $1, end_1
    and $5, $3, $2
    xori $1, $4, -23606
    lb $2, 869($0)
    sra $4, $4, 1
    multu $3, $8
    mtlo $5
    jr $ra

自动测试工具

  • 测试代码生成工具(python)

    我们先将指令分类,每类指令使用列表储存。每一类指令分别用一个单独封装的函数来随机生成,在后面可以跟据需要排列组合

    import random
    
    list_R = ["addu", "subu", "and", "or", "nor", "xor", "sltu", "slt", "sllv", "srlv", "srav"]
    list_I = ["andi", "addiu", "ori", "xori", "lui", "slti", "sltiu"]
    list_LS = ["lw", "sw", "lh", "lhu", "sh", "lb", "lbu", "sb"]
    list_shift = ["sll", "srl", "sra"]
    list_B = ["bne", "beq","bgtz", "blez", "bltz", "bgez"]
    list_MD = ["mult", "multu", "div", "divu"]
    list_MTMF = ["mfhi", "mflo", "mthi", "mtlo"]
    
    #length是生成的指令所用到的寄存器个数
    length = 8 #为了增大冒险概率,我们将寄存器的范围缩小到0~7
    
    def R_test(file, n):
        for i in range(n):
            k = random.randint(0, 10000000) % len(list_R)
            rs = random.randint(0, 10000000) % length;
            rt = random.randint(0, 10000000) % length;
            rd = random.randint(0, 10000000) % length;
            s = "{} ${}, ${}, ${}\n".format(list_R[k], rd, rs, rt)
            file.write(s)
    
    def I_test(file, n):
        for i in range(n):
            k = random.randint(0, 10000000) % len(list_I)
            rs = random.randint(0, 10000000) % length
            rt = random.randint(0, 10000000) % length
            imm = random.randint(-32768, 32768)
            abs_imm = random.randint(0, 65536)
            if list_I[k] == "lui":
                s = "{} ${},{}\n".format(list_I[k], rt, abs_imm)
            else:
                s = "{} ${}, ${}, {}\n".format(list_I[k], rt, rs, imm)
            file.write(s)
    
    def LS_test(file, n):
        for i in range(n):
            k = random.randint(0,10000000) % len(list_LS)
            ins = list_LS[k]
            num = 0
            if(ins[1] == "w"):
                num = (random.randint(0,10000000) << 2) % 4096
            elif(ins[1] == "h"):
                num = (random.randint(0,10000000) << 1) % 4096
            else:
                num = (random.randint(0,10000000)) % 4096
            
            rt = random.randint(0, 10000000) % length
            s = "{} ${}, {}($0)\n".format(ins, rt, num)
            file.write(s)
    
    def shift_test(file, n):
        for i in range(n):
            k = random.randint(0,10000000) % len(list_shift)
            shamt = random.randint(0, 10000000) % length
            rd = random.randint(0, 10000000) % length
            rt = random.randint(0, 10000000) % length
            s = "{} ${}, ${}, {}\n".format(list_shift[k], rd, rt, shamt)
            file.write(s)
    
    def MD_test(file, n):
        for i in range(n):
            k = random.randint(0,10000000) % len(list_MD)
            rs = random.randint(0, 10000000) % length
            rt = random.randint(0, 10000000) % length
            if(list_MD[k] == "mult" or list_MD[k] == "mul"):
                s = "{} ${}, ${}\n".format(list_MD[k], rs, rt)
            else:
                s = "{} ${}, ${}\n".format(list_MD[k], rs, 8)
            file.write(s)
    
    def MTMF_test(file, n):
        for i in range(n):
            k = random.randint(0,10000000) % len(list_MTMF)
            rs = random.randint(0, 10000000) % length
            s = "{} ${}\n".format(list_MTMF[k], rs)
            file.write(s)
    
    def B_test(file, lable):
        k = random.randint(0,10000000) % len(list_B)
        if(k == 0 or k == 1):
            rs = random.randint(0, 10000000) % length
            rt = random.randint(0, 10000000) % length
            s = "{} ${}, ${}, {}\n".format(list_B[k], rs, rt, lable)
            file.write(s)
        else:
            rs = random.randint(0, 10000000) % length
            s = "{} ${}, {}\n".format(list_B[k], rs, str(lable))
            file.write(s)
    
    
    def b_begin(file, n):
        file.write("\nb_test_{}_one:\n".format(n))
        B_test(file, "b_test_{}_one_then".format(n))
        R_test(file,1)
    
        file.write("b_test_{}_two:\n".format(n))
        B_test(file, "b_test_{}_two_then".format(n))
        I_test(file,1)
    
        file.write("jal_test_{}:\n".format(n))
        file.write("jal jal_test_{}_then\n".format(n))
        I_test(file,1)
    
        file.write("end_{}:\n\n".format(n))
    
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
    
    def b_end(file, n):
        file.write("\nb_test_{}_one_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MTMF_test(file, 1)
        file.write("j b_test_{}_two\n".format(n))
        R_test(file, 1)
        
        file.write("\nb_test_{}_two_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MTMF_test(file, 1)
        file.write("jal jal_test_{}\n".format(n))
        file.write("addu $1, $ra, $0\n")
    
        file.write("\njal_test_{}_then:\n".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MTMF_test(file, 1)
        file.write("addiu $ra,$ra, 8\n".format(n))
        B_test(file, "end_{}".format(n))
        R_test(file, 1)
        I_test(file, 1)
        LS_test(file, 1)
        shift_test(file, 1)
        MD_test(file, 1)
        MTMF_test(file, 1)
        file.write("jr $ra\n".format(n))
    
    
    with open("mips_code.asm", "w") as file:
        for i in range(length):
            temp = random.randint(-2147483648, 2147483648)
            s = "li ${} {}\n".format(i, temp)
            file.write(s)
        file.write("li $8, {}\n".format(random.randint(-2147483648, 2147483648) % 10000 + 1))
        
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
    
        for i in range(10):
            R_test(file, 2)
            MD_test(file,1)
            MTMF_test(file, 1)
            I_test(file, 2)
            LS_test(file, 2)
            MTMF_test(file, 1)
            I_test(file, 2)
            shift_test(file, 2)
            file.write("\n")
    
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
        # index = random.randint(0,10000000) % length
        # file.write("li  ${} 0\n".format(index))
    
        # for i in range(10):
        #     b_begin(file, i+1)
        # file.write("j   final\n")
        # for i in range(10):
        #     b_end(file, 1+i)
        # file.write("final:\nnop\n")
       
  • 自动测试工具(python)

    在使用前,我们需要将下方r_roadxilinx_path中输入工程文件夹路径和ISE路径。在终端运行这个代码(test.py)后,首先需要输入测试次数,对于每一次测试,我们运行一次代码自动生成工具(generate_2.py),生成MIPS代码(mips_code.asm),然后会启动mars和ISE分别获得标准输出(mips_out.txt)和测试输出(verilog_out.txt),最后进行文本比较,在终端会显示比较结果期望执行周期实际执行周期。如果出现错误,会自动生成错误日志文件,显示错误行数和相应mips机器码、标准输出、测试输出。如果想要对自己写的MIPS代码进行测试,只需要将第132行注释掉,运行次数填写为1即可。

    import os
    import shutil
    import re
    run_time = "30us"
    xilinx_path = "E:\\Xilinx\\14.7\\ISE_DS\\ISE"
    p_path = "D:\\Code\\Verilog\\homework_code\\P6_cpu"
    mips_code_name = "mips_code.asm"
    mips_out_name = "mips_out.txt"
    verilog_out_name = "verilog_out.txt"
    
    error = []
    passed = int(0)
    
    def run_mars():
        print("编译并运行MIPS文件……")
        os.system("java -jar Mars_perfect.jar db mc CompactDataAtZero a dump .text HexText code.txt nc " + mips_code_name)
        os.system("java -jar Mars_perfect.jar db mc CompactDataAtZero nc " + mips_code_name + " > " + mips_out_name)
    def load_hex_code():
        list_temp = []
        with open("code.txt", "r+") as hex_code:
            list_temp = hex_code.readlines()
        with open(p_path + "\\code.txt", "w") as file_to_IM:
            file_to_IM.writelines(list_temp)
        hex_code.close()
        file_to_IM.close()
    
    
    
    def run_ise():
        print("编译并运行Verilog文件……")
        
        file_list = []
        for i, j ,k in os.walk(p_path):
            for file in k:
                if file.endswith(".v"):
                    file_list.append(file)
    
        with open(p_path + "\\mips.prj", "w") as prj:
            for i in range(len(file_list)):
                prj.write("Verilog work \"" + p_path + "\\" + file_list[i] + "\"\n")           
    
        with open(p_path + "\mips.tcl", "w") as tcl:
            tcl.write("run " + run_time +";\nexit")
        
        prj.close()
        tcl.close()
    
        os.environ["XILINX"] = xilinx_path
        os.system(xilinx_path + "\\bin\\nt64\\fuse -nodebug -prj " + p_path + "\\mips.prj -o mips.exe mips_tb > compile_log.txt")
        os.system("mips.exe -nolog -tclbatch " + p_path + "\\mips.tcl> " + verilog_out_name)
        # print("mips.exe -nolog -tclbatch " + p_path + "\\mips.tcl > verilog_out.txt")
    
    def copy_file(name, target_road):
        f_1 = open(name, "r")
        list_temp = f_1.readlines()
        f_2 = open(target_road, "w")
        f_2.writelines(list_temp)
        f_1.close()
        f_2.close()
    
    # def file_adj():
    #     list_temp = list()
    #     with open(verilog_out_name, "r") as f:
    #         list_temp = f.readlines()[5:]
    #         for i in range(len(list_temp) - 1):
    #             out_1 = re.findall(r"(.+?)@(.+?):", list_temp[i])[0]
    #             out_2 = re.findall(r"(.+?)@(.+?):", list_temp[i+1])[0]
    #             if (out_1[0] == out_2[0]) and (int(out_1[1], 16) > int(out_2[1], 16)):
    #                     str_temp = list_temp[i+1]
    #                     list_temp[i+1] = list_temp[i]
    #                     list_temp[i] = str_temp;
    #     with open(verilog_out_name, "w") as f:
    #         f.writelines(list_temp)
            
    def file_cmp(order):
        global passed
        time = int()
        with open(mips_out_name, "r") as out_1:
            out_std = out_1.readlines()
            if('\n' in out_std):
                out_std.remove('\n')
        with open(verilog_out_name, "r") as out_2:
            out_test = out_2.readlines()[5:]
            time = int(out_test[-1][:out_test[-1].index("@")].strip())
        
        flag = 0
        with open(".\\log.txt", "w") as log:
            if(len(out_std) > len(out_test)):
                flag = 1
                log.write("Too few output! Expected output-lines is {}, But your output-lines is {}\n\n".format(len(out_std), len(out_test)))
            elif(len(out_std) < len(out_test)):
                flag = 1
                log.write("Too more output! Expected output-lines is {}, But your output-lines is {}\n\n".format(len(out_std), len(out_test)))
            for i in range(min(len(out_std), len(out_test))):
                if(out_std[i] != out_test[i][out_test[i].index("@"): ]):
                    if(re.findall("(.+?)@", out_test[i]) != re.findall("(.+?)@", out_test[i+1])):
                        flag = 1;
                        log.write("Error in line {}: \nExpected output is \"{}\", but your outout is \"{}\"\n\n".format(i+1, out_std[i][:-1], out_test[i][out_test[i].index("@"):-1]))
                    else:
                        str_temp = out_test[i+1]
                        out_test[i+1] = out_test[i]
                        out_test[i] = str_temp;
        with open(verilog_out_name, "w") as f:
            f.writelines(out_test)
            
        if(flag):
            print("测试结果:  Failed!")
            error.append(order)
            os.makedirs(".\\test_log_file\\log_{}\\".format(order))
            copy_file("log.txt", ".\\test_log_file\\log_{}\\log.txt".format(order))
            copy_file(mips_code_name, ".\\test_log_file\\log_{}\\mips_code.asm".format(order))
            copy_file(mips_out_name, ".\\test_log_file\\log_{}\\mips_out.txt".format(order))
            copy_file(verilog_out_name, ".\\test_log_file\\log_{}\\verilog_out.txt".format(order))
        else:
            print("测试结果:  Accepted!")
            passed = passed + 1
    
        s = f'java -jar Hazard-Calculator.jar code.txt --hz  --im-base 0x3000 --im-size 16384 --dm-base 0 --dm-size 12288 '
        os.system(s)
        print("   your circle: {}".format(time >> 2))
    
    test_times = int(input("请输入测试次数:"))
    print("测试开始!\n")
    
    if os.path.exists(".\\test_log_file"):
        shutil.rmtree(".\\test_log_file") 
    os.makedirs(".\\test_log_file\\")
    print("错误日志文件夹已创建!")
    
    for i in range(1,test_times + 1):
        print("\n测试进度:  {}/{}".format(i, test_times))
        os.system("python generate.py")
    
        run_mars()
        load_hex_code()
        run_ise()
        file_cmp(i)
    
    print("\n通过率:{}/{}".format(passed,test_times))
    if(len(error) != 0):
        print("错误样例:", error)

思考题

  • Q:为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?

    A:如果乘除法部件在ALU中,那么在乘除运算延迟的过程中,其他指令因为无法使用ALU而无法继续执行。而单独的乘除法部件可以保证在运算延迟的过程中,其他无关指令仍然可以正常访问ALU并执行,提高执行速度。hilo是与乘法运算器相关的两个寄存器,的用来存放结果的地方。它们并不是 通用寄存器,除了用在乘除法之外,也不能有做其他用途。由高内聚、低耦合的原则,我们必须将他们独立。

  • Q:参照你对延迟槽的理解,试解释 “乘除槽”。

    A:乘除槽是专门进行乘除运算的结构,与其他模块独立。乘除槽的设置使得乘除运算独立出来,在单独的模块中进行运算,在运算延迟中不会对其他无关指令的执行产生任何影响。

  • Q:举例说明并分析何时按字节访问内存相对于按字访问内存性能上更有优势。(Hint: 考虑 C 语言中字符串的情况)

    A:当我们只需要对字节半字访问时,按字节访问内存性能更由优势。如果此时还采用按字访问,则需要首先将整个字从内存中拿出来,然后再从字中寻找,效率会更低。

  • Q:在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?

    如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。

    A:我是采用半随机生成半手动构造的方法。对于功能型指令,完全通过python脚本自动生成。并且为了提高数据冲突的概率,我们仅仅使用0~7号寄存器进行测试。对于跳转指令,我们先使用一定模板进行构建,然后为了增加数据冒险和控制冒险,我们又手动进行一定修改,使得测试样例尽可能更多的覆盖所有可能的情况

  • Q:为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?

    A:我们根据不同指令之间的相似性将指令分成了几类——calc_R、calc_I、shift、shiftv、load、store、B类、J类、md类、mf类、mt类, 并设置对应信号帮助译码,防止计算表达式过长,而且在处理数据冲突时我们只需要将表示该类的信号写入表达式即可。此外,我们将相似功能的控制信号用一个多位宽信号来表示,如针对DM的访存功能,我们设置一个3位LSOp信号;针对乘除槽中的md、mf、mt功能,我们设置一个MDUOp信号来控制,从而减少了流水寄存器的接口数目,从而降低复杂度。


文章作者: Hyggge
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hyggge !
  目录