Verilog always & assign Blocks
The assign statement and always block are the two fundamental ways to describe hardware behavior in Verilog. Getting them right — especially the sensitivity list and the combinational/sequential split — is the most important skill in RTL design.
1. Continuous assign Statement
The assign statement drives a wire continuously. Whenever the right-hand side expression changes, the left-hand side updates immediately.
module logic_gates ( input a, b, sel, output y_and, y_or, y_mux ); assign y_and = a & b; assign y_or = a | b; assign y_mux = sel ? a : b; // mux endmodule
- Only drives
wiretypes (or output ports not declaredreg) - Multiple
assignstatements all evaluate concurrently - No
begin / endneeded — eachassignis one statement - Cannot have procedural statements (
if,for, etc.) — usealwaysfor those
assign (or one module output). Driving from two assign statements creates a multi-driver conflict — the result is X in simulation.2. The always Block
An always block runs repeatedly (forever) in simulation. It is triggered by events in its sensitivity list. The hardware it infers depends on what triggers it.
always @(/* sensitivity list */) begin // procedural statements // if / case / for / while // = (blocking) or <= (non-blocking) assignments end
Key properties:
- Drives
regtypes only (notwire) - Multiple
alwaysblocks in the same module all run concurrently - Each signal may only be driven by one
alwaysblock - A module can have any number of
alwaysblocks
3. Sensitivity List
The sensitivity list is the event list that triggers the always block. Three forms:
// 1. Explicit list — trigger on any change to a or b or c always @(a, b, sel) begin out = sel ? a : b; end // 2. Wildcard @(*) — automatically includes all signals read inside always @(*) begin out = sel ? a : b; end // 3. Edge-triggered — trigger only on posedge or negedge always @(posedge clk) begin q <= d; end // 4. Multiple edges (async reset) always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 0; else q <= d; end
@(*) for combinational blocks — never list signals manually. Forgetting a signal in an explicit list creates a simulation/synthesis mismatch and is a very common bug.4. Combinational always @(*)
A combinational always block models logic without memory. Rules:
- Use
@(*)— wildcard picks up all inputs automatically - Use blocking assignment (
=) - Assign a value to every output in every code path (no latch!)
- May use
if / else,case,forloops
module mux4 ( input [1:0] sel, input d0, d1, d2, d3, output reg out ); always @(*) begin case (sel) 2'b00: out = d0; 2'b01: out = d1; 2'b10: out = d2; default: out = d3; // covers 2'b11 and any X endcase end endmodule
5. Sequential always @(posedge clk)
A sequential always block models flip-flops — state that changes only on clock edges. Rules:
- Sensitivity list:
@(posedge clk)or@(posedge clk or negedge rst_n) - Use non-blocking assignment (
<=) - It is OK not to assign a signal — the flip-flop retains its old value
module dff ( input clk, d, output reg q ); always @(posedge clk) q <= d; // non-blocking: q captures d on rising edge endmodule
module shift_reg8 ( input clk, rst_n, en, sin, output reg [7:0] q ); always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 8'b0; else if (en) q <= {q[6:0], sin}; // shift left, insert sin // en=0: q retains its value (flip-flop holds state) end endmodule
6. Asynchronous vs Synchronous Reset
Asynchronous Reset (most common)
Reset takes effect immediately, regardless of clock. Include rst_n in sensitivity list.
Synchronous Reset
Reset only takes effect on the next clock edge. rst_n is NOT in the sensitivity list.
// Asynchronous active-low reset always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 0; else q <= d; end // Synchronous active-high reset always @(posedge clk) begin if (rst) q <= 0; // rst NOT in sensitivity list else q <= d; end
rst_n) is the most widely used style in professional RTL. Most standard cell libraries have both D and RB (reset-bar) pins on their flip-flops. Always name your reset signal with _n suffix to indicate active-low.7. Latch Inference — How to Avoid It
A latch is inferred when a reg inside always @(*) is not assigned in every execution path. The synthesizer must insert a level-sensitive latch to hold the old value.
always @(*) begin if (en) out = data; // when en=0: out retains old value → LATCH end
// Method 1: always use else always @(*) begin if (en) out = data; else out = 8'b0; // explicit else: out always assigned end // Method 2: assign default at top of block always @(*) begin out = 8'b0; // default: covers all unspecified paths if (en) out = data; // override only when en=1 end // Method 3: case with default always @(*) begin case (sel) 2'd0: out = a; 2'd1: out = b; default: out = 8'b0; // covers 2'd2, 2'd3, X endcase end
8. Summary Table
| Feature | assign | always @(*) | always @(posedge clk) |
|---|---|---|---|
| Drives | wire | reg | reg |
| Assignment operator | = | = (blocking) | <= (non-blocking) |
| Triggered by | Any RHS change | Any input in @(*) | Rising clock edge |
| Infers hardware | Combinational | Combinational | Flip-flop |
| Procedural statements | No | Yes (if/case/for) | Yes (if/case/for) |
| Missing assignment path | N/A | Latch ⚠️ | Flip-flop retains (OK) |
9. Full Design Example: 4-bit Up Counter with Load
module counter4 ( input clk, rst_n, load, en, input [3:0] d, output [3:0] q, output carry ); reg [3:0] count; // Sequential block: flip-flops always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= 4'b0; else if (load) count <= d; else if (en) count <= count + 1; // en=0, load=0: count retains value end // Continuous assigns: combinational outputs assign q = count; assign carry = (count == 4'hF) & en; // carry out on 15→0 rollover endmodule