Packaging and binding
Write read-channel rules once with named expressions, sequences and properties, package them in a module or a checker, bind them into a DUT you cannot edit, and reuse them on every port.
- Lesson
- A7.1
- Time
- 25 min
- Runs on
- Verilator
- Check
- 6 questions
The read-channel rules had been copied into six testbenches. A fix to the hold rule reached two of the copies, and the other four went on checking the old, wrong version. The design team also refused edits to their RTL just to add checks. Packaging the rules in one module and attaching it with bind solved both problems.
The same bound rule set reports each port by its own path.
Named sequences and properties
A sequence declaration names a sequence, and a property declaration names a property. sequence beat_then_last; (rvalid && rready) ##1 rlast; endsequence lets every rule say beat_then_last instead of repeating the timing.
Both can take arguments, like functions. property needs_valid(logic sig); sig |-> rvalid; endproperty serves rlast, ruser and any other signal that is only meaningful with rvalid.
A named sequence can be used inside other sequences, so small pieces build up into larger rules. Give each one a name that reads well inside a property, such as burst_done.
Range bounds such as ##[0:MAX_STALL] must be constants known when the design is elaborated, so they come from parameters rather than from property arguments.
Naming an expression with let
Many rules share a plain condition with no timing in it, such as "the beat is stalled". let stalled = rvalid && !rready; names that expression once. Every assertion that writes stalled gets the same definition, and a fix reaches all of them.
let can take arguments, let in_window(x, lo, hi) = x >= lo && x <= hi;, and it works anywhere an expression does: in concurrent assertions, in immediate assertions and in ordinary procedural code. The example uses one let in two rules.
A text macro
`define STALLED rvalid && !rready
assert property (!`STALLED |-> ...);
// expands to !rvalid && !rready
Text substitution: the ! binds to rvalid alone, and the name is global from its definition on.
A let declaration
let stalled = rvalid && !rready;
assert property (!stalled |-> ...);
// means !(rvalid && !rready)
An expression: it behaves as if parenthesised, and its name is scoped like any declaration.
Use let for conditions, a sequence for anything that spans clock edges, and a property for a complete rule with an implication.
A module of rules
Collect the rules for one interface in a checker module: a module whose ports are inputs only and whose body is assertions, plus any auxiliary code they need. default clocking and default disable iff at the top keep every assertion short.
Parameters adapt the rules to each use, such as the longest legal stall. Because the module only reads its inputs, it never changes the behaviour of the design it watches.
Binding into a DUT you cannot edit
bind rd_port rd_rules #(.MAX_STALL(2)) rules (.*); instantiates rd_rules inside every instance of rd_port, without touching the rd_port source. The .* connects each checker port to the signal of the same name in the target.
The bound instance can see the target's internal signals, not only its ports, so assertions can watch state that never reaches an output. A bind can also name one instance instead of every instance of a module.
Rules pasted into each testbench
// tb_port0.sv
assert property (... hold rule ...);
// tb_port1.sv
assert property (... old hold rule ...);
Every copy must be fixed separately, and some never are.
Rules in one module, bound everywhere
module rd_rules ...;
hold: assert property (holds_offer);
endmodule
bind rd_port rd_rules rules (.*);
One fix reaches every port, and the DUT source is untouched.
Use %m in fail messages to print the full path of the failing assertion. With rules bound into many ports, tb.port1.rules.stall says at once which port broke which rule.
Binding one instance
Sometimes only one instance needs a rule set, or needs different parameters. The bind statement can name a single instance instead of a module, so one port can have a tighter stall bound than the others.
Binding by module is the safer default. Every new instance of the module then gets the rules automatically, while an instance-specific bind has to be remembered each time the design grows.
Assertions in the interface
Rules can also sit inside the interface that bundles the signals, as Module 6 suggested. Every instance of the interface then checks itself, with no bind statement at all.
The cost is that the rules travel with the interface into every testbench, whether or not they apply there. A separate checker module bound where needed keeps that choice explicit and easy to change.
Organising a rule library
Group the named sequences and properties for one protocol in one place, and give each assertion a label that says its rule, such as hold or last_needs_valid. Labels end up in reports, waivers and coverage databases, so they should not change casually.
Document each rule with the spec sentence it implements, as a comment above it. When a rule fires, that comment is the first thing an engineer needs.
A checker instead of a module
The checker construct is a container made for assertions and the modelling code they need. It is declared with checker ... endchecker, instantiated like a module, and bound like a module. Inside it, %m names the checker instance, such as tb.p1.bound.hold, so reports stay as precise as with a module of rules.
The standard gives checkers more than modules: they can be instantiated inside procedural code, their arguments can be sequences and properties, and synthesis tools normally ignore them. A module is the general container, with no limits on its modelling code.
In this track both work. Use a module when the rules need a real model, such as the queues of lesson A8.1. Use a checker for a self-contained rule set that you want to reuse as a library unit.
Try it
One checker module, bound into both ports, reported three different rules: port 0 changed data while stalled, port 1 stalled longer than its bound, and port 1 raised rlast without rvalid. Each line carries the full path, so the port and the rule are obvious.
Change the bind to #(.MAX_STALL(3)) and predict which line disappears.
t=35 tb.port0.rules.hold failed t=85 tb.port1.rules.stall failed t=115 tb.port1.rules.last failedCheck yourself
Remember
- Name repeated sequences and properties, and give them arguments for reuse.
- Name plain conditions with
let: it is scoped and behaves as one parenthesised expression, unlike a macro. - Put an interface's rules in one module or
checkerwith default clocking and default disable iff. bindinstantiates the rules inside a DUT module without editing its source.- Print
%min fail messages so each report names its instance.