VERILOG SERIES · MODULE 10

Behavioral Modelling in Verilog — VLSI Trainers
Verilog Series · Module 10

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.

📝
Procedural Blocks
Behavioral code lives inside initial or always blocks — procedural contexts where statements execute one after another.
🔄
Sequential Execution
Inside a block, statements run in order — unlike assign statements which all run simultaneously.
📦
Drives reg Variables
Procedural blocks assign to reg (and integer, real, time) variables — never directly to wire nets.
⚠️
Synthesis Caveats
Not all behavioral constructs synthesize correctly. initial, #delays, and some loop forms are simulation-only.
Fig 1 — Behavioral vs lower levels: same D flip-flop
// ── 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
Fig 2 — Behavioral level in the abstraction hierarchy
▶ Behavioral Level — initial / always ◀ YOU ARE HERE Data Flow Level — assign statements Gate Level — and / or / nand / nor / xor Switch Level — nmos / pmos / cmos ↑ Most abstract ↓ Most detailed

⚙️ 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 Assignment
Executes and completes before the next statement runs. The assigned value is immediately visible to subsequent lines in the same block. Models combinational logic.
<=
Non-Blocking Assignment
All RHS expressions are evaluated first, then all LHS targets are updated simultaneously at the end of the time step. Models sequential/registered logic (flip-flops).
Fig 3 — Blocking vs Non-Blocking: execution order and simulation result

= 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
Never mix = and <= in the same always block. Mixing creates subtle race conditions that simulate correctly but synthesize wrong — or vice versa. Pick one per block based on its purpose.

Fig 4 — All Statement Types Inside Procedural Blocks

Operations available inside initial and always 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.

Fig 5 — Blocking assignment execution flow
1
Evaluate RHS
Compute the right-hand side expression using current variable values
2
Update LHS Immediately
Assign the result to the left-hand side variable right now
3
Proceed to Next Statement
The next line sees the newly updated value — execution is sequential
// 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.

Fig 6 — Non-blocking execution: two-phase update
1
Phase 1 — Evaluate ALL RHS (Active Region)
Compute every non-blocking RHS using current values — nothing is updated yet
2
Phase 2 — Update ALL LHS (NBA Region)
All scheduled updates happen simultaneously — models parallel flip-flop capture
3
Continue simulation
Other always blocks and assign statements see the updated values
// 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!
Why non-blocking works for shift registers: In real hardware, all flip-flops in a chain capture their inputs at the same instant (the clock edge). Non-blocking assignments model this perfectly — every q <= input reads the old value of its input, so the pipeline shifts correctly.

🔀 Functional Bifurcation

Verilog’s behavioral model provides two distinct procedural constructsinitial and always. This split (bifurcation) is intentional: each serves a fundamentally different purpose in the design and verification flow.

initial Executes once at time zero — simulation / testbench only
  • 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 initial blocks in one module all start simultaneously
  • Uses #delays and event controls for timing
always Repeats forever — models hardware behaviour
  • 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 always blocks in one module all run concurrently
  • Triggered by events (posedge clk) or sensitivity lists (@(*))
Fig 7 — Simulation timeline: initial runs once, always loops forever
t=0 t=10 t=20 t=30 t=40 initial block — runs once $finish init always always always always alw

🟠 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.

Fig 8 — initial block syntax and usage patterns
// ── 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

Fig 9 — Initialise a register file or ROM using initial + for loop
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 is not synthesizable. Synthesis tools ignore 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.

Fig 10 — always block: three forms and when to use each
// ── 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.

@(*) Infer all — automatic for combinational
@(posedge clk) Rising edge — synchronous DFF
@(negedge clk) Falling edge — less common
@(posedge clk or negedge rst_n) Edge + async reset
@(a or b or c) Level sensitive — old style combinational (avoid — use @(*) instead)
Fig 11 — Sensitivity list: correct vs incomplete (causes sim/synth mismatch)

❌ 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

Incomplete if/case → Latch! If a combinational 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.
Fig 12 — Latch inference: missing else clause

❌ 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

Fig 13 — Conditional branching in always blocks
// 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

Fig 14 — case statement forms: full-match, x-wildcard, z-wildcard
// 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

Fig 15 — Loop constructs: for, while, repeat, forever
// 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
Synthesis and loops: 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

Sequential always block — demonstrates all common RTL patterns
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)

Two-always FSM pattern — state register + next-state/output logic
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

Parallel-In Serial-Out shift register — load or shift each cycle
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

Complete testbench: clock gen, reset, stimulus, auto-check
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

SituationUseReason
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 / defaultAlways include Prevents unintentional latch inference in combinational always blocks
Mixing = and <= Never mix in same block Causes simulation/synthesis mismatches
The two-always FSM pattern (one always for the state register, one for next-state/output logic) is the most portable, readable, and synthesis-friendly way to write state machines in Verilog. It cleanly separates sequential and combinational concerns, making the design easy to verify and maintain.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top