目次
一. 设计草稿
进入p4,我们开始使用Verilog语言搭建单周期的cpu,这一部分相对来说不是很难,很多部分只需要把Logisim中的模块移植过去即可 因此,我们仍然从模块的设计思路入手,给出设计草稿
-
IM
功能: 在时钟的上升沿根据PC所指向的地址取出一条指令,并改变PC的值
说明: 使用了一个ROM储存要执行的指令,ROM规格为4096 * 32bit 因为PC是4的倍数,故使用2-13位作地址线即可
端口名称 端口格式 说明 clk input 时钟 reset input 异步复位 npc input[31:0] 由NPC模块计算出的下一次的pc Instr output[31:0] 取出的指令 PC output[31:0] PC的值 代码如下:
module IM ( input clk, input reset, input [31:0] npc, output [31:0] instr, output reg [31:0] pc ); reg [31:0] ROM [0:4095]; wire [11:0] address; wire [31:0] pc_count; initial begin $readmemh("code.txt",ROM); pc = 32'h0000_3000; end assign pc_count = pc - 32'h0000_3000; assign address = pc_count[13:2]; assign instr = ROM[address]; always @(posedge clk) begin if (reset) begin pc <= 32'h0000_3000; end else begin pc <= npc; end end endmodule
-
NPC
功能: 根据指令内容计算下一次的pc
说明: 支持
beq,jal,jr三种情况时的跳转 无特殊情况时默认$npc=pc+4$端口名称 端口格式 说明 pc input[31:0] pc的值 jump input 是否是jump branch input 是否是branch jal input 是否是jal imm1 input[15:0] 16位立即数 imm2 input[25:0] 26位立即数 rd1 input[31:0] GRF[rs],用来读取 $ra的值npc output[31:0] 由NPC模块计算出的下一次的pc 代码如下:
module NPC ( input [31:0] pc, input jump, input branch, input jr, input [15:0] imm1, input [25:0] imm2, input [31:0] rd1, output [31:0] npc ); wire [31:0] mid; wire [31:0] branch_target; wire [31:0] jump_target; wire [31:0] pc_add4; assign pc_add4 = pc + 32'h0000_0004; assign mid = {{14{imm1[15]}}, imm1, 2'b0}; assign branch_target = $signed(pc_add4) + $signed(mid); assign jump_target = {pc_add4[31:28], imm2, 2'b0}; assign npc = branch? branch_target : jump? jump_target : jr? rd1 : pc_add4; endmodule
-
Control
功能: 根据指令的内容确定要进行的操作
说明: 在实际进行操作中我没有沿用P3中的与或逻辑进行设计 而是使用了两个
case语句这样粗暴的方法进行实现 虽然功能可以保持一样,但是看起来并不清晰美观 在后期的进一步涉及中,我想使用宏对各类指令进行定义 再使用或的逻辑,这样代码应该可以更清晰一些对要实现的
add,sub,ori,sw,lw,beq,lui,nop,jal,jr等指令的分析如下指令名称 数据写入 写入的地址 写入数据来自 ALU操作 ALU第二操作数 add 寄存器 Instr[15:11] ALU结果 加法 从GRF读取的RD2 sub 寄存器 Instr[15:11] ALU结果 减法 从GRF读取的RD2 ori 寄存器 Instr[16:20] ALU结果 或运算 扩展后的imm sw 内存 ALUresult 从GRF读取的RD2 加法 扩展后的imm lw 寄存器 Instr[16:20] Memory 加法 扩展后的imm beq —– —– —– 比较 从GRF读取的RD2 lui 寄存器 Instr[16:20] ALU结果 输出B 扩展后的imm jal 寄存器 $ra($31)PC+4 —– —– jr IM —– GRF[rs] —– —– 由此我们可以设计出Control 所需要的端口
端口名称 端口格式 说明 OP input[5:0] Instr[31:26] Funct input[5:0] Instr[5:0] ALUop output[2:0] ALU的操作 RegDst output 选择写入寄存器的地址 RegWrite output 是否对GRF进行写入操作 MemWrite output 是否对Mem进行写入操作 MemRead output 写入的数据是否来自Mem beq output 是否是beq操作 jal output 是否是jal操作 jr output 是否是jr操作 ALUsrc output ALU第二操作数是否是imm EXTop output[1:0] EXT的操作 代码如下:
module CTRL ( input [5:0] op, input [5:0] funct, output reg RegDst, output reg RegWrite, output reg MemWrite, output reg MemRead, output reg ALUsrc, output reg beq, output reg [2:0] ALUop, output reg [1:0] EXTop, output reg jal, output reg jr ); always @(*) begin case (op) 6'b000000: begin case (funct) 6'b100000: begin // add RegDst <= 0; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 0; ALUop <= 3'b010; EXTop <= 2'b0; jal <= 0; jr <= 0; end 6'b100010: begin // sub RegDst <= 0; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 0; ALUop <= 3'b011; EXTop <= 2'b0; jal <= 0; jr <= 0; end 6'b001000: begin // jr RegDst <= 0; RegWrite <= 0; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 0; ALUop <= 3'b000; EXTop <= 2'b0; jal <= 0; jr <= 1; end default: begin // nop RegDst <= 0; RegWrite <= 0; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 0; ALUop <= 3'b0; EXTop <= 2'b0; jal <= 0; jr <= 0; end endcase end 6'b001101: begin // ori RegDst <= 1; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 1; beq <= 0; ALUop <= 3'b100; EXTop <= 2'b0; jal <= 0; jr <= 0; end 6'b100011: begin // lw RegDst <= 1; RegWrite <= 1; MemWrite <= 0; MemRead <= 1; ALUsrc <= 1; beq <= 0; ALUop <= 3'b010; EXTop <= 2'b01; jal <= 0; jr <= 0; end 6'b101011: begin // sw RegDst <= 0; RegWrite <= 0; MemWrite <= 1; MemRead <= 0; ALUsrc <= 1; beq <= 0; ALUop <= 3'b010; EXTop <= 2'b01; jal <= 0; jr <= 0; end 6'b000100: begin // beq RegDst <= 0; RegWrite <= 0; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 1; ALUop <= 3'b000; EXTop <= 2'b01; jal <= 0; jr <= 0; end 6'b001111: begin // lui RegDst <= 1; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 1; beq <= 0; ALUop <= 3'b001; EXTop <= 2'b10; jal <= 0; jr <= 0; end 6'b000011: begin // jal RegDst <= 0; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 1; beq <= 0; ALUop <= 3'b000; EXTop <= 2'b0; jal <= 1; jr <= 0; end default: begin // nop RegDst <= 0; RegWrite <= 0; MemWrite <= 0; MemRead <= 0; ALUsrc <= 0; beq <= 0; ALUop <= 3'b0; EXTop <= 2'b0; jal <= 0; jr <= 0; end endcase end endmodule
-
ALU
功能: 根据输入的操作类型对两个操作数进行运算,输出运算结果
说明: 目前支持的运算类型有
A,B,add,sub,or,cmp默认输出第一个运算数A 初版设计时我把比较器的一位输出扩展为32位与其它运算类型的输出使用了同一个多路选择器,而后觉得过于繁琐,在室友的建议下改为了多加了一个单独的一位输出端口名称 端口格式 说明 A input[31:0] 操作数A B input[31:0] 操作数B ALUop input[2:0] 操作类型 ALUresult output[31:0] 输出 equal output A与B是否相等 代码如下:
module ALU( input [31:0] A, input [31:0] B, input [2:0] ALUop, output reg equal, output reg [31:0] result ); always @(*)begin case(ALUop) 3'b000: result <= A; 3'b001: result <= B; 3'b010: result <= A + B; 3'b011: result <= A - B; 3'b100: result <= A | B; endcase if (A == B) begin equal <= 1'b1; end else begin equal <= 1'b0; end end endmodule
-
GRF
功能: 根据输入的地址对寄存器堆进行读写操作
说明: 注意
$0寄存器不可被写入代码如下:
module GRF ( input clk, input reset, input we, input [31:0] pc, input [4:0] read_addr1, input [4:0] read_addr2, input [4:0] write_addr, input [31:0] write_data, output [31:0] read_data1, output [31:0] read_data2 ); reg [31:0] registers[31:0]; integer i; assign read_data1 = registers[read_addr1]; assign read_data2 = registers[read_addr2]; always @(posedge clk) begin if (reset) begin for (i = 0; i < 32; i = i + 1) begin registers[i] <= 32'b0; end end else if (we && write_addr != 5'b0) begin registers[write_addr] <= write_data; $display("@%h: $%d <= %h", pc, write_addr, write_data); end end endmodule
-
DM
功能: 根据输入的地址对内存进行读写操作
说明: 大小为4096*32bit 在真实的内存地址中,最小单位是8bit,所以我们在计算得到地址时,应该使用MemAddr[13:2]作为地址线访问内存
代码如下:
module DM ( input clk, input reset, input we, input [31:0] pc, input [11:0] addr, input [31:0] WriteData, output [31:0] ReadData ); reg [31:0] memory [4095:0]; wire [31:0] address; integer i; assign ReadData = memory[addr]; assign address = {18'b0, addr, 2'b0}; always @(posedge clk) begin if (reset) begin for (i = 0; i < 4096; i = i + 1) begin memory[i] <= 32'b0; end end else if (we) begin memory[addr] <= WriteData; $display("@%h: *%h <= %h", pc, address, WriteData); end end endmodule
-
EXT
功能: 对立即数imm进行扩展
说明: 支持功能有0扩展,符号扩展,高位拼接
代码如下:
module EXT ( input [15:0] in, input [1:0] EXTop, output reg [31:0] out ); always @(*) begin case (EXTop) 2'b00: out <= {16'b0, in}; 2'b01: out <= {{16{in[15]}}, in}; 2'b10: out <= {in, 16'b0}; default: out <= {16'b0, in}; endcase end endmodule
经过以上的分析,我们对CPU的工作模式和通路连接就更清楚了,接下来在mips模块中进行连线 如图:
module mips (
input clk,
input reset
);
wire [31:0] pc; // pc的值
wire [31:0] npc; // 下次的pc
wire [31:0] Instr; // 指令内容
wire jump; // j指令标志
wire branch; // 跳转标志
wire [15:0] imm1; // 16位立即数
wire [25:0] imm2; // 26位立即数
wire [5:0] op; // op
wire [5:0] funct; // funct
wire [4:0] rs; // rs
wire [4:0] rt; // rt
wire [4:0] rd; // rd
wire [31:0] pc_add4; // pc+4
wire RegDst; // 选择写入的寄存器地址
wire RegWrite; // 写入信号
wire MemWrite; // 内存写入信号
wire MemRead; // 是否对内存有读取操作
wire ALUsrc; // 选择alu的第二个操作数是不是立即数
wire beq; // 是否是beq指令
wire [2:0] ALUop; // ALUop
wire [1:0] EXTop; // EXTop
wire jal; // jal
wire jr; // jr
wire [31:0] imm32; // 扩展后的立即数
wire [31:0] RD1; // ReadData1
wire [31:0] RD2; // ReadData2
wire [4:0] A1; // addr1
wire [4:0] A2; // addr2
wire [4:0] A3; // addr3
wire [31:0] WD; // WriteData
wire [31:0] A; // 第一个运算数
wire [31:0] B; // 第二个运算数
wire [31:0] ALUresult; // 运算结果
wire equal; // 是否相等
wire [11:0] addr;
wire [31:0] WriteData;
wire [31:0] ReadData;
assign imm1 = Instr[15:0];
assign imm2 = Instr[25:0];
assign op = Instr[31:26];
assign funct = Instr[5:0];
assign rs = Instr[25:21];
assign rt = Instr[20:16];
assign rd = Instr[15:11];
assign branch = beq & equal;
assign jump = jal;
assign pc_add4 = pc + 32'h0000_0004;
assign A1 = rs;
assign A2 = rt;
assign A3 = RegDst? rt :
jal? 5'b11111 : rd;
assign WD = MemRead? ReadData :
jal? pc_add4 : ALUresult;
assign A = RD1;
assign B = ALUsrc? imm32:RD2;
assign addr = ALUresult[13:2];
assign WriteData = RD2;
// IM
IM im(
.clk(clk),
.reset(reset),
.npc(npc),
.instr(Instr),
.pc(pc)
);
// NPC
NPC Npc(
.pc(pc),
.jump(jump),
.branch(branch),
.jr(jr),
.imm1(imm1),
.imm2(imm2),
.rd1(RD1),
.npc(npc)
);
// Control
CTRL ctrl(
.op(op),
.funct(funct),
.RegDst(RegDst),
.RegWrite(RegWrite),
.MemWrite(MemWrite),
.MemRead(MemRead),
.ALUsrc(ALUsrc),
.beq(beq),
.ALUop(ALUop),
.EXTop(EXTop),
.jal(jal),
.jr(jr)
);
// EXT
EXT ext(
.in(imm1),
.EXTop(EXTop),
.out(imm32)
);
// GRF
GRF grf(
.clk(clk),
.reset(reset),
.we(RegWrite),
.pc(pc),
.read_addr1(A1),
.read_addr2(A2),
.write_addr(A3),
.write_data(WD),
.read_data1(RD1),
.read_data2(RD2)
);
// ALU
ALU alu(
.A(A),
.B(B),
.ALUop(ALUop),
.equal(equal),
.result(ALUresult)
);
// DM
DM dm(
.clk(clk),
.reset(reset),
.we(MemWrite),
.pc(pc),
.addr(addr),
.WriteData(WriteData),
.ReadData(ReadData)
);
endmodule
二.测试方案
初步调试
针对每一条指令设计一个简单的测试样例,来观察指令本身的执行是否顺畅无误 如:
ori $t0, $0, 10000
sw $t0, 0($0)
这个简单的样例可以帮助测试两条指令的正确性,在测试完全部的单条指令后,我们再去构建规模更大的指令
综合调试
构建一个初具规模的样例,判断我们的CPU的连续执行指令的能力 以教程给的样例为例:
16进制码如下
341c0000
341d0000
34013456
00210820
8c010004
ac010004
3c027878
00411822
3c051234
34040005
00000000
ac85ffff
8c83ffff
10650003
00000000
10000011
00000000
34670404
10e3000e
00000000
3c087777
3508ffff
00080022
34001100
00e65020
34080000
34090001
340a0001
010a4020
1109fffe
0c000c22
00000000
014a5020
1000ffff
014a5020
03e00008
00000000
转译成对应的MIPS指令后
li $28, 0x00000000
li $29, 0x00000000
li $1, 0x3456
add $1, $1, $2
lw $1, 4($0)
sw $1, 4($0)
lui $2, 0x7878
sub $1, $1, $4
lui $5, 0x1234
li $4, 5
nop
sw $5, -1($8)
lw $3, -1($8)
beq $5, $0, 3
nop
j 0x00000011
nop
lui $7, 0x404
beq $3, $0, 14
nop
lui $8, 0x7777
ori $8, $8, 0xFFFF
sw $8, 0x1100($0)
add $6, $6, $0
lui $8, 0x0000
li $9, 1
li $10, 1
add $10, $10, $1
bne $9, $0, -2
jal 0x00000c22
nop
add $10, $10, $0
nop
我们可以在仿真中查看波形来判断自己的代码是否正确
三.思考题
-
阅读下面给出的 DM 的输入示例中(示例 DM 容量为 4KB,即 32bit × 1024字),根据你的理解回答,这个 addr 信号又是从哪里来的?地址信号 addr 位数为什么是 [11:2] 而不是 [9:0] ?
addr信号来自ALU的输出ALUresult,在真实的内存地址中,最小单位是8bit,所以我们在计算得到地址时,应该使用ALUresult[11:2]作为地址线访问内存
-
思考上述两种控制器设计的译码方式,给出代码示例,并尝试对比各方式的优劣。
-
指令对应的控制信号如何取值 代码如下:
6'b001101: begin // ori RegDst <= 1; RegWrite <= 1; MemWrite <= 0; MemRead <= 0; ALUsrc <= 1; beq <= 0; ALUop <= 3'b100; EXTop <= 2'b0; jal <= 0; jr <= 0; end优点:简单明了,易于实现。 缺点:对于复杂指令集,增加了逻辑复杂性,不易扩展。
-
控制信号每种取值所对应的指令 代码如下:
assign RegDst = add || sub || jar;优点:灵活性高,能够适应指令集的变化,易于扩展新指令。 缺点:需要复杂的控制逻辑。
-
-
在相应的部件中,复位信号的设计都是同步复位,这与 P3 中的设计要求不同。请对比同步复位与异步复位这两种方式的 reset 信号与 clk 信号优先级的关系。
- 同步复位:
clk信号优先于reset信号,必须当clk信号的上升沿到来时才能进行复位操作
- 异步复位:
reset信号优先于clk信号,可以随时进行复位操作
- 同步复位:
-
C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分。
addi:这是一个带符号的加法立即数指令。它对两个操作数(寄存器和立即数)进行相加,并将结果存储到目标寄存器。如果计算结果溢出,addi 会产生一个溢出异常。addiu:这是一个不带符号的加法立即数指令。它的操作与 addi 相同,但在发生溢出时,它不会产生异常。addiu 直接将结果放入目标寄存器,不会检查结果是否溢出。add:这是一个带符号的加法指令,用于两个寄存器的值相加。如果结果溢出,会产生一个溢出异常。addu:这是一个不带符号的加法指令,功能与 add 相同,但不会在结果溢出时产生异常。
因此,在忽略溢出的情况下,addi与addiu执行的操作是一样的,add与addu也是一样的
DISCUSSION
コメント