core-v-verif系列之cv32e40p UVM环境介绍<18>

UVM环境介绍
HEAD commitID: 1f968ef

1. tb/core/uvmt/uvmt_cv32e40p_dut_wrap.sv



`ifndef __UVMT_CV32E40P_DUT_WRAP_SV__
`define __UVMT_CV32E40P_DUT_WRAP_SV__


/**
 * Module wrapper for CV32E40P RTL DUT.
 */
module uvmt_cv32e40p_dut_wrap #(
                            // CV32E40P parameters.  See User Manual.
                            parameter PULP_XPULP          =  0,
                                      PULP_CLUSTER        =  0,
                                      FPU                 =  0,
                                      PULP_ZFINX          =  0,
                                      NUM_MHPMCOUNTERS    =  1,
                            // Remaining parameters are used by TB components only
                                      INSTR_ADDR_WIDTH    =  32,
                                      INSTR_RDATA_WIDTH   =  32,
                                      RAM_ADDR_WIDTH      =  22
                           )

                           (
                            uvma_clknrst_if              clknrst_if,
                            uvma_interrupt_if            interrupt_if,
                            // vp_status_if is driven by ENV and used in TB
                            uvma_interrupt_if            vp_interrupt_if,
                            uvmt_cv32e40p_core_cntrl_if  core_cntrl_if,
                            uvmt_cv32e40p_core_status_if core_status_if,
                            uvma_obi_memory_if           obi_memory_instr_if,
                            uvma_obi_memory_if           obi_memory_data_if
                           );

    import uvm_pkg::*; // needed for the UVM messaging service (`uvm_info(), etc.)

    // signals connecting core to memory
    logic                         instr_req;
    logic                         instr_gnt;
    logic                         instr_rvalid;
    logic [INSTR_ADDR_WIDTH-1 :0] instr_addr;
    logic [INSTR_RDATA_WIDTH-1:0] instr_rdata;

    logic                         data_req;
    logic                         data_gnt;
    logic                         data_rvalid;
    logic [31:0]                  data_addr;
    logic                         data_we;
    logic [3:0]                   data_be;
    logic [31:0]                  data_rdata;
    logic [31:0]                  data_wdata;

    logic [31:0]                  irq_vp;
    logic [31:0]                  irq_uvma;
    logic [31:0]                  irq;
    logic                         irq_ack;
    logic [ 4:0]                  irq_id;

    logic                         debug_req_vp;
    logic                         debug_req_uvma;
    logic                         debug_req;
    logic                         debug_havereset;
    logic                         debug_running;
    logic                         debug_halted;

    assign debug_if.clk      = clknrst_if.clk;
    assign debug_if.reset_n  = clknrst_if.reset_n;
    assign debug_req_uvma    = debug_if.debug_req;

    assign debug_req = debug_req_vp | debug_req_uvma;

        

    // --------------------------------------------
    // Instruction bus is read-only, OBI v1.0
    assign obi_memory_instr_if.we        = 'b0;
    assign obi_memory_instr_if.be        = '1;
    // Data bus is read/write, OBI v1.0

    // --------------------------------------------
    // Connect to uvma_interrupt_if
    assign interrupt_if.clk                     = clknrst_if.clk;
    assign interrupt_if.reset_n                 = clknrst_if.reset_n;
    assign irq_uvma                             = interrupt_if.irq;
    assign vp_interrupt_if.clk                  = clknrst_if.clk;
    assign vp_interrupt_if.reset_n              = clknrst_if.reset_n;
    assign irq_vp                               = vp_interrupt_if.irq;
    assign interrupt_if.irq_id                  = irq_id;
    assign interrupt_if.irq_ack                 = irq_ack;

    assign irq = irq_uvma | irq_vp;

    // --------------------------------------------
    // instantiate the core
    cv32e40p_wrapper #(
                 .PULP_XPULP       (PULP_XPULP),
                 .PULP_CLUSTER     (PULP_CLUSTER),
                 .FPU              (FPU),
                 .PULP_ZFINX       (PULP_ZFINX),
                 .NUM_MHPMCOUNTERS (NUM_MHPMCOUNTERS)
                )
    cv32e40p_wrapper_i
        (
         .clk_i                  ( clknrst_if.clk                 ),
         .rst_ni                 ( clknrst_if.reset_n             ),

         .pulp_clock_en_i        ( core_cntrl_if.pulp_clock_en    ),
         .scan_cg_en_i           ( core_cntrl_if.scan_cg_en       ),

         .boot_addr_i            ( core_cntrl_if.boot_addr        ),
         .mtvec_addr_i           ( core_cntrl_if.mtvec_addr       ),
         .dm_halt_addr_i         ( core_cntrl_if.dm_halt_addr     ),
         .hart_id_i              ( core_cntrl_if.hart_id          ),
         .dm_exception_addr_i    ( core_cntrl_if.dm_exception_addr),

         .instr_req_o            ( obi_memory_instr_if.req        ), // core to agent
         .instr_gnt_i            ( obi_memory_instr_if.gnt        ), // agent to core
         .instr_rvalid_i         ( obi_memory_instr_if.rvalid     ),
         .instr_addr_o           ( obi_memory_instr_if.addr       ),
         .instr_rdata_i          ( obi_memory_instr_if.rdata      ),

         .data_req_o             ( obi_memory_data_if.req         ),
         .data_gnt_i             ( obi_memory_data_if.gnt         ),
         .data_rvalid_i          ( obi_memory_data_if.rvalid      ),
         .data_we_o              ( obi_memory_data_if.we          ),
         .data_be_o              ( obi_memory_data_if.be          ),
         .data_addr_o            ( obi_memory_data_if.addr        ),
         .data_wdata_o           ( obi_memory_data_if.wdata       ),
         .data_rdata_i           ( obi_memory_data_if.rdata       ),

         // APU not verified in cv32e40p (future work)
         .apu_req_o              (                                ),
         .apu_gnt_i              ( 1'b0                           ),
         .apu_operands_o         (                                ),
         .apu_op_o               (                                ),
         .apu_flags_o            (                                ),
         .apu_rvalid_i           ( 1'b0                           ),
         .apu_result_i           ( {32{1'b0}}                     ),
         .apu_flags_i            ( {5{1'b0}}                      ), // APU_NUSFLAGS_CPU

         .irq_i                  ( irq_uvma                       ),
         .irq_ack_o              ( irq_ack                        ),
         .irq_id_o               ( irq_id                         ),

         .debug_req_i            ( debug_req_uvma                 ),
         .debug_havereset_o      ( debug_havereset                ),
         .debug_running_o        ( debug_running                  ),
         .debug_halted_o         ( debug_halted                   ),

         .fetch_enable_i         ( core_cntrl_if.fetch_en         ),
         .core_sleep_o           ( core_status_if.core_busy       )
        ); // cv32e40p_wrapper_i


endmodule : uvmt_cv32e40p_dut_wrap

`endif // __UVMT_CV32E40P_DUT_WRAP_SV__



1. 文件概述

uvmt_cv32e40p_dut_wrap.sv 是一个 SystemVerilog 文件,其主要功能是作为 CV32E40P RTL(寄存器传输级)设计的封装模块,用于测试平台(TB)环境。该文件对 CV32E40P 核心、内存以及一些虚拟外设进行封装,并且处理了这些组件之间的信号连接。

2. 版权和许可信息

文件开头包含了版权声明以及遵循 Solderpad Hardware Licence 和 Apache License 2.0 的许可信息,这表明文件的使用和分发需要遵循这些开源许可证的规定。

3. 模块定义

module uvmt_cv32e40p_dut_wrap #(
    // CV32E40P parameters.  See User Manual.
    parameter PULP_XPULP          =  0,
              PULP_CLUSTER        =  0,
              FPU                 =  0,
              PULP_ZFINX          =  0,
              NUM_MHPMCOUNTERS    =  1,
    // Remaining parameters are used by TB components only
              INSTR_ADDR_WIDTH    =  32,
              INSTR_RDATA_WIDTH   =  32,
              RAM_ADDR_WIDTH      =  22
)
(
    uvma_clknrst_if              clknrst_if,
    uvma_interrupt_if            interrupt_if,
    // vp_status_if is driven by ENV and used in TB
    uvma_interrupt_if            vp_interrupt_if,
    uvmt_cv32e40p_core_cntrl_if  core_cntrl_if,
    uvmt_cv32e40p_core_status_if core_status_if,
    uvma_obi_memory_if           obi_memory_instr_if,
    uvma_obi_memory_if           obi_memory_data_if
);
  • 参数部分:定义了一系列参数,这些参数有的与 CV32E40P 核心的配置相关(如 PULP_XPULPFPU 等),有的则是供测试平台组件使用(如 INSTR_ADDR_WIDTHRAM_ADDR_WIDTH 等)。
  • 端口部分:包含多个接口类型的端口,这些接口用于与时钟复位、中断、核心控制、核心状态以及内存访问等功能模块进行连接。

4. 信号声明

// signals connecting core to memory
logic                         instr_req;
logic                         instr_gnt;
logic                         instr_rvalid;
logic [INSTR_ADDR_WIDTH-1 :0] instr_addr;
logic [INSTR_RDATA_WIDTH-1:0] instr_rdata;

logic                         data_req;
logic                         data_gnt;
logic                         data_rvalid;
logic [31:0]                  data_addr;
logic                         data_we;
logic [3:0]                   data_be;
logic [31:0]                  data_rdata;
logic [31:0]                  data_wdata;

logic [31:0]                  irq_vp;
logic [31:0]                  irq_uvma;
logic [31:0]                  irq;
logic                         irq_ack;
logic [ 4:0]                  irq_id;

logic                         debug_req_vp;
logic                         debug_req_uvma;
logic                         debug_req;
logic                         debug_havereset;
logic                         debug_running;
logic                         debug_halted;

声明了一系列逻辑信号,这些信号用于连接核心与内存、处理中断和调试请求等。

5. 信号赋值

assign debug_if.clk      = clknrst_if.clk;
assign debug_if.reset_n  = clknrst_if.reset_n;
assign debug_req_uvma    = debug_if.debug_req;

assign debug_req = debug_req_vp | debug_req_uvma;

// --------------------------------------------
// Instruction bus is read-only, OBI v1.0
assign obi_memory_instr_if.we        = 'b0;
assign obi_memory_instr_if.be        = '1;
// Data bus is read/write, OBI v1.0

// --------------------------------------------
// Connect to uvma_interrupt_if
assign interrupt_if.clk                     = clknrst_if.clk;
assign interrupt_if.reset_n                 = clknrst_if.reset_n;
assign irq_uvma                             = interrupt_if.irq;
assign vp_interrupt_if.clk                  = clknrst_if.clk;
assign vp_interrupt_if.reset_n              = clknrst_if.reset_n;
assign irq_vp                               = vp_interrupt_if.irq;
assign interrupt_if.irq_id                  = irq_id;
assign interrupt_if.irq_ack                 = irq_ack;

assign irq = irq_uvma | irq_vp;

对信号进行赋值操作,将时钟、复位信号进行连接,设置指令总线为只读,同时处理中断信号的合并。

6. 核心实例化

cv32e40p_wrapper #(
    .PULP_XPULP       (PULP_XPULP),
    .PULP_CLUSTER     (PULP_CLUSTER),
    .FPU              (FPU),
    .PULP_ZFINX       (PULP_ZFINX),
    .NUM_MHPMCOUNTERS (NUM_MHPMCOUNTERS)
)
cv32e40p_wrapper_i
(
    .clk_i                  ( clknrst_if.clk                 ),
    .rst_ni                 ( clknrst_if.reset_n             ),

    .pulp_clock_en_i        ( core_cntrl_if.pulp_clock_en    ),
    .scan_cg_en_i           ( core_cntrl_if.scan_cg_en       ),

    .boot_addr_i            ( core_cntrl_if.boot_addr        ),
    .mtvec_addr_i           ( core_cntrl_if.mtvec_addr       ),
    .dm_halt_addr_i         ( core_cntrl_if.dm_halt_addr     ),
    .hart_id_i              ( core_cntrl_if.hart_id          ),
    .dm_exception_addr_i    ( core_cntrl_if.dm_exception_addr),

    .instr_req_o            ( obi_memory_instr_if.req        ), // core to agent
    .instr_gnt_i            ( obi_memory_instr_if.gnt        ), // agent to core
    .instr_rvalid_i         ( obi_memory_instr_if.rvalid     ),
    .instr_addr_o           ( obi_memory_instr_if.addr       ),
    .instr_rdata_i          ( obi_memory_instr_if.rdata      ),

    .data_req_o             ( obi_memory_data_if.req         ),
    .data_gnt_i             ( obi_memory_data_if.gnt         ),
    .data_rvalid_i          ( obi_memory_data_if.rvalid      ),
    .data_we_o              ( obi_memory_data_if.we          ),
    .data_be_o              ( obi_memory_data_if.be          ),
    .data_addr_o            ( obi_memory_data_if.addr        ),
    .data_wdata_o           ( obi_memory_data_if.wdata       ),
    .data_rdata_i           ( obi_memory_data_if.rdata       ),

    // APU not verified in cv32e40p (future work)
    .apu_req_o              (                                ),
    .apu_gnt_i              ( 1'b0                           ),
    .apu_operands_o         (                                ),
    .apu_op_o               (                                ),
    .apu_flags_o            (                                ),
    .apu_rvalid_i           ( 1'b0                           ),
    .apu_result_i           ( {32{1'b0}}                     ),
    .apu_flags_i            ( {5{1'b0}}                      ), // APU_NUSFLAGS_CPU

    .irq_i                  ( irq_uvma                       ),
    .irq_ack_o              ( irq_ack                        ),
    .irq_id_o               ( irq_id                         ),

    .debug_req_i            ( debug_req_uvma                 ),
    .debug_havereset_o      ( debug_havereset                ),
    .debug_running_o        ( debug_running                  ),
    .debug_halted_o         ( debug_halted                   ),

    .fetch_enable_i         ( core_cntrl_if.fetch_en         ),
    .core_sleep_o           ( core_status_if.core_busy       )
);

实例化 cv32e40p_wrapper 模块,将之前定义的参数和信号连接到该模块的对应端口,实现核心与其他模块的交互。

2. tb/core/uvmt/uvmt_cv32e40p_interrupt_assert.sv


module uvmt_cv32e40p_interrupt_assert  
  import uvm_pkg::*;
  import cv32e40p_pkg::*;
  (
    
    input clk,   // Gated clock
    input clk_i, // Free-running core clock
    input rst_ni,

    // Core inputs
    input        fetch_enable_i, // external core fetch enable

    // External interrupt interface
    input [31:0] irq_i,
    input        irq_ack_o,
    input [4:0]  irq_id_o,

    // External debug req (for WFI modeling)
    input        debug_req_i,
    input        debug_mode_q,

    // CSR Interface
    input [5:0]  mcause_n, // mcause_n[5]: interrupt, mcause_n[4]: vector
    input [31:0] mip,     // machine interrupt pending
    input [31:0] mie_q,   // machine interrupt enable
    input [31:0] mie_n,   // machine interrupt enable
    input        mstatus_mie, // machine mode interrupt enable
    input [1:0]  mtvec_mode_q, // machine mode interrupt vector mode

    // Instruction fetch stage
    input        if_stage_instr_rvalid_i, // Instruction word is valid
    input [31:0] if_stage_instr_rdata_i, // Instruction word data

    // Instruction ID stage (determines executed instructions)  
    input        id_stage_instr_valid_i, // instruction word is valid
    input [31:0] id_stage_instr_rdata_i, // Instruction word data
    input [4:0]  ctrl_fsm_cs,            // Controller FSM to get hint for interrupt taken

    // Determine whether to cancel instruction if branch taken
    input branch_taken_ex,

    // WFI Interface
    input core_sleep_o
  );

  // ---------------------------------------------------------------------------
  // Local parameters
  // ---------------------------------------------------------------------------  
  localparam NUM_IRQ        = 32;  
  localparam VALID_IRQ_MASK = 32'hffff_0888; // Valid external interrupt signals

  localparam WFI_INSTR_MASK = 32'hffffffff;
  localparam WFI_INSTR_DATA = 32'h10500073;

  localparam WFI_WAKEUP_LATENCY = 40;

  // ---------------------------------------------------------------------------
  // Local variables
  // ---------------------------------------------------------------------------
  string info_tag = "CV32E40P_IRQ_ASSERT";

  wire [31:0] pending_enabled_irq;
  wire [31:0] pending_enabled_irq_q;

  wire id_instr_is_wfi; // ID instruction is a WFI
  reg  in_wfi; // Local model of WFI state of core

  reg[31:0] irq_q;

  reg[31:0] next_irq;
  reg       next_irq_valid;

  reg[31:0] next_irq_q;    
  reg       next_irq_valid_q;
  reg[31:0] saved_mie_q;

  reg[31:0] expected_irq;
  logic     expected_irq_ack;

  reg[31:0] last_instr_rdata;

  // ---------------------------------------------------------------------------
  // Clocking blocks
  // ---------------------------------------------------------------------------

  // Single clock, single reset design, use default clocking
  default clocking @(posedge clk_i); endclocking
  default disable iff !(rst_ni);

  // ---------------------------------------------------------------------------
  // Begin module code
  // ---------------------------------------------------------------------------
  assign pending_enabled_irq   = irq_i & mie_n;
  assign pending_enabled_irq_q = irq_q & mie_n;

  // ---------------------------------------------------------------------------
  // Interrupt interface checks
  // ---------------------------------------------------------------------------

  // irq_ack_o is always a pulse
  property p_irq_ack_o_pulse;
    irq_ack_o |=> !irq_ack_o;
  endproperty
  a_irq_ack_o_pulse: assert property(p_irq_ack_o_pulse)
    else
      `uvm_error(info_tag,
                 "Interrupt ack was asserted for more than one cycle");

  // irq_id_o is never a reserved irq
  property p_irq_id_o_not_reserved;
    irq_ack_o |-> VALID_IRQ_MASK[irq_id_o];
  endproperty    
  a_irq_id_o_not_reserved: assert property(p_irq_id_o_not_reserved)
    else
      `uvm_error(info_tag,
                 $sformatf("int_id_o output is 0x%0x which is reserved", irq_id_o));

  // irq_id_o is never a disabled irq
  property p_irq_id_o_mie_enabled;
    irq_ack_o |-> mie_n[irq_id_o];
  endproperty    
  a_irq_id_o_mie_enabled: assert property(p_irq_id_o_mie_enabled)
    else
      `uvm_error(info_tag,
                 $sformatf("irq_id_o output is 0x%0x which is disabled in MIE: 0x%08x", irq_id_o, mie_n));

  // irq_ack_o cannot be asserted if mstatus_mie is deasserted
  property p_irq_id_o_mstatus_mie_enabled;
    irq_ack_o |-> mstatus_mie;
  endproperty
  a_irq_id_o_mstatus_mie_enabled: assert property(p_irq_id_o_mstatus_mie_enabled)
    else
      `uvm_error(info_tag,
                 $sformatf("int_id_o output is 0x%0x but MSTATUS.MIE is disabled", irq_id_o));
  
  // ---------------------------------------------------------------------------
  // Interrupt CSR checks
  // ---------------------------------------------------------------------------

  // Coverage for individual interupt assertions
  sequence s_irq_taken(irq);
    irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie ##0 irq_ack_o ##0 irq_id_o == irq;
  endsequence : s_irq_taken

  // Interrupt fired, global interrupts enabled, but not taken due to global MSTATUS.MIE setting
  property p_irq_masked(irq);
    irq_i[irq] ##0 !mie_q[irq] ##0 mstatus_mie;    
  endproperty : p_irq_masked

  // Interrupt fired and locally enabled in MIE, but masked due to MSTATUS_MIE
  property p_irq_masked_mstatus(irq);
    irq_i[irq] ##0 mie_q[irq] ##0 !mstatus_mie;
  endproperty : p_irq_masked_mstatus

  // Interrupt taken
  property p_irq_taken(irq);
    s_irq_taken(irq);
  endproperty : p_irq_taken

  // Interrupt enabled via MIE locally masked
  property p_irq_masked_then_enabled(irq);
    irq_i[irq] ##0 !mie_q[irq] ##0 mstatus_mie ##1 irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie;
  endproperty : p_irq_masked_then_enabled

  // Interrupt enabled via MSTATUS_MIE locally masked
  property p_irq_masked_mstatus_then_enabled(irq);
    irq_i[irq] ##0 mie_q[irq] ##0 !mstatus_mie ##1 irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie;
  endproperty : p_irq_masked_mstatus_then_enabled

  // Interrupt request deasserted when enabled but not acked
  property p_irq_deasserted_while_enabled_not_acked(irq);
    irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie ##0 !irq_ack_o ##1 
    !irq_i[irq] ##0 !irq_ack_o;
  endproperty : p_irq_deasserted_while_enabled_not_acked  

  // Interrupt taken in each supported mtvec mode
  property p_irq_in_mtvec(irq, mtvec);
    s_irq_taken(irq) ##0 mtvec_mode_q == mtvec;
  endproperty

  generate for(genvar gv_i = 0; gv_i < NUM_IRQ; gv_i++) begin : gen_irq_cov
    if (VALID_IRQ_MASK[gv_i]) begin : gen_valid
      c_irq_masked: cover property(p_irq_masked(gv_i));
      c_irq_masked_mstatus: cover property(p_irq_masked_mstatus(gv_i));
      c_irq_taken: cover property(p_irq_taken(gv_i));
      c_irq_masked_then_enabled: cover property(p_irq_masked_then_enabled(gv_i));
      c_irq_masked_mstatus_then_enabled: cover property(p_irq_masked_mstatus_then_enabled(gv_i));
      c_irq_deasserted_while_enabled_not_acked: cover property(p_irq_deasserted_while_enabled_not_acked(gv_i));
      c_irq_in_mtvec_fixed: cover property(p_irq_in_mtvec(gv_i, 0));
      c_irq_in_mtvec_vector: cover property(p_irq_in_mtvec(gv_i, 1));
    end
  end
  endgenerate

  // Detect arbitration of interrupt assertion
  always @* begin
    next_irq_valid = 1'b0;
    next_irq = '0;
    casex ({pending_enabled_irq_q[31:16], pending_enabled_irq_q[11], pending_enabled_irq_q[3], pending_enabled_irq_q[7]})
      19'b1???_????_????_????_???: begin next_irq = 'd31; next_irq_valid = '1; end
      19'b01??_????_????_????_???: begin next_irq = 'd30; next_irq_valid = '1; end
      19'b001?_????_????_????_???: begin next_irq = 'd29; next_irq_valid = '1; end
      19'b0001_????_????_????_???: begin next_irq = 'd28; next_irq_valid = '1; end
      19'b0000_1???_????_????_???: begin next_irq = 'd27; next_irq_valid = '1; end
      19'b0000_01??_????_????_???: begin next_irq = 'd26; next_irq_valid = '1; end
      19'b0000_001?_????_????_???: begin next_irq = 'd25; next_irq_valid = '1; end
      19'b0000_0001_????_????_???: begin next_irq = 'd24; next_irq_valid = '1; end
      19'b0000_0000_1???_????_???: begin next_irq = 'd23; next_irq_valid = '1; end
      19'b0000_0000_01??_????_???: begin next_irq = 'd22; next_irq_valid = '1; end
      19'b0000_0000_001?_????_???: begin next_irq = 'd21; next_irq_valid = '1; end
      19'b0000_0000_0001_????_???: begin next_irq = 'd20; next_irq_valid = '1; end
      19'b0000_0000_0000_1???_???: begin next_irq = 'd19; next_irq_valid = '1; end
      19'b0000_0000_0000_01??_???: begin next_irq = 'd18; next_irq_valid = '1; end
      19'b0000_0000_0000_001?_???: begin next_irq = 'd17; next_irq_valid = '1; end
      19'b0000_0000_0000_0001_???: begin next_irq = 'd16; next_irq_valid = '1; end
      19'b0000_0000_0000_0000_1??: begin next_irq = 'd11; next_irq_valid = '1; end
      19'b0000_0000_0000_0000_01?: begin next_irq = 'd3; next_irq_valid = '1; end
      19'b0000_0000_0000_0000_001: begin next_irq = 'd7; next_irq_valid = '1; end
    endcase
  end

  always @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      irq_q <= 0;
      next_irq_q <= 0;
      next_irq_valid_q <= 0;
      saved_mie_q <= 0;
    end    
    else begin
      irq_q <= irq_i;
      next_irq_q <= next_irq;
      next_irq_valid_q <= next_irq_valid;
      saved_mie_q <= mie_q;
    end
  end

  always @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni)
      expected_irq <= 0;
    else
      expected_irq <= next_irq_q;
  end

  assign expected_irq_ack = next_irq_valid & mstatus_mie;

  // Check expected interrupt wins
  property p_irq_arb;
    irq_ack_o |-> irq_id_o == next_irq;
  endproperty
  a_irq_arb: assert property(p_irq_arb)
    else
      `uvm_error(info_tag,
                 $sformatf("Expected winning interrupt: %0d, actual interrupt: %0d", next_irq, irq_id_o))  

  // Check that an interrupt is expected
  property p_irq_expected;
    irq_ack_o |-> expected_irq_ack;
  endproperty
  a_irq_expected: assert property(p_irq_expected)
    else
      `uvm_error(info_tag,
                 $sformatf("Did not expect interrupt ack: %0d", irq_id_o))

  // ---------------------------------------------------------------------------
  // The infamous "first" flag (kludge for $past() handling of t=0 values)
  // Would like to use a leading ##1 in the property instead but this currently
  // does not work with dsim
  // ---------------------------------------------------------------------------
  reg first;
  always @(posedge clk or negedge rst_ni)
    if (!rst_ni)
      first <= 1'b1;
    else
      first <= 1'b0;

  // mip reflects flopped interrupt inputs (irq_i) regardless of other configuration
  // Note that this runs on the gated clock
  property p_mip_irq_i;
    @(posedge clk)
      !first |-> mip == ($past(irq_i) & VALID_IRQ_MASK);
  endproperty
  a_mip_irq_i: assert property(p_mip_irq_i)
    else 
      `uvm_error(info_tag,
                 $sformatf("MIP of 0x%08x does not follow flopped irq_i input: 0x%08x", mip, $past(irq_i)));

  // mip should not be reserved
  property p_mip_not_reserved;
    (mip & ~VALID_IRQ_MASK) == 0;
  endproperty
  a_mip_not_reserved: assert property(p_mip_not_reserved)
    else
      `uvm_error(info_tag,
                 $sformatf("MIP of reserved interrupt is asserted: mip = 0x%08x", mip));

  // ---------------------------------------------------------------------------
  // Instruction coverage when taking an interrupt
  // ---------------------------------------------------------------------------
  always @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      last_instr_rdata <= '0;
    end
    else if (id_stage_instr_valid_i) begin
      last_instr_rdata <= id_stage_instr_rdata_i;
    end
  end

  // ---------------------------------------------------------------------------
  // WFI Checks
  // ---------------------------------------------------------------------------
  assign is_wfi = id_stage_instr_valid_i && 
                  ((id_stage_instr_rdata_i & WFI_INSTR_MASK) == WFI_INSTR_DATA) &&
                  !branch_taken_ex;
  always @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      in_wfi <= 1'b0;
    end
    else begin
      if (is_wfi) 
        in_wfi <= 1'b1;
      else if (|pending_enabled_irq || debug_req_i)
        in_wfi <= 1'b0;
    end
  end

  // WFI assertion will assert core_sleep_o in 6 clocks
  property p_wfi_assert_core_sleep_o;
    !pending_enabled_irq_q ##0 !in_wfi ##1 !pending_enabled_irq_q ##0
      ((!pending_enabled_irq && !debug_mode_q && !debug_req_i) throughout in_wfi[*38]) 
             |-> core_sleep_o;
  endproperty
  a_wfi_assert_core_sleep_o: assert property(p_wfi_assert_core_sleep_o)
    else
      `uvm_error(info_tag,
                 "Aassertion of core_sleep_o did not occur within 6 clocks")

  // core_sleep_o deassertion in wfi should be followed by WFI deassertion
  property p_core_sleep_deassert;
    $fell(core_sleep_o) ##0 in_wfi |-> ##1 !in_wfi;
  endproperty
  a_core_sleep_deassert: assert property(p_core_sleep_deassert)
    else
      `uvm_error(info_tag,
                 "Deassertion of core_sleep_o in WFI not followed by WFI wakeup");
  
  // When WFI deasserts the core should be awake
  property p_wfi_deassert_core_sleep_o;
    core_sleep_o ##1 pending_enabled_irq |-> !core_sleep_o;
  endproperty
  a_wfi_deassert_core_sleep_o: assert property(p_wfi_deassert_core_sleep_o) 
    else
      `uvm_error(info_tag,
                 "Deassertion of WFI occurred and core is still asleep");

  // WFI wakeup to next instruction fetch
  property p_wfi_wake_to_instr_fetch;
    disable iff (!rst_ni || !fetch_enable_i || debug_mode_q)
      core_sleep_o ##0 in_wfi ##1 !in_wfi[->1] |-> ##[1:WFI_WAKEUP_LATENCY] if_stage_instr_rvalid_i;
  endproperty
  a_wfi_wake_to_instr_fetch: assert property(p_wfi_wake_to_instr_fetch)
    else
      `uvm_error(info_tag,
                 $sformatf("Core did not start fetching %0d cycles after WFI completed", WFI_WAKEUP_LATENCY));

  // Cover property, detect sleep deassertion due to asserted and non-asserted interrupts
  property p_wfi_wake_mstatus_mie(irq, mie);
    $fell(in_wfi) ##0 irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie == mie;
  endproperty 
  generate for(genvar gv_i = 0; gv_i < 32; gv_i++) begin : gen_wfi_cov
    if (VALID_IRQ_MASK[gv_i]) begin
      c_wfi_wake_mstatus_mie_0: cover property(p_wfi_wake_mstatus_mie(gv_i, 0));
      c_wfi_wake_mstatus_mie_1: cover property(p_wfi_wake_mstatus_mie(gv_i, 1));
    end
  end
  endgenerate

endmodule : uvmt_cv32e40p_interrupt_assert

uvmt_cv32e40p_interrupt_assert.sv 是一个 SystemVerilog 文件,用于对 cv32e40p 处理器的中断接口和相关控制状态寄存器(CSR)进行断言检查和覆盖率收集。

1. 版权和许可声明

文件开头包含版权声明,表明该文件由 OpenHW Group 和 Datum Technology Corporation 拥有版权,并遵循 Solderpad Hardware Licence, Version 2.0 许可。

2. 模块定义

module uvmt_cv32e40p_interrupt_assert  
  import uvm_pkg::*;
  import cv32e40p_pkg::*;
  (
    // 端口定义
    input clk,   // Gated clock
    input clk_i, // Free-running core clock
    input rst_ni,
    // 其他端口...
  );
  • 模块名称uvmt_cv32e40p_interrupt_assert,用于对 cv32e40p 处理器的中断相关信号进行断言检查。
  • 导入包:导入了 uvm_pkgcv32e40p_pkg 包,可能用于使用 UVM 框架和 cv32e40p 相关的类型和常量。
  • 端口定义:包含时钟信号(clkclk_i)、复位信号(rst_ni)、核心输入信号、外部中断接口信号、调试请求信号、CSR 接口信号、指令阶段信号等。

3. 局部参数定义

localparam NUM_IRQ        = 32;  
localparam VALID_IRQ_MASK = 32'hffff_0888; // Valid external interrupt signals
// 其他局部参数...
  • 定义了一些局部参数,如中断数量 NUM_IRQ、有效中断掩码 VALID_IRQ_MASK 等,用于后续的断言和覆盖率收集。

4. 局部变量定义

string info_tag = "CV32E40P_IRQ_ASSERT";
wire [31:0] pending_enabled_irq;
// 其他局部变量...
  • 定义了一些局部变量,如信息标签 info_tag、待处理且使能的中断信号 pending_enabled_irq 等,用于存储和处理中间结果。

5. 时钟块定义

default clocking @(posedge clk_i); endclocking
default disable iff !(rst_ni);
  • 定义了默认的时钟块,使用 clk_i 的上升沿作为时钟信号,并在复位信号 rst_ni 为低电平时禁用断言。

6. 组合逻辑和连续赋值

assign pending_enabled_irq   = irq_i & mie_n;
assign pending_enabled_irq_q = irq_q & mie_n;
  • 使用 assign 语句进行连续赋值,计算待处理且使能的中断信号。

7. 中断接口检查

// irq_ack_o is always a pulse
property p_irq_ack_o_pulse;
  irq_ack_o |=> !irq_ack_o;
endproperty
a_irq_ack_o_pulse: assert property(p_irq_ack_o_pulse)
  else
    `uvm_error(info_tag,
               "Interrupt ack was asserted for more than one cycle");
  • 定义了一系列属性(property)和断言(assert),用于检查中断接口信号的合法性,如 irq_ack_o 是否为单周期脉冲、irq_id_o 是否为有效中断号、irq_id_o 是否为使能的中断等。

8. 中断 CSR 检查

// Coverage for individual interupt assertions
sequence s_irq_taken(irq);
  irq_i[irq] ##0 mie_q[irq] ##0 mstatus_mie ##0 irq_ack_o ##0 irq_id_o == irq;
endsequence : s_irq_taken

// 其他中断 CSR 相关的序列、属性和覆盖率收集...
  • 定义了一些序列(sequence)、属性和覆盖率收集(cover property),用于检查中断 CSR 的状态和行为,如中断是否被正确屏蔽、是否被正确处理等。

9. 中断仲裁检测

always @* begin
  next_irq_valid = 1'b0;
  next_irq = '0;
  casex ({pending_enabled_irq_q[31:16], pending_enabled_irq_q[11], pending_enabled_irq_q[3], pending_enabled_irq_q[7]})
    // 各种情况...
  endcase
end
  • 使用 always @* 块进行组合逻辑设计,根据待处理且使能的中断信号,确定下一个要处理的中断。

10. 时序逻辑

always @(posedge clk_i or negedge rst_ni) begin
  if (!rst_ni) begin
    irq_q <= 0;
    // 其他复位操作...
  end    
  else begin
    irq_q <= irq_i;
    // 其他更新操作...
  end
end
  • 使用 always @(posedge clk_i or negedge rst_ni) 块进行时序逻辑设计,在时钟上升沿或复位信号下降沿时更新寄存器的值。

11. 指令覆盖和 WFI 检查

// Instruction coverage when taking an interrupt
always @(posedge clk_i or negedge rst_ni) begin
  if (!rst_ni) begin
    last_instr_rdata <= '0;
  end
  else if (id_stage_instr_valid_i) begin
    last_instr_rdata <= id_stage_instr_rdata_i;
  end
end

// WFI Checks
assign is_wfi = id_stage_instr_valid_i && 
                ((id_stage_instr_rdata_i & WFI_INSTR_MASK) == WFI_INSTR_DATA) &&
                !branch_taken_ex;
// 其他 WFI 相关的属性和断言...
  • 记录最后执行的指令数据,用于指令覆盖率收集。
  • 定义了与等待中断(WFI)指令相关的逻辑和断言,如 WFI 指令是否正确触发、WFI 唤醒后是否正确恢复执行等。

总结

该文件主要用于对 cv32e40p 处理器的中断接口和相关 CSR 进行断言检查和覆盖率收集,确保中断系统的正确性和可靠性。通过定义各种属性、序列和断言,对中断信号的合法性、中断仲裁、CSR 状态等进行了全面的检查。同时,还对 WFI 指令的行为进行了检查,确保处理器在等待中断时能够正确响应。

3. tb/core/uvmt/uvmt_cv32e40p_iss_wrap.sv


`ifndef __UVMT_CV32E40P_ISS_WRAP_SV__
`define __UVMT_CV32E40P_ISS_WRAP_SV__

/**
 * Module wrapper for Imperas OVP.
 * Instanitates "CPU", the OVP wrapper, and "RAM" a spare memory model.
 */
module uvmt_cv32e40p_iss_wrap
  #(
    parameter int ROM_START_ADDR = 'h00000000,
    parameter int ROM_BYTE_SIZE  = 'h0,
    parameter int RAM_BYTE_SIZE  = 'h1B000000,
    parameter int ID = 0
   )

   (
    input realtime            clk_period,
    uvma_clknrst_if           clknrst_if,
    uvmt_cv32e40p_step_compare_if step_compare_if,
    uvmt_cv32e40p_isa_covg_if     isa_covg_if
   );

    RVVI_bus    bus();
    RVVI_io     io();

    MONITOR     mon(bus, io);
    RAM         #(
                .ROM_START_ADDR(ROM_START_ADDR),
                .ROM_BYTE_SIZE(ROM_BYTE_SIZE),
                .RAM_BYTE_SIZE(RAM_BYTE_SIZE)) ram(bus);

    CPU #(.ID(ID), .VARIANT("CV32E40P")) cpu(bus, io);

   assign bus.Clk = clknrst_if.clk;

   // monitor rvvi updates
   always @(cpu.state.notify) begin
       int i;
       step_compare_if.ovp_cpu_PCr = cpu.state.pc;
       for(i=0; i<32; i++)
           step_compare_if.ovp_cpu_GPR[i] = cpu.state.x[i];

       // generate events
       if (cpu.state.valid) begin
           // $display("Instruction Retired %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_valid;
       end else if (cpu.state.trap &&
                    cpu.state.decode == "ecall" &&
                    cpu.io.irq_ack_o == 0 &&
                    (!cpu.io.DM || !cpu.state.csr["dcsr"][2])) begin
           // With introduction of OVPSIM model v20200821.377
           // the valid behavior was changed, the above clause was introduced to all signal valid
           // instruction on valid ecalls, which must be checked out by the step and compare interface
           // Eventually this module should be replaced by a RVFI/RVVI UVM scoreboard which will not rely on this
           // $display("Instruction Retired %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_valid;
       end else if (cpu.state.trap) begin
           // $display("Instruction Fault %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_trap;
       end

   end

   // monitor debug control updates
   always @(cpu.control.notify) begin
       step_compare_if.ovp_cpu_state_idle  = cpu.control.state_idle;
       step_compare_if.ovp_cpu_state_stepi = cpu.control.state_stepi;
       step_compare_if.ovp_cpu_state_stop  = cpu.control.state_stop;
       step_compare_if.ovp_cpu_state_cont  = cpu.control.state_cont;
   end

    function automatic void split(ref string in_s, ref string s1, s2);
        automatic int i;
        for (i=0; i<in_s.len(); i++) begin
            if (in_s.getc(i) == ":")
                break;
         end
         if (i==0 ) begin
            `uvm_fatal("ERROR not : found in split '%0s'", in_s);
         end
         s1 = in_s.substr(0,i-1);
         s2 = in_s.substr(i+1,in_s.len()-1);
    endfunction

    function automatic void sample();
        string decode = cpu.state.decode;
        string ins_str, op[4], key, val;
        int i;

        int num = $sscanf (decode, "%s %s %s %s %s", ins_str, op[0], op[1], op[2], op[3]);
        isa_covg_if.ins.ins_str = ins_str;
        isa_covg_if.ins.pc = step_compare_if.ovp_cpu_PCr;
        isa_covg_if.ins.compressed = dut_wrap.cv32e40p_wrapper_i.tracer_i.insn_compressed;
        for (i=0; i<num-1; i++) begin
            split(op[i], key, val);
            isa_covg_if.ins.ops[i].key=key;
            isa_covg_if.ins.ops[i].val=val;
        end
        `uvm_info("OVPSIM", $sformatf("Decoded instr: %s%s pc: 0x%08x",
                                      isa_covg_if.ins.compressed ? "c." : "",
                                      decode,
                                      isa_covg_if.ins.pc),
                            UVM_DEBUG)
        ->isa_covg_if.ins_valid;
    endfunction

   always @(step_compare_if.ovp_cpu_valid) begin
       sample();
   end

   initial begin
     clknrst_if.clk = 1'b0;
      #1;  // time for clknrst_if_dut to set the clk_period
      wait (clk_period != 0.0);
      `uvm_info("OVPWRAP", "Starting OVP clock", UVM_LOW)
      clknrst_if.set_period(clk_period);
      clknrst_if.start_clk();
   end

endmodule : uvmt_cv32e40p_iss_wrap

`endif // __UVMT_CV32E40P_ISS_WRAP_SV__


uvmt_cv32e40p_iss_wrap.sv 是一个 SystemVerilog 文件,其作用是对 Imperas OVP(Open Virtual Platform)进行封装,在测试平台里模拟 CPU 及其内存环境。

1. 版权和许可声明

文件开头包含版权信息与许可声明,表明此文件遵循 Solderpad Hardware Licence, Version 2.0。

2. 宏定义与模块声明

`ifndef __UVMT_CV32E40P_ISS_WRAP_SV__
`define __UVMT_CV32E40P_ISS_WRAP_SV__

/**
 * Module wrapper for Imperas OVP.
 * Instanitates "CPU", the OVP wrapper, and "RAM" a spare memory model.
 */
module uvmt_cv32e40p_iss_wrap
  #(
    parameter int ROM_START_ADDR = 'h00000000,
    parameter int ROM_BYTE_SIZE  = 'h0,
    parameter int RAM_BYTE_SIZE  = 'h1B000000,
    parameter int ID = 0
   )
   (
    input realtime            clk_period,
    uvma_clknrst_if           clknrst_if,
    uvmt_cv32e40p_step_compare_if step_compare_if,
    uvmt_cv32e40p_isa_covg_if     isa_covg_if
   );
  • 运用条件编译防止头文件被重复包含。
  • uvmt_cv32e40p_iss_wrap 模块的功能是对 Imperas OVP 进行封装,模块有四个参数:ROM_START_ADDRROM_BYTE_SIZERAM_BYTE_SIZEID,分别代表 ROM 的起始地址、ROM 的字节大小、RAM 的字节大小以及实例 ID。
  • 模块的端口包括:
    • clk_period:时钟周期,为实时类型。
    • clknrst_if:时钟和复位接口。
    • step_compare_if:步骤比较接口。
    • isa_covg_if:指令集覆盖接口。

3. 接口和实例化

    RVVI_bus    bus();
    RVVI_io     io();

    MONITOR     mon(bus, io);
    RAM         #(
                .ROM_START_ADDR(ROM_START_ADDR),
                .ROM_BYTE_SIZE(ROM_BYTE_SIZE),
                .RAM_BYTE_SIZE(RAM_BYTE_SIZE)) ram(bus);

    CPU #(.ID(ID), .VARIANT("CV32E40P")) cpu(bus, io);
  • 实例化 RVVI_busRVVI_io 接口。
  • 实例化 MONITORRAMCPU 模块,并且将模块参数传递给 RAMCPU

4. 时钟连接

   assign bus.Clk = clknrst_if.clk;

把时钟信号 clknrst_if.clk 连接到 bus.Clk

5. 状态监测与事件触发

   // monitor rvvi updates
   always @(cpu.state.notify) begin
       int i;
       step_compare_if.ovp_cpu_PCr = cpu.state.pc;
       for(i=0; i<32; i++)
           step_compare_if.ovp_cpu_GPR[i] = cpu.state.x[i];

       // generate events
       if (cpu.state.valid) begin
           // $display("Instruction Retired %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_valid;
       end else if (cpu.state.trap &&
                    cpu.state.decode == "ecall" &&
                    cpu.io.irq_ack_o == 0 &&
                    (!cpu.io.DM || !cpu.state.csr["dcsr"][2])) begin
           // With introduction of OVPSIM model v20200821.377
           // the valid behavior was changed, the above clause was introduced to all signal valid
           // instruction on valid ecalls, which must be checked out by the step and compare interface
           // Eventually this module should be replaced by a RVFI/RVVI UVM scoreboard which will not rely on this
           // $display("Instruction Retired %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_valid;
       end else if (cpu.state.trap) begin
           // $display("Instruction Fault %08X %08X", cpu.state.pc, cpu.state.pcw);
           -> step_compare_if.ovp_cpu_trap;
       end
   end
  • 监测 cpu.state.notify 信号,当该信号变化时,更新 step_compare_if 接口里的 CPU 状态信息。
  • 根据 cpu.state 的状态,触发不同的事件:
    • cpu.state.valid 为真,触发 step_compare_if.ovp_cpu_valid 事件。
    • 若满足特定条件(cpu.state.trapcpu.state.decode"ecall" 等),触发 step_compare_if.ovp_cpu_valid 事件。
    • cpu.state.trap 为真,触发 step_compare_if.ovp_cpu_trap 事件。

6. 调试控制监测

   // monitor debug control updates
   always @(cpu.control.notify) begin
       step_compare_if.ovp_cpu_state_idle  = cpu.control.state_idle;
       step_compare_if.ovp_cpu_state_stepi = cpu.control.state_stepi;
       step_compare_if.ovp_cpu_state_stop  = cpu.control.state_stop;
       step_compare_if.ovp_cpu_state_cont  = cpu.control.state_cont;
   end

监测 cpu.control.notify 信号,当该信号变化时,更新 step_compare_if 接口里的 CPU 调试控制状态信息。

7. 字符串分割函数

    function automatic void split(ref string in_s, ref string s1, s2);
        automatic int i;
        for (i=0; i<in_s.len(); i++) begin
            if (in_s.getc(i) == ":")
                break;
         end
         if (i==0 ) begin
            `uvm_fatal("ERROR not : found in split '%0s'", in_s);
         end
         s1 = in_s.substr(0,i-1);
         s2 = in_s.substr(i+1,in_s.len()-1);
    endfunction

split 函数的作用是把输入字符串 in_s 按冒号 : 分割成两部分 s1s2。若字符串中没有冒号,使用 uvm_fatal 宏输出错误信息。

8. 采样函数

    function automatic void sample();
        string decode = cpu.state.decode;
        string ins_str, op[4], key, val;
        int i;

        int num = $sscanf (decode, "%s %s %s %s %s", ins_str, op[0], op[1], op[2], op[3]);
        isa_covg_if.ins.ins_str = ins_str;
        isa_covg_if.ins.pc = step_compare_if.ovp_cpu_PCr;
        isa_covg_if.ins.compressed = dut_wrap.cv32e40p_wrapper_i.tracer_i.insn_compressed;
        for (i=0; i<num-1; i++) begin
            split(op[i], key, val);
            isa_covg_if.ins.ops[i].key=key;
            isa_covg_if.ins.ops[i].val=val;
        end
        `uvm_info("OVPSIM", $sformatf("Decoded instr: %s%s pc: 0x%08x",
                                      isa_covg_if.ins.compressed ? "c." : "",
                                      decode,
                                      isa_covg_if.ins.pc),
                            UVM_DEBUG)
        ->isa_covg_if.ins_valid;
    endfunction
  • sample 函数的作用是对 CPU 状态进行采样,将解码后的指令信息存储到 isa_covg_if.ins 中。
  • 利用 $sscanf 函数解析 cpu.state.decode 字符串,提取指令信息。
  • 调用 split 函数分割操作数,将操作数的键值对存储到 isa_covg_if.ins.ops 中。
  • 使用 uvm_info 宏输出解码后的指令信息。
  • 触发 isa_covg_if.ins_valid 事件。

9. 采样触发

   always @(step_compare_if.ovp_cpu_valid) begin
       sample();
   end

step_compare_if.ovp_cpu_valid 事件触发时,调用 sample 函数进行采样。

10. 时钟初始化

   initial begin
     clknrst_if.clk = 1'b0;
      #1;  // time for clknrst_if_dut to set the clk_period
      wait (clk_period != 0.0);
      `uvm_info("OVPWRAP", "Starting OVP clock", UVM_LOW)
      clknrst_if.set_period(clk_period);
      clknrst_if.start_clk();
   end
  • 在初始块中,将时钟信号 clknrst_if.clk 初始化为 0
  • 等待 clk_period 不为 0
  • 使用 uvm_info 宏输出启动 OVP 时钟的信息。
  • 调用 clknrst_if.set_period 函数设置时钟周期,调用 clknrst_if.start_clk 函数启动时钟。

总结

该文件构建了一个用于 Imperas OVP 的封装模块,对 CPU 及其内存环境进行模拟。模块通过监测 CPU 状态和调试控制信号,更新接口信息并触发相应事件,同时对指令信息进行采样,最终实现时钟的初始化和启动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值