The bind construct instantiates a verification module inside an RTL module without editing the RTL source. The bound instance elaborates as a child of the target scope, seeing the target's ports and internal signals.
That makes bind the standard delivery mechanism for assertion checkers, protocol monitors, and coverage collectors on design code that DV does not own. The next lesson, Writing Assertion Modules for bind, covers what a bind-safe module looks like.
bind instead of editing assertions into the RTL?Three reasons: the RTL files stay untouched and synthesis-clean, the checkers live under DV ownership and review, and the bound instance still sees internal signals that a testbench-side checker could only reach through fragile hierarchical paths.
A bind statement names a target, a checker module to insert, an instance name for the copy, and a port map. Ports connect by name to signals visible inside the target's scope, ports and internal signals alike.
Targeting the module name binds the checker into every instance of that module. Targeting a specific instance path binds only that one copy, for instances that are configured differently or singly interesting.
// snippet
// By module: every fifo_stub in the design gets a checker.
bind fifo_stub fifo_bound_checks #(.DEPTH(DEPTH)) chk (.*);
// By instance: only the command FIFO inside the DMA gets one.
bind top.u_dma.u_cmd_fifo fifo_bound_checks #(.DEPTH(8)) chk (
.clk(clk), .rst_n(rst_n), .push(push), .pop(pop),
.full(full), .empty(empty), .count(count)
);.* works when checker port names match target signal names exactly. Explicit maps survive renames better and make the connection auditable, so prefer them outside throwaway binds.
After elaboration the bound instance is a real child of the target, present under the target's path in every instance for a module bind. It elaborates almost as if the instantiation line were the last statement inside the target: visibility follows the target's scope, not a literal inline copy.
ELABORATED HIERARCHY
source: bind fifo_stub fifo_bound_checks #(...) chk (...);
top
|- u_dma
| |- u_cmd_fifo (fifo_stub)
| | |- ...rtl contents...
| | └- chk (fifo_bound_checks) <- inserted by bind
| └- u_data_fifo (fifo_stub)
| |- ...rtl contents...
| └- chk (fifo_bound_checks) <- every instance
Wave path: top.u_dma.u_cmd_fifo.chk.a_count_range
Scope: chk sees fifo_stub's clk, push, count, ... by nameParameters pass through normally, so one checker scales across differently sized instances. A bound unit can be a module, interface, program, or checker, and one target can receive several binds under distinct instance names.
fifo_stub and the design has three instances. How many checkers elaborate, and where do they appear?Three, one per instance. Each appears as chk under its own target instance, for example top.u_dma.u_cmd_fifo.chk.
count to occ_q. Which file do you fix, and how does the failure show up?The DV bind file. Elaboration reports a signal-not-found at the bind line, which is routine maintenance after RTL drops, never a reason to keep legacy names in RTL.
A two-instance FIFO stub with a bound occupancy checker. Traffic stays legal, so a silent assertion run plus a reference-model count check is the pass. The testbench also reads u_small.chk.DEPTH and u_big.chk.DEPTH: those paths exist only because the bind put a checker under each instance, so the bench fails to elaborate without it.
// engine: verilator
timeunit 1ns; timeprecision 1ps;
// A module bind elaborates one checker per instance; each tracks its own target.
module fifo_stub #(parameter int DEPTH = 8) (
input logic clk, rst_n,
input logic push, pop,
output logic full, empty,
output logic [$clog2(DEPTH):0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0;
else case ({push && !full, pop && !empty})
2'b10: count <= count + 1'b1;
2'b01: count <= count - 1'b1;
default: count <= count;
endcase
end
assign full = (count == DEPTH);
assign empty = (count == '0);
endmodule
module fifo_bound_checks #(parameter int DEPTH = 8) (
input logic clk, rst_n,
input logic push, pop, full, empty,
input logic [$clog2(DEPTH):0] count
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
a_no_push_full : assert property (full |-> !push || pop)
else $error("push into full fifo");
a_no_pop_empty : assert property (empty |-> !pop)
else $error("pop from empty fifo");
a_count_range : assert property (count <= DEPTH);
endmodule
bind fifo_stub fifo_bound_checks #(.DEPTH(DEPTH)) chk (
.clk(clk), .rst_n(rst_n), .push(push), .pop(pop),
.full(full), .empty(empty), .count(count)
);