为什么验证占了大部分工作量
有个数字会让刚入行的人吃惊:在一款正经设计上,验证通常占总工作量的 60–70%——投入的工程师、算力和日历时间都比设计本身还多。为什么这么悬殊?因为硬件不留情面。一个网页应用出了 bug,你当天下午就能推个修复上去。一颗芯片出了 bug,修复的代价是一套新掩模、一次新流片,外加你人生里的一个季度。
所以这份活儿不是「让它跑通一次」,而是要用证据说服你自己:对硅片这辈子会遇到的每一种输入,它都是对的。你不可能把输入全试一遍——哪怕只是一个 32 位加法器,也有两个 32 位操作数,也就是 2^64 种输入组合——所以验证其实是一门不靠穷举却令人信服地周全的艺术:选对激励,盯对该看的东西,再衡量你究竟驱动了设计的多少部分。
测试平台
测试平台是硬件的测试夹具——但它本身不是芯片的一部分。把它想成电子实验室里的一张试验台:一套夹住你电路、给它喂信号、再盯着回来什么的装置。在 Verilog 里,它是一个没有端口的模块——它坐在设计外面,把被测对象(DUT)实例化进来,驱动它的输入,再检查它的输出。
比起「光看波形」,关键的升级在于让测试平台自己会查:它事先就知道正确答案,一旦现实跟它不符就当场喊出来。那一句 `$error` 就是全部的关键——它把「我用肉眼扫了一遍,看着还行」变成了「机器把每一个结果都和黄金期望值比对过,没发现一个错的」。
module tb;
logic [3:0] a, b;
logic [4:0] sum;
adder dut (.a(a), .b(b), .sum(sum)); // DUT: the design under test
initial begin
a = 4'd7; b = 4'd9; // drive inputs
#1; // let combinational logic settle
if (sum !== a + b) // compare against expected
$error("bad sum: %0d + %0d gave %0d", a, b, sum);
$finish;
end
endmodule定向测试 vs 约束随机
上面那个测试平台是一个定向测试:是你这个工程师亲手挑了 7 + 9,因为你直觉它要紧。定向测试精确、可读,对那些你叫得出名字的情形再合适不过——复位行为、溢出边界、那个你心里有数会咬人的角落。它的弱点也正是它的本性:你只能找到那些你早就想到要去找的 bug。
约束随机把这件事反了过来。你不再一个个去点名情形,而是描述输入的合法空间,让工具朝设计扔出成千上万条「随机但合法」的激励。约束让随机性保持理智——一个合法的操作码、一个在范围内的地址——而随机性则能触及你这辈子也不会手挑出来的角落。让它跑一整夜,它会探索出没有哪个人会去敲出来的组合。
class packet;
rand bit [7:0] addr;
rand bit [3:0] len;
constraint legal { addr < 8'd200; len inside {[1:8]}; } // stay in the legal space
endclass
// call randomize() in a loop -> thousands of valid, varied stimuli覆盖率:我们到底测了多少?
约束随机带出一个让人不安的问题:既然工具是在乱扔随机输入,你怎么知道它真的试到了那些要紧的情形? 没准十万个随机包里,长度恰好为 8 的一次都没碰上。覆盖率就是答案——它是一块记分牌,告诉你设计被你实际驱动了多少,把「我们跑了好多测试」变成「我们命中了自己声明在意的场景里的 94%」。
它有两种口味。代码覆盖率是自动的,问的是「每一行、每个分支、每次翻转都被驱动到了吗?」——必要,但浅。[[functional-coverage|功能覆盖率]]才是真正撑得起价值的那个:是*你*来写下覆盖点,给那些要紧的情形点名——每个操作码都见过了、FIFO 既碰到过空也碰到过满、每个合法的长度值——再由工具统计究竟哪些真的发生了。它回答的是那个真问题:我们做完了吗?
covergroup cg @(posedge clk);
coverpoint len { bins all[] = {[1:8]}; } // did we see every length 1..8?
coverpoint state; // did we visit every FSM state?
endgroup断言:当场抓住 bug
自检测试平台是在输出端抓 bug——而且是等坏值传播出来之后才抓到。断言则是在案发现场抓它:它是一条直接嵌进设计里的规则,说「这件事必须永远为真」,一旦被违反就在那一瞬间触发,径直指向出问题的信号和周期。
它们在跨越时间的规则上最出彩。普通的 `if` 只检查某一个瞬间;一条 SVA(SystemVerilog 断言)检查的是跨越多个周期的关系——「一个请求之后,三个时钟之内必须有应答」「这个独热信号永远不会有两位同时为 1」。那个 `|->` 读作蕴含:左边一旦发生,右边就必须跟着发生。
// after every request, an ack must arrive within 1 to 3 cycles property req_gets_ack; @(posedge clk) req |-> ##[1:3] ack; endproperty assert property (req_gets_ack);
瞥一眼形式验证与 UVM
仿真哪怕跑上几十亿个随机周期,测到的也只是你恰好生成出来的那些输入——它是在对一片汪洋做采样。形式验证走的是另一条路:它把你的断言交给一个数学引擎,让引擎证明它们对*所有*可能的输入都成立,或者递给你一个具体的反例。没有激励,没有覆盖率盲区——而是一份证明。它撑不到整颗芯片的规模,但在一个棘手的模块上(一个仲裁器、一个 FIFO、一个协议的角落),它带来的差别就是「我们没看到它出错」和「它不可能出错」之间的差别。
随着设计变大,临时拼凑的测试平台会撑不住。UVM(通用验证方法学)是业界用来搭建大型测试平台的标准框架——可复用的组件(driver、monitor、scoreboard)、一个用来生成约束随机流量的 sequencer,以及一个覆盖率模型,全都按照每个验证团队都认得的方式接在一起。把它想成那套脚手架,让一个测试平台从一个工程师的脚本,长成一整个团队可以维护多年的东西。