Behavioral Modelling in Verilog
The highest level of abstraction in Verilog — describe what a circuit does using procedural constructs, blocking and non-blocking assignments, and the full power of initial and always blocks.
🧠 Introduction
Behavioral modelling is the highest level of abstraction in Verilog. Instead of describing circuit structure (gate level) or signal flow (data flow), it describes what the circuit does — its algorithm, its sequence of operations, its response to events — and lets the synthesis tool figure out the hardware implementation.
Behavioral code looks very similar to a C program: it uses variables, conditional statements, loops, and sequential execution inside procedural blocks. This makes complex designs fast to write, easy to read, and straightforward to simulate.
initial or always blocks — procedural contexts where statements execute one after another.assign statements which all run simultaneously.reg (and integer, real, time) variables — never directly to wire nets.initial, #delays, and some loop forms are simulation-only.// ── Gate Level (8 NAND gates, cross-coupled) ────────────────── nand g1(s, d, clk); nand g2(r,s,clk); // ... 6 more gates // ── Data Flow Level (complex expression) ────────────────────── // Not natural for flip-flops — behavioral is much better // ── Behavioral Level (readable, concise) ────────────────────── always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; // async reset else q <= d; // capture D on rising edge end
⚙️ Operations and Assignments
Inside a procedural block, statements assign values to reg variables using one of two assignment operators — blocking (=) and non-blocking (<=). Choosing the correct one is critical for correct simulation and synthesis.
= Blocking — Sequential execution
always @(posedge clk) begin a = b; // a gets b NOW b = a; // b gets new a! end // Result: a=b, b=b (b unchanged) // NOT a swap!
<= Non-Blocking — Parallel update
always @(posedge clk) begin a <= b; // schedule: a ← b b <= a; // schedule: b ← a end // Result: a=old_b, b=old_a // ✅ Correct register swap!
The Golden Rules
✅ Use = (Blocking) for:
- Combinational logic in
always @(*) - Temporary variables inside a block
- Test bench stimulus generation
- Loop counters and index variables
✅ Use <= (Non-Blocking) for:
- Clocked sequential logic (flip-flops)
always @(posedge clk)blocks- State machine next-state updates
- Any register that holds a value clock-to-clock
Fig 4 — All Statement Types Inside Procedural Blocks
// ── Variable Assignments ────────────────────────────────────── a = b & c; // blocking — immediate update q <= d; // non-blocking — deferred update // ── Conditional Statements ──────────────────────────────────── if (en) out = in; else if (rst) out = 0; else out = out; // hold — may infer latch! // ── Case Statements ─────────────────────────────────────────── case (sel) 2'b00: out = in0; 2'b01: out = in1; default: out = 8'bx; endcase // ── Loops ───────────────────────────────────────────────────── for (i=0; i<8; i=i+1) mem[i] = 8'h00; // initialise memory // ── Timing Controls ─────────────────────────────────────────── #10; // delay 10 time units (sim only) @(posedge clk); // wait for rising clock edge @(a or b); // wait for any change on a or b wait(done); // wait until done goes high // ── System Tasks (simulation) ───────────────────────────────── $display("q = %b", q); $finish;
🟠 Blocking Assignments ( = )
A blocking assignment blocks execution of the next statement until it completes. The updated value is immediately available for subsequent statements in the same block — just like a variable assignment in C.
// Combinational always block — use blocking assignments always @(*) begin p = a & b; // p updated immediately q = p | c; // q uses the NEW value of p ✅ y = ~q; // y uses the NEW value of q ✅ end // Using a temporary variable (blocking only makes sense) always @(*) begin tmp = a + b; // intermediate result result = (tmp > 8'hFF) ? 8'hFF : tmp; // saturate end
🟢 Non-Blocking Assignments ( <= )
A non-blocking assignment schedules an update without blocking. All RHS values are captured first, then all LHS targets are updated simultaneously at the end of the current time step. This is how real flip-flops behave — all capture their inputs at the same clock edge.
// Clocked sequential block — use non-blocking assignments always @(posedge clk) begin q0 <= d; // captures d at clock edge q1 <= q0; // captures OLD q0 — correct shift-register behaviour ✅ q2 <= q1; // captures OLD q1 end // All three capture simultaneously — models 3-stage shift register // With blocking (=): q1=q0=d, q2=q1=d — all become d! Wrong!
q <= input reads the old value of its input, so the pipeline shifts correctly.
🔀 Functional Bifurcation
Verilog’s behavioral model provides two distinct procedural constructs — initial and always. This split (bifurcation) is intentional: each serves a fundamentally different purpose in the design and verification flow.
- Starts at time 0, executes its body once, then stops
- Used in testbenches for stimulus, waveform generation, and memory initialisation
- Not synthesizable — ignored by synthesis tools
- Multiple
initialblocks in one module all start simultaneously - Uses
#delaysand event controls for timing
- Starts at time 0, executes body, then loops back forever
- Used for all synthesizable RTL — combinational logic, flip-flops, FSMs
- Synthesizable when written correctly with proper sensitivity list
- Multiple
alwaysblocks in one module all run concurrently - Triggered by events (
posedge clk) or sensitivity lists (@(*))
🟠 The initial Construct
The initial block executes its body exactly once, starting at simulation time zero. It is used almost exclusively in testbenches — for driving stimulus, initialising memories, and controlling simulation flow.
// ── Single statement ────────────────────────────────────────── initial clk = 0; // no begin/end needed for one statement // ── Multiple statements — needs begin/end ───────────────────── initial begin clk = 0; reset = 1; data = 8'h00; #20 reset = 0; // release reset after 20 time units #5 data = 8'hAB; // apply data at t=25 #10 data = 8'hCD; // change data at t=35 #50 $finish; // end simulation at t=85 end // ── Multiple initial blocks (all run at t=0) ────────────────── initial clk = 0; // block 1: initialise clock initial begin // block 2: apply test stimulus rst = 1; #10 rst = 0; end initial begin // block 3: monitor outputs $monitor("%0t: q=%b", $time, q); end
Memory Initialisation with initial
reg [7:0] rom [0:15]; // 16-entry × 8-bit ROM integer i; initial begin // Method 1: explicit values rom[0] = 8'h00; rom[1] = 8'hFF; rom[2] = 8'hA5; rom[3] = 8'h5A; // Method 2: loop initialisation for (i=0; i<16; i=i+1) rom[i] = i[7:0]; // fill with 0,1,2...15 // Method 3: load from file $readmemh("program.hex", rom); // read hex values from file end
initial blocks entirely. For reset behaviour in hardware, use a synchronous or asynchronous reset signal in your always block — not an initial statement.
🔵 The always Construct
The always block is the workhorse of behavioral RTL modelling. It runs continuously, re-triggering whenever its sensitivity list events occur. Every flip-flop, latch, and combinational logic block in a real design is described using always.
// ── Form 1: Combinational logic — @(*) ─────────────────────── always @(*) begin // @* = all signals on RHS case (opcode) 2'b00: alu_out = a + b; 2'b01: alu_out = a - b; 2'b10: alu_out = a & b; default: alu_out = 8'b0; endcase end // ── Form 2: Synchronous sequential logic — posedge clk ──────── always @(posedge clk) begin if (rst) q <= 1'b0; else q <= d; end // ── Form 3: Async reset — posedge clk + negedge rst_n ───────── always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; // reset any time rst_n goes low else q <= d; end // ── Form 4: Clock generation (testbench) ────────────────────── always #5 clk = ~clk; // toggle every 5 units → period=10
📡 Sensitivity List
The sensitivity list (the part inside @(...)) tells the always block which events trigger re-evaluation. Getting this right is critical — a wrong or incomplete sensitivity list is one of the most common RTL bugs.
❌ Incomplete sensitivity list
// Missing 'c' — simulation latch always @(a or b) begin y = a & b | c; // c not in list! end // Sim: y doesn't update when c changes // Synth: correct combinational logic // → sim/synth mismatch!
✅ Complete sensitivity list
// Use @(*) — always correct always @(*) begin y = a & b | c; end // @* automatically includes all // signals read on the RHS — a, b, c // Sim and synth always match ✅
Latch Inference Warning
always block doesn’t assign a value to an output in every possible condition, the synthesizer infers a latch to hold the last value. This is almost always unintentional. Always provide a default in case and an else in every if for combinational blocks.
❌ Infers a latch
always @(*) begin if (en) q = d; // No else — q holds when en=0 // → latch inferred! end
✅ Pure combinational
always @(*) begin if (en) q = d; else q = 1'b0; // defined for all cases end
🔀 Control Flow Statements
Behavioral blocks support the full range of procedural control flow constructs — similar to C but with hardware-specific semantics.
if / else if / else
// Simple if-else always @(*) begin if (a > b) result = a; else if (a == b) result = 8'h00; else result = b; end // Priority encoder (if-else chain = priority hardware) always @(*) begin if (in[3]) code = 2'b11; // highest priority else if (in[2]) code = 2'b10; else if (in[1]) code = 2'b01; else code = 2'b00; end
case / casex / casez
// case — exact match (no wildcards) always @(*) begin case (opcode) 3'b000: out = a + b; 3'b001: out = a - b; 3'b010: out = a & b; 3'b011: out = a | b; default: out = 8'bx; // ← always include default! endcase end // casez — z (or ?) acts as wildcard (don't-care) always @(*) begin casez (priority_in) 4'b1???: out = 2'b11; // bit 3 set — highest priority 4'b01??: out = 2'b10; 4'b001?: out = 2'b01; 4'b0001: out = 2'b00; default: out = 2'bxx; endcase end // casex — both x and z act as wildcards // Use casez in RTL; casex can mask real x bugs in simulation
Loops
// for — counted loop (unrolled by synthesis for fixed bounds) always @(*) begin for (i=0; i<8; i=i+1) out[i] = in[7-i]; // bit reversal end // while — condition-controlled (use with care in RTL) initial begin i = 0; while (i < 10) begin @(posedge clk); i = i + 1; end end // repeat — fixed number of iterations initial begin repeat(5) @(posedge clk); // wait 5 clock cycles $display("5 cycles elapsed"); end // forever — infinite loop (testbench clock gen) initial begin clk = 0; forever #5 clk = ~clk; // equivalent to always #5 clk=~clk end
for loops with constant bounds are synthesizable — the tool unrolls them. while, repeat, and forever are generally not synthesizable and should be used in testbenches only.
💡 Complete Examples
Fig 16 — 4-bit Up/Down Counter with Sync Load
module counter_4bit ( input clk, rst_n, // clock, active-low async reset input up_down, // 1=count up, 0=count down input load, // synchronous load enable input [3:0] data_in, output reg [3:0] count, output tc // terminal count flag ); always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= 4'b0000; // async reset else if (load) count <= data_in; // synchronous load else if (up_down) count <= count + 1'b1; // count up else count <= count - 1'b1; // count down end // Combinational flag — terminal count assign tc = (up_down && count==4'hF) || (!up_down && count==4'h0); endmodule
Fig 17 — Mealy FSM: Sequence Detector (101)
module seq_detector_101 ( input clk, rst_n, x, output detected ); // State encoding localparam S0=2'b00, S1=2'b01, S2=2'b10, S3=2'b11; reg [1:0] state, next_state; // Always 1: State register — sequential, non-blocking always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S0; else state <= next_state; end // Always 2: Next-state + output — combinational, blocking always @(*) begin next_state = S0; // default — prevents latches case (state) S0: next_state = x ? S1 : S0; // waiting for 1 S1: next_state = x ? S1 : S2; // got 1, wait for 0 S2: next_state = x ? S3 : S0; // got 10, wait for 1 S3: next_state = x ? S1 : S2; // got 101, detected! endcase end // Mealy output — combinational assign detected = (state == S3) && x; endmodule
Fig 18 — 8-bit PISO Shift Register
module piso_8bit ( input clk, rst_n, input [7:0] parallel_in, input load, // 1=load parallel, 0=shift output serial_out // MSB first ); reg [7:0] shift_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) shift_reg <= 8'h00; else if (load) shift_reg <= parallel_in; // load else shift_reg <= {shift_reg[6:0], 1'b0}; // shift left end assign serial_out = shift_reg[7]; // MSB is always the output endmodule
Fig 19 — Behavioral Testbench with Self-Checking
module counter_tb; reg clk, rst_n, up_down, load; reg [3:0] data_in; wire [3:0] count; wire tc; // Instantiate DUT counter_4bit dut ( .clk(clk), .rst_n(rst_n), .up_down(up_down), .load(load), .data_in(data_in), .count(count), .tc(tc) ); // Clock generator — 10ns period initial clk = 0; always #5 clk = ~clk; // Stimulus and self-checking initial begin // Initialise and reset {rst_n, up_down, load} = 3'b001; data_in = 4'h5; @(posedge clk); #1; if (count !== 4'h5) $display("FAIL load: expected 5, got %0d", count); // Release load, count up load = 0; rst_n = 1; up_down = 1; repeat(5) @(posedge clk); if (count !== 4'hA) $display("FAIL count_up: expected 10, got %0d", count); else $display("PASS count_up"); $finish; end // Waveform dump initial begin $dumpfile("counter_tb.vcd"); $dumpvars(0, counter_tb); end endmodule
Summary: Behavioral Modelling Best Practices
| Situation | Use | Reason |
|---|---|---|
| Combinational logic | always @(*) with = | @(*) is complete, = gives immediate values for intermediate calculations |
| Sequential logic | always @(posedge clk) with <= | <= models simultaneous capture correctly, prevents race conditions |
| Async reset | @(posedge clk or negedge rst_n) | rst_n in sensitivity list triggers reset without waiting for clock |
| FSM state register | Separate sequential always block | Clean separation of state register and combinational next-state logic |
| Stimulus / testbench | initial with = and #delays | One-shot, can use delays, not synthesized |
| Missing else / default | Always include | Prevents unintentional latch inference in combinational always blocks |
| Mixing = and <= | Never mix in same block | Causes simulation/synthesis mismatches |
