SystemVerilog/Verilog中forever语法:从基础到实战的深度解析

张开发
2026/4/15 20:58:51 15 分钟阅读

分享文章

SystemVerilog/Verilog中forever语法:从基础到实战的深度解析
1. forever语法基础从分号陷阱到执行逻辑第一次接触Verilog的forever语句时我踩过一个经典坑在仿真测试中写了满屏的$display却始终看不到输出。后来发现是forever后面多打了个分号导致整个begin...end块变成了僵尸代码。这个看似简单的语法细节其实是很多初学者都会遇到的绊脚石。forever语句最基础的形式有两种写法// 写法A带分号错误示范 forever (posedge clk); begin // 这里代码永远不会执行 end // 写法B不带分号正确用法 forever (posedge clk) begin // 这里代码每个时钟周期都会执行 end这两种写法的区别就像红绿灯的通行规则分号相当于在forever后面加了个永久的红灯而省略分号则是给后续代码开了绿灯。我在带新人时常用这个比喻他们立刻就能理解其中的执行逻辑差异。更隐蔽的陷阱是begin...end的包裹范围。来看这段实际工程中的地址生成代码logic [31:0] addr; initial begin forever (posedge clk) addr addr 1; // 只有这行会循环执行 begin // 这个begin...end永远等不到执行 if (addr 32hFFFF) $finish; end end这个bug会导致仿真永远无法自动结束因为条件判断被排除在循环体外了。正确的写法应该用begin...end包裹所有需要循环的语句forever (posedge clk) begin addr addr 1; if (addr 32hFFFF) $finish; end2. 时钟驱动场景下的实战应用在FPGA原型验证中我经常用forever构建时钟同步的监控进程。比如这个DDR接口的实时监测模块initial begin forever (posedge ddr_clk) begin if (ddr_cmd_valid) begin monitor_fifo.push_back({ddr_cmd, ddr_addr}); $display([%t] DDR命令捕获%h %h, $time, ddr_cmd, ddr_addr); end // 每1us生成一次吞吐量报告 #1000ns report_throughput(); end end这里结合了事件触发(posedge)和延时控制(#)实现了多速率协同。注意这种写法只能用于仿真综合时会报错。另一个典型应用是构建可配置的时钟门控测试task automatic clk_gating_test(input int cycles); fork begin // 时钟生成 forever begin clk 1b0; #(CLK_PERIOD/2); clk 1b1; #(CLK_PERIOD/2); end end begin // 门控测试 repeat (cycles) begin (negedge clk); gating_enable $urandom_range(0,1); #10ns; end $finish; end join endtask这个例子展示了如何用forever配合fork-join构建并行的时钟生成器。实际项目中我会用这种结构来做电源管理单元的验证。3. 条件触发与异步事件处理在验证AXI总线时foreverwait的组合堪称黄金搭档。比如这个等待DMA完成的典型场景initial begin forever begin wait(dma_start 1b1); // 等待触发信号 $display([%t] DMA传输启动, $time); fork : timeout_check begin #100ms; $error(DMA超时未完成); disable timeout_check; end begin wait(dma_done 1b1); $display([%t] DMA传输完成, $time); disable timeout_check; end join end end这种结构完美解决了异步事件同步化的问题。我在PCIe验证中曾用类似结构捕获TLP包配合覆盖率收集实现了自动化验证。对于多条件复合触发推荐使用wait结合事件表达式forever begin wait(arbiter.grant (packet_fifo.size 0)); send_packet(); wait(arbiter.grant 0 || packet_fifo.size 0); end注意这里第二个wait的作用是防止send_packet被重复调用这种触发-执行-释放的模式在总线仲裁中非常常见。4. 可综合代码与测试平台的最佳实践虽然forever在RTL设计中受限但在某些可综合场景仍有妙用。比如这个时钟分频器实现always begin // 等效于forever #(PERIOD/2) clk_div2 ~clk_div2; end综合工具会将这种写法识别为时钟生成电路。但要注意三点必须使用#延时控制不能有事件触发需要配合initial或always块使用在UVM测试平台中forever常用于构建后台检查器class mem_checker extends uvm_component; virtual task run_phase(uvm_phase phase); forever begin (posedge vif.clk); if (vif.mem_en) begin check_mem_access(vif.addr, vif.wdata, vif.rdata); end end endtask endclass这种监控器会持续运行整个仿真周期我在一次DDR4验证中用它发现了地址映射错误。最后分享几个血泪教训在forever循环内一定要设置退出条件或超时机制避免仿真死锁使用disable语句终止命名块时注意作用域范围在SystemVerilog中优先使用always_comb/always_ff代替forever对时钟敏感的forever循环要添加$error提示仿真时间

更多文章