单周期处理器的架构
📚 使用须知
- 本博客内容仅供学习参考
- 建议理解思路后独立实现
- 欢迎交流讨论
理解addi指令的执行过程
addi指令是I-type指令,格式如下:
imm[11:0] | rs1 | funct3 | rd | opcode
功能
rd = rs1 + imm
在单周期处理器中,所有指令在一个时钟周期内完成。因此,我们需要设计一个数据通路,能够完成addi指令的操作。
所需组件
指令存储器(`IMEM`):用于存储指令,根据PC给出指令。
寄存器文件(`RegFile`):用于读取`rs1`和写入`rd`。
立即数生成器:从指令中提取立即数并符号扩展至32位。
加法器:用于计算`rs1`和立即数的和。
PC更新逻辑:每周期PC+4,但后续可能支持跳转,这里先只考虑顺序执行。
注意:addi指令需要从寄存器文件中读取rs1,然后与立即数相加,结果写入rd。
因此,架构图应包含以下部分:
PC -> IMEM -> 指令解码(提取rs1、rd、imm)->寄存器文件(读rs1)->加法器(加立即数)->寄存器文件(写rd)
同时,PC更新为PC+4。
由于是单周期,所有操作在一个周期内完成,因此不需要控制信号来区分指令(因为只有一条指令,但为了后续扩展,可以先画出控制信号)。
但是,我们只实现addi,所以可以简化,不需要复杂的控制逻辑,只需要将指令解码后的信号连接到相应的部件。
步骤
从PC指向的地址取指令。
将指令拆分成不同的字段:`opcode, rd, funct3, rs1, imm`。
根据`rs1`从寄存器文件中读取数据。
将立即数`imm`进行符号扩展至32位。
将读出的寄存器数据和立即数进行加法运算。
将结果写入寄存器文件的`rd`中。
更新PC为PC+4。
数据通路
PC寄存器 -> 指令存储器(IMEM) -> 指令拆解(得到rs1、rd、imm等)
寄存器文件:读地址1为`rs1`,读数据1输出;写地址为`rd`,写数据为加法器结果,写使能由控制信号决定(这里`addi`指令需要写寄存器,所以写使能有效)。
立即数生成:从指令中提取`imm`(I-type立即数),并符号扩展至32位。
加法器:两个输入分别为寄存器读数据1和立即数,输出为两者之和。
还有一个PC加法器,用于计算PC+4,并更新到PC寄存器。
注意:在单周期中,所有操作都是组合逻辑,除了PC和寄存器文件(和存储器)是时序逻辑。
因此,时钟上升沿到来时,PC更新,同时指令存储器根据新的PC输出指令,然后经过组合逻辑,得到结果,在时钟下降沿或者下一个上升沿将结果写回寄存器文件?
实际上,在单周期设计中,通常是在时钟周期内完成所有操作,并在时钟上升沿将结果写入寄存器文件。
但是,为了简单,我们假设寄存器文件是在时钟上升沿触发的,那么我们在时钟周期内计算出结果,然后在下一个时钟上升沿将结果写入寄存器文件。
但是,这样指令执行就需要两个时钟周期了?这不符合单周期的定义。
实际上,典型的单周期处理器设计是:在同一个时钟周期内,从寄存器文件读出的数据经过组合逻辑运算,然后写回寄存器文件。
但是,这样会形成组合逻辑环路,因为写回的数据会在同一个周期被读到?
因此,通常寄存器文件是在时钟上升沿写入,而读操作是组合逻辑。
这样,在一个时钟周期内,我们使用时钟上升沿后的寄存器文件数据,然后经过组合逻辑,结果在时钟上升沿之前稳定,然后下一个时钟上升沿将结果写入。
所以,实际上一个指令的执行需要从一个时钟上升沿开始,到下一个时钟上升沿结束。
但是,我们这里先不考虑时序,只考虑功能。
在功能上,我们可以在一个周期内完成读寄存器、运算、写寄存器。
在RTL设计中,我们可以这样描述:在时钟上升沿,如果指令是addi,那么我们将立即数和寄存器值相加,然后写入目标寄存器。
但是,这样写的话,寄存器文件读出的值是这个时钟上升沿之前的值,而写入的值是当前时钟上升沿之后的新值。
所以,在同一个周期内,读和写是不同的值,不会冲突。
因此,我们可以这样设计:
时钟上升沿:
1. 根据PC从指令存储器中取出指令(指令存储器是同步读吗?通常指令存储器可以设计为组合逻辑,即地址变化,指令立即变化。但为了时序,也可以设计为同步读。这里我们先设计为组合逻辑,即指令存储器的输出随地址变化而变化。)
2. 寄存器文件在时钟上升沿写入,读操作是组合逻辑。所以,在时钟上升沿之后,寄存器文件输出的是之前写入的值。
3. 然后经过组合逻辑(立即数生成和加法器)得到结果,这个结果会在时钟上升沿之前稳定,并准备写入寄存器文件。但是,写入发生在下一个时钟上升沿。
所以,实际上,在一个时钟周期内,我们使用时钟上升沿时的寄存器值,进行计算,然后在下一个时钟上升沿将结果写入。这样,一个指令的执行需要两个时钟周期?这不对。
如果寄存器文件是边沿触发的,那么我们需要在同一个时钟边沿完成读和写,这是不可能的。
因此,通常的处理是:寄存器文件的写操作发生在时钟上升沿,而读操作是异步的(组合逻辑)。
在时钟上升沿,我们写入上一次指令的结果,同时,根据新的PC值,指令存储器输出指令,然后解码,从寄存器文件中读取数据(此时读出的数据是刚写入的数据),然后经过组合逻辑,得到结果,这个结果将在下一个时钟上升沿写入。
这样,每条指令的执行时间是一个时钟周期,但是延迟是一个周期。也就是说,指令i的结果在指令i+1的时钟上升沿被写入。所以,指令i的写回和指令i+1的读寄存器发生在同一个时钟上升沿,但是读出的数据是刚写入的,所以指令i+1使用指令i的结果。
因此,我们的设计可以这样:
在时钟上升沿:
PC更新(如果是顺序执行,则更新为PC+4)。
寄存器文件写入端口(如果写使能有效)将数据写入目标寄存器。
时钟上升沿之后:
指令存储器根据新的PC输出指令。
指令被拆解,得到rs1、rd、imm等。
寄存器文件根据rs1输出数据(这是上一个时钟上升沿写入的数据,或者是初始值)。
立即数生成器生成32位立即数。
加法器计算寄存器值和立即数的和,得到结果。
这个结果将作为寄存器文件的写数据,但是写使能由控制单元根据指令产生(对于addi,写使能有效)。
所以,在下一个时钟上升沿,这个结果将被写入寄存器文件。
数据通路
组件:
PC寄存器(时钟上升沿更新)、指令存储器(组合逻辑,根据PC输出指令)
寄存器文件(时钟上升沿写入,组合逻辑读)、立即数生成器(组合逻辑)
加法器(组合逻辑)、控制单元(根据指令opcode和funct3等产生控制信号,这里只有addi,所以可以简化)
连接:
PC输出到指令存储器地址输入;指令存储器的输出拆解为:opcode, rd, rs1, imm等。
rs1连接到寄存器文件的读地址1,寄存器文件读数据1输出;imm经过立即数生成器得到32位立即数。
寄存器读数据1和立即数输入加法器,加法器输出结果;加法器输出连接到寄存器文件的写数据输入。
rd连接到寄存器文件的写地址;控制单元根据opcode和funct3产生寄存器写使能(RegWrite),对于addi,这个信号为1。
另外,PC更新逻辑:一个加法器,计算PC+4,然后连接到PC的输入。在时钟上升沿,PC更新为PC+4。
架构图
┌─────────────────────────────────────────────────────────────┐
│ 单周期处理器数据通路 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ PC │ │ │ │ 控制单元 │ │
│ │ 寄存器 │─────▶│ 指令 │─────▶│ │ │
│ │ │ │ 存储器 │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ PC+4 │ │指令译码器 │ │控制信号 │ │
│ │ 加法器 │ │ │─────▶│(RegWrite)│ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ │ ├──────────────┐ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ PC │ │寄存器文件 │ │立即数 │ │
│ │ 输入 │ │ │ │扩展单元 │ │
│ │多路选择器 │ │ rs1 │ │ │ │
│ │ │◀─────│ 数据 │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌─────────┐ │ ┌─────────┐ │
│ │ PC │ │ │ 32位 │ │
│ │(下一个)│ │ │ 立即数 │ │
│ │ │ ▼ │ │ │
│ └─────────┘ ┌─────────┐ └─────────┘ │
│ │ ALU │ │ │
│ │ │◀────────────┘ │
│ │ 加法器 │ │
│ └─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ ALU │ │寄存器文件 │ │
│ │ 结果 │───────▶│ 写数据 │ │
│ │ │ │ 输入 │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
在NPC中实现addi指令
具体地, 你需要注意以下事项:
* PC的复位值设置为0x80000000
* 存储器中可以放置若干条addi指令的二进制编码(可以利用0号寄存器的特性来编写行为确定的指令)
* 由于目前未实现跳转指令, 因此NPC只能顺序执行, 你可以在NPC执行若干指令之后停止仿真
* 可以通过查看波形, 或者在RTL代码中打印通用寄存器的状态, 来检查addi指令是否被正确执行
* 关于通用寄存器, 你需要思考如何实现0号寄存器的特性; 此外, 为了避免选择Verilog的同学编写出不太合理的行为建模代码, 我们给出如下不完整的代码供大家补充(大家无需改动always代码块中的内容):
module RegisterFile #(ADDR_WIDTH = 1, DATA_WIDTH = 1) (
input clk,
input [DATA_WIDTH-1:0] wdata,
input [ADDR_WIDTH-1:0] waddr,
input wen
);
reg [DATA_WIDTH-1:0] rf [ADDR_WIDTH-1:0];
always @(posedge clk) begin
if (wen) rf[waddr] <= wdata;
end
endmodule
* 使用NVBoard需要RTL代码比较好地支持设备, 这将会在A阶段进行介绍, 目前不必接入NVBoard
在NPC中实现addi指令
top.v
点击查看代码
// 顶层模块定义
module top(
input clk, // 时钟输入
input rst, // 复位信号输入
output reg[31:0] outdata // 32位数据输出寄存器
);
// 内部信号声明
reg [31:0] next_pc; // 下一个PC值寄存器
wire [6:0] op; // 操作码(7位)
wire [4:0] rd; // 目标寄存器地址(5位)
wire [2:0] func3; // 功能码(3位)
reg [31:0] pc; // 程序计数器寄存器(32位)
wire [31:0] ins; // 指令数据线(32位)
// 寄存器文件相关信号
wire [4:0] rs1_addr; // 源寄存器1地址线
wire [31:0] rs1_data; // 源寄存器1数据线
wire [31:0] imm; // 立即数数据线
wire rf_wen; // 寄存器文件写使能信号
wire [31:0] rf_wdata; // 寄存器文件写数据线
wire [4:0] rf_waddr; // 寄存器文件写地址线
// ALU输出信号线
wire [31:0] alu_out; // ALU计算结果输出线
// 初始化块:设置初始值
initial begin
pc = 32'h8000_0000; // 初始化PC为0x80000000
next_pc = pc + 4; // 计算下一个PC值
outdata = 32'b0; // 初始化输出数据为0
end
// PC更新逻辑:时钟上升沿或复位时触发
always @(posedge clk or posedge rst) begin
// 复位处理
if(rst) begin
pc <= 32'h8000_0000; // 复位PC到初始值
next_pc <= pc + 4; // 复位下一个PC值
outdata <= 32'b0; // 复位输出数据
end
// 正常时钟沿处理
else begin
pc <= next_pc; // 更新PC为下一个PC值
next_pc <= pc + 4; // 计算下一个PC值(PC+4)
outdata <= alu_out; // 将ALU输出赋值给输出端口
end
end
// 取指单元实例化
ysyx_25110281_ifu ifu(
.pc(pc), // 连接PC到取指单元
.clk(clk), // 连接时钟信号
.ins(ins) // 连接指令输出
);
// 译码单元实例化
ysyx_25110281_idu idu(
.ins(ins), // 连接指令输入
.op(op), // 连接操作码输出
.rd(rd), // 连接目标寄存器地址输出
.func3(func3), // 连接功能码输出
.rs1_addr(rs1_addr), // 连接源寄存器1地址输出
.imm(imm), // 连接立即数输出
.rf_wen(rf_wen) // 连接寄存器写使能输出
);
// 寄存器文件实例化
RegisterFile #(
.ADDR_WIDTH(5), // 设置地址宽度为5位(32个寄存器)
.DATA_WIDTH(32) // 设置数据宽度为32位
) regfile (
.clk(clk), // 连接时钟信号
.wdata(rf_wdata), // 连接写数据
.waddr(rf_waddr), // 连接写地址
.wen(rf_wen), // 连接写使能
.raddr1(rs1_addr), // 连接读地址1
.rdata1(rs1_data) // 连接读数据1
);
// ALU执行单元实例化
ysyx_25110281_alu alu(
.imm(imm), // 连接立即数输入
.rs1(rs1_data), // 连接源寄存器1数据输入
.func3(func3), // 连接功能码输入
.op(op), // 连接操作码输入
.rd(rd), // 连接目标寄存器地址输入
.rf_wen(rf_wen), // 连接寄存器写使能输入
.rf_wdata(rf_wdata), // 连接寄存器写数据输出
.rf_waddr(rf_waddr), // 连接寄存器写地址输出
.alu_out(alu_out) // 连接ALU计算结果输出
);
endmodule
// 寄存器文件模块定义
module RegisterFile #(
parameter ADDR_WIDTH = 5, // 地址宽度参数,默认5位
parameter DATA_WIDTH = 32 // 数据宽度参数,默认32位
)(
input clk, // 时钟输入
input [DATA_WIDTH-1:0] wdata, // 写数据输入
input [ADDR_WIDTH-1:0] waddr, // 写地址输入
input wen, // 写使能输入
input [ADDR_WIDTH-1:0] raddr1, // 读地址1输入
output reg [DATA_WIDTH-1:0] rdata1 // 读数据1输出
);
// 寄存器存储阵列定义
reg [DATA_WIDTH-1:0] rf [0:(1<<ADDR_WIDTH)-1];
// 初始化块:清零所有寄存器
integer i;
initial begin
// 遍历所有寄存器并初始化为0
for(i = 0; i < (1<<ADDR_WIDTH); i = i + 1) begin
rf[i] = {DATA_WIDTH{1'b0}};
end
end
// 读端口1的组合逻辑
always @(*) begin
// 如果读地址为0,返回0(x0寄存器恒为0)
if(raddr1 == {ADDR_WIDTH{1'b0}}) begin
rdata1 = {DATA_WIDTH{1'b0}};
end
// 否则返回对应寄存器的值
else begin
rdata1 = rf[raddr1];
end
end
// 写端口的时序逻辑
always @(posedge clk) begin
// 当写使能有效且地址不为0时执行写操作
if(wen && (waddr != {ADDR_WIDTH{1'b0}})) begin
rf[waddr] <= wdata; // 将数据写入寄存器
// 打印调试信息
$display("Time=%0t: Write to reg[%0d] = 0x%08x", $time, waddr, wdata);
end
end
// 调试任务:打印所有寄存器值
task print_registers;
integer j;
begin
$display("\n--- Register File Contents ---");
// 遍历32个寄存器
for(j = 0; j < 32; j = j + 1) begin
$display("x%0d: 0x%08x", j, rf[j]);
end
$display("------------------------------\n");
end
endtask
endmodule
// 取指单元模块定义
module ysyx_25110281_ifu(
input [31:0] pc, // PC输入
input clk, // 时钟输入
output reg [31:0] ins // 指令输出
);
// 指令存储器阵列
reg [31:0] mem [0:15];
// 初始化指令存储器
initial begin
// 第0条指令:addi x1, x0, 5 (x1 = 0 + 5)
mem[0] = 32'b000000000101_00000_000_00001_0010011;
// 第1条指令:addi x2, x1, 10 (x2 = x1 + 10)
mem[1] = 32'b000000001010_00001_000_00010_0010011;
// 第2条指令:addi x2, x0, 1 (x2 = 0 + 1)
mem[2] = 32'b000000000001_00000_000_00010_0010011;
// 第3条指令:addi x2, x0, 2 (x2 = 0 + 2)
mem[3] = 32'b000000000010_00000_000_00010_0010011;
// 第4条指令:addi x2, x1, 5 (x2 = x1 + 5)
mem[4] = 32'b000000000101_00001_000_00010_0010011;
// 剩余地址填充为nop指令
for (integer i = 5; i < 16; i = i + 1) begin
mem[i] = 32'b000000000000_00000_000_00000_0010011;
end
end
// 取指逻辑:时钟上升沿触发
always @(posedge clk) begin
// 检查PC地址是否在有效范围内
if (pc >= 32'h8000_0000 && pc < 32'h8000_0040) begin
// 计算内存索引并读取指令
ins = mem[(pc - 32'h8000_0000) >> 2];
// 打印调试信息
$display("Time=%0t: IFU: pc=0x%08x, ins=0x%08x", $time, pc, ins);
end
else begin
// PC地址无效,输出0
ins = 32'b0;
$display("Time=%0t: IFU: Invalid PC address: 0x%08x", $time, pc);
end
end
endmodule
// 译码单元模块定义
module ysyx_25110281_idu(
input [31:0] ins, // 指令输入
output reg [6:0] op, // 操作码输出
output reg [4:0] rd, // 目标寄存器地址输出
output reg [2:0] func3, // 功能码输出
output reg [4:0] rs1_addr, // 源寄存器1地址输出
output reg [31:0] imm, // 立即数输出
output reg rf_wen // 寄存器写使能输出
);
// 组合逻辑:随时对指令进行译码
always @(*) begin
// 解析指令各字段
op = ins[6:0]; // 提取操作码(最低7位)
rd = ins[11:7]; // 提取目标寄存器地址(位7-11)
func3 = ins[14:12]; // 提取功能码(位12-14)
rs1_addr = ins[19:15]; // 提取源寄存器1地址(位15-19)
// I-type立即数符号扩展
if (ins[31] == 1'b1) begin
// 最高位为1,进行符号扩展为负数
imm = {20'hFFFFF, ins[31:20]};
end
else begin
// 最高位为0,扩展为0
imm = {20'b0, ins[31:20]};
end
// 判断是否为addi指令(opcode=0010011, func3=000)
if (op == 7'b0010011 && func3 == 3'b000) begin
rf_wen = 1'b1; // addi指令需要写寄存器
// 打印调试信息
$display("Time=%0t: IDU: Decoded addi rd=%0d, rs1=%0d, imm=%0d",
$time, rd, rs1_addr, $signed(imm));
end
else begin
rf_wen = 1'b0; // 非addi指令或未知操作码
$display("Time=%0t: IDU: Not an addi instruction or unknown opcode", $time);
end
end
endmodule
// ALU执行单元模块定义
module ysyx_25110281_alu(
input [31:0] imm, // 立即数输入
input [31:0] rs1, // 源寄存器1数据输入
input [2:0] func3, // 功能码输入
input [6:0] op, // 操作码输入
input [4:0] rd, // 目标寄存器地址输入
input rf_wen, // 寄存器写使能输入
output reg [31:0] rf_wdata, // 寄存器写数据输出
output reg [4:0] rf_waddr, // 寄存器写地址输出
output reg [31:0] alu_out // ALU计算结果输出
);
// 组合逻辑:执行算术逻辑运算
always @(*) begin
// 检查是否为addi指令
if (op == 7'b0010011 && func3 == 3'b000) begin
// addi指令执行:rs1 + imm
rf_wdata = rs1 + imm; // 计算结果
// 如果写使能有效,设置写地址为目标寄存器,否则为0
rf_waddr = (rf_wen) ? rd : 5'b0;
// 将计算结果同时输出到alu_out
alu_out = rf_wdata;
// 打印调试信息
$display("Time=%0t: ALU: rs1=0x%08x (%0d), imm=0x%08x (%0d), result=0x%08x (%0d)",
$time, rs1, $signed(rs1), imm, $signed(imm), rf_wdata, $signed(rf_wdata));
end
else begin
// 非addi指令,输出为0
rf_wdata = 32'b0;
rf_waddr = 5'b0;
alu_out = 32'b0;
end
end
endmodule
main.cpp
#include "verilated.h"
#include "verilated_vcd_c.h"
#include "../obj_dir/Vtop.h"
VerilatedContext* contextp = NULL;
VerilatedVcdC* tfp = NULL;
static Vtop* top;
void step_and_dump_wave(){
top->eval();
contextp->timeInc(1);
tfp->dump(contextp->time());
}
void sim_init(){
contextp = new VerilatedContext;
tfp = new VerilatedVcdC;
top = new Vtop;
contextp->traceEverOn(true);
top->trace(tfp, 0);
tfp->open("waveform.vcd");
}
void sim_exit(){
step_and_dump_wave();
tfp->close();
}
int main() {
sim_init();
// 初始化(与Verilog测试完全一致)
top->clk = 0;
top->rst = 1;
// 复位周期(20ns后释放)
for (int i = 0; i < 4; i++) {
top->clk = !top->clk;
step_and_dump_wave();
}
top->rst = 0;
for (int i = 0; i < 20; i++) {
top->clk = !top->clk;
step_and_dump_wave();
}
sim_exit();
}
Makefile
# @echo "Write this Makefile by your self."
VSRC = $(wildcard ./vsrc/*.v)
CSRC = $(wildcard ./csrc/*.cpp)
all:
@echo "Write this Makefile by your self."
sim:
verilator -Wno-fatal $(VSRC) $(CSRC) --top-module top --cc --trace --exe
make -C obj_dir -f Vtop.mk Vtop
./obj_dir/Vtop
# gtkwave wave.vcd
.PHONY:clean
clean:
rm -rf obj_dir wave.vcd
终端输出
点击查看代码
Time=0: IDU: Not an addi instruction or unknown opcode
Time=2: IFU: pc=0x80000000, ins=0x00500093
Time=2: IDU: Decoded addi rd=1, rs1=0, imm=5
Time=2: ALU: rs1=0x00000000 (0), imm=0x00000005 (5), result=0x00000005 (5)
Time=4: IFU: pc=0x80000000, ins=0x00500093
Time=4: Write to reg[1] = 0x00000005
Time=4: IDU: Decoded addi rd=1, rs1=0, imm=5
Time=4: ALU: rs1=0x00000000 (0), imm=0x00000005 (5), result=0x00000005 (5)
Time=6: IFU: pc=0x80000004, ins=0x00a08113
Time=6: Write to reg[1] = 0x00000005
Time=6: IDU: Decoded addi rd=2, rs1=1, imm=10
Time=6: ALU: rs1=0x00000005 (5), imm=0x0000000a (10), result=0x0000000f (15)
Time=8: IFU: pc=0x80000004, ins=0x00a08113
Time=8: Write to reg[2] = 0x0000000f
Time=8: IDU: Decoded addi rd=2, rs1=1, imm=10
Time=8: ALU: rs1=0x00000005 (5), imm=0x0000000a (10), result=0x0000000f (15)
Time=10: IFU: pc=0x80000008, ins=0x00100113
Time=10: Write to reg[2] = 0x0000000f
Time=10: IDU: Decoded addi rd=2, rs1=0, imm=1
Time=10: ALU: rs1=0x00000000 (0), imm=0x00000001 (1), result=0x00000001 (1)
Time=12: IFU: pc=0x80000008, ins=0x00100113
Time=12: Write to reg[2] = 0x00000001
Time=12: IDU: Decoded addi rd=2, rs1=0, imm=1
Time=12: ALU: rs1=0x00000000 (0), imm=0x00000001 (1), result=0x00000001 (1)
Time=14: IFU: pc=0x8000000c, ins=0x00200113
Time=14: Write to reg[2] = 0x00000001
Time=14: IDU: Decoded addi rd=2, rs1=0, imm=2
Time=14: ALU: rs1=0x00000000 (0), imm=0x00000002 (2), result=0x00000002 (2)
Time=16: IFU: pc=0x8000000c, ins=0x00200113
Time=16: Write to reg[2] = 0x00000002
Time=16: IDU: Decoded addi rd=2, rs1=0, imm=2
Time=16: ALU: rs1=0x00000000 (0), imm=0x00000002 (2), result=0x00000002 (2)
Time=18: IFU: pc=0x80000010, ins=0x00508113
Time=18: Write to reg[2] = 0x00000002
Time=18: IDU: Decoded addi rd=2, rs1=1, imm=5
Time=18: ALU: rs1=0x00000005 (5), imm=0x00000005 (5), result=0x0000000a (10)
Time=20: IFU: pc=0x80000010, ins=0x00508113
Time=20: Write to reg[2] = 0x0000000a
Time=20: IDU: Decoded addi rd=2, rs1=1, imm=5
Time=20: ALU: rs1=0x00000005 (5), imm=0x00000005 (5), result=0x0000000a (10)
Time=22: IFU: pc=0x80000014, ins=0x00000013
Time=22: Write to reg[2] = 0x0000000a
Time=22: IDU: Decoded addi rd=0, rs1=0, imm=0
Time=22: ALU: rs1=0x00000000 (0), imm=0x00000000 (0), result=0x00000000 (0)
# gtkwave wave.vcd
波形

让程序决定仿真何时结束
尝试DPI-C机制
Verilator 是一款开源的 Verilog 仿真工具,可以将 Verilog 代码转化为 C / C++ 代码,继而编译成可执行文件,从而实现 Verilog 代码的仿真。Verilator 官方手册在这里。

通过DPI-C实现ebreak
在RTL代码中利用DPI-C机制, 使得在NPC执行ebreak指令的时候通知仿真环境结束仿真.实现后, 在存储器中放置一条ebreak指令来进行测试. 如果你的实现正确, 仿真环境就无需关心程序何时结束仿真了, 它只需要不停地进行仿真, 直到程序执行ebreak指令为止.
修改RTL代码,添加DPI-C调用
点击查看代码
// 顶层模块定义
module top(
input clk, // 时钟输入
input rst, // 复位信号输入
output reg[31:0] outdata // 32位数据输出寄存器
);
// 导入DPI-C函数
import "DPI-C" function void ebreak_handler(); // DPI-C函数声明
// 内部信号声明
reg [31:0] next_pc; // 下一个PC值寄存器
wire [6:0] op; // 操作码(7位)
wire [4:0] rd; // 目标寄存器地址(5位)
wire [2:0] func3; // 功能码(3位)
reg [31:0] pc; // 程序计数器寄存器(32位)
wire [31:0] ins; // 指令数据线(32位)
// 寄存器文件相关信号
wire [4:0] rs1_addr; // 源寄存器1地址线
wire [31:0] rs1_data; // 源寄存器1数据线
wire [31:0] imm; // 立即数数据线
wire rf_wen; // 寄存器文件写使能信号
wire [31:0] rf_wdata; // 寄存器文件写数据线
wire [4:0] rf_waddr; // 寄存器文件写地址线
// ALU输出信号线
wire [31:0] alu_out; // ALU计算结果输出线
// ebreak检测信号
wire is_ebreak; // ebreak指令检测信号
// 初始化块:设置初始值
initial begin
pc = 32'h8000_0000; // 初始化PC为0x80000000
next_pc = pc + 4; // 计算下一个PC值
outdata = 32'b0; // 初始化输出数据为0
end
// PC更新逻辑:时钟上升沿或复位时触发
always @(posedge clk or posedge rst) begin
// 复位处理
if(rst) begin
pc <= 32'h8000_0000; // 复位PC到初始值
next_pc <= pc + 4; // 复位下一个PC值
outdata <= 32'b0; // 复位输出数据
end
// 正常时钟沿处理
else begin
pc <= next_pc; // 更新PC为下一个PC值
next_pc <= pc + 4; // 计算下一个PC值(PC+4)
outdata <= alu_out; // 将ALU输出赋值给输出端口
// 检测到ebreak指令时调用DPI-C函数
if (is_ebreak) begin
$display("Time=%0t: Detected ebreak instruction, calling DPI-C handler", $time);
ebreak_handler(); // 调用DPI-C函数
end
end
end
// 取指单元实例化
ysyx_25110281_ifu ifu(
.pc(pc), // 连接PC到取指单元
.clk(clk), // 连接时钟信号
.ins(ins) // 连接指令输出
);
// 译码单元实例化
ysyx_25110281_idu idu(
.ins(ins), // 连接指令输入
.op(op), // 连接操作码输出
.rd(rd), // 连接目标寄存器地址输出
.func3(func3), // 连接功能码输出
.rs1_addr(rs1_addr), // 连接源寄存器1地址输出
.imm(imm), // 连接立即数输出
.rf_wen(rf_wen), // 连接寄存器写使能输出
.is_ebreak(is_ebreak) // 连接ebreak检测信号
);
// 寄存器文件实例化
RegisterFile #(
.ADDR_WIDTH(5), // 设置地址宽度为5位(32个寄存器)
.DATA_WIDTH(32) // 设置数据宽度为32位
) regfile (
.clk(clk), // 连接时钟信号
.wdata(rf_wdata), // 连接写数据
.waddr(rf_waddr), // 连接写地址
.wen(rf_wen), // 连接写使能
.raddr1(rs1_addr), // 连接读地址1
.rdata1(rs1_data) // 连接读数据1
);
// ALU执行单元实例化
ysyx_25110281_alu alu(
.imm(imm), // 连接立即数输入
.rs1(rs1_data), // 连接源寄存器1数据输入
.func3(func3), // 连接功能码输入
.op(op), // 连接操作码输入
.rd(rd), // 连接目标寄存器地址输入
.rf_wen(rf_wen), // 连接寄存器写使能输入
.rf_wdata(rf_wdata), // 连接寄存器写数据输出
.rf_waddr(rf_waddr), // 连接寄存器写地址输出
.alu_out(alu_out) // 连接ALU计算结果输出
);
endmodule
// 寄存器文件模块定义
module RegisterFile #(
parameter ADDR_WIDTH = 5, // 地址宽度参数,默认5位
parameter DATA_WIDTH = 32 // 数据宽度参数,默认32位
)(
input clk, // 时钟输入
input [DATA_WIDTH-1:0] wdata, // 写数据输入
input [ADDR_WIDTH-1:0] waddr, // 写地址输入
input wen, // 写使能输入
input [ADDR_WIDTH-1:0] raddr1, // 读地址1输入
output reg [DATA_WIDTH-1:0] rdata1 // 读数据1输出
);
// 寄存器存储阵列定义
reg [DATA_WIDTH-1:0] rf [0:(1<<ADDR_WIDTH)-1];
// 初始化块:清零所有寄存器
integer i;
initial begin
// 遍历所有寄存器并初始化为0
for(i = 0; i < (1<<ADDR_WIDTH); i = i + 1) begin
rf[i] = {DATA_WIDTH{1'b0}};
end
end
// 读端口1的组合逻辑
always @(*) begin
// 如果读地址为0,返回0(x0寄存器恒为0)
if(raddr1 == {ADDR_WIDTH{1'b0}}) begin
rdata1 = {DATA_WIDTH{1'b0}};
end
// 否则返回对应寄存器的值
else begin
rdata1 = rf[raddr1];
end
end
// 写端口的时序逻辑
always @(posedge clk) begin
// 当写使能有效且地址不为0时执行写操作
if(wen && (waddr != {ADDR_WIDTH{1'b0}})) begin
rf[waddr] <= wdata; // 将数据写入寄存器
// 打印调试信息
$display("Time=%0t: Write to reg[%0d] = 0x%08x", $time, waddr, wdata);
end
end
// 调试任务:打印所有寄存器值
task print_registers;
integer j;
begin
$display("\n--- Register File Contents ---");
// 遍历32个寄存器
for(j = 0; j < 32; j = j + 1) begin
$display("x%0d: 0x%08x", j, rf[j]);
end
$display("------------------------------\n");
end
endtask
endmodule
// 取指单元模块定义
module ysyx_25110281_ifu(
input [31:0] pc, // PC输入
input clk, // 时钟输入
output reg [31:0] ins // 指令输出
);
// 指令存储器阵列
reg [31:0] mem [0:15];
// 初始化指令存储器
initial begin
// 第0条指令:addi x1, x0, 5 (x1 = 0 + 5)
mem[0] = 32'b000000000101_00000_000_00001_0010011;
// 第1条指令:addi x2, x1, 10 (x2 = x1 + 10)
mem[1] = 32'b000000001010_00001_000_00010_0010011;
// 第2条指令:addi x2, x0, 1 (x2 = 0 + 1)
mem[2] = 32'b000000000001_00000_000_00010_0010011;
// 第3条指令:addi x2, x0, 2 (x2 = 0 + 2)
mem[3] = 32'b000000000010_00000_000_00010_0010011;
// 第4条指令:addi x2, x1, 5 (x2 = x1 + 5)
mem[4] = 32'b000000000101_00001_000_00010_0010011;
// 第5条指令:ebreak (00100073) - 用于结束仿真
mem[5] = 32'b000000000001_00000_000_00000_1110011;
// 剩余地址填充为nop指令
for (integer i = 6; i < 16; i = i + 1) begin
mem[i] = 32'b000000000000_00000_000_00000_0010011;
end
end
// 取指逻辑:时钟上升沿触发
always @(posedge clk) begin
// 检查PC地址是否在有效范围内
if (pc >= 32'h8000_0000 && pc < 32'h8000_0040) begin
// 计算内存索引并读取指令
ins = mem[(pc - 32'h8000_0000) >> 2];
// 打印调试信息
$display("Time=%0t: IFU: pc=0x%08x, ins=0x%08x", $time, pc, ins);
end
else begin
// PC地址无效,输出0
ins = 32'b0;
$display("Time=%0t: IFU: Invalid PC address: 0x%08x", $time, pc);
end
end
endmodule
// 译码单元模块定义
module ysyx_25110281_idu(
input [31:0] ins, // 指令输入
output reg [6:0] op, // 操作码输出
output reg [4:0] rd, // 目标寄存器地址输出
output reg [2:0] func3, // 功能码输出
output reg [4:0] rs1_addr, // 源寄存器1地址输出
output reg [31:0] imm, // 立即数输出
output reg rf_wen, // 寄存器写使能输出
output reg is_ebreak // ebreak指令检测输出
);
// 组合逻辑:随时对指令进行译码
always @(*) begin
// 解析指令各字段
op = ins[6:0]; // 提取操作码(最低7位)
rd = ins[11:7]; // 提取目标寄存器地址(位7-11)
func3 = ins[14:12]; // 提取功能码(位12-14)
rs1_addr = ins[19:15]; // 提取源寄存器1地址(位15-19)
// 检测ebreak指令 (opcode=1110011, func3=000, imm12=1)
// ebreak指令格式:000000000001 00000 000 00000 1110011
if (op == 7'b1110011 && func3 == 3'b000 && ins[31:20] == 12'b000000000001) begin
is_ebreak = 1'b1; // 设置ebreak检测标志
$display("Time=%0t: IDU: Detected ebreak instruction", $time);
end
else begin
is_ebreak = 1'b0; // 不是ebreak指令
end
// I-type立即数符号扩展
if (ins[31] == 1'b1) begin
// 最高位为1,进行符号扩展为负数
imm = {20'hFFFFF, ins[31:20]};
end
else begin
// 最高位为0,扩展为0
imm = {20'b0, ins[31:20]};
end
// 判断是否为addi指令(opcode=0010011, func3=000)
if (op == 7'b0010011 && func3 == 3'b000) begin
rf_wen = 1'b1; // addi指令需要写寄存器
// 打印调试信息
$display("Time=%0t: IDU: Decoded addi rd=%0d, rs1=%0d, imm=%0d",
$time, rd, rs1_addr, $signed(imm));
end
else begin
rf_wen = 1'b0; // 非addi指令或未知操作码
if (!is_ebreak) begin // 如果不是ebreak指令才打印
$display("Time=%0t: IDU: Not an addi instruction or unknown opcode", $time);
end
end
end
endmodule
// ALU执行单元模块定义
module ysyx_25110281_alu(
input [31:0] imm, // 立即数输入
input [31:0] rs1, // 源寄存器1数据输入
input [2:0] func3, // 功能码输入
input [6:0] op, // 操作码输入
input [4:0] rd, // 目标寄存器地址输入
input rf_wen, // 寄存器写使能输入
output reg [31:0] rf_wdata, // 寄存器写数据输出
output reg [4:0] rf_waddr, // 寄存器写地址输出
output reg [31:0] alu_out // ALU计算结果输出
);
// 组合逻辑:执行算术逻辑运算
always @(*) begin
// 检查是否为addi指令
if (op == 7'b0010011 && func3 == 3'b000) begin
// addi指令执行:rs1 + imm
rf_wdata = rs1 + imm; // 计算结果
// 如果写使能有效,设置写地址为目标寄存器,否则为0
rf_waddr = (rf_wen) ? rd : 5'b0;
// 将计算结果同时输出到alu_out
alu_out = rf_wdata;
// 打印调试信息
$display("Time=%0t: ALU: rs1=0x%08x (%0d), imm=0x%08x (%0d), result=0x%08x (%0d)",
$time, rs1, $signed(rs1), imm, $signed(imm), rf_wdata, $signed(rf_wdata));
end
else begin
// 非addi指令,输出为0
rf_wdata = 32'b0;
rf_waddr = 5'b0;
alu_out = 32'b0;
end
end
endmodule
创建DPI-C接口文件
// csrc/dpi_interface.cpp
#include <iostream>
#include <cstdlib>
#ifdef __cplusplus
extern "C" {
#endif
// DPI-C函数实现
void ebreak_handler() {
std::cout << "=========================================" << std::endl;
std::cout << "EBREAK instruction executed!" << std::endl;
std::cout << "Simulation will now terminate gracefully." << std::endl;
std::cout << "=========================================" << std::endl;
// 退出仿真
exit(0);
}
#ifdef __cplusplus
}
#endif
修改主测试文件以支持DPI-C
// csrc/main.cpp
#include "Vtop.h"
#include "verilated.h"
#include "verilated_vcd_c.h"
#include <iostream>
// DPI-C函数声明
extern "C" void ebreak_handler();
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Vtop* top = new Vtop;
// 启用波形跟踪
Verilated::traceEverOn(true);
VerilatedVcdC* tfp = new VerilatedVcdC;
top->trace(tfp, 99);
tfp->open("waveform.vcd");
// 初始化
top->clk = 0;
top->rst = 1;
std::cout << "Starting simulation..." << std::endl;
std::cout << "Program will run until ebreak instruction is executed." << std::endl;
// 应用复位
for (int i = 0; i < 5; i++) {
top->clk = !top->clk;
top->eval();
tfp->dump(10 * i);
}
// 释放复位
top->rst = 0;
int cycle_count = 0;
const int MAX_CYCLES = 50; // 最大仿真周期,防止无限循环
// 运行仿真,直到ebreak指令被执行或达到最大周期
while (cycle_count < MAX_CYCLES) {
top->clk = !top->clk;
top->eval();
tfp->dump(100 + 10 * cycle_count);
// 打印输出(在时钟上升沿后)
if (top->clk == 1) {
std::cout << "Cycle " << cycle_count/2 << ": outdata = 0x"
<< std::hex << top->outdata << std::dec << std::endl;
}
cycle_count++;
// 检查Verilator是否要求退出(当ebreak_handler调用exit()时)
if (Verilated::gotFinish()) {
std::cout << "Verilator received finish request." << std::endl;
break;
}
}
if (cycle_count >= MAX_CYCLES) {
std::cout << "Warning: Simulation reached maximum cycles without ebreak." << std::endl;
}
// 清理
tfp->close();
delete top;
delete tfp;
std::cout << "Simulation completed!" << std::endl;
return 0;
}
修改Makefile以链接DPI-C文件
# Makefile
VERILATOR = verilator
VERILATOR_FLAGS = -Wno-fatal --top-module top --cc --trace --exe
VERILATOR_FLAGS += -CFLAGS "-I./csrc" -LDFLAGS "-L./csrc"
VERILATOR_FLAGS += --build
# 源文件
VERILOG_SRC = ./vsrc/top.v
CPP_SRC = ./csrc/main.cpp ./csrc/dpi_interface.cpp
# 目标
all: sim
# 编译
sim:
$(VERILATOR) $(VERILATOR_FLAGS) $(VERILOG_SRC) $(CPP_SRC) -o Vtop
./obj_dir/Vtop
# 清理
clean:
rm -rf obj_dir waveform.vcd
.PHONY: all sim clean
实现要点说明
-
DPI-C函数声明:在Verilog中使用import "DPI-C" function void ebreak_handler();声明C函数
-
ebreak指令检测:
在IDU译码单元中添加ebreak指令检测逻辑 ebreak指令编码:opcode=7'b1110011,func3=3'b000,imm12=12'b000000000001 -
DPI-C调用时机:
在顶层模块的时钟沿always块中检测到ebreak信号时调用DPI-C函数 这样可以确保在指令执行周期结束时才结束仿真 -
C++端实现:
实现ebreak_handler()函数,打印相关信息并调用exit(0)结束程序 这样当RTL执行到ebreak指令时,会自动通知仿真环境结束 -
测试程序:
在指令存储器中添加一条ebreak指令(第6条指令) 处理器会顺序执行前5条addi指令,然后执行ebreak指令结束仿真
这种实现方式使得仿真环境无需预先知道程序何时结束,只需持续运行直到程序执行ebreak指令,这更接近真实硬件的行为。
终端输出
点击查看代码
Time=0: IDU: Not an addi instruction or unknown opcode
Time=2: IFU: pc=0x80000000, ins=0x00500093
Time=2: IDU: Decoded addi rd=1, rs1=0, imm=5
Time=2: ALU: rs1=0x00000000 (0), imm=0x00000005 (5), result=0x00000005 (5)
Time=4: IFU: pc=0x80000000, ins=0x00500093
Time=4: Write to reg[1] = 0x00000005
Time=4: IDU: Decoded addi rd=1, rs1=0, imm=5
Time=4: ALU: rs1=0x00000000 (0), imm=0x00000005 (5), result=0x00000005 (5)
Time=6: IFU: pc=0x80000004, ins=0x00a08113
Time=6: Write to reg[1] = 0x00000005
Time=6: IDU: Decoded addi rd=2, rs1=1, imm=10
Time=6: ALU: rs1=0x00000005 (5), imm=0x0000000a (10), result=0x0000000f (15)
Time=8: IFU: pc=0x80000004, ins=0x00a08113
Time=8: Write to reg[2] = 0x0000000f
Time=8: IDU: Decoded addi rd=2, rs1=1, imm=10
Time=8: ALU: rs1=0x00000005 (5), imm=0x0000000a (10), result=0x0000000f (15)
Time=10: IFU: pc=0x80000008, ins=0x00100113
Time=10: Write to reg[2] = 0x0000000f
Time=10: IDU: Decoded addi rd=2, rs1=0, imm=1
Time=10: ALU: rs1=0x00000000 (0), imm=0x00000001 (1), result=0x00000001 (1)
Time=12: IFU: pc=0x80000008, ins=0x00100113
Time=12: Write to reg[2] = 0x00000001
Time=12: IDU: Decoded addi rd=2, rs1=0, imm=1
Time=12: ALU: rs1=0x00000000 (0), imm=0x00000001 (1), result=0x00000001 (1)
Time=14: IFU: pc=0x8000000c, ins=0x00200113
Time=14: Write to reg[2] = 0x00000001
Time=14: IDU: Decoded addi rd=2, rs1=0, imm=2
Time=14: ALU: rs1=0x00000000 (0), imm=0x00000002 (2), result=0x00000002 (2)
Time=16: IFU: pc=0x8000000c, ins=0x00200113
Time=16: Write to reg[2] = 0x00000002
Time=16: IDU: Decoded addi rd=2, rs1=0, imm=2
Time=16: ALU: rs1=0x00000000 (0), imm=0x00000002 (2), result=0x00000002 (2)
Time=18: IFU: pc=0x80000010, ins=0x00508113
Time=18: Write to reg[2] = 0x00000002
Time=18: IDU: Decoded addi rd=2, rs1=1, imm=5
Time=18: ALU: rs1=0x00000005 (5), imm=0x00000005 (5), result=0x0000000a (10)
Time=20: IFU: pc=0x80000010, ins=0x00508113
Time=20: Write to reg[2] = 0x0000000a
Time=20: IDU: Decoded addi rd=2, rs1=1, imm=5
Time=20: ALU: rs1=0x00000005 (5), imm=0x00000005 (5), result=0x0000000a (10)
Time=22: IFU: pc=0x80000014, ins=0x00100073
Time=22: Write to reg[2] = 0x0000000a
Time=22: IDU: Detected ebreak instruction
Time=24: Detected ebreak instruction, calling DPI-C handler
=========================================
EBREAK instruction executed!
Simulation will now terminate gracefully.
=========================================

单周期处理器是一种计算机处理器设计模型,其特点是每个指令在一个时钟周期内完成所有执行步骤。这种设计简化了控制逻辑,但时钟频率受限于最慢指令的执行时间。
浙公网安备 33010602011771号