RISC-V设计之Decoder的封装与函数(二)

RISC-V设计之封装与函数(SV)

写在前面:今天去见了导师,他强烈要求我把设计中的decoder删去,去掉宏定义引入局部变量,使用封装的函数来取而代之。并在其他运算模块调用函数的返回值,提高代码简洁度和清晰度,避免全局变量污染环境,下面是根据导师的主页总结的设计笔记。-----2025/7/1

  • 示例代码:这个 opcodes 包是为一个简单的处理器设计的辅助模块,作用是封装指令解析相关的功能,供 CPU 的其他模块调用,比如alu。可以把它当作“指令解释器”:外部模块不需要知道一条 16 位指令的结构细节,只需要调用这里提供的函数即可获取操作码、操作数、ALU 操作类型等信息。
// This package knows about the instruction coding
// the rest of the processor accesses this information by calling functions

package opcodes;

// Define ALU Operations Type:
//
typedef 
  enum logic [3:0] { aluACC, aluMem, aluADD, aluSUB, aluAND, aluOR, aluNOT, aluLSL, aluLSR }
  alu_operation_t;

// Define Opcodes:
localparam NOP		= 4'd0;
localparam JMP		= 4'd1;
localparam JMPZ		= 4'd2;
localparam JMPNZ	= 4'd3;
localparam LDA		= 4'd4;
localparam ADD		= 4'd5;
localparam SUB		= 4'd6;
localparam AND		= 4'd7;
localparam OR		= 4'd8;
localparam NOT		= 4'd9;
localparam LSL		= 4'd10;
localparam LSR		= 4'd11;
localparam STA		= 4'd15;

//
// Extract opcode and operand from instruction
//
   function [11:0] get_operand(input [15:0] instruction);
      return instruction[11:0];
   endfunction

   function [3:0] get_opcode(input [15:0] instruction);
      return instruction[15:12];
   endfunction

   function is_store(input [15:0] instruction);
      return (get_opcode(instruction) == STA);
   endfunction

   function is_unconditional_jump(input [15:0] instruction);
      return (get_opcode(instruction) == JMP);
   endfunction

   function is_conditional_jump(input [15:0] instruction);
      return ((get_opcode(instruction) == JMPZ) || (get_opcode(instruction) == JMPNZ));
   endfunction

   function expected_flag(input [15:0] instruction);
      return (get_opcode(instruction) == JMPZ);
   endfunction

//
// Decode Opcode to create ALU Function code
//
   function alu_operation_t get_alu_operation(input [15:0] instruction);
      case (get_opcode(instruction))
         LDA		: return aluMem;
         ADD		: return aluADD;
         SUB		: return aluSUB;
         AND		: return aluAND;
         OR		: return aluOR;
         NOT		: return aluNOT;
         LSL		: return aluLSL;
         LSR		: return aluLSR;
         default	: return aluACC;
      endcase
   endfunction


endpackage

- 如何在其他模块中使用?

步骤一:引入包

import opcodes::*;

步骤二:使用函数

例如,在你的 CPU 控制单元中:

always_comb begin
   logic [15:0] instr = instruction_register;

   if (is_unconditional_jump(instr)) begin
      pc = get_operand(instr);
   end

   alu_op = get_alu_operation(instr);
end

这样你就不需要手动位提取指令中的字段,而是使用包内封装好的接口函数,代码更清晰,复用性强。

  • each control signal is a function !!!

  • 目的是为运算表达式提供返回值,便于简化代码和维护大型代码。

  • import opcodes::*;
    
  • 从 opcodes.svh 中导入全部定义(枚举类型、函数、局部参数等)。

    • 表示导入所有内容,相当于 using namespace 的意思。

    • 什么时候使用typedef enum? 什么时候使用localparam 什么时候使用package ?什么时候使用宏定义呢?(下面是一些使用场景)

    • 场景 推荐用法 原因
      枚举、状态码、类型分类(立即数类型、状态机) typedef enum 更清晰、类型安全、IDE 支持好
      单个有名称的常数(立即数宽度、总线宽度) localparam 模块内部常量,稳定安全
      跨文件复用的常量值(硬件标准中定义的) localparam + package 替代宏定义更安全
      条件编译(例如仿真开关、版本切换) \define` 宏定义 宏能控制编译行为
    • 那么思考下一个问题,为啥不推荐 SystemVerilog 中用宏定义 (\define`) 来定义常量?

虽然可以写:


`define IMM_NONE_R 3'd0

但这样做有这些问题:

  • 宏没有作用域(会污染全局),容易冲突;
  • 宏没有类型(不参与类型检查);
  • 调试时无法在 waveform 中显示名字,只看到值;
  • 不支持自动补全,不容易管理。
写法对比:
enum(推荐,适合类型分类):
typedef enum logic [2:0] {
  IMM_I = 3'd1, IMM_S = 3'd2
} imm_type_e;
localparam(推荐,适合常量):
localparam int XLEN = 32;
localparam logic [3:0] ALU_ADD = 4'd0;
只在必要时用 \define`(比如编译开关):
`define ENABLE_DEBUG
`ifdef ENABLE_DEBUG
  $display("Debug mode");
`endif

总结

  • 分类枚举 ➜ 用 typedef enum(像 imm_type_e, alu_op_t

  • 模块内部常量 ➜ 用 localparam

  • 跨模块常量 ➜ 用 localparam + package:把多个 localparam 常量集中写到一个 package(包)里,然后在其他模块中 import 这个包,就能像使用本地变量一样使用这些常量。

  • 为什么这样写好?
    传统写法 (\define`) localparam + package 好处
    \define ALU_ADD 4’d0` 有作用域(包),不污染全局命名空间
    所有模块共享一个全局命名 每个包管理自己的命名,更安全
    没有类型检查 localparam logic [3:0] 有类型
    调试中只看到数字不知含义 名称保留,有利调试和 IDE 提示
  • 更改示例,下面是根据导师的修改建议我修改decoder内部控制信号的过程展示。以下是原始代码:

    // Immediate type enum for decoding purposes
    typedef enum logic [2:0] {
        IMM_NONE_R = 3'd0, // R-type: no immediate field
        IMM_I    = 3'd1, // I-type: arithmetic immediate, load, JALR
        IMM_S    = 3'd2, // S-type: store instructions
        IMM_B    = 3'd3, // B-type: branch instructions
        IMM_U    = 3'd4, // U-type: upper immediate (LUI, AUIPC)
        IMM_J    = 3'd5  // J-type: jump and link (JAL)
    } imm_type_e;
    
    imm_type_e imm_type_sel;
    assign imm_type = imm_type_sel; // output to datapath or control unit
    // Decode opcode to determine the immediate format
    always_comb begin
        unique case (opcode)
            7'b0110011: imm_type_sel = IMM_NONE_R; // R-type: register-register (e.g. ADD, SUB)
    
            7'b0010011,                          // I-type: immediate ALU ops (e.g. ADDI)
            7'b0000011,                          // I-type: load (e.g. LW)
            7'b1100111: imm_type_sel = IMM_I;    // I-type: JALR (jump and link register)
            7'b0100011: imm_type_sel = IMM_S;    // S-type: store (e.g. SW, SH, SB)
            7'b1100011: imm_type_sel = IMM_B;    // B-type: branch (e.g. BEQ, BNE)
            7'b0110111,                          // U-type: LUI (load upper immediate)
            7'b0010111: imm_type_sel = IMM_U;    // U-type: AUIPC (add upper immediate to PC)
            7'b1101111: imm_type_sel = IMM_J;    // J-type: JAL (jump and link)
            default:    imm_type_sel = IMM_NONE_R; // Default: treat as no immediate (safe fallback)
        endcase
    end
    
  • 修改后的代码:

    • function automatic 代表函数可重入(reentrant),每次调用函数的时候,都会新建一个局部变量副本,可以在多个线程中同时使用,不会出现冲突或者覆盖的情况。 相比于静态的function(static),它拥有自己的栈空间,线程安全
    • 返回值为imm_type_e 我设定的枚举类型
    • 不依赖时钟、不存储历史状态。组合逻辑
//Tmmediate type decoder
function automatic imm_type_e get_imm_type (input logic [6:0] opcode);
    case (opcode)
        7'b0110011: return IMM_NONE_R; // R-type: register-register (e.g. ADD, SUB)
        7'b0010011,                          // I-type: immediate ALU ops (e.g. ADDI)
        7'b0000011,                          // I-type: load (e.g. LW)
        7'b1100111: return IMM_I;    // I-type: JALR (jump and link register)
        7'b0100011: return IMM_S;    // S-type: store (e.g. SW, SH, SB)
        7'b1100011: return IMM_B;    // B-type: branch (e.g. BEQ, BNE)
        7'b0110111,                          // U-type: LUI (load upper immediate)
        7'b0010111: return IMM_U;    // U-type: AUIPC (add upper immediate to PC)
        7'b1101111: return IMM_J;    // J-type: JAL (jump and link)
        default:    return IMM_NONE_R; // Default: treat as no immediate (safe fallback)
    endcase
endfunction
  • 总结:SystemVerilog 函数的返回值可以是:
    类型 示例
    位宽信号 logic [31:0], logic [3:0]
    布尔 logic//return (opcode == 7’b1100011); // BEQ, BNE, etc.
    整型 int, integer
    枚举类型 imm_type_e
    结构体 reg_fields_t
    数组 logic [3:0][7:0]
  • 那么啥是结构体呢?
    ----结构体知识补充:

    typedef struct 就是把一组相关的***数据“打包”***成一个结构体,用起来更方便、更安全,是 SystemVerilog 面向系统设计的关键特性之一。(不是输出一堆线,是把多个变量打包成一个结构体处理)

struct reg_fields_t {
  uint5_t rs1, rs2, rd;
};

然后函数返回这个结构体:

function automatic reg_fields_t decode_regs(input logic [31:0] instr);
  decode_regs.rs1 = instr[19:15];
  decode_regs.rs2 = instr[24:20];
  decode_regs.rd  = instr[11:7];
endfunction
//调用
assign rs1 = decode_regs(instr).rs1
  • function [return_type] function_name ([input arguments]);

    function_name 是函数本身的名字;

    函数返回值直接通过 return 语句返回;

    返回值的名字只能是函数名本身(它在函数体内部也当作一个隐式变量使用);

    不能给返回值再起一个单独名字

或也可以写成这样,不用 return,而是给函数名赋值(函数名是隐式的返回变量):

function automatic logic [31:0] get_imm(input logic [31:0] instr, input imm_type_e imm_type);
  case (imm_type)
    IMM_I: get_imm = {{20{instr[31]}}, instr[31:20]};
    ...
  endcase
endfunction

如果你希望函数调用更清晰,也可以用中间变量来接收返回值,例如:

logic [31:0] imm;
imm = get_imm(instr, imm_type);
  • ctrl+F2 批量重命名 或者右键(vscode)

  • 使用返回值为结构体的函数来定义pc_control 的信号(多个变量打包放在一起)

    typedef struct packed {

    } struct_name;

  • 关于是否直接用函数名作为隐式返回变量
  • 当前写法(标准写法)
    systemverilog复制编辑function automatic pc_ctrl_t get_pc_ctrl(input logic [6:0] opcode);
      pc_ctrl_t ctrl;
      ctrl.pc_relbranch = ...;
      ...
      return ctrl; // 推荐做法,明确返回值
    endfunction
    

    替代写法:用函数名作为隐式返回变量

    SystemVerilog 允许这么写:

    systemverilog复制编辑function automatic pc_ctrl_t get_pc_ctrl(input logic [6:0] opcode);
      get_pc_ctrl.pc_relbranch = (opcode == 7'b1101111 || opcode == 7'b1100011);
      get_pc_ctrl.pc_absbranch = (opcode == 7'b1100111);
      get_pc_ctrl.pc_incr = ~(get_pc_ctrl.pc_relbranch || get_pc_ctrl.pc_absbranch);
    endfunction
    
    说明:
    • get_pc_ctrl 在函数内部相当于一个自动创建的结构体变量,表示返回值;
    • 你给它字段赋值,相当于“构造”这个返回值;
    • 函数结束时它会自动返回,不需要写 return

以下代码片段展示了我对pc_control信号的两种写法展示:

//Control signal struct for PC
typedef struct packed {
    logic pc_incr,
    logic pc_relbranch,
    logic pc_absbranch
} pc_control_fields_t;
function automatic pc_control_fields_t get_pc_control (input [6:0] opcode);
    // 1.pc_control_fields_t ctrl;
    // ctrl.pc_incr = ...;
    //return ctrl
    //2.
    get_pc_control.pc_incr = ~ (get_pc_control.pc_absbranch || get_pc_control.pc_relbranch);
    get_pc_control.pc_absbranch = (opcode == 7'b1101111);
    get_pc_control.pc_relbranch = (opcode == 7'b1101111 || opcode == 7'b1100011)
endfunction

你可能感兴趣的:(RISC-V设计之Decoder的封装与函数(二))