一、为什么传统仿真总有些死角擦不干净?

想象你正在设计一个红绿灯控制器。用仿真测试时,你可能模拟了100种车流情况,但总会有第101种情况没测到——比如某个行人按钮信号和救护车优先信号同时触发的极端场景。传统仿真就像用手电筒照房间,光束之外永远存在黑暗角落。

形式化验证则像把整个房间的电路图摊开,用数学方法证明"在任何输入组合下,红灯和绿灯永远不会同时亮起"。比如下面这个简单的Verilog交通灯模块:

// 技术栈:Verilog + SymbiYosys形式化验证工具
module traffic_light(
    input emergency,  // 紧急车辆信号
    input pedestrian, // 行人请求信号
    output reg red,
    output reg green
);
always @(*) begin
    if (emergency) begin
        red = 0;
        green = 1;  // 紧急情况永远绿灯
    end else begin
        red = ~pedestrian;
        green = pedestrian;
    end
end

// 形式化属性:红绿互斥验证
`ifdef FORMAL
always @(*) begin
    assert(red != green);  // 数学证明两者永不相等
end
`endif
endmodule

这个assert语句会被形式化工具穷举所有可能的输入组合(emergency和pedestrian的4种组合),验证其正确性。而传统仿真可能漏掉emergency和pedestrian同时为1的情况。

二、形式化验证工具怎么当"数学老师"?

主流工具如SymbiYosys的工作流程就像解数学题:

  1. 把设计代码转化为数学模型
  2. 根据断言(assert)生成证明目标
  3. 用引擎(如ABC、Z3)进行证明

看个带算术运算的例子——验证一个简单的累加器不会溢出:

// 技术栈:Verilog + SymbiYosys
module accumulator(
    input clk,
    input [7:0] data,
    output reg [7:0] sum
);
always @(posedge clk) begin
    sum <= sum + data;
end

// 形式化验证部分
`ifdef FORMAL
always @(*) begin
    assume(data < 8'd100);          // 约束输入范围
    assert(sum >= 8'd0);            // 和永远非负
    assert(sum < 8'd200);           // 防止溢出
end
`endif
endmodule

这里assume相当于给工具出题时的已知条件,assert是需要证明的结论。工具会尝试找到使assert失败的输入序列,如果找不到就算验证通过。

三、实际工程中的花式验证技巧

真实芯片设计往往需要组合多种验证手段。比如验证一个FIFO(先入先出队列)时:

// 技术栈:Verilog + SymbiYosys
module fifo(
    input clk,
    input wr, rd,
    input [7:0] din,
    output [7:0] dout,
    output full, empty
);
reg [7:0] mem [0:7];
reg [2:0] wptr, rptr;

// 形式化验证覆盖三个场景:
`ifdef FORMAL
// 场景1:连续写入不溢出
property no_overflow;
    @(posedge clk) disable iff(!wr)
    (wptr - rptr <= 3'd7);
endproperty

// 场景2:读出数据正确性
sequence data_match;
    ##1 (rd && !empty) |-> (dout == $past(mem[rptr]));
endsequence

// 场景3:空满信号正确
always @(*) begin
    if (wptr == rptr) assert(empty != full);
    assume(wr != rd || wr == 0);  // 避免同时读写
end
`endif
endmodule

这种验证可以捕捉到诸如"连续写入8次后full信号未置位"、"空满信号同时有效"等隐蔽错误,这些在仿真中可能需要数万次随机测试才能发现。

四、该用形式化验证的三大场景

  1. 生命攸关系统:如医疗设备中"呼吸机不会在患者呼气时送气"的验证
  2. 极端条件验证:CPU设计中"任何异常指令组合都不会导致死锁"
  3. 协议合规性:确保AXI总线遵守ARM制定的所有规则

但也要注意它的局限:

  • 对复杂算法(如机器学习加速器)验证难度指数级上升
  • 需要人工编写断言,考验工程师的"脑洞"
  • 验证耗时可能比仿真更长

五、新手容易踩的五个坑

  1. 过度约束:把assume条件设得太严格,导致验证失去意义。比如假设"输入永远为0",那当然所有assert都成立
  2. 漏掉边界:忘记验证复位状态或初始条件
  3. 抽象陷阱:用非可综合语句写断言,与实际电路行为不符
  4. 工具误判:有时需要调整证明引擎参数
  5. 性能黑洞:验证10行代码可能只要1秒,100行代码或许要1小时

六、最佳实践路线图

建议按这个顺序渐进式验证:

  1. 先验证单个模块的简单属性(如状态机不会卡死)
  2. 再验证模块间接口协议(如握手信号正确性)
  3. 最后挑战系统级特性(如死锁自由)

例如验证UART发送模块时,可以分三步:

// 阶段1:验证基本功能
assert(tx_busy |-> ##[1:10] !tx_busy);  // 发送总会结束

// 阶段2:验证数据完整性
assert(##1 start_bit |-> ##[8:9] stop_bit); 

// 阶段3:验证波特率容错
assume(clk_period >= 520ns && <= 540ns);
assert(bit_width >= 515ns && <= 545ns);

七、与传统仿真的黄金组合

聪明的做法是:

  1. 用形式化验证保证基础属性绝对正确
  2. 用随机仿真覆盖复杂场景
  3. 用形式化生成的"反例测试向量"增强仿真

就像建筑既要用CAD验证结构强度,又要做实体模型的风洞测试。两者结合才能既避免数学遗漏,又防止现实中的意外情况。