数字设计实验:RISC-V指令单周期CPU
文章目录
- RISC-V指令单周期CPU设计
- 一、实验目的
- 二、实验内容
- 三、实验设计
- 1. CPU功能分解与设计
- 1.1 单周期运行逻辑
- 1.2 控制信号生成逻辑
- 2. CPU主要部件设计分块思路
- 3. 具体模块设计
- 3.1 寄存器堆设计
- 3.2 PC取指模块设计
- 3.3 指令存储器(PM)设计
- 3.4 数据存储器(DM)设计
- 3.5 立即数扩展单元设计
- 3.6 主控单元设计
- 3.7 ALU信号控制单元设计
- 3.8 算术逻辑单元(ALU)设计
- 3.9 CPU顶层实现单元设计
- 四、仿真过程及其结果分析
- 1. `addi X1, X0, 0x8`指令实现验证
- 2. `lw X2, 4(X1)`指令实现验证
- 3. `add X3, X1, X2`指令实现验证
- 4. `sub X4, X3, X1`指令实现验证
- 5. `or X5, X1, X4`指令实现验证
- 6. `ori X6, X5, 1`指令实现验证
- 7. `sw X6, 0(X2)`指令实现验证
- 8. `slt X7, X2, X4`指令实现验证
- 9. `slti X8, X2, 8`指令实现验证
- 10. ` beq X3, X5, - 12`指令实现验证
- 五、实验体会
RISC-V指令单周期CPU设计
一、实验目的
在这个项目中,需要完成一个能够实现指定RISC-V指令功能的简单单周期CPU的电路设计。并用verilog编程编写项目,通过vivado软件仿真验证所设计的CPU功能的正确性。
二、实验内容
对于这个32位单周期CPU电路,有如下具体的要求:
-
该CPU需要完成的指令有以下9个:add,sub,or,slt,addi,ori,slti,sw,lw。每条指令长度都为32bits。
-
该CPU的部件资源包括:1)X0-X31共32个通用寄存器;2)特殊寄存器PC(program counter)和指令暂存寄存器IR(instruction Register)。以上寄存器都是32bits字长。
-
存储器包括256bytes的memory(地址为0~255,采用little endian方式存储数据或者指令)。其中地址0-127存放程序指令(PM)(最多32条指令),地址128-255存放数据(DM)。也可以使用两块128byte的独立存储器(本实验采用两块独立存储器方式)。
-
最后的波形仿真应当采用功能仿真,且所有存储器件中的数据都应当被显示。
三、实验设计
1. CPU功能分解与设计
1.1 单周期运行逻辑
本项目中采取如下的流程阶段实现CPU的一个完整周期的功能。
-
取指:PC 作为指令存储器的地址,在时钟上升沿读取指令并存储到 IR 寄存器中。同时,PC 自动指向下一条指令的地址(PC = PC + 4),除非遇到跳转或分支指令改变 PC 的值。
-
译码:控制单元根据 IR 寄存器中的指令操作码和功能码生成各种控制信号,同时从寄存器堆中读取指令所需的操作数(对于寄存器操作数指令)。
-
执行:根据控制信号,ALU 对操作数进行相应的运算(如加法、减法、逻辑运算等),或者数据存储器进行数据的读写操作。运算结果或读取的数据会被暂存起来,准备写入目的寄存器或用于后续的操作。
-
访存:将执行结果读取或者写入到存储器。在RISC中,CPU需要通过寄存器对内存操作,而寄存器与内存间的通信则需要LSU来完成。load store unit为加载存储单元,管理所有load, store操作。
-
写回:如果指令需要将结果写回寄存器(由 RegWr 信号控制),则在时钟上升沿将执行阶段的结果写入目的寄存器(由 rd 字段指定)。对于 lw 指令,从数据存储器读取的数据会由 MemtoReg 信号控制写入目的寄存器。
1.2 控制信号生成逻辑
根据指令格式和操作码,使用组合逻辑电路生成控制信号。例如,对于 R 型指令(如 add、sub、slt 等),当 opcode 为 0110011 时,根据 funct3 和 funct7 的值进一步确定具体的操作,并生成相应的 ALUctr 信号。对于 I 型指令(如 addi、ori、slti、lw 等),当 opcode 为 0010011 时,根据 funct3 的值生成对应的控制信号,同时控制立即数扩展单元进行符号扩展或零扩展(根据指令要求)。对于 S 型指令(sw),当 opcode 为 0100011 时,生成控制信号控制数据存储器的写操作。对于 B 型指令(beq),当 opcode 为 1100011 时,根据比较结果和偏移量生成分支控制信号。
2. CPU主要部件设计分块思路
本项目中分为以下几个模块设计CPU的不同部件:寄存器堆、PC取指模块、指令存储器(PM)、数据存储器(DM)、立即数扩展单元、主控单元、ALU信号控制单元、算术逻辑单元(ALU)、CPU顶层实现单元,按其分块设计的CPU电路图如图1所示。
- 图1.软件生成的CPU结构电路图
3. 具体模块设计
3.1 寄存器堆设计
3.1.1 结构与功能说明
本模块内部引入32个32位通用寄存器register_file
,CPU在运行过程可以调用。该模块通过根据输入的rs1、rs2寄存器地址,读取其存储的数据,并在写使能端有效时将待写入的数据写入目的寄存器rd中。同时,若有从内存写回寄存器使能端有效,则将从内存中的数据写入目的寄存器rd中。
3.1.2 接口列表
除了为了仿真而用于查看的样例寄存器值输出,以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 输入的时钟信号 |
reset | input | 1 | 输入的复位信号 |
reg_write_enable | input | 1 | 寄存器的写使能信号 |
rd_addr | input | 5 | 要更改的寄存器序号,就是目的寄存器的序号 |
wr_data | input | 32 | 要更改寄存器的值 |
rs1_addr | input | 5 | 输入的寄存器rs1的序号 |
rs1_data | output | 32 | 从rs1中读出来的值 |
rs2_addr | input | 5 | 输入的寄存器rs2的序号 |
rs2_data | output | 32 | 从rs2中读出来的值 |
mem_to_reg | input | 1 | 从内存写回寄存器使能 |
mem_data | input | 32 | 从内存中读取的数据 |
3.1.3 代码实现
Register_file.v
`timescale 1ns / 1ps
module Register_file(
input wire clk,
input wire reset,
input wire [4:0] rs1_addr,
input wire [4:0] rs2_addr,
input wire [4:0] rd_addr,
input wire [31:0] wr_data,
input wire [31:0] mem_data,
input wire reg_write_enable,
input wire mem_to_reg,
output wire [31:0] rs1_data,
output wire [31:0] rs2_data,
//查看样例寄存器值
output wire [7:0] X0,
output wire [7:0] X1,
output wire [7:0] X2,
output wire [7:0] X3,
output wire [7:0] X4,
output wire [7:0] X5,
output wire [7:0] X6,
output wire [7:0] X7,
output wire [7:0] X8
);
reg [31:0] register_file[31:0];
// 组合逻辑读取寄存器数据
assign rs1_data = (rs1_addr == 5'b0)? 32'b0 : register_file[rs1_addr];
assign rs2_data = (rs2_addr == 5'b0)? 32'b0 : register_file[rs2_addr];
// 时序逻辑写寄存器数据
integer i;
always @(posedge clk or posedge reset) begin
if (reset) begin
// 复位时将所有寄存器清零
for (i = 0; i < 32; i = i + 1)
register_file[i] <= 32'b0;
end else if (reg_write_enable && rd_addr!= 5'b0) begin
if (mem_to_reg) begin // 根据mem_to_reg信号判断是从内存读数据写回还是其他情况(如ALU结果写回)
register_file[rd_addr] <= mem_data;
// $display("Writing data from memory %h to register %d", mem_data, rd_addr); // 添加显示语句查看写回情况
end else begin
register_file[rd_addr] <= wr_data;
// $display("Writing data from ALU %h to register %d", wr_data, rd_addr); // 添加显示语句查看写回情况
end
end
end
assign X0 = register_file[0][7:0];
assign X1 = register_file[1][7:0];
assign X2 = register_file[2][7:0];
assign X3 = register_file[3][7:0];
assign X4 = register_file[4][7:0];
assign X5 = register_file[5][7:0];
assign X6 = register_file[6][7:0];
assign X7 = register_file[7][7:0];
assign X8 = register_file[8][7:0];
endmodule
3.2 PC取指模块设计
3.2.1 结构与功能说明
本模块用于实现CPU的PC更新功能,正常指令下进行自增4的操作以进入下一个指令,如果当前指令为beq
指令,则根据指令中具体的立即数值来进行指令地址跳转的操作。
3.2.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 输入的时钟信号 |
reset | input | 1 | 输入的复位信号 |
branch_offset | input | 32 | 与beq指令相关的立即数扩展量 |
beq_taken | input | 1 | 上步指令是否为beq指令 |
PC | input | 32 | 下步指令地址 |
3.2.3 代码实现
PC_module.v
`timescale 1ns / 1ps
module PC_module(
input wire clk,
input wire reset,
input wire [31:0] branch_offset,
input wire beq_taken,
output reg [31:0] PC
);
always @(posedge clk or posedge reset) begin
if (reset)
PC <= 32'b0;
else if (beq_taken)
PC <= PC + branch_offset;
else
PC <= PC + 4;
end
endmodule
3.3 指令存储器(PM)设计
3.3.1 结构与功能说明
本模块中包含一个128字节大小的指令存储器(PM),CPU可根据输入的PC值(指令地址)从指令存储器中取出对应的指令值instruction
。
3.3.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
PC | input | 32 | 当前PC值(指令地址) |
instruction | output | 32 | 取出的指令值 |
3.3.3 代码实现
PM_module.v
`timescale 1ns / 1ps
module PM_module(
input wire [31:0] PC,
output wire [31:0] instruction
);
reg [7:0] PM[127:0];
// 根据PC读取指令
assign instruction = {PM[PC[6:0]+3], PM[PC[6:0]+2], PM[PC[6:0]+1], PM[PC[6:0]]};
endmodule
3.4 数据存储器(DM)设计
3.4.1 结构与功能说明
本模块包含一个128字节大小的数据存储器(DM),根据输入的读写操作起始内存地址以及读写使能端的有效情况,读出以该地址为小端的四字节内存中的数据值,或者将输入的数据写入以该地址为小端的四字节内存中。
3.4.2 接口列表
除了为了仿真而用于查看的样例存储器值输出,以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 输入的时钟信号 |
address | input | 32 | 读写操作的起始内存地址 |
write_data | input | 32 | 将要写入内存的数据值 |
mem_write_enable | input | 1 | 写使能端 |
mem_read_enable | input | 1 | 读使能端 |
read_data | output | 32 | 从内存中读出的数据值 |
3.4.3 代码实现
DM_module.v
`timescale 1ns / 1ps
module DM_module(
input wire clk,
input wire [31:0] address,
input wire [31:0] write_data,
input wire mem_write_enable,
input wire mem_read_enable,
output reg [31:0] read_data,
output wire [7:0] DM_4, //查看地址为4的内存值
output wire [7:0] DM_12 //查看地址为12的内存值
);
reg [7:0] DM[127:0];
// 读数据
always @(*) begin
if (mem_read_enable) begin
read_data[7:0] <= DM[address[6:0]];
read_data[15:8] <= DM[address[6:0] + 1];
read_data[23:16] <= DM[address[6:0] + 2];
read_data[31:24] <= DM[address[6:0] + 3];
// $display("Reading data from memory at address %h, read_data = %h", address, read_data); // 添加显示语句便于调试查看读数据情况
end
end
// always @(*) begin
// $display("adress ", address);
// $display("DM[adr] ", DM[address]);
// $display("read_data ", read_data);
// end
// 写数据
always @(posedge clk) begin
// $display("memwrt", mem_write_enable);
if (mem_write_enable) begin
DM[address[6:0]] <= write_data[7:0];
DM[address[6:0]+1] <= write_data[15:8];
DM[address[6:0]+2] <= write_data[23:16];
DM[address[6:0]+3] <= write_data[31:24];
// $display("Writing data %h to memory at address %h", write_data, address); // 添加显示语句便于调试查看写数据情况
end
end
assign DM_4 = DM[4];
assign DM_12 = DM[12];
endmodule
3.5 立即数扩展单元设计
3.5.1 结构与功能说明
本模块用于实现对输入指令中的立即数进行扩展的功能。
3.5.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
instruction | input | 32 | 输入当前指令值 |
imm_extended | output | 32 | 输出指令中的立即数扩展 |
3.5.3 代码实现
ImmExtend.v
`timescale 1ns / 1ps
module ImmExtend(
input wire [31:0] instruction,
output reg [31:0] imm_extended
);
always @(*) begin
case (instruction[6:0])
7'b0010011: // addi, ori, slti
imm_extended = {{20{instruction[31]}}, instruction[31:20]};
7'b0000011: // lw
imm_extended = {{20{instruction[31]}}, instruction[31:20]};
7'b0100011: // sw
imm_extended = {{20{instruction[31]}}, instruction[31:25], instruction[11:7]};
7'b1100011: // beq
imm_extended = {{20{instruction[31]}}, instruction[7], instruction[30:25], instruction[11:8], 1'b0};
default:
imm_extended = 32'b0;
endcase
end
endmodule
3.6 主控单元设计
3.6.1 结构与功能说明
本模块实现了对输入指令进行解析的功能。根据输入的指令,生成各种控制信号。
3.6.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
instruction | input | 32 | 输入当前的指令值 |
branch | output | 1 | 根据指令值解析是否为beq指令的信号 |
memread | output | 1 | 控制是否进行存储器读操作的信号 |
ALUop | output | 2 | 传递给ALU控制单元的操作码,用于决定ALU执行何种运算 |
memtoreg | output | 1 | 控制是否加载内存值写入寄存器 |
memwrite | output | 1 | 控制是否进行存储器写操作的信号 |
ALUsrc | output | 1 | 控制ALU的操作数来源(是寄存器还是立即数) |
regwrite | output | 1 | 控制是否将结果写回寄存器堆的信号 |
3.6.3 代码实现
Control.v
`timescale 1ns / 1ps
module control(
input wire [6:0] instruction, // 输入的指令,取其低7位用于判断指令类型
output reg branch, // 控制是否进行分支操作的信号
output reg memread, // 控制是否进行存储器读操作的信号
output reg [1:0] ALUop, // 传递给ALU控制单元的操作码,用于决定ALU执行何种运算
output reg memtoreg, //控制是否加载内存值写入寄存器
output reg memwrite, // 控制是否进行存储器写操作的信号
output reg ALUsrc, // 控制ALU的操作数来源(是寄存器还是立即数)
output reg regwrite // 控制是否将结果写回寄存器堆的信号
);
always @(*) begin
case (instruction[6:0])
7'b0110011: begin // 常规指令 add,sub,or,slt
branch <= 1'b0; // 不进行分支操作
memread <= 1'b0; // 不进行存储器读操作
memtoreg <= 1'b0; // 按特定方式(此处具体由整体设计决定)写回寄存器堆
ALUop <= 2'b11; // 设置ALU操作码,指示ALU执行对应操作
memwrite <= 1'b0; // 不进行存储器写操作
ALUsrc <= 1'b0; // ALU操作数来源选择(这里是从寄存器获取)
regwrite <= 1'b1; // 将运算结果写回寄存器堆
end
7'b0010011: begin // 立即数相关指令 addi,ori,slti
branch <= 1'b0;
memread <= 1'b0;
memtoreg <= 1'b0;
ALUop <= 2'b10;
memwrite <= 1'b0;
ALUsrc <= 1'b1; // ALU操作数来源选择立即数
regwrite <= 1'b1;
end
7'b0000011: begin // lw
branch <= 1'b0;
memread <= 1'b1; // 需要进行存储器读操作
memtoreg <= 1'b1; // 按对应方式写回寄存器堆
ALUop <= 2'b10;
memwrite <= 1'b0;
ALUsrc <= 1'b1;
regwrite <= 1'b1;
end
7'b0100011: begin // sw
branch <= 1'b0;
memread <= 1'b0;
memtoreg <= 1'b0;
ALUop <= 2'b10;
memwrite <= 1'b1; // 需要进行存储器写操作
ALUsrc <= 1'b1;
regwrite <= 1'b0; // 不需要写回寄存器堆(因为是存储操作)
end
7'b1100011: begin // beq
branch <= 1'b1; // 进行分支操作
memread <= 1'b0;
memtoreg <= 1'b0;
ALUop <= 2'b01;
memwrite <= 1'b0;
ALUsrc <= 1'b0;
regwrite <= 1'b0;
end
default: begin // 对于其他未明确匹配的指令类型
branch <= 1'b0;
memread <= 1'b0;
memtoreg <= 1'b0;
ALUop <= 2'b00;
memwrite <= 1'b0;
ALUsrc <= 1'b0;
regwrite <= 1'b0;
end
endcase
end
// always @(*) begin
// $display("ALU_src ", ALUsrc);
// end
endmodule
3.7 ALU信号控制单元设计
3.7.1 结构与功能说明
本模块实现了将指令值解析成更具体的ALU运算的功能。根据先前得到的ALUop信号和原指令值,产生ALU控制信号control_signal
。
3.7.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
ALUop | input | 1 | 在主控单元产生的控制信号 |
instruction | input | 32 | 原指令值 |
control_signal | input | 4 | 输出ALU控制信号 |
3.7.3 代码实现
ALU_control.v
`timescale 1ns / 1ps
module ALU_control(
input wire [1:0] ALUop,
input wire [31:0] instruction,
output reg [3:0] control_signal
);
always @(ALUop or instruction) begin
case (ALUop)
2'b00:
control_signal = 4'b0000;
2'b01:
control_signal = 4'b1000; // beq
2'b10:
case (instruction[14:12]) // funct3
3'b010:
if (instruction[4] == 0)
control_signal = 4'b0010; // lw, sw
else
control_signal = 4'b0110; // slti
3'b000:
control_signal = 4'b0000; // addi
3'b110:
control_signal = 4'b0011; // ori
default:
control_signal = 4'b1111; // 默认合理值
endcase
2'b11:
case (instruction[14:12]) // funct3
3'b000:
if (instruction[30] == 0)
control_signal = 4'b0000; // add
else
control_signal = 4'b0001; // sub
3'b110:
control_signal = 4'b0011; // or
3'b011:
control_signal = 4'b0110; // slt
default:
control_signal = 4'b1111; // 默认合理值
endcase
endcase
end
// always @(*) begin
// $display("At time %t", $time);
// $display("control_signal", control_signal);
// end
endmodule
3.8 算术逻辑单元(ALU)设计
3.8.1 结构与功能说明
本模块实现了ALU的核心运算功能。即根据输入的ALU控制信号,选择对应的运算种类进行运算,并输出运算结果、lw/sw指令的内存地址和分支控制信号ALU_zero。
下面是ALU的主要功能说明:
-
算术运算:ALU执行所有基本的算术运算,包括
add
、addi
、sub
。 -
逻辑运算:除了算术运算外,ALU还能执行逻辑运算,本项目中只需要实现
or
、ori
-
比较运算:ALU可以比较两个数值的大小,支持等于、不等于、大于、小于等比较操作。比较结果通常用于程序流程控制,如循环和条件跳转,本项目中,有
slt
、slti
判断小于指令和beq
的相等时分支指令都基于此实现。 -
内存访问和处理:在一些设计中,ALU也参与内存数据的传输操作,如数据之间的复制和修改。虽然这些功能可能更多地由其他CPU组件处理,但ALU在这方面仍然发挥作用(如计算
sw
、lw
指令相关的内存地址)。
3.8.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
ALU_src | input | 1 | 控制第二个运算值是来自立即数还是寄存器 |
contrl_signal | input | 4 | ALU的控制信号 |
read_data1 | input | 32 | 输入ALU的1号数据 |
read_data2 | input | 32 | 输入ALU的2号数据 |
immediate | input | 32 | 输入ALU的立即数 |
ALU_result | output | 32 | 运算结果 |
mem_adr | output | 32 | 由ALU计算出来的lw/sw指令的内存地址 |
ALU_zero | output | 1 | 判断是否为0的信号,用于决定beq指令是否分支 |
3.8.3 代码实现
ALU.v
`timescale 1ns / 1ps
module ALU(
input wire ALU_src,
input wire [3:0] control_signal,
input wire [31:0] read_data1,
input wire [31:0] read_data2,
input wire [31:0] immediate,
output reg [31:0] ALU_result,
output reg [31:0] mem_adr,
output reg ALU_zero
);
reg [31:0] ALU_data2;
// always @(*) begin
// $display("ALU_src ", ALU_src);
// end
// always @(*) begin
// $display("control ", control_signal);
// $display("read_data1 ", read_data1);
// $display("ALU_data2 ", ALU_data2);
// $display("ALU_result ", ALU_result);
// end
always @(*) begin
if (ALU_src)
ALU_data2 = immediate;
else
ALU_data2 = read_data2;
end
always @(read_data1 or ALU_data2 or control_signal) begin
case (control_signal)
4'b0000: ALU_result <= read_data1 + ALU_data2; // add, addi
4'b0001: ALU_result <= read_data1 - ALU_data2; // sub
4'b0011: ALU_result <= read_data1 | ALU_data2; // ori or
4'b0110: ALU_result <= (read_data1 < ALU_data2)? 32'd1 : 32'd0; // slt slti
4'b0010: begin // 针对lw和sw指令,计算访存地址并存入mem_adr
mem_adr <= read_data1 + ALU_data2;
ALU_result <= read_data1 + ALU_data2; // 可以根据需求决定是否同时更新ALU_result,这里假设同时更新
end
default:begin
ALU_result = 32'd0;
mem_adr = 32'b0;
end
endcase
end
always @(*) begin
case (control_signal)
4'b1000:
ALU_zero = (read_data1 == ALU_data2)? 1'b1 : 1'b0; // beq
default:ALU_zero = 1'b0;
endcase
end
endmodule
3.9 CPU顶层实现单元设计
3.9.1 结构与功能说明
这个模块是设计的顶层文件,相当于C语言中的main函数。需要按照先前的逻辑电路图来编写。将前面编写的8个模块进行实例化,并且将各个模块的输出与需要其作为输入的模块端口进行连接,从而完成整个CPU的功能实现编写。
3.9.2 接口列表
以下为该模块主要接口列表:
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 从外部输入的时钟信号 |
reset | input | 1 | 从外部输入的复位信号 |
3.9.3 代码实现
CPU.v
`timescale 1ns / 1ps
module CPU(
input wire clk,
input wire reset,
output wire [31:0] PC,
output wire [31:0] instruction,
output wire [31:0] rs1_data,
output wire [31:0] rs2_data,
output wire [31:0] imm_extended,
output wire [31:0] ALU_result,
output wire [31:0] mem_adr,
// 内存值和寄存器值查看(由于测试用例都是单字节数据,因此简化观察,用8位来输出)
output wire [7:0] DM_4,
output wire [7:0] DM_12,
output wire [7:0] X0,
output wire [7:0] X1,
output wire [7:0] X2,
output wire [7:0] X3,
output wire [7:0] X4,
output wire [7:0] X5,
output wire [7:0] X6,
output wire [7:0] X7,
output wire [7:0] X8
);
// wire [31:0] PC;
// wire [31:0] instruction;
// wire [31:0] rs1_data;
// wire [31:0] rs2_data;
// wire [31:0] imm_extended;
// wire [31:0] ALU_result;
wire [31:0] rd;
wire ALU_zero;
wire branch;
wire memread;
wire memtoreg;
wire [1:0] ALUop;
wire [3:0 ] control_signal ;
wire memwrite;
wire ALUsrc;
wire regwrite;
// always @(*) begin
// $display("ALU_src ", ALUsrc);
// end
// 实例化各个模块
PC_module pc_module(
.clk(clk),
.reset(reset),
.branch_offset(imm_extended), // 根据指令情况传递合适的偏移量,这里假设是立即数扩展后的结果用于分支偏移
.beq_taken(branch & ALU_zero), // beq指令执行条件为branch信号有效且ALU结果为零
.PC(PC)
);
PM_module pm_module(
.PC(PC),
.instruction(instruction)
);
Register_file register_file(
.clk(clk),
.reset(reset),
.rs1_addr(instruction[19:15]),
.rs2_addr(instruction[24:20]),
.rd_addr(instruction[11:7]),
.wr_data(ALU_result),
.mem_data(rd),
.reg_write_enable(regwrite),
.mem_to_reg(memtoreg),
.rs1_data(rs1_data),
.rs2_data(rs2_data),
.X0(X0),
.X1(X1),
.X2(X2),
.X3(X3),
.X4(X4),
.X5(X5),
.X6(X6),
.X7(X7),
.X8(X8)
);
ImmExtend imm_extend(
.instruction(instruction),
.imm_extended(imm_extended)
);
control ctrl(
.instruction(instruction),
.branch(branch),
.memread(memread),
.memtoreg(memtoreg),
.ALUop(ALUop),
.memwrite(memwrite),
.ALUsrc(ALUsrc),
.regwrite(regwrite)
);
ALU_control alu_ctrl(
.ALUop(ALUop),
.instruction(instruction),
.control_signal(control_signal)
);
ALU alu(
.ALU_src(ALUsrc),
.control_signal(control_signal), // 此处control_signal需要来自ALUcontrol模块
.read_data1(rs1_data),
.read_data2(rs2_data),
.immediate(imm_extended),
.ALU_result(ALU_result),
.ALU_zero(ALU_zero),
.mem_adr(mem_adr)
);
DM_module dm_module(
.clk(clk),
.address(mem_adr),
.write_data(rs2_data),
.mem_write_enable(memwrite),
.mem_read_enable(memread),
.read_data(rd),
.DM_4(DM_4),
.DM_12(DM_12)
);
endmodule
四、仿真过程及其结果分析
现有如下指令存储器和数据存储器数据测试用例:
PM初始值:
PM地址 | 指令序号指令(小端排序合并后) | 对应指令 | 说明 |
---|---|---|---|
3~0 | 0x00800093 | addi X1, X0, 0x8 | 寄存器 X0 机器代码:值恒为 0,X1 = 8 |
7~4 | 0x0040a103 | lw X2, 4(X1) | X2 = 4 |
11~8 | 0x002081b3 | add X3, X1, X2 | X3 = 12 (0xc) |
15~12 | 0x40118233 | sub X4, X3, X1 | X4 = 4 |
19~16 | 0x0040e2b3 | or X5, X1, X4 | X5 = 0xc |
23~20 | 0x0012e313 | ori X6, X5, 1 | X6 = 0xd |
27~24 | 0x00612023 | sw X6, 0(X2) | dm(0x4) = 0xd |
31~28 | 0x004123b3 | slt X7, X2, X4 | X7 = 0 |
35~32 | 0x00812413 | slti X8, X2, 8 | X8 = 1 |
39~36 | 0xfe518ae3 | beq X3, X5, - 12 | PC = PC - 12 |
DM初始值:
DM地址(小端模式) | 指令代码 |
---|---|
0 | 0x00 |
1 | 0x01 |
2 | 0x02 |
3 | 0x03 |
4 | 0x04 |
5 | 0x05 |
6 | 0x06 |
7 | 0x07 |
8 | 0x08 |
9 | 0x00 |
10 | 0x00 |
11 | 0x00 |
12 | 0x04 |
13 | 0x00 |
14 | 0x00 |
15 | 0x00 |
根据此写一个仿真平台CPU_tb:
CPU_tb.v
`timescale 1ns / 1ps
module CPU_tb;
// 输入信号
reg clk;
reg reset;
// 输出信号
wire [31:0] PC;
wire [31:0] instruction;
wire [31:0] rs1_data;
wire [31:0] rs2_data;
wire [31:0] imm_extended;
wire [31:0] ALU_result;
wire [31:0] mem_adr;
// 样例寄存器和内存值
wire [7:0] DM_4;
wire [7:0] DM_12;
wire [7:0] X0;
wire [7:0] X1;
wire [7:0] X2;
wire [7:0] X3;
wire [7:0] X4;
wire [7:0] X5;
wire [7:0] X6;
wire [7:0] X7;
wire [7:0] X8;
// 数据存储器和指令存储器模块实例化
reg [7:0] PM [0:127]; // 44条指令
reg [7:0] DM [0:127]; // 16个数据存储单元
// CPU模块实例
CPU cpu (
.clk(clk),
.reset(reset),
.PC(PC),
.instruction(instruction),
.rs1_data(rs1_data),
.rs2_data(rs2_data),
.imm_extended(imm_extended),
.ALU_result(ALU_result),
.mem_adr(mem_adr),
.DM_4(DM_4),
.DM_12(DM_12),
.X0(X0),
.X1(X1),
.X2(X2),
.X3(X3),
.X4(X4),
.X5(X5),
.X6(X6),
.X7(X7),
.X8(X8)
);
// 时钟生成
always begin
#5 clk = ~clk; // 时钟周期为10ns
end
// 初始化过程
initial begin
// 初始化时钟和复位
clk = 1;
reset = 1;
// 初始化指令存储器(PM) - 按照小端格式初始化
PM[0] = 8'h93; PM[1] = 8'h00; PM[2] = 8'h80; PM[3] = 8'h00; // 0x93008000
PM[4] = 8'h03; PM[5] = 8'hA1; PM[6] = 8'h40; PM[7] = 8'h00; // 0x03A14000
PM[8] = 8'hB3; PM[9] = 8'h81; PM[10] = 8'h20; PM[11] = 8'h00; // 0xB3182000
PM[12] = 8'h33; PM[13] = 8'h82; PM[14] = 8'h11; PM[15] = 8'h40; // 0x33211840
PM[16] = 8'hB3; PM[17] = 8'hE2; PM[18] = 8'h40; PM[19] = 8'h00; // 0xB23E4000
PM[20] = 8'h13; PM[21] = 8'hE3; PM[22] = 8'h12; PM[23] = 8'h00; // 0x13E31200
PM[24] = 8'h23; PM[25] = 8'h20; PM[26] = 8'h61; PM[27] = 8'h00; // 0x23012000
PM[28] = 8'hB3; PM[29] = 8'h23; PM[30] = 8'h41; PM[31] = 8'h00; // 0x3B234100
PM[32] = 8'h13; PM[33] = 8'h24; PM[34] = 8'h81; PM[35] = 8'h00; // 0x13248100
PM[36] = 8'hE3; PM[37] = 8'h8A; PM[38] = 8'h51; PM[39] = 8'hFE; // 0xE38A51FE
PM[40] = 8'h00; PM[41] = 8'h00; PM[42] = 8'h00; PM[43] = 8'h00; // 0x00000000 (No-op)
// 初始化数据存储器(DM) - 小端排序数据
DM[0] = 8'h00;
DM[1] = 8'h01;
DM[2] = 8'h02;
DM[3] = 8'h03;
DM[4] = 8'h04;
DM[5] = 8'h05;
DM[6] = 8'h06;
DM[7] = 8'h07;
DM[8] = 8'h08;
DM[9] = 8'h00;
DM[10] = 8'h00;
DM[11] = 8'h00;
DM[12] = 8'h04;
DM[13] = 8'h00;
DM[14] = 8'h00;
DM[15] = 8'h00;
// 将指令存储器和数据存储器初始化值传递给CPU模块
cpu.pm_module.PM[0] = PM[0];
cpu.pm_module.PM[1] = PM[1];
cpu.pm_module.PM[2] = PM[2];
cpu.pm_module.PM[3] = PM[3];
cpu.pm_module.PM[4] = PM[4];
cpu.pm_module.PM[5] = PM[5];
cpu.pm_module.PM[6] = PM[6];
cpu.pm_module.PM[7] = PM[7];
cpu.pm_module.PM[8] = PM[8];
cpu.pm_module.PM[9] = PM[9];
cpu.pm_module.PM[10] = PM[10];
cpu.pm_module.PM[11] = PM[11];
cpu.pm_module.PM[12] = PM[12];
cpu.pm_module.PM[13] = PM[13];
cpu.pm_module.PM[14] = PM[14];
cpu.pm_module.PM[15] = PM[15];
cpu.pm_module.PM[16] = PM[16];
cpu.pm_module.PM[17] = PM[17];
cpu.pm_module.PM[18] = PM[18];
cpu.pm_module.PM[19] = PM[19];
cpu.pm_module.PM[20] = PM[20];
cpu.pm_module.PM[21] = PM[21];
cpu.pm_module.PM[22] = PM[22];
cpu.pm_module.PM[23] = PM[23];
cpu.pm_module.PM[24] = PM[24];
cpu.pm_module.PM[25] = PM[25];
cpu.pm_module.PM[26] = PM[26];
cpu.pm_module.PM[27] = PM[27];
cpu.pm_module.PM[28] = PM[28];
cpu.pm_module.PM[29] = PM[29];
cpu.pm_module.PM[30] = PM[30];
cpu.pm_module.PM[31] = PM[31];
cpu.pm_module.PM[32] = PM[32];
cpu.pm_module.PM[33] = PM[33];
cpu.pm_module.PM[34] = PM[34];
cpu.pm_module.PM[35] = PM[35];
cpu.pm_module.PM[36] = PM[36];
cpu.pm_module.PM[37] = PM[37];
cpu.pm_module.PM[38] = PM[38];
cpu.pm_module.PM[39] = PM[39];
cpu.pm_module.PM[40] = PM[40];
cpu.pm_module.PM[41] = PM[41];
cpu.pm_module.PM[42] = PM[42];
cpu.pm_module.PM[43] = PM[43];
cpu.dm_module.DM[0] = DM[0];
cpu.dm_module.DM[1] = DM[1];
cpu.dm_module.DM[2] = DM[2];
cpu.dm_module.DM[3] = DM[3];
cpu.dm_module.DM[4] = DM[4];
cpu.dm_module.DM[5] = DM[5];
cpu.dm_module.DM[6] = DM[6];
cpu.dm_module.DM[7] = DM[7];
cpu.dm_module.DM[8] = DM[8];
cpu.dm_module.DM[9] = DM[9];
cpu.dm_module.DM[10] = DM[10];
cpu.dm_module.DM[11] = DM[11];
cpu.dm_module.DM[12] = DM[12];
cpu.dm_module.DM[13] = DM[13];
cpu.dm_module.DM[14] = DM[14];
cpu.dm_module.DM[15] = DM[15];
// 激活复位信号
#5 reset = 0;
// 仿真运行几个时钟周期
#100; // 运行200ns,足够执行指令
// 结束仿真
#20;
$finish;
end
endmodule
在vivado软件上进行behavioral simulation仿真,得到图2所示的仿真波形图。
- 图2.仿真全过程总波形图
下面对每条指令的实现验证进行逐一分解说明:
1. addi X1, X0, 0x8
指令实现验证
如图3所示,此时PC值为0,instruction值为0x00800093,解析后为addi X1, X0, 0x8
指令,寄存器X0的值0读进rs1_data中,立即数0x8读进imm_extended中,ALU运算结果为8,结果将要写入寄存器X1中,代表的运算为X1=X0+8=0+8=8
,发现在下一步操作前X1的值变成了8,说明这条指令正确执行了。
- 图3.
addi X1, X0, 0x8
指令波形图
2. lw X2, 4(X1)
指令实现验证
如图4所示,此时PC值为4,instruction值为0x0004a103,解析后为lw X2, 4(X1)
指令,寄存器X1的值8读进rs1_data中,立即数0x4读进imm_extended中,内存地址mem_adr运算结果为12,代表的运算为4(X1)=4+8=12
,而图中DM[12]的值是4,所以这条指令应该把4赋值给X2,发现在下一步操作前X2值变成了4,说明这条指令正确执行了。
- 图4.
lw X2, 4(X1)
指令波形图
3. add X3, X1, X2
指令实现验证
如图5所示,此时PC值为8,instruction值为0x002081b3,解析后为add X3, X1, X2
指令,寄存器X1的值8读进rs1_data中,寄存器X2的值4读进rs2_data中,ALU运算结果为12,结果将要写入寄存器X3中,代表的运算为X3=X1+X2=8+4=12
,发现在下一步操作前X3的值变成了12,说明这条指令正确执行了。
- 图5.
add X3, X1, X2
指令波形图
4. sub X4, X3, X1
指令实现验证
如图6所示,此时PC值为12,instruction值为0x40118233,解析后为sub X4, X3, X1
指令,寄存器X3的值12读进rs1_data中,寄存器X1的值8读进rs2_data中,ALU运算结果为4,结果将要写入寄存器X4中,代表的运算为X4=X3-X1=12-8=4
,发现在下一步操作前X4的值变成了4,说明这条指令正确执行了。
- 图6
sub X4, X3, X1
指令波形图
5. or X5, X1, X4
指令实现验证
如图7所示,此时PC值为16,instruction值为0x0040e2b3,解析后为or X5, X1, X4
指令,寄存器X1的值8读进rs1_data中,寄存器X4的值4读进rs2_data中,ALU运算结果为12,结果将要写入寄存器X5中,代表的运算为X5=X1|X4=8|4=12
,发现在下一步操作前X5的值变成了12,说明这条指令正确执行了。
- 图7.
or X5, X1, X4
指令波形图
6. ori X6, X5, 1
指令实现验证
如图8所示,此时PC值为20,instruction值为0x0012e313,解析后为ori X6, X5, 1
指令,寄存器X5的值12读进rs1_data中,立即数0x1读进imm_extended中,ALU运算结果为13,结果将要写入寄存器X6中,代表的运算为X6=X5|1=12|1=13
,发现在下一步操作中的X6的值变成了13,说明这条指令正确执行了。
- 图8.
ori X6, X5, 1
指令波形图
7. sw X6, 0(X2)
指令实现验证
如图9所示,此时PC值为24,instruction值为0x00612023,解析后为sw X6, 0(X2)
指令,寄存器X2的值4读进rs1_data中,立即数0x0读进imm_extended中,内存地址mem_adr运算结果为4,代表的运算为0(X2)=0+4=4
,这条指令应该把X6的值13写进内存DM[4]中,发现在下一步操作前内存DM[4]值变成了13,说明这条指令正确执行了。
- 图9.
sw X6, 0(X2)
指令波形图
8. slt X7, X2, X4
指令实现验证
如图10所示,此时PC值为28,instruction值为0x004123b3,解析后为slt X7, X2, X4
指令,寄存器X2的值4读进rs1_data中,寄存器X4的值4读进rs2_data中,ALU运算结果为0,结果将要写入寄存器X7中,代表的运算为X7=X2<X4=4<4=0
,发现在下一步操作前X7的值变成了0,说明这条指令正确执行了。
- 图10.
slt X7, X2, X4
指令波形图
9. slti X8, X2, 8
指令实现验证
如图11所示,此时PC值为32,instruction值为0x00812413,解析后为slti X8, X2, 8
指令,寄存器X2的值4读进rs1_data中,立即数0x8读进imm_extended中,ALU运算结果为1,结果将要写入寄存器X8中,代表的运算为X8=X2<8=4<8=1
,发现在下一步操作中的X8的值变成了1,说明这条指令正确执行了。
- 图11.
slti X8, X2, 8
指令波形图
10. beq X3, X5, - 12
指令实现验证
如图12所示,此时PC值为36,instruction值为0xfe518ae3,解析后为 beq X3, X5, - 12
指令,寄存器X3的值12读进rs1_data中,寄存器X5的值12读进rs2_data中,两值相等,立即数-12读进imm_extended中,则需要跳转到PC-12
的指令地址,发现在下一步中的PC的值变成了前面执行过的24,instruction值跳转成前面执行过的0x00612023,并且继续一条一条执行下去之至仿真结束,说明这条指令正确执行了。
- 图12.
beq X3, X5, - 12
指令波形图
五、实验体会
在本次单周期 CPU 设计实验中,我深入地探索了计算机底层硬件的工作原理,从电路设计到代码编写,再到仿真验证,整个过程充满挑战但收获颇丰。
在实验设计阶段,我将 CPU 功能进行细致分解,从取指、译码、执行、访存到写回,每一步都需要精心设计与严谨逻辑。这让我对 CPU 的工作流程有了极为清晰的认识,理解了各个部件如何协同运作以完成复杂的指令任务。设计各个模块时,如寄存器堆、PC 取指模块、存储器模块等,不仅要考虑其独立功能的实现,还需关注模块间的接口与交互,这锻炼了我的系统设计与整合能力。
编写 Verilog 代码实现各个模块是一项艰巨任务。在代码编写过程中,我需要精确地运用硬件描述语言来描述每个模块的功能与行为。例如,在控制信号生成模块中,要依据不同指令的操作码与功能码准确地生成相应控制信号,这要求我对指令格式和操作码有深入理解。通过不断调试与修改代码,我逐渐掌握了 Verilog 语言在硬件设计中的应用技巧,也提升了自己的代码编写能力与逻辑思维能力。
仿真验证环节至关重要。通过编写测试用例并在 Vivado 软件中进行仿真,我能够直观地观察到 CPU 内部各个信号的变化以及指令的执行过程。当看到每条指令都能按照预期正确执行,各个寄存器和存储器的值都准确无误时,我获得了极大的成就感。在这个过程中,我学会了如何分析仿真波形,从波形中找出潜在问题并追溯到代码中的错误根源,这极大地提高了我的问题排查与解决能力。
然而,实验过程并非一帆风顺。我在控制信号的生成与传递方面遇到了不少问题,由于不同模块间的信号依赖关系复杂,一旦某个控制信号出现错误,就会导致整个 CPU 功能出错。任何一个小的失误都可能引发连锁反应,导致整个系统无法跑出正常的仿真结果。
通过本次实验,我不仅在专业知识和技能上取得了显著进步,更培养了自己的耐心、细心和解决问题的能力。虽然本项目只需要完成仿真验证,但我依然深刻明白硬件设计是一个严谨而细致的过程,需要对每个细节都精益求精。这次实验也让我对计算机底层世界有了更深刻、更直观的认识与理解。