计算机组成 P4 设计文档 (Verilog 单周期 CPU)


设计文档综述

数据通路设计

数据通路设计和参考设计几乎一致,如下图所示:

image-20221107003221627

模块文件结构

上面数据通路涉及到的模块,PCRF等等,在下面都有对应的文件实现。然后额外实现了 Controller(就是一个套壳),以及配套的 MUX,当然还有用于全局的 macro

文件总览

模块结构

模块叙述

PC.v

模块声明:

module PC (
    input clk,
    input reset,
    input [31:0] npc,
    output reg [31:0] pc
);

很简单的模块,只是一个用于存储 pc 的寄存器。

时钟上升沿到来时,如果 reset 在高电平,pc0x3000,否则 pc 置为 npc

PCjump.v

模块声明:

module PCjump (
    input [31:0] pc,
    input [31:0] jpc,
    input enable,
    output [31:0] pc4,
    output reg [31:0] npc
);

很简单的模块,只是一个专用于 pcmux 部件。

其中 pc4 始终等于 pc + 4enalbe 为高电平时 npc 的值为 jpc(跳转值),否则为 pc4

IM.v

模块声明:

module IM (
    input  [31:0] programCounter,
    output [31:0] instruction
);

很简单的模块,包含一个储存指令的 ROM

首先在 initial 中使用 $readmemh 指令读取指令机器码。然后 assign instruction = ROM[programCounter[13:2]-12'hc00];。其中 0x0c00 就是 0x3000 >> 2

DM.v

模块声明:

module DM (
    input clk,
    input reset,
    input memWrite,
    input [31:0] addr,
    input [31:0] dataWrite,
    input [31:0] programCounter,
    output [31:0] dataRead
);

基础模块。接口意义同 P3pc 接口只是用于课程要求的输出。

主要逻辑是,时钟上升沿时,若 reset 为高电平,用 for 循环置零所有数据;否则,检查写使能,考虑是否写入;并且始终读取数据(并非在写数据时读取浮空值)。

RF.v

模块声明:

module RF (
    input clk,
    input reset,
    input writeEnable,
    input [4:0] writeAddr,
    input [4:0] readAddr1,
    input [4:0] readAddr2,
    input [31:0] writeData,
    output [31:0] readData1,
    output [31:0] readData2,
    input [31:0] programCounter
);

基础模块。接口意义同 P3pc 接口只是用于课程要求的输出。

主要逻辑是,时钟上升沿时,若 reset 为高电平,用 for 循环置零所有数据;否则,检查写使能,考虑是否写入;并且始终读取数据。

ALU.v

模块定义:

module ALU (
    input      [31:0] rs,     // %rs, may be $zero
    input      [31:0] rt,     // %rt, may be extend(imm)
    input      [ 4:0] shamt,  // shamt
    input      [ 3:0] op,     // ALUop
    output reg [31:0] rd      // %rd, may be %rt if rt is imm
);

基础模块。相较于 P3 做了一些改动。新增 shamt 接口直接实现移位指令。

对于普通指令 rd = rs op rt,对于移位指令 rd = rt op shamt,对于单目指令 rd op rt

op 的意义在 macro.v 宏文件中定义。

Controller

模块定义:

module Controller (
    input [31:0] pc,
    input [31:0] pc4,
    input [31:0] instr,
    output reg [31:0] npc,
    output reg pcJumpEnable,
    output [31:0] memAddr,
    output reg memWE,
    input [31:0] memRead,
    output [31:0] memWrite,
    output reg grfWE,
    output [4:0] grfRA1,
    output [4:0] grfRA2,
    output [4:0] grfWA,
    input [31:0] grfRD1,
    input [31:0] grfRD2,
    output [31:0] grfWD,
    output [31:0] aluRs,
    output [31:0] aluRt,
    input [31:0] aluRd,
    output [4:0] shamt,
    output reg [3:0] aluOp
);

几乎就是数据通路了,因此顶层模块实际上只是套壳。

模块内部代码如下:

wire [5:0] opcode, funct;
assign opcode = instr[31:26];

// one-hot code: RIJ
wire [2:0] instrType;
assign instrType = {
  // R format
  opcode == `MIPS_SPEC,
  // I format
  opcode == `MIPS_ORI ||
  opcode == `MIPS_LW  ||
  opcode == `MIPS_SW  ||
  opcode == `MIPS_LUI ||
  opcode == `MIPS_BEQ ,
  // J format
  opcode ==
  `MIPS_JAL
};

// R format {opcode, rs, rt, rd, shamt, funct} = instr;
wire [4:0] rs, rt, rd;
assign rs = instr[25:21];
assign rt = instr[20:16];
assign rd = instr[15:11];
assign shamt = instr[10:6];
assign funct = instr[5:0];

// I format {opcode, rs, rt, imm} = instr;
wire [15:0] imm;
assign imm = instr[15:0];

// J format {opcode, jump} = instr;
wire [25:0] jump;
assign jump = instr[25:0];

// ALU controll
reg alu_rt_sel;
assign aluRs = grfRD1;
MUX_alu_rt mux_alu_rt (
    .rt(grfRD2),
    .imm(imm),
    .sel(alu_rt_sel),
    .aluRt(aluRt)
);

// GRF controll
reg [1:0] grf_wa_sel, grf_wd_sel;
assign grfRA1 = rs;
assign grfRA2 = rt;
MUX_grf_wa mux_grf_wa (
    .rd(rd),
    .rt(rt),
    .sel(grf_wa_sel),
    .grfWA(grfWA)
);
MUX_grf_wd mux_grf_wd (
    .alu  (aluRd),
    .mem  (memRead),
    .pc4  (pc4),
    .sel  (grf_wd_sel),
    .grfWD(grfWD)
);

// DM controll
assign memAddr  = grfRD1 + {{16{imm[15]}}, imm};
assign memWrite = grfRD2;

// PC controll


always @(*) begin
  case (instrType)
    `MIPS_RFORMAT: begin
      alu_rt_sel = 0;
      grf_wa_sel = 0;
      grf_wd_sel = 0;
      memWE = 0;
      npc = grfRD1;
      case (funct)
        `FUNCT_ADD: begin
          aluOp = `ALU_ADD;
          grfWE = 1;
          pcJumpEnable = 0;
        end
        `FUNCT_SUB: begin
          aluOp = `ALU_SUB;
          grfWE = 1;
          pcJumpEnable = 0;
        end
        `FUNCT_SLL: begin
          aluOp = `ALU_SLL;
          grfWE = 1;
          pcJumpEnable = 0;
        end
        `FUNCT_JR: begin
          aluOp = `ALU_NOP;
          grfWE = 0;
          pcJumpEnable = 1;
        end
        default: begin
          aluOp = `ALU_NOP;
          grfWE = 0;
          pcJumpEnable = 0;
        end
      endcase
    end
    `MIPS_IFORMAT: begin
      alu_rt_sel = 1;
      grf_wa_sel = 1;
      npc = pc4 + {{14{imm[15]}}, imm, 2'b00};
      case (opcode)
        `MIPS_ORI: begin
          aluOp = `ALU_OR;
          grfWE = 1;
          grf_wd_sel = 0;
          memWE = 0;
          pcJumpEnable = 0;
        end
        `MIPS_BEQ: begin
          aluOp = `ALU_NOP;
          grfWE = 0;
          grf_wd_sel = 0;
          memWE = 0;
          pcJumpEnable = grfRD1 == grfRD2;
        end
        `MIPS_LW: begin
          aluOp = `ALU_NOP;
          grfWE = 1;
          grf_wd_sel = 1;
          memWE = 0;
          pcJumpEnable = 0;
        end
        `MIPS_SW: begin
          aluOp = `ALU_NOP;
          grfWE = 0;
          grf_wd_sel = 0;
          memWE = 1;
          pcJumpEnable = 0;
        end
        `MIPS_LUI: begin
          aluOp = `ALU_LUI;
          grfWE = 1;
          grf_wd_sel = 0;
          memWE = 0;
          pcJumpEnable = 0;
        end
        default: begin
          aluOp = `ALU_NOP;
          grfWE = 0;
          grf_wd_sel = 0;
          memWE = 0;
          pcJumpEnable = 0;
        end
      endcase
    end
    `MIPS_JFORMAT: begin
      alu_rt_sel = 1;
      aluOp = `ALU_NOP;
      grf_wa_sel = 2;
      grf_wd_sel = 2;
      memWE = 0;
      npc = {pc[31:28], jump, 2'b00};
      case (opcode)
        `MIPS_JAL: begin
          grfWE = 1;
          pcJumpEnable = 1;
        end
        default: begin
          grfWE = 0;
          pcJumpEnable = 0;
        end
      endcase
    end
    default:  /* default */;
  endcase
end

mips.v

顶层模块代码:

module mips (
    input clk,
    input reset
);

  wire [31:0] pc, npc, pc4, instr, jpc;
  wire jumpEnable;
  PC ppc (
      .clk(clk),
      .reset(reset),
      .pc(pc),
      .npc(npc)
  );
  PCjump pcj (
      .pc(pc),
      .npc(npc),
      .jpc(jpc),
      .enable(jumpEnable),
      .pc4(pc4)
  );
  IM im (
      .programCounter(pc),
      .instruction(instr)
  );


  wire [31:0] regRD1, regRD2, regWD, memRD, memWD;
  wire [4:0] regRA1, regRA2, regWA;
  wire [31:0] memAddr;
  wire regWE, memWE;
  DM dm (
      .clk(clk),
      .reset(reset),
      .programCounter(pc),
      .memWrite(memWE),
      .addr(memAddr),
      .dataWrite(memWD),
      .dataRead(memRD)
  );
  RF rf (
      .clk(clk),
      .reset(reset),
      .programCounter(pc),
      .writeEnable(regWE),
      .writeAddr(regWA),
      .readAddr1(regRA1),
      .readAddr2(regRA2),
      .writeData(regWD),
      .readData1(regRD1),
      .readData2(regRD2)
  );


  wire [31:0] aluA, aluB, aluC;
  wire [4:0] shamt;
  wire [3:0] aluOp;
  ALU alu (
      .rs(aluA),
      .rt(aluB),
      .rd(aluC),
      .shamt(shamt),
      .op(aluOp)
  );


  Controller controller (
      .pc(pc),
      .pc4(pc4),
      .instr(instr),
      .npc(jpc),
      .pcJumpEnable(jumpEnable),
      .memAddr(memAddr),
      .memWrite(memWD),
      .memRead(memRD),
      .memWE(memWE),
      .grfRA1(regRA1),
      .grfRA2(regRA2),
      .grfWA(regWA),
      .grfRD1(regRD1),
      .grfRD2(regRD2),
      .grfWD(regWD),
      .grfWE(regWE),
      .aluRs(aluA),
      .aluRt(aluB),
      .aluRd(aluC),
      .shamt(shamt),
      .aluOp(aluOp)
  );

endmodule

顶层模块只是做了一个连线工作,就结束了。只要把名字相同的连在一起就算完工。但是如果不小心连错了的话……

自动化测试

mips_tb.v

`timescale 1ns / 1ps

module mips_tb;

  // Inputs
  reg clk;
  reg reset;

  initial begin
    $dumpfile("mips.vcd");
    $dumpvars();
  end

  // Instantiate the Unit Under Test (UUT)
  mips uut (
      .clk  (clk),
      .reset(reset)
  );

  initial begin
    // Initialize Inputs
    clk   = 0;
    reset = 1;

    // Wait 100 ns for global reset to finish
    #100;

    // Add stimulus here
    reset = 0;

    #100000;
    $finish;
  end

  always #5 clk = ~clk;

endmodule

非常正常的 testbench。因为自动化测试使用的 iverilog,所以查看波形也用的 .vcd

randomMIPS.py

自动化测试数据生成脚本。强度可以说是很高了。详见中间那个字符串。

#!/usr/bin/python

import random
import os, sys, difflib

INSTRUCTIONSET = {'add', 'sub', 'ori', 'lw',
                  'sw', 'beq', 'lui', 'jal', 'jr', 'nop'}


def calculationInstr(instrLength: int, readableReg: list[str], writableReg: list[str]) -> list[str]:
    instr: list[str] = []
    myInstr = ['adduo', 'subuo', 'ori', 'lui']
    for _ in range(instrLength):
        randInstr = random.choice(myInstr)
        rs = random.choice(readableReg)
        rt = random.choice(readableReg)
        rd = random.choice(writableReg)
        imm = str(random.randint(0, 0xffff))
        if randInstr == 'ori':
            instr.append(' '.join([randInstr, rd, rt, imm]))
        elif randInstr == 'lui':
            instr.append(' '.join([randInstr, rd, imm]))
        else:
            instr.append(' '.join([randInstr, rd, rs, rt]))
    assert len(instr) == instrLength
    return instr


def memoryInstr(instrLength: int, writeRegs: list[str], tempRegs: list[str]) -> list[str]:
    instr: list[str] = []
    myInstr = ['lw', 'sw']
    for _ in range(instrLength):
        randInstr = random.choice(myInstr)
        rd = random.choice(writeRegs)
        base = random.choice(tempRegs)
        imm1 = random.randint(-0xfff, 0xfff)
        imm2 = random.randint(-imm1, 0x2fff - imm1)
        imm2 -= (imm1 + imm2) % 4
        if imm1 >= 0:
            in1 = 'ori ' + base + ' $zero ' + str(imm1)
        else:
            in1 = 'lui ' + base + ' 0xffff\nori ' + base + ' ' + base + ' ' + str(imm1 + 0x10000)
        in2 = ' '.join([randInstr, rd, str(imm2) + '(' + base + ')'])
        instr.append('\n'.join([in1, in2]))
    assert len(instr) == instrLength
    return instr


'''
100 calculationInstr
jal A
200 calculationInstr
100 memoryInstr
ori $a0 $0 0
beq $a0 $0 B
A:
300 calculationInstr
300 memoryInstr
beq $s0 $s1 B
jr $ra
B:
300 calculationInstr
100 memoryInstr
jal C
beq $a0 $a1 END
jal END
C:
300 calculationInstr
200 memoryInstr
jr $ra
END:
300 calculationInstr
200 memoryInstr
'''

REGISTERS = ['$' + str(i) for i in range(2, 26)] + ['$zero']
OTHERS = ['$at', '$k0', '$k1']


def singleTest():
    instrList = calculationInstr(100, REGISTERS, REGISTERS) + ['jal A']
    temp = calculationInstr(200, REGISTERS, REGISTERS) + \
        memoryInstr(100, REGISTERS, OTHERS)
    random.shuffle(temp)
    instrList += temp
    instrList += ['ori $a0 $0 0', 'beq $a0 $0 B', 'A:']
    temp = calculationInstr(300, REGISTERS, REGISTERS) + \
        memoryInstr(300, REGISTERS, OTHERS)
    random.shuffle(temp)
    instrList += temp
    instrList += ['beq $s0 $s1 B', 'jr $ra', 'B:']
    temp = calculationInstr(300, REGISTERS, REGISTERS) + \
        memoryInstr(100, REGISTERS, OTHERS)
    random.shuffle(temp)
    instrList += temp
    instrList += ['jal C', 'beq $a0 $a1 END', 'jal END', 'C:']
    temp = calculationInstr(300, REGISTERS, REGISTERS) + \
        memoryInstr(200, REGISTERS, OTHERS)
    random.shuffle(temp)
    instrList += temp
    instrList += ['jr $ra', 'END:']
    temp = calculationInstr(300, REGISTERS, REGISTERS) + \
        memoryInstr(200, REGISTERS, OTHERS)
    random.shuffle(temp)
    instrList += temp

    with open('code.asm', 'w') as f:
        f.write('\n'.join(instrList))

    os.system(
        'java -jar mars.jar code.asm np nc mc CompactLargeText cl adduo.class cl subuo.class coL1 > code.ans')
    # os.system(
    #     'java -jar mars.jar code.asm np mc CompactLargeText cl adduo.class cl subuo.class coL2 > code.log')

    with open('code.asm', 'a') as f:
        # call a never end loop to makesure my verilog CPU output correct
        f.write('\nlabel_self: beq $0 $0 label_self\n')
    os.system(
        'java -jar mars.jar code.asm np nc mc CompactLargeText a dump .text HexText code.txt cl adduo.class cl subuo.class')
    os.system('vvp mips > code.out')


def specialJudge():
    ans = open('code.ans').readlines()
    out = open('code.out').readlines()
    out = out[2:-1]
    ans = ans[:-1]
    if ans == out:
        return True
    else:
        print('Check failed!')
        print(''.join(difflib.context_diff(out, ans)), end="")
        return False


if __name__ == '__main__':
    for Ti in range(30):
        singleTest()
        flag = specialJudge()
        if flag == False:
            print()
            print(f'Failed in #{Ti}')
            sys.exit(1)
        print(f'Check #{Ti}: Good!')
    print('Check over!')
    print('No difference found!')

Mars.jar

这个就不展开讲了。发在讨论区和 github了。

只不过目前 $gp,$sp 还没有改,会每次赋得初值(而非 0),所以上面那个 python 脚本也没有生成这两个指令相关代码。

思考题

  1. 阅读下面给出的 DM 的输入示例中(示例 DM 容量为 4KB,即 32bit × 1024字),根据你的理解回答,这个 addr 信号又是从哪里来的?地址信号 addr 位数为什么是 [11:2] 而不是 [9:0] ?

    题目

因为我们的内存是以字为单位寻址的,所以最低两位是无意义的,可以直接舍去。
addr 端口来源是 GRF[base] + imm,而且由于 MIPS 只用这种访问内存方式,所以只可能是这个值。

  1. 思考上述两种控制器设计的译码方式,给出代码示例,并尝试对比各方式的优劣。

    指令对应的控制信号如何取值:从指令出发,是正向思维,列表简单高效,查指令集即可获取表格

    控制信号每种取值所对应的指令:调试是可以看到波形图获取执行的指令是什么,方便快捷。麻烦是获取表格是逆向思维,比较繁琐。

  2. 在相应的部件中,复位信号的设计都是同步复位,这与 P3 中的设计要求不同。请对比同步复位异步复位这两种方式的 reset 信号与 clk 信号优先级的关系。

    同步复位:clk 信号具有高优先级。如果 reset 信号在 clk 的上升沿以外的时候为高电平,那么是起不到任何作用的。

    异步复位:reset 信号具有高优先级。无论 clk 信号处于什么阶段,只要 reset 信号来临,就必须进行重置操作。

  3. C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分。

    参考指令集如下:
    指令集
    指令2

    显然除了检测溢出,addaddu 执行的操作时一模一样的,addiaddiu 执行的操作也是一致的,对立即数都是符号扩展。


评论
  目录