Back to EECS 31L Index

3. EECS 31L / 03 Behavioral Modeling

EECS 31L • Study Notes • Behavioral Modeling
Mahmoud Elfar • Spring 2026 • v0.4

v0.1: Initial version
v0.2: Added procedural assignments
v0.3: Added conditional statements
v0.4: Added looping statements


Table of Contents


3.1. What is Behavioral Modeling?

Recall the three abstraction levels:

Level How you describe the circuit
Dataflow assign statements: describe what each signal equals
Behavioral Procedural blocks: describe what the circuit does, step by step
Structural Instantiate components and wire them together

Behavioral modeling is the most abstract of the three. Instead of expressing a circuit as a network of gates or a set of simultaneous equations, we model its behavior as a sequence of statements that executes when certain conditions are met.

Two things are always true in behavioral modeling:

  1. Behavioral code lives inside procedural blocks (initial or always).
  2. Inside those blocks, you assign values using procedural assignment (= or <=), not assign.

Remember: You cannot have a module that uses assign statements with always or initial blocks.

There is a loophole to this where you instantiate a submodule of a different abstraction level than the parent one, but that discussion is for another day.

↑ Back to top

3.2. Procedural Blocks

See: IEEE 1800-2017, 9.2 Structured procedures, pg. 205

A procedural block is a list of statements that:

  1. execute based on a triggering condition, and
  2. execute sequentially (from top to bottom).

There are two types of procedural blocks: initial and always. In a nutshell:

The construct used to mark the beginning and end of a procedural block is the begin-end block.

In behavioral modeling, your module:

Remember:

3.2.1. initial Blocks

Syntax:

initial begin
  // statements
end

Semantics:

Example:

initial begin
  a = 0; b = 0;         // set initial values
  #10 a = 1;            // change a after 10 time units
  #10 b = 1;            // change b 10 time units later
  #10 $finish;          // end simulation
end

Don’t get fazed by the delay operator here — we will cover that later.

3.2.2. always Blocks

Syntax:

always @(<sensitivity_list>) begin
  // statements
end

Semantics:

The sensitivity list is what distinguishes an always block modeling combinational logic from one modeling sequential logic:

// Combinational: re-evaluate whenever any input changes
always @(*) begin
  y = a & b | c;
end

// Sequential: re-evaluate only on the rising edge of clk
always @(posedge clk) begin
  q <= d;
end

To understand the example above, keep reading.

3.2.3. Procedural Timing Controls

Procedural timing control is concerned with specifying when a procedural block executes. There are two main constructs (that we care about) that can be used for this purpose:

But for now, what we care about is understanding what the sensitivity list is and how to write one.
To this end, keep reading.

3.2.4. The Sensitivity List

See: IEEE 1800-2017, 9.4.2 Event control, pg. 217

The sensitivity list @(...) specifies what triggers the block to execute. There are multiple ways to specify the list inside @(...):

Examples:

Syntax Semantics
@(*) Trigger whenever any signal read inside the block changes (wildcard)
@(a) Trigger whenever a changes (either edge)
@(a, b, c) Trigger whenever a, b, or c changes
@(posedge clk) Trigger on the rising edge of clk
@(negedge clk) Trigger on the falling edge of clk
@(posedge clk, negedge rst_n) Trigger on rising clk or falling rst_n
@(posedge clk or negedge rst_n) Trigger on rising clk or falling rst_n

There are generally two common patterns for sensitivity lists that you should be very aware of:

3.2.5. The begin-end Block

Syntax:

begin
  statement_1;
  statement_2;
  ...
  statement_N;
end

Semantics:

The construct begin-end is Verilog’s version of { } in C/C++.

// Without begin-end: only the first statement is part of the always block
always @(*) 
  y = a & b;   // fine for one statement

// With begin-end: both statements are part of the block
always @(*) begin
  y = a & b;
  z = a | b;
end

// Invalid: do not do this at home
begin
  // This is not valid as it appears at the module level and not inside initial or always
  // The compiler will whine and complain about this.
  y = a & b;
  z = a | b;
end

↑ Back to top

3.3. Procedural Assignments

See IEEE 1800-2017, 10.4 Procedural assignments, pg. 236

Inside a procedural block, you use procedural assignments to update values.

Syntax:

<Variable> = <expression>;   // blocking assignment
<Variable> <= <expression>;  // nonblocking assignment

Remember:

We will discuss the semantic differences between both types of procedural assignments in the rest of this section.

3.3.1. Blocking Procedural Assignments

Syntax:

<Variable> = <expression>;   // blocking assignment

Semantics:

Example: Swapping values.

always @(*) begin
  temp = a ^ b;        // Statement 1: temp gets updated first
  y    = temp & cin;   // Statement 2: y sees the updated value of temp
end

While the two statements above execute in sequence, they both execute within the same time step, and even within the same delta cycle. It’s a nuance that sounds harmless now, but it will haunt us later.

Blocking assignments model combinational logic well, because the intent is to compute a result step by step within the same time step.

3.3.2. Nonblocking Procedural Assignments

Syntax:

<Variable> <= <expression>;  // nonblocking assignment

Note: The operator here resembles a left arrow < = (without the space) — a very common symbol in fields related to programming language theory and formal semantics. Fonts that use ligatures may render this as a single less-than-or-equal symbol in your browser, so don’t panic if you see the latter.

Semantics:

Example: Swapping values.

always @(posedge clk) begin
  a <= b;   // Statement 1: a gets the old value of b
  b <= a;   // Statement 2: b gets the old value of a
end
// Result: a and b are swapped. Works correctly because both RHS values
// are captured before either LHS is updated.

If you used blocking assignments here, a would be updated before b <= a executes, so b would get the new value of a, not the old one — a bug.

Example: 4-bit Ring Shift Register (also known as a Ring Counter). We will assume fixed value of 4'b0001 for simplicity.

module ring_shift_4 (input clk, output reg [3:0] q);
  // set initial value at the start of simulation
  initial q = 4'b0001; // Legal to remove begin-end since there is only one statement
  // Shift left by one bit on every rising edge of clk
  always @(posedge clk) begin // positive-edge triggered
    q[0] <= q[3];   // q[0] gets the old value of q[3]
    q[1] <= q[0];   // q[1] gets the old value of q[0]
    q[2] <= q[1];   // q[2] gets the old value of q[1]
    q[3] <= q[2];   // q[3] gets the old value of q[2]
  end
endmodule

3.3.3. When to Use What

This is one of the most common sources of bugs in Verilog. The rule is simple and you should follow it without exception:

What you are modeling Assignment to use
Combinational logic (always @(*)) Blocking =
Sequential logic (always @(posedge clk)) Nonblocking <=

The following are best practices and not language rules. Ignoring them should be criminalized.

Following these rules consistently eliminates an entire lineage of bugs that you could do without.

↑ Back to top

3.4. Conditional Statements

Conditional statements are language constructs that allow you to decide about whether a procedural statement or block should execute based on some condition. We will cover two

Note: if-else and case can be called “conditional statements” or “constructs”. The former emphasizes their syntactic form; the latter emphasizes their semantic role as a language feature. Also, if statements and case statements themselves are not procedural statements — they are procedural programming statements/constructs.
You can safely ignore this note and use the terms interchangeably.

3.4.1. Conditional If-Else Statements

Syntax:

// If construct (with single statement)
if (<condition>) // single statement if condition is true
  statement_1;
// If construct (with blocks)
if (<condition>) begin
  // statements if condition is true
end
// If-else construct
if (<condition>) begin
  // statements if condition is true
end else begin
  // statements if condition is false
end
// If-else-if construct
if (<condition>) begin
  // statements if condition is true
end else if (<condition_2>) begin
  // statements if condition_2 is true
end else if (<condition_3>) begin
  // statements if condition_3 is true, and so on ...
end else begin // Else is always optional
  // statements if none of the above are true
end

Condition Semantics: (the if-else condition semantics in Verilog is slightly more convoluted than in C/C++)

Branching Semantics:

Example: a 4-to-1 MUX.

always @(*) begin
  if      (sel == 2'b00) y = d0;
  else if (sel == 2'b01) y = d1;
  else if (sel == 2'b10) y = d2;
  else                   y = d3;
end

When modeling combinational logic with if-else (using behavioral modeling), it is a good practice to ensure that the output has explicit assignment statement in every possible branch of the condition. If there is a branch where the output is not assigned, the output retains its value from the previous time step. Remember, the compiler will force you to declare any outputs of a behavioral design as a reg.

Note: Textbooks like to call this a “latch inference”, “latch behavior”, or a “latch is inferred”. I don’t like this terminology — a latch is less inferred and more explicitly enforced by the language semantics that makes it mandatory to use reg for outputs in this case. You can safely ignore this note since it argues semantics. Literally.

Example: bad (and incorrect) vs good designs of a 4-to-1 MUX.

// BAD: y is not assigned when sel = 2'b11
always @(*) begin
  if      (sel == 2'b00) y = d0;
  else if (sel == 2'b01) y = d1;
  else if (sel == 2'b10) y = d2;
  // no else: what is y when sel = 11?
end

// GOOD: always assign a default first, then override as needed
always @(*) begin
  y = d3;   // default
  if      (sel == 2'b00) y = d0;
  else if (sel == 2'b01) y = d1;
  else if (sel == 2'b10) y = d2;
end

3.4.2. Case Statements

There are three variants of the case statement: case, casez, and casex. They share the same syntax and branching semantics, but have different matching semantics.

Syntax:

case (<expression>) // casez or casex
  value_1: begin ... end // branch 1
  value_2: begin ... end // branch 2
  ...
  default: begin ... end // default branch (optional)
endcase

Branching Semantics:

Matching Semantics for case:

Matching Semantics for casez:

Matching Semantics for casex:

Example: same 4-to-1 MUX.

always @(*) begin
  case (sel)
    2'b00: y = d0;
    2'b01: y = d1;
    2'b10: y = d2;
    2'b11: y = d3;
    default: begin
      $error("OMG we didn't see this coming! sel = %b", sel);
      y = 0; // assign something out of habit
    end
  endcase
end

Example: Priority encoder.

// casez: z in case items is a don't-care wildcard
always @(*) begin
  casez (req)
    4'b1???: grant = 2'b11; // If req[3] is 1, ignore the rest
    4'b01??: grant = 2'b10;
    4'b001?: grant = 2'b01;
    4'b0001: grant = 2'b00;
    default: grant = 2'b00;
  endcase
end

When to use what:

↑ Back to top

3.5. Looping Statements

See: IEEE 1800-2017, 12.7 Loop statements, pg. 313

Looping statements are constructs that allow for executing a block of statements multiple times.
In Verilog, this is primarily useful for the following use cases:

For synthesizable design code, loops must be statically unrollable: the simulator/synthesizer must be able to expand them into a fixed, finite number of statements at compile time. This means the loop bounds must be constants.

New Terms:

We will cover the following loop constructs: for, while, repeat, and forever.

3.5.1. For Loop Statement

Syntax:

for (<init>; <condition>; <step>) begin
  // statements
end

Semantics:

Note:

Example: initialize all bits of a register in a testbench.

integer i; // Declare loop variable, must be integer
initial begin
  for (i = 0; i < 8; i = i + 1) begin
    mem[i] = 0;
  end
end

When this gets unrolled, it is replaced by:

initial begin
  mem[0] = 0;
  mem[1] = 0;
  mem[2] = 0;
  mem[3] = 0;
  mem[4] = 0;
  mem[5] = 0;
  mem[6] = 0;
  mem[7] = 0;
end

Example: Compute a bitwise parity using a loop.

integer i;
reg parity;
always @(*) begin
  parity = 0;
  for (i = 0; i < 8; i = i + 1) begin
    parity = parity ^ data[i];
  end
end

This synthesizes correctly because the loop bounds are constants and the tool unrolls it into 8 XOR operations.

3.5.2. While Loop Statement

Syntax:

while (<condition>) begin
  // statements
end

Semantics:

Example: In a testbench, wait until a signal goes high.

// Testbench: wait until a signal goes high
initial begin
  while (done == 0) begin
    #10;   // check every 10 time units
  end
  $display("done is high at time %0t", $time);
end

3.5.3. Repeat Loop Statement

Syntax:

repeat (<N>) begin
  // statements
end

Semantics:

Example: In a testbench, apply 5 clock cycles (flip clock value 10 times)

// Testbench: apply 10 clock cycles
initial begin
  repeat (10) begin
    #5 clk = ~clk;
  end
end

3.5.4. Forever Loop Statement

Syntax:

forever begin
  // statements
end

Semantics:

// Testbench: generate a clock with 10 ns period
initial begin
  clk = 0;
  forever #5 clk = ~clk;
end

↑ Back to top