单周期处理器的架构

📚 使用须知

  • 本博客内容仅供学习参考
  • 建议理解思路后独立实现
  • 欢迎交流讨论

task : 单周期处理器的架构

理解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输出指令)

寄存器文件(时钟上升沿写入,组合逻辑读)、立即数生成器(组合逻辑)

加法器(组合逻辑)、控制单元(根据指令opcodefunct3等产生控制信号,这里只有addi,所以可以简化)

连接:
PC输出到指令存储器地址输入;指令存储器的输出拆解为:opcode, rd, rs1, imm等。

rs1连接到寄存器文件的读地址1,寄存器文件读数据1输出;imm经过立即数生成器得到32位立即数。

寄存器读数据1和立即数输入加法器,加法器输出结果;加法器输出连接到寄存器文件的写数据输入。

rd连接到寄存器文件的写地址;控制单元根据opcodefunct3产生寄存器写使能(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

task : 在NPC中实现addi指令

在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 官方手册在这里

Verilator 使用指南

image

通过DPI-C实现ebreak

task : 通过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

实现要点说明

  1. DPI-C函数声明:在Verilog中使用import "DPI-C" function void ebreak_handler();声明C函数

  2. ebreak指令检测:

     在IDU译码单元中添加ebreak指令检测逻辑
    
     ebreak指令编码:opcode=7'b1110011,func3=3'b000,imm12=12'b000000000001
    
  3. DPI-C调用时机:

      在顶层模块的时钟沿always块中检测到ebreak信号时调用DPI-C函数
    
      这样可以确保在指令执行周期结束时才结束仿真
    
  4. C++端实现:

      实现ebreak_handler()函数,打印相关信息并调用exit(0)结束程序
    
      这样当RTL执行到ebreak指令时,会自动通知仿真环境结束
    
  5. 测试程序:

      在指令存储器中添加一条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.
=========================================
posted @ 2025-12-03 16:30  mo686  阅读(9)  评论(0)    收藏  举报