Back to EECS 31L Index
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
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:
initial or always).= 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.
See: IEEE 1800-2017, 9.2 Structured procedures, pg. 205
A procedural block is a list of statements that:
There are two types of procedural blocks: initial and always. In a nutshell:
initial: executes once at the start of the simulation and then terminates; is not synthesizable; used only in testbenches.always: executes repeatedly whenever its sensitivity condition is met; is generally synthesizable; used in both design and testbench code.The construct used to mark the beginning and end of a procedural block is the begin-end block.
begin: marks the start of a procedural block.end: marks the end of a procedural block.begin and end.In behavioral modeling, your module:
Remember:
initial BlocksSyntax:
initial begin
// statements
end
Semantics:
$finish.initial in a design module (not a testbench), the synthesizer will either ignore it or complain.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.
always BlocksSyntax:
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.
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:
# — waits for a specific amount of time to pass. We will dedicate an entire note later just for this topic.@ — waits for one of a set of events to occur, and is used to specify the sensitivity list for a procedural block. We will also talk about this in more detail later.But for now, what we care about is understanding what the sensitivity list is and how to write one.
To this end, keep reading.
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 @(...):
*: execute whenever any of the signals (nets or variables) read inside the block changes., or or: execute whenever any of those signals change values.posedge, negedge, or edge: execute on the rising edge, falling edge, or either edge of a specific signal.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:
@(*). @(*) infers the list automatically and is the safe default.posedge or negedge. posedge and negedge correspond to the rising and falling edge of a clock (or reset) signal.begin-end BlockSyntax:
begin
statement_1;
statement_2;
...
statement_N;
end
Semantics:
begin-end groups multiple statements into a single compound statement.always or initial block (or any conditional branch) contains more than one statement.begin-end is optional but still good practice.always or initial); cannot be used alone at the module level.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
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:
assign statements -> LHS must be of Net type (e.g., wire).reg).We will discuss the semantic differences between both types of procedural assignments in the rest of this section.
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.
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
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.
always block.always block.always block.Following these rules consistently eliminates an entire lineage of bugs that you could do without.
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.
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++)
<condition> is any expression that evaluates to a 4-state logical value: 0, 1, x, or z, either single-bit (scalar) or multi-bit (vector).
true if bit value is 1.false if bit value is 0, x, or z.true if it has at least one bit that is 1false if all bits are 0s, zs, xs, or a combination of those.true if and only if the value contains at least one bit that is 1.false.else if and else branches are optional.begin-end can be ommitted if the branch contains only one statement.Branching Semantics:
true executes, and the rest are skipped.else branch has no condition (is always true).true and there is no else, then none of the branches execute.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
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:
<expression> thenvalue_1. If they match, execute branch 1 and skip the rest.value_2. If they match, execute branch 2 and skip the rest. And so on.default branch:
Matching Semantics for case:
<expression> is compared against the case items using exact equality operator === (three equals signs).
0 matches only 0, 1 matches only 1, x matches only x, and z matches only z.Matching Semantics for casez:
z is treated as a don’t-care (wildcard). So, z matches 0, 1, x, and z.Matching Semantics for casex:
x and z are treated as don’t-cares (wildcards). So, x matches 0, 1, x, and z; and z matches 0, 1, x, and z.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:
if-else: Preferred when conditions involve comparisons between different signals or over an interval of values (e.g., if (a > b)).case: Preferred when selecting among many discrete values of the same expression (e.g., case (opcode)).
case: Everyday logic (LUT, controllers)casez: Pattern matching (decoding, priority logic)casex: Probably never (dynamic masking)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.
Syntax:
for (<init>; <condition>; <step>) begin
// statements
end
Semantics:
<init> is executed once at the beginning of the loop.<condition> is evaluated. If it is true:
<step> is executed.<condition> again.<condition> is false, the loop terminates and execution continues with whatever follows the loop.Note:
for in C.integer.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.
Syntax:
while (<condition>) begin
// statements
end
Semantics:
<condition>.<condition> is true, execute the body, then go back to the first step and evaluate <condition> again.<condition> is false, the loop terminates and execution continues with whatever follows the loop.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
Syntax:
repeat (<N>) begin
// statements
end
Semantics:
N times.N is both constant and statically determinable (known at elaboration time).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
Syntax:
forever begin
// statements
end
Semantics:
#) somewhere inside, otherwise the simulation hangs.// Testbench: generate a clock with 10 ns period
initial begin
clk = 0;
forever #5 clk = ~clk;
end