前言 :请勿直接COPY本教程中的代码! 本篇文章只限于提供思路与常见错误的分享与交流
实验目的 实验内容(总体) 设计实现单周期CPU。
设计实现能够执行addu指令 的单周期CPU;
增加实现以下R型指令 :addu
,subu
,add
,and
,or
,slt
;
增加实现以下I型指令 :addi
,addiu
,andi
,ori
,lui
;
增加实现以下和数据存储器相关的指令:lw
,sw
;
增加实现以下跳转指令:beq
,j
,jal
,jr
。
在单周期CPU的基础上开发实现流水线CPU。
设计实现不考虑冒险的五级流水线CPU;
处理各种数据冒险;
处理各种控制冒险。
第一次实验 单周期CPU
1.PC模块
1 2 3 4 5 6 7 module pc(pc,clock,reset,npc); output [31:0] pc; input clock; input reset; input [31:0] npc; 注意: 1.clock上升沿有效,reset低电平有效; 2.reset信号有效时,pc复位为0x0000 3000:采用同步复位。
解析 :PC寄存器设计 存储当前执行指令的地址; 对于单周期CPU,每个clock更新一次; 复位为一个初值。
功能: 复位功能: reset低电平时,同步复位pc值为0x3000。 写入功能: reset高电平时,在clock的上升沿把npc写入pc寄存器。结果 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module pc( output reg [31 :0 ] pc, input wire clock, input wire reset, input wire [31 :0 ] npc ); always @(posedge clock) begin if (reset == 1'b0 ) begin pc <= 32'h3000 ; end else begin pc <= npc; end end endmodule
常见错误 :
pc.v
中,如果你的always是这样写的:always @(posedge clock or negedge reset)
或always@(posedge clock,negedge reset )
,那么请去掉negedge reset
,只留always @(posedge clock)
。
为什么这道题使用always @(posedge clock)
而不是posedge clock or negedge reset
?解答 : 1.这个问题很好,实际上,在某些情况下,确实需要同时考虑时钟边沿和异步复位信号的边沿来确保复位操作的即时性和准确性。但根据题目要求,“采用同步复位”,这意味着复位信号应该与时钟信号同步处理,因此只在时钟的上升沿检查复位信号是合理的。 2.使用always @(posedge clock)
确保了所有的状态变化都发生在时钟的上升沿,这样可以减少亚稳态的可能性,并便于时序分析和避免竞争冒险现象。对于同步复位,当复位信号在时钟上升沿保持低电平时,复位动作才会被执行,这是同步电路设计中常见的做法。 3.而使用posedge clock or negedge reset
会监听时钟上升沿和复位信号的下降沿,这通常用于异步复位情况,即无论时钟状态如何,只要复位信号从高变低,就会立即响应复位。由于题目明确指出了“同步复位”,所以仅使用always @(posedge clock)
是正确的选择。
2.im模块
1 2 3 4 5 6 7 8 9 module im(instruction,pc); output [31:0] instruction; input [31:0] pc; reg [31:0] ins_memory[1023:0]; //4k指令存储器 说明:im模块的输入pc为32位,但指令存储器只有4kB大小,所以取指令时只取pc的低12位作为地址。
解析 存储指令(写端口) 为了简化设计,指令存储器不设计写端口,指令通过其它方式提前存入。 取指令 → 一个读端口
1 2 3 4 5 6 7 8 9 module im ( output reg [31 :0 ] instruction, input [31 :0 ] pc ); reg [31 :0 ] ins_memory[1023 :0 ]; assign instruction = ins_memory[pc[11 :2 ]]; endmodule
在这个模块中,我们使用了一个32位宽的寄存器数组ins_memory
来模拟指令存储器。assign
语句是一个连续赋值语句,它将输出instruction
与指令存储器中对应于pc
低12位的地址的值绑定。这样,每当pc
的值改变时,instruction
就会输出相应的指令。
结果 : (见上)常见错误 :
文件通过编译,提交至平台后结果错误,具体表现为PC输出的地址全红。 主要原因:检查你的instruction = ins_memory[pc[11:2]]
部分,多数的人可能写为instruction = ins_memory[pc[11:0]]
,为什么是11:2呢,不是低12位吗? 答:1024=2^10 ,虽然低12位用于寻址,但1024条指令只会占用最多10个位,所以1和0一直没有用到,对于一个4kB的存储器(即2^10个存储单元),我们只需要10位来寻址,但又不能违反题目用低12位的要求(不然直接用9:0),所以使用11:2或[11:0]>>2,右移两位, 这样就ok了。
3.gpr模块 题目
1 2 3 4 5 6 7 8 9 10 11 12 module gpr(a,b,clock,reg_write,num_write,rs,rt,data_write); output [31:0] a; output [31:0] b; input clock; input reg_write; input [4:0] rs; //读寄存器1 input [4:0] rt; //读寄存器2 input [4:0] num_write; //写寄存器 input [31:0] data_write; //写数据 reg [31:0] gp_registers[31:0]; //32个寄存器 提示: gp_registers[0] 永远等于0
分析
不是每种指令都写GPR ,写端口需要写使能信号控制。结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 module gpr( output reg [31 :0 ] a, output reg [31 :0 ] b, input clock, input reg_write, input [4 :0 ] rs, input [4 :0 ] rt, input [4 :0 ] num_write, input [31 :0 ] data_write ); reg [31 :0 ] gp_registers[31 :0 ]; initial begin gp_registers[0 ] = 32'h00000000 ; end always @(posedge clock) begin if (reg_write) begin if (num_write != 5'b00000 ) begin gp_registers[num_write] <= data_write; end end gp_registers[0 ] <= 32'h00000000 ; end always @(*) begin a = (rs == 5'b0 ) ? 32'h00000000 : gp_registers[rs]; b = (rt == 4'b0 ) ? 32'h00000000 : gp_registers[rt]; end always @(posedge clock) begin if (reg_write && num_write != 0 ) begin gp_registers[num_write] <= data_write; end end endmodule
常见问题 : (暂无)
4.alu模块 题目
1 2 3 4 5 module alu(c,a,b); output [31:0] c; input [31:0] a; input [31:0] b; 说明:目前只是实现 + 功能。其他功能和输入输出信号根据需要慢慢添加。
分析 加法器设计结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 module alu( output reg [31 :0 ] c, input [31 :0 ] a, input [31 :0 ] b ); always @(posedge clock) begin c <= a + b; end endmodule
常见错误 (暂无)
5.单周期CPU(ADDU) 题目
1 2 3 4 5 6 7 8 9 10 11 利用实现的各个基本模块,实现一个能执行addu指令的 单周期CPU。顶层模块定义如下: module s_cycle_cpu(clock,reset); //输入 input clock; input reset; 说明:各模块的实例化命名必须按照如下规则:如pc模块实例命名为:PC。
分析
结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 `include "pc.v" `include "im.v" `include "gpr.v" `include "alu.v" module s_cycle_cpu(clock,reset);input clock;input reset;wire [31 :0 ] pc_cpu;wire [31 :0 ] instruction_cpu;wire [31 :0 ] a_cpu;wire [31 :0 ] b_cpu;wire [31 :0 ] c_cpu;wire [31 :0 ] npc;wire [4 :0 ] a;wire [4 :0 ] b;wire [4 :0 ] c;wire reg_write;pc PC(.pc (pc_cpu),.clock (clock),.reset (reset),.npc (npc)); im IM(.instruction (instruction_cpu),.pc (pc_cpu)); assign a = instruction_cpu[25 :21 ];assign b = instruction_cpu[20 :16 ];assign c = instruction_cpu[15 :11 ];gpr GPR(.clock (clock),.reg_write (reg_write),.rs (a),.rt (b),.num_write (c),.data_write (c_cpu),.a (a_cpu),.b (b_cpu)); alu ALU(.c (c_cpu),.a (a_cpu),.b (b_cpu)); assign reg_write = 1 ;assign npc = pc_cpu + 4 ;endmodule
常见错误 绷不住了,我第一遍提交后data_write总是zzzzzzzz,百思不得其解。原以为是gpr模块写错了或没初始化, 结果发现,是我在s_cycle_cpu
中将data_write
(运算结果)定义为c_cpu
,但在实例化时使用的还是data_write
, (但如果只是这样,应该编译报错才对)。结果,我巧合地还多定义了一个无用的data_write
的空端口,导致编译也没有报错,结果输出 时候data_write总是zzzzzzzz(没写入/没初始化)。
6.实现R型指令的单周期CPU 题目
1 2 利用实验一实现的各个模块,设计一个能实现下列R型指令的单周期CPU: addu,subu,add,and,or,slt。 注:以后的题目中对ALU模块的接口没有要求。
分析 结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 `include "pc.v" `include "im.v" `include "gpr.v" `include "alu.v" module s_cycle_cpu(clock,reset);input clock;input reset;wire [31 :0 ] pc_cpu;wire [31 :0 ] instruction_cpu;wire [31 :0 ] a_cpu;wire [31 :0 ] b_cpu;wire [31 :0 ] c_cpu;wire [31 :0 ] npc;wire [4 :0 ] a;wire [4 :0 ] b;wire [4 :0 ] c;wire reg_write;wire [5 :0 ] funct;wire [5 :0 ] alu_if;pc PC(.pc (pc_cpu),.clock (clock),.reset (reset),.npc (npc)); im IM(.instruction (instruction_cpu),.pc (pc_cpu)); assign a = instruction_cpu[25 :21 ];assign b = instruction_cpu[20 :16 ];assign c = instruction_cpu[15 :11 ];assign funct = instruction_cpu[5 :0 ];assign alu_if = (funct == 6'b100001 ) ? 6'b100001 :(funct == 6'b100011 ) ? 6'b100011 : (funct == 6'b100000 ) ? 6'b100000 : (funct == 6'b100100 ) ? 6'b100100 : (funct == 6'b100101 ) ? 6'b100101 : (funct == 6'b101010 ) ? 6'b101010 : 6'b000000 ;gpr GPR(.clock (clock),.reg_write (reg_write),.rs (a),.rt (b),.num_write (c),.data_write (c_cpu),.a (a_cpu),.b (b_cpu)); alu ALU(.c (c_cpu),.a (a_cpu),.b (b_cpu),.alu_if (alu_if)); assign reg_write = 1 ;assign npc = pc_cpu + 4 ;endmodule
alu模块修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 module alu( output reg [31 :0 ] c, input [31 :0 ] a, input [31 :0 ] b, input [5 :0 ] alu_if ); always @(*) begin case (alu_if) 6'b100001 : c = a + b; 6'b100011 : c = a - b; 6'b100000 : c = a + b; 6'b100100 : c = a & b; 6'b100101 : c = a | b; 6'b101010 : c = ($signed (a) < $signed (b)) ? 32'b1 : 32'b0 ; default : c = 32'b0 ; endcase end endmodule
常见错误
在ALU中实现SLT操作时,千万注意考虑到a和b是否是有符号数,我们默认定义成了无符号数, 这种情况下会出现一个错误,那就是当a是负数而b是正数时,正常应该是a < b,输出1;但由于符号位的存在, 负数首位为1而正数为0,如果以无符号数来进行操作,会出现错误的结果a > b,输出0;例如: a = ffffffff, b = 0000001d # GPR.regwrite = 1, GPR.num_write = 1e, GPR.data_write = 00000000 错误的结果, 00000001 正确的结果
所以我们实现SLT操作时, 需要将a和b转换为有符号数。如下:
1 6'b101010 : c = ($signed (a) < $signed (b)) ? 32'b1 : 32'b0 ;
或直接定义a,b为有符号数
1 2 input signed [31 :0 ] a,input signed [31 :0 ] b,
这样,就可以正确进行有符号数的操作了。