4. SystemVerilog基础:数据类型、过程块、接口、断言、随机化

各位好,咱们今天聊聊SystemVerilog。说实话,我刚从Verilog转到SV那会儿,心里还挺抵触的——觉得Verilog够用了,干嘛要学新东西?后来被项目逼着用,才发现真香。SV不是一门新语言,它是对Verilog的增强和扩展。你想想看,Verilog做验证就像用螺丝刀拧螺丝,而SV给你配了把电动螺丝刀。

4.1 数据类型:从wire/reg到logic和自定义结构

先说说数据类型。Verilog里最让人头疼的就是wire和reg的区分——组合逻辑用assign就得wire,时序逻辑用always就得reg。SV引入了一个叫logic的类型,它把两者统一了。

核心要点:在SV中,logic可以替代绝大多数wire和reg的场景。但注意,它不能用于多驱动(比如双向总线),那种情况还得用wire

我个人习惯是:90%的信号都用logic声明。只有遇到inout端口或者多个驱动源时,才切回wire。这样代码清爽很多。

// Verilog老写法
wire        clk;
reg  [31:0] data;
reg         valid;

// SV新写法
logic        clk;
logic [31:0] data;
logic        valid;

SV还引入了typedefstruct,这个太实用了。我在项目中遇到过一个大模块,有几十个控制信号,每次端口列表都写到手软。后来用struct一包,清爽多了。

typedef struct packed {
    logic       valid;
    logic [7:0] addr;
    logic [31:0] data;
    logic       rw;
} bus_trans_t;

bus_trans_t master_req;  // 一个变量搞定

还有枚举类型,这个在状态机里特别好用。以前用parameter定义状态,仿真波形里看到的全是数字,调试时得对着表查。用枚举后,波形直接显示状态名,爽。

typedef enum logic [1:0] {
    IDLE   = 2'b00,
    READ   = 2'b01,
    WRITE  = 2'b10,
    WAIT   = 2'b11
} state_t;

小技巧:枚举类型默认是int,如果你要控制位宽,记得像我上面那样显式声明logic [1:0]。不然综合时可能会多出不必要的触发器。

4.2 过程块:always_comb、always_ff、always_latch

Verilog里一个always块打天下,组合逻辑、时序逻辑、锁存器全靠你写代码的风格来区分。SV把它拆成了三个明确的过程块。

过程块 用途 敏感列表
always_comb 组合逻辑 自动推断
always_ff 时序逻辑(触发器) 需指定时钟沿
always_latch 锁存器 自动推断

为什么要有这三个?说白了,就是让工具帮你检查。比如你用always_comb,工具会检查你是不是漏写了赋值——如果某个分支没覆盖到,它会报warning。我用always_ff时,如果敏感列表写错了,工具直接报error。

// 组合逻辑
always_comb begin
    if (sel)
        out = a;
    else
        out = b;
end

// 时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= '0;
    else
        q <= d;
end

注意:千万别在always_comb里用阻塞赋值以外的写法。我曾经见过有人在always_comb里写<=,仿真能过,但综合出来的电路跟你想象的不一样。嗯,那哥们后来改bug改了一整天。

4.3 接口:告别冗长的端口列表

接口(interface)是SV里我最喜欢的特性之一。你想想看,一个AXI总线有多少信号?地址、数据、握手、ID、突发长度...写端口列表能写一页纸。用interface,一个名字搞定。

interface axi_if;
    logic        aclk;
    logic        aresetn;
    logic [31:0] awaddr;
    logic [2:0]  awprot;
    logic        awvalid;
    logic        awready;
    // ... 还有几十个信号
endinterface

// 模块声明
module axi_slave (
    axi_if.slave  bus
);
    // 直接用 bus.awaddr, bus.awvalid 等
endmodule

接口还支持modport,用来定义不同角色的视角。比如master看到的信号方向和slave是反的。modport就是干这个的。

interface axi_if;
    // 信号声明...
    
    modport master (
        output awaddr, awvalid,
        input  awready
        // ...
    );
    
    modport slave (
        input  awaddr, awvalid,
        output awready
        // ...
    );
endinterface

我在项目中遇到过一个问题:多个master共享一个slave,每个master的接口信号都要单独例化。用interface配合数组,代码量直接减半。

4.4 断言:让bug无处遁形

断言(assertion)说白了就是给设计加个监控。你告诉工具:「这个信号应该永远这样」,如果它不这样了,工具就报错。断言分两种:立即断言和并发断言。

立即断言:写在过程块里,像if语句一样执行。

always_ff @(posedge clk) begin
    if (valid) begin
        assert (data !== 'x)
            else $error("data is X when valid is high!");
    end
end

并发断言:基于时钟周期,可以描述跨周期的时序关系。

// 握手协议:valid拉高后,ready必须在3个周期内拉高
property handshake;
    @(posedge clk)
    valid |-> ##[1:3] ready;
endproperty

assert_handshake: assert property (handshake);

我的经验:断言一定要在写RTL的时候就加,别等验证工程师来催你。我见过太多项目,验证发现bug后,设计才补断言——那时候已经晚了。断言是设计者的护身符,不是验证者的负担。

4.5 随机化:验证的核武器

随机化是SV验证的灵魂。说白了,就是让工具自动生成各种合法的输入组合,帮你找到那些你没想到的边界情况。

class packet;
    rand logic [7:0]  addr;
    rand logic [31:0] data;
    rand bit          burst;
    
    // 约束:地址范围
    constraint addr_range {
        addr inside {[0:16], [32:48]};
    }
    
    // 约束:burst模式下数据不能为0
    constraint burst_data {
        burst -> (data != 0);
    }
endclass

packet pkt = new();
repeat (100) begin
    assert(pkt.randomize());
    // 用pkt.addr, pkt.data驱动DUT
end

随机化最牛的地方在于约束。你可以告诉工具:「给我随机生成数据,但要满足这些条件」。工具会自动在约束空间里找解。如果约束冲突了,randomize()会返回0,你得检查约束是不是写矛盾了。

避坑指南:我曾经写过一条约束addr > 0 && addr < 0,想当然地以为能生成非零地址。结果randomize一直失败,查了半天才发现自己写了个永远不可能满足的约束。嗯,从那以后我写约束都会先跑个空跑测试。

随机化配合覆盖率,就是验证的黄金搭档。你随机生成一万个包,然后看覆盖率——哪些分支没跑到?哪些状态没跳到?再针对性地加约束,把覆盖率补上去。

好了,这一章的内容就这些。数据类型让你写代码更简洁,过程块让意图更明确,接口让连接更清爽,断言让bug无处藏,随机化让验证更高效。这五个东西,是SV验证的基石。下一章咱们聊聊面向对象编程——class、继承、多态,那才是SV真正的大招。