Advanced SystemVerilog Design Verification
Advanced Procedural Modeling
Beyond the Basic 'always' Block
If you've written some Verilog, you're familiar with the versatile always @(*) block. It's the workhorse for describing logic. However, for professional hardware design, stating your intent clearly is crucial. You don't just want to write code that works in a simulation; you need code that synthesizes into predictable, efficient hardware. This is where specialized procedural blocks come into play.
SystemVerilog introduced always_comb, always_ff, and always_latch to allow designers to be explicit about the type of hardware they are modeling. Using these blocks correctly is a major step toward eliminating common design flaws like and ensuring your simulated behavior matches your final chip. These constructs aren't just syntactic sugar; they are powerful tools for creating robust Register-Transfer Level (RTL) code.
Modeling Combinational Logic
For purely combinational logic, like a multiplexer or an arithmetic unit, you should use always_comb. This block signals to both the simulator and the synthesis tool that you are describing logic without any memory or clocking.
The key advantage of
always_combis that it automatically infers its own sensitivity list. You don't need to write@(*). The tool figures out all the signals on the right-hand side of assignments that should trigger re-evaluation.
More importantly, always_comb performs built-in checks. For instance, if you write code where a variable might not be assigned a value under all conditions, the tool will flag a warning. This is how it helps you find and fix potential unintentional latches before they become a problem.
Here’s a simple 2-to-1 multiplexer using always_comb.
// Good practice: using always_comb for combinational logic
module mux2to1 (
output logic out,
input logic a, b, sel
);
always_comb begin
if (sel == 1'b1) begin
out = b;
end else begin
out = a;
end
end
endmodule
What if you do want to create a latch intentionally? For that, SystemVerilog provides always_latch. It behaves similarly to always_comb but tells the tools that you are creating a level-sensitive memory element on purpose. This makes your design intent clear and suppresses the warnings you would otherwise get.
Sequential Logic and Assignments
For sequential logic—circuits that depend on a clock, like flip-flops and registers—the correct construct is always_ff. The ff stands for flip-flop. This block enforces that you specify a clock edge in the sensitivity list, such as @(posedge clk) or @(negedge clk).
Inside an always_ff block, a critical rule emerges: always use non-blocking assignments (<=). This is arguably the most important convention in RTL design for preventing and race conditions.
A blocking assignment (=) is immediate. The right-hand side is evaluated and assigned to the left-hand side before the next line of code is executed. A non-blocking assignment (<=) schedules the assignment to happen at the end of the current time step. All right-hand sides in the block are evaluated first, and then all the assignments occur simultaneously.
Think of it this way: non-blocking assignments model the behavior of real flip-flops, which all capture their new values on the clock edge at the same instant.
Here are two examples modeling a simple two-stage shift register. The first uses non-blocking assignments correctly. The second uses blocking assignments and creates incorrect hardware.
Correct Shift Register (<=) | Incorrect Shift Register (=) |
|---|---|
| ```systemverilog | |
| always_ff @(posedge clk) begin | |
| q1 <= d; // Scheduled | |
| q2 <= q1; // Scheduled | |
| end | |
| ``` | ```systemverilog |
| always_ff @(posedge clk) begin | |
| q1 = d; // Immediate | |
| q2 = q1; // Immediate | |
| end |
In the correct version, at the clock edge, q1 is scheduled to get the value of d, and q2 is scheduled to get the old value of q1. This creates a two-stage register. In the incorrect version, q1 is immediately updated with d. The next line then reads this new value of q1 and assigns it to q2. The result is that both q1 and q2 get the value of d, and the shift register is broken. It becomes a one-stage register with two identical outputs.
The rule of thumb is simple and robust:
- Use
always_combfor combinational logic with blocking assignments (=). - Use
always_fffor sequential logic with non-blocking assignments (<=).
Following these two rules will save you countless hours of debugging.
Ready to check your understanding?
Why were specialized procedural blocks like always_comb and always_ff introduced in SystemVerilog?
What is a key advantage of using always_comb over a generic always @(*) for describing combinational logic?
By mastering these intent-driven blocks and assignment rules, you are writing code that is not only easier to read and maintain but also far more likely to produce the exact hardware you intended.
