Back to EECS 31L Index

2. EECS 31L / 02 Dataflow Modeling

EECS 31L • Study Notes • Dataflow Modeling
Mahmoud Elfar • Spring 2026 • v0.1

v0.1: Initial version


Table of Contents

2.1. What is Dataflow Modeling?

There are three main abstraction levels for describing a digital circuit in Verilog:

Level Description How it looks
Structural Describe the circuit as a network of connected components (gates, modules) Instantiate gates and modules, wire them together
Dataflow Describe how data flows and transforms between signals assign statements expressing Boolean/arithmetic relationships
Behavioral Describe what the circuit should do algorithmically always and initial blocks

This note covers dataflow modeling. It maps most naturally to the combinational logic you already know: you have inputs, you compute some function of them, and you drive an output.

The key idea: you describe what a signal equals, not how to compute it step by step. The simulator (and synthesizer) figures out the hardware.

A Hot Take:

If dataflow modeling doesn’t sound very convincing to you, that’s because the term does not convey anything meaningful. There is no data flowing in a dataflow model — no more or less than the other two levels of abstraction.

A better name for this style would have been continuous assignment modeling. Whatever you call it, the core concept is modeling through a collection of continuous, concurrent assignments. That’s all to it. Forget about the “relationship between inputs and outputs” and all that.

↑ Back to top

2.2. The assign Statement

See: IEEE 1800-2017, 10.3 Continuous assignments, pg. 233

2.2.1. Syntax

assign <net> = <expression>;

With an optional delay:

assign #<delay> <net> = <expression>;

2.2.2. Semantics

The assign statement creates a continuous assignment. This means:

Example:

wire y;
wire a, b, c;
assign y = a & b | c;   // y is continuously driven by this expression

If a changes, y is re-evaluated. If b changes, y is re-evaluated. And so on.

Contrast this with procedural assignment (inside always blocks), which only executes when the block is triggered. That is a different modeling style covered in a future note.

2.2.3. Rules and Restrictions

// These two are equivalent regardless of order
assign y = a & b;
assign z = y | c;

assign z = y | c;
assign y = a & b;

↑ Back to top

2.3. Expressions and Operands

An expression on the RHS can use:

Examples:

wire [3:0] a, b, y;
wire s, cout;

assign y    = a & b;        // bitwise AND of two 4-bit vectors
assign s    = a[0] ^ b[0];  // XOR of the LSBs
assign cout = a[0] & b[0];  // AND of the LSBs
assign y    = a[3:1];       // 3-bit part-select assigned to 4-bit wire: MSB is zero-extended

Width matching: When the LHS and RHS have different widths, Verilog zero-extends if the RHS is narrower, or truncates from the MSB if the RHS is wider. Always make sure widths match intentionally.

↑ Back to top

2.4. The Conditional Operator

The conditional operator ?: is the only conditional construct that can be used in a dataflow model.
It is a ternary (three-operand) operator that selects one of two values based on a condition.

Syntax:

assign <lhs> = (<condition>) ? <value_if_true> : <value_if_false>;

Semantics:

Example — a 2-to-1 MUX:

wire [3:0] a, b, y;
wire sel;
assign y = (sel) ? b : a;   // if sel=1, y=b; if sel=0, y=a

Conditional operators can be nested to model larger MUXes. Example — a 4-to-1 MUX:

wire [3:0] d0, d1, d2, d3, y;
wire [1:0] sel;
assign y = (sel == 2'b00) ? d0 : // if sel=00, then y=d0, else ...
           (sel == 2'b01) ? d1 : // if sel=01, then y=d1, else ...
           (sel == 2'b10) ? d2 : // if sel=10, then y=d2, else ...
                            d3;  // y=d3

Notice the formatting: align the ? and : vertically for readability. The last alternative has no condition — it is the default (like the final else in an if-else chain).

↑ Back to top

2.5. Delays in Dataflow Modeling

See: IEEE 1800-2017, 10.3.3 Delays, pg. 235

A delay can be added to a continuous assignment:

assign #5 y = a & b;   // y updates 5 time units after a or b changes

The time unit is set by your `timescale directive. With `timescale 1ns/1ps, #5 means 5 ns.

There are two distinct delay behaviors to understand:

Inertial/Assign delay (default for assign):

Transport/Wire delay (default for wire):

// Inertial/Assign delay: declared on the assignment
assign #3 y = a & b;   // pulses shorter than 3 units are suppressed

// Transport/Wire delay: declared on the wire itself
wire #3 y;
assign y = a & b;      // every transition on (a & b) is delayed by exactly 3 units, including short pulses

For this course: you will mostly use assign delays. The inertial vs. transport distinction is important when analyzing glitches and timing diagrams — a glitch that appears on paper may not actually propagate to the output if the gate delay is longer than the glitch pulse width.

↑ Back to top

2.6. Worked Examples

2.6.1. Half Adder

In this implementation, we model the half adder using its gate-level Boolean equations.

module half_adder (input a, input b, output s, output c);
  assign s = a ^ b;
  assign c = a & b;
endmodule

In this implementation, we model the half adder using its functional behavior directly. A half adder basically computes the sum of two bits, so we use an arithmetic addition operator.

module half_adder (input a, input b, output s, output c);
  assign {c, s} = a + b;   // treat a and b as 1-bit numbers, sum is 2 bits: {carry, sum}
endmodule

Both designs are valid, correct, and synthesizable. In a more complex design, the second style (functional modeling) can be easier to write and less prone to errors.

2.6.2. Full Adder

module full_adder (input a, input b, input cin, output s, output cout);
  assign s    = a ^ b ^ cin;
  assign cout = (a & b) | (a & cin) | (b & cin);
endmodule

2.6.3. 4-to-1 MUX

We can use intermediate wires to implement a MUX:

module mux4 (
  input  [3:0] d,     // data inputs: d[3], d[2], d[1], d[0]
  input  [1:0] sel,
  output       y
);
  wire w0, w1;
  assign w0 = sel[1] ? d[2] : d[0];
  assign w1 = sel[1] ? d[3] : d[1];
  assign y  = sel[0] ? w1   : w0;
endmodule

Intermediate wire declarations break a complex expression into readable, named pieces. The result is still purely combinational and dataflow. This is the preferred style when the single-expression version becomes hard to read.

However, if you read the design above, it is a good example of a bad design. It is logically correct but cognitively painful to comprehend.

A better design would follow the natural way by which the functionality of a MUX is defined:

module mux4 (
  input  [3:0] d,     // data inputs: d[3], d[2], d[1], d[0]
  input  [1:0] sel,
  output       y
);
  assign y = (sel == 2'b00) ? d[0] :
             (sel == 2'b01) ? d[1] :
             (sel == 2'b10) ? d[2] :
                              d[3];
endmodule

That was much better, don’t you think? Now, think about a 128-to-1 MUX, which would require 127 nested conditionals. So, another way to describe the functionality of a MUX is to use indexing:

module mux4 (
  input  [3:0] d,     // data inputs: d[3], d[2], d[1], d[0]
  input  [1:0] sel,
  output       y
);
  assign y = d[sel];   // use sel as an index into the vector d
endmodule

2.6.4. Bit manipulation

Just swapping the upper and lower nibbles of an 8-bit input.

module swap_halfs (input [7:0] in, output [7:0] out);
  assign out = {in[3:0], in[7:4]};   // swap upper and lower halfs
endmodule

2.6.5. Priority encoder (4-to-2)

A priority encoder takes multiple request signals and encodes the index of the highest-priority active request.
The second output indicates whether any request is active (valid bit).

module priority_enc (
  input  [3:0] req,
  output [1:0] grant,
  output       valid
);
  assign valid = |req;   // OR reduction: valid if any request is active
  assign grant = req[3] ? 2'b11 :
                 req[2] ? 2'b10 :
                 req[1] ? 2'b01 :
                          2'b00;
endmodule

Note the use of the OR reduction operator |req to compute valid in a single expression.

↑ Back to top