计组实验1_后四次

Uncategorized
8.5k words

前言
请勿直接COPY本教程中的代码!
本篇文章只限于提供思路与常见错误的分享与交流

实验目的

实验内容(总体)

设计实现单周期CPU。

  1. 设计实现能够执行addu指令的单周期CPU;
  2. 增加实现以下R型指令addusubuaddandorslt
  3. 增加实现以下I型指令addiaddiuandiorilui
  4. 增加实现以下和数据存储器相关的指令:lwsw
  5. 增加实现以下跳转指令:beqjjaljr

在单周期CPU的基础上开发实现流水线CPU。

  1. 设计实现不考虑冒险的五级流水线CPU;
  2. 处理各种数据冒险;
  3. 处理各种控制冒险。

第一次实验 单周期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更新一次;
复位为一个初值。
alt text

功能:
复位功能: 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 //下条指令地址
);

// Sequential logic for PC update
always @(posedge clock) begin
if (reset == 1'b0) begin // synchronous reset, active low
pc <= 32'h3000; // Reset PC to 0x0000_3000
end else begin
pc <= npc; // Otherwise, update PC with the next program counter value (npc)
end
end

endmodule

常见错误

  1. pc.v中,如果你的always是这样写的:always @(posedge clock or negedge reset)always@(posedge clock,negedge reset ),那么请去掉negedge reset,只留always @(posedge clock)

  2. 为什么这道题使用always @(posedge clock)而不是posedge clock or negedge reset
    解答
    1.这个问题很好,实际上,在某些情况下,确实需要同时考虑时钟边沿和异步复位信号的边沿来确保复位操作的即时性和准确性。但根据题目要求,“采用同步复位”,这意味着复位信号应该与时钟信号同步处理,因此只在时钟的上升沿检查复位信号是合理的。
    2.使用always @(posedge clock)确保了所有的状态变化都发生在时钟的上升沿,这样可以减少亚稳态的可能性,并便于时序分析和避免竞争冒险现象。对于同步复位,当复位信号在时钟上升沿保持低电平时,复位动作才会被执行,这是同步电路设计中常见的做法。
    3.而使用posedge clock or negedge reset会监听时钟上升沿和复位信号的下降沿,这通常用于异步复位情况,即无论时钟状态如何,只要复位信号从高变低,就会立即响应复位。由于题目明确指出了“同步复位”,所以仅使用always @(posedge clock)是正确的选择。

2.im模块

题目2

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位作为地址。

解析
存储指令(写端口)
为了简化设计,指令存储器不设计写端口,指令通过其它方式提前存入。
取指令 → 一个读端口
alt text

1
2
3
4
5
6
7
8
9
module im (
output reg [31:0] instruction, //取出的指令
input [31:0] pc //指令的地址
);
// 指令存储器,大小为4kB,即1023条32位宽的指令
reg [31:0] ins_memory[1023:0];
// 将指令与从指令存储器中读取的指令值绑定
assign instruction = ins_memory[pc[11:2]];//见常见问题
endmodule

在这个模块中,我们使用了一个32位宽的寄存器数组ins_memory来模拟指令存储器。assign语句是一个连续赋值语句,它将输出instruction与指令存储器中对应于pc低12位的地址的值绑定。这样,每当pc的值改变时,instruction就会输出相应的指令。

结果
(见上)
常见错误

  1. 文件通过编译,提交至平台后结果错误,具体表现为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

分析
alt text
alt text

不是每种指令都写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
);//你也可以在外部定义输入输出端口

// 寄存器组,共32个寄存器,每个寄存器32位宽
reg [31:0] gp_registers[31:0];


// 初始化第一个寄存器为0
initial begin
gp_registers[0] = 32'h00000000;
//32'h00000000表示32位宽的0,也可以写成32'b0
// 初始化其他寄存器为随机值或其他初始值

end

//始终确保 gp_registers[0] 为 0
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 @(posedge clock) begin
if (reg_write) begin
a <= data_write;
b <= data_write;
end else begin
a <= gp_registers[rs];
b <= gp_registers[rt];
end
if (!reg_write) begin
a <= gp_registers[rs];
b <= gp_registers[rt];
end
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

/* //写入操作二
always @(posedge clock) begin//posedge表示上升沿,*表示任何输入信号变化都会触发.gp_registers[0] 永远等于0
if (reg_write && num_write != 0) begin
gp_registers[num_write] <= data_write;
end else begin
gp_registers[num_write] <= gp_registers[num_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;
说明:目前只是实现 + 功能。其他功能和输入输出信号根据需要慢慢添加。

分析
加法器设计
alt text
结果

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
/*
//加法操作二
always@(*)
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
/*利用实现的各个基本模块,实现一个能执行addu指令的 单周期CPU。顶层模块定义如下:   

module s_cycle_cpu(clock,reset);

//输入

input clock;

input reset;



说明:各模块的实例化命名必须按照如下规则:如pc模块实例命名为:PC。*/
`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;//寄存器a

wire [31:0] b_cpu;//寄存器b

wire [31:0] c_cpu;//运算结果

//内部信号

wire [31:0] npc;//下一条指令的地址

wire [4:0] a;//寄存器a的编号

wire [4:0] b;//寄存器b的编号

wire [4:0] c;//寄存器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];//取出指令的rs字段

assign b = instruction_cpu[20:16];//取出指令的rt字段

assign c = instruction_cpu[15:11];//取出指令的rd字段

//通用寄存器

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;//对于addu指令,寄存器写使能信号总是1

assign npc = pc_cpu + 4;//每个时钟周期PC+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模块的接口没有要求。

分析
单周期CPU-R型指令
结果

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
/*利用实验一实现的各个模块,设计一个能实现下列R型指令的单周期CPU:  addu,subu,add,and,or,slt。*/
`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;//寄存器a
wire [31:0] b_cpu;//寄存器b
wire [31:0] c_cpu;//运算结果

//内部信号
wire [31:0] npc;//下一条指令的地址
wire [4:0] a;//寄存器a的编号
wire [4:0] b;//寄存器b的编号
wire [4:0] c;//寄存器c的编号
wire reg_write;//写寄存器的使能信号
wire [5:0] funct;//功能码
wire [5:0] alu_if;//ALU控制信号

//实例化各个模块
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];//取出指令的rs字段
assign b = instruction_cpu[20:16];//取出指令的rt字段
assign c = instruction_cpu[15:11];//取出指令的rd字段
assign funct = instruction_cpu[5:0];//取出指令的功能码

//ALU控制信号生成
assign alu_if = (funct == 6'b100001) ? 6'b100001 ://ADDU
(funct == 6'b100011) ? 6'b100011 ://SUBU
(funct == 6'b100000) ? 6'b100000 ://ADD
(funct == 6'b100100) ? 6'b100100 ://AND
(funct == 6'b100101) ? 6'b100101 ://OR
(funct == 6'b101010) ? 6'b101010 ://SLT
6'b000000;//默认为0

//通用寄存器
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;//对于addu指令,寄存器写使能信号总是1
assign npc = pc_cpu + 4;//每个时钟周期PC+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
);
/*设计一个能实现下列R型指令的单周期CPU: addu,subu,add,and,or,slt。*/
// 加法操作
always @(*) // 记得修改这里为*
begin
case (alu_if)
6'b100001: c = a + b; // ADDU
6'b100011: c = a - b; // SUBU
6'b100000: c = a + b; // ADD
6'b100100: c = a & b; // AND
6'b100101: c = a | b; // OR
6'b101010: c = ($signed(a) < $signed(b)) ? 32'b1 : 32'b0; // SLT
default: c = 32'b0;//默认为0
endcase

end

endmodule

常见错误

  1. 在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; // SLT

或直接定义a,b为有符号数

1
2
input signed [31:0] a,// ALU输入A
input signed [31:0] b,// ALU输入B

这样,就可以正确进行有符号数的操作了。

Comments