4、激励生成:时钟信号生成、复位信号生成、数据激励生成、随机激励生成

做仿真验证,说白了就是给设计喂数据,看它怎么反应。而喂什么数据、怎么喂,这就是激励生成要解决的问题。我刚开始带项目那会儿,有个同事写了个复杂的测试用例,结果仿真跑了三天三夜没跑完——后来发现是时钟生成写了个死循环。嗯,这种坑咱们得避开。

4.1 时钟信号生成

时钟是数字电路的心跳。没有时钟,一切归零。生成时钟看起来简单,但实际项目中容易出幺蛾子。

最基本的时钟生成,用 Verilog 写就是:

reg clk;
initial begin
    clk = 0;
    forever #5 clk = ~clk;  // 10ns周期,100MHz
end

我个人习惯用 always 块来写,更直观:

reg clk = 0;
always #5 clk = ~clk;

这里有个细节——reg clk = 0 这种初始化写法,在有些仿真器里可能不认。我建议用 initial 块给初值,兼容性更好。

小技巧: 如果你需要多个不同频率的时钟,可以用一个高频时钟分频得到。别每个时钟都写一个 forever,那样仿真效率会变差。

带使能的时钟,在低功耗验证中很常见:

reg clk_gated;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        clk_gated <= 1'b0;
    else if (clk_en)
        clk_gated <= #1 ~clk_gated;
end

注意那个 #1 延迟——不加的话,仿真波形上会出现奇怪的毛刺。我曾经因为这个被折腾了一整天,后来发现是门控时钟的仿真模型没加延迟。

4.2 复位信号生成

复位信号,说白了就是让设计回到初始状态。但怎么复位、什么时候释放,这里头有讲究。

异步复位,最常用的写法:

reg rst_n;
initial begin
    rst_n = 1'b0;
    #100;          // 保持复位100ns
    rst_n = 1'b1;  // 释放复位
    #200;
    rst_n = 1'b0;  // 再来一次复位
    #50;
    rst_n = 1'b1;
end

你想想看,如果设计里有多个时钟域,每个时钟域的复位释放时间应该错开。我见过一个案例,就是因为所有复位同时释放,导致电源瞬间电流过大,仿真直接崩了。

注意: 复位释放时,最好在时钟上升沿之后一点点。比如用 @(posedge clk); #1; 来释放,这样可以避免复位释放和时钟上升沿同时发生,导致仿真出现亚稳态问题。

同步复位的生成方式不同:

reg rst_sync;
always @(posedge clk) begin
    if (rst_n)
        rst_sync <= 1'b0;
    else
        rst_sync <= 1'b1;
end

嗯,这里要注意,同步复位需要时钟才能工作。如果时钟停了,复位就失效了。所以芯片上电时,一般先用异步复位把关键路径稳住,再切到同步复位。

4.3 数据激励生成

数据激励,就是给设计喂的各种输入数据。最简单的做法是直接赋值,但实际项目中,数据往往有特定的协议和时序要求。

固定数据序列,适合做基本功能测试:

reg [7:0] data_in;
initial begin
    data_in = 8'h00;
    #10;
    data_in = 8'hA5;
    #10;
    data_in = 8'h5A;
    #10;
    data_in = 8'hFF;
end

循环数据,适合做压力测试:

reg [31:0] addr;
initial begin
    addr = 32'h0000_0000;
    repeat (1024) begin
        @(posedge clk);
        addr = addr + 4;
    end
end

我在项目中遇到过一个问题:用 repeat 循环生成地址时,忘了加 @(posedge clk),结果地址在一个时钟周期内跳了1024次。仿真波形看起来就像一根毛刺,根本没法分析。

核心原则: 数据激励必须和时钟同步。要么用 @(posedge clk) 同步,要么用非阻塞赋值 <=。否则仿真结果和实际芯片行为会差十万八千里。

4.4 随机激励生成

随机激励,是验证的杀手锏。它能覆盖你没想到的边界情况。SystemVerilog 里用 $random 或者 std::randomize 来实现。

简单的随机数:

reg [7:0] rand_data;
initial begin
    repeat (100) begin
        @(posedge clk);
        rand_data = $random % 256;  // 0~255的随机数
    end
end

但 $random 有个问题——它是均匀分布的。你想想看,如果地址范围是0~1023,但实际设计中只有地址0~15和1000~1023有特殊逻辑,均匀随机就很难覆盖到这些边界。

带约束的随机,才是正道:

class Packet;
    rand bit [7:0] length;
    rand bit [31:0] address;
    
    constraint addr_range {
        address inside {[0:15], [1000:1023]};
    }
    constraint len_range {
        length > 0;
        length < 64;
    }
endclass

initial begin
    Packet pkt = new();
    repeat (1000) begin
        assert(pkt.randomize());
        @(posedge clk);
        // 把 pkt.length 和 pkt.address 送给设计
    end
end

我曾经犯过一个错:随机约束写得太松,结果生成了大量无效数据,仿真跑了两天没发现问题。后来加了覆盖率分析才发现,关键路径只被覆盖了30%。

避坑指南: 随机激励一定要配合覆盖率分析。光有随机,没有覆盖率,就像蒙着眼睛打靶——打中了也不知道打的是哪儿。

4.5 知识体系总览

下面这张图,是我自己总结的激励生成知识体系。你可以把它当作一个检查清单,写测试用例时对照着看,不容易漏东西。

激励生成知识体系 时钟信号生成 复位信号生成 数据激励生成 随机激励生成 • 基本时钟生成 • 多时钟域 • 门控时钟 • 时钟使能 • 异步复位 • 同步复位 • 复位释放时序 • 多域复位同步 • 固定序列 • 循环数据 • 协议时序 • 边界数据 • $random • 约束随机 • 覆盖率驱动 • 边界随机 核心原则:同步 + 约束 + 覆盖率 时钟同步所有激励,约束确保有效性,覆盖率衡量完整性

这张图把激励生成的四个核心模块串起来了。时钟是基础,复位是起点,数据是主体,随机是补充。四者缺一不可。

最后说一句:写激励的时候,别总想着一次搞定所有情况。先写简单的、固定的,跑通了再上随机。我见过太多人一上来就写复杂的随机约束,结果仿真跑不动,debug 也找不到问题在哪。循序渐进,才是正道。

总结: 时钟要稳,复位要准,数据要全,随机要狠。把这四个字记住,激励生成这块你就拿下了。

可可精品资料库