Modelling at Data Flow Level — VLSI Trainers
VLSI Trainers Verilog Series · 7 / 18
Verilog Series · Module 09

Modelling at Data Flow Level

Master Verilog’s continuous assignment model — how signals are driven, how delays are specified, how vectors are assigned, and every operator available at the data-flow level.

🌊 Introduction

Data flow level modelling sits between gate level (primitives and wires) and behavioural level (procedural blocks). It describes a circuit in terms of how data flows through logical and arithmetic expressions, using the assign keyword for continuous assignments.

You no longer need to know exactly which gates implement a function — you only need to express the logical relationship between inputs and outputs. The synthesis tool figures out which gates to use.

🔄
Continuous Evaluation
An assign statement is always active. The moment any input changes, the output re-evaluates — just like a physical wire.
📝
Compact Description
Complex combinational logic that takes 10+ gate primitives can be expressed in a single assign expression.
⚙️
Synthesizable
All standard assign expressions are directly synthesizable — tools map them to gates using the target library.
🔗
No Topology Required
Unlike gate level, you don’t need to know or specify how gates connect. Express intent, not implementation.
Fig 1 — The three modelling styles for the same AND-OR-INVERT function
// y = ~((a & b) | (c & d))

// ── Gate Level (structural) ────────────────────────────────────
wire ab, cd;
and  g1 (ab, a, b);
and  g2 (cd, c, d);
nor  g3 (y,  ab, cd);

// ── Data Flow Level (continuous assignment) ────────────────────
assign y = ~((a & b) | (c & d));   // ✅ one line, no intermediate wires

// ── Behavioural Level (procedural) ────────────────────────────
always @(*) y = ~((a & b) | (c & d));

⚖️ Data Flow vs Gate Level

Understanding when to choose data-flow over gate-level is key to writing efficient, maintainable Verilog.

🌊 Data Flow Level

  • Uses assign keyword
  • Describes logical relationships
  • No need to know gate topology
  • More compact and readable
  • Easily synthesizable
  • Supports all Verilog operators

🔗 Gate Level

  • Uses gate primitives (and, or…)
  • Describes circuit structure
  • Full control over topology
  • Verbose for complex functions
  • Synthesizable
  • Closer to physical implementation
Fig 2 — Where data flow fits in the abstraction hierarchy
Behavioural Level — always / initial / if / case ▶ Data Flow Level — assign ◀ YOU ARE HERE Gate Level — and / or / nand / nor / xor Switch Level — nmos / pmos / cmos ↑ More abstract ↓ More detailed

♾️ Continuous Assignment Structures

A continuous assignment is a statement that continuously drives a net with a value. It uses the keyword assign and is always active — whenever the right-hand side changes, the left-hand side updates immediately (or after a specified delay).

This models exactly how a wire behaves in real hardware: the output of a gate is always driven by its inputs, not just when you “tell it to” run.

🔌
Always Active
Unlike procedural blocks, a continuous assignment doesn’t wait to be triggered. It re-evaluates whenever any RHS operand changes.
📡
Drives Nets Only
The left-hand side must always be a wire (or tri, wand, etc.) — never a reg. The RHS can be any expression.
🔁
Implicit Wire
In a module port declaration, if an output is directly assigned, Verilog implicitly treats it as a wire — no separate declaration needed.
Parallel Execution
All assign statements in a module run simultaneously — order of declaration does not matter.

📐 assign Syntax

The assign statement has a clear and consistent structure:

assign
Keyword
always required
#(d)
Delay
optional — sim only
net_lhs
LHS Target
must be a wire/net
=
Assignment
continuous driver
expression
RHS Expression
any valid Verilog expression
;
Terminate
semicolon required
Fig 3 — Three forms of continuous assignment
// ── Form 1: Explicit assign statement ─────────────────────────
wire y;
assign y = a & b;             // declared separately, then assigned

// ── Form 2: Inline assign during net declaration ───────────────
wire y = a & b;               // declare and assign in one line

// ── Form 3: Drive strength specified ──────────────────────────
assign (strong1, weak0) y = a & b;  // explicit drive strengths

// ── Form 4: Multiple targets (separate statements) ─────────────
assign sum  = a ^ b;
assign cout = a & b;            // both run simultaneously always

// ── All four styles below are equivalent ──────────────────────
assign y1 = ~a;
assign y2 = ~a;
assign y3 = ~a;              // three drivers on three different nets — legal
Fig 4 — Common combinational circuits using assign
// Basic gates
assign y_and  = a & b;
assign y_or   = a | b;
assign y_xor  = a ^ b;
assign y_not  = ~a;
assign y_nand = ~(a & b);
assign y_nor  = ~(a | b);

// 2-to-1 Multiplexer
assign mux_out = sel ? a : b;

// 4-to-1 Multiplexer (nested ternary)
assign mux4 = (sel==2'b00) ? in0 :
              (sel==2'b01) ? in1 :
              (sel==2'b10) ? in2 : in3;

// Half adder
assign sum  = a ^ b;
assign cout = a & b;

// Full adder
assign {cout, sum} = a + b + cin;

// Priority encoder (4-to-2)
assign code = in[3] ? 2'b11 :
              in[2] ? 2'b10 :
              in[1] ? 2'b01 : 2'b00;

// Tri-state driver
assign bus = oe ? data : 8'bz;

// Comparator
assign eq  = (a == b);
assign gt  = (a >  b);
assign lt  = (a <  b);

📋 Rules for Continuous Assignments

RuleDetail
LHS must be a netThe left-hand side of assign must be a wire, tri, wand, wor, etc. — never a reg or integer.
RHS can be any expressionThe right-hand side can use any combination of nets, regs, constants, and operators. It can include function calls.
Always activeContinuous assignments are evaluated whenever any operand on the RHS changes value — not just at specific trigger events.
Runs in parallelAll assign statements in a module run concurrently. Execution order is not sequential — order of declaration doesn’t matter.
No procedural contextassign cannot appear inside an always or initial block. It is a module-level statement only.
Single driver per wireA plain wire should be driven by at most one assign. Multiple drivers cause strength resolution (or x for equal-strength conflict).
Width matchingIf LHS and RHS widths differ, the RHS is zero-padded (narrower) or truncated from the MSB (wider). Signed expressions sign-extend.
No delay in synthesisDelay annotations (#5) are legal in assign but are ignored by synthesis tools. They only affect simulation.
Common mistake — driving a reg with assign: assign can only drive a wire. If you write reg y; assign y = a & b; many simulators will flag a warning or error. Use always @(*) y = a & b; instead for reg targets.

Delays and Continuous Assignments

A propagation delay can be added to any assign statement. This tells the simulator to wait a specified time before updating the output after a change on the RHS. The hardware doesn’t delay — it models the time a real gate takes to switch.

Simulation-only: Delays on assign statements are completely ignored by synthesis tools. The actual timing of a synthesized circuit is determined by standard cell library characterization, not HDL delays.
Fig 5 — assign with delay: syntax forms
// ── Single delay — same for all transitions ────────────────────
assign #10       y = a & b;    // 10 time units for any 0→1 or 1→0

// ── Two delays — (rise_delay, fall_delay) ─────────────────────
assign #(3, 5)   y = a & b;    // rise=3 time units, fall=5 time units

// ── Three delays — (rise, fall, turn-off to z) ─────────────────
assign #(2, 4, 1) bus = oe ? data : 8'bz; // rise=2, fall=4, z=1

// ── Min:Typ:Max — process variation corners ────────────────────
assign #(1:2:4)   y = a | b;    // min=1, typical=2, max=4

// ── With timescale ────────────────────────────────────────────
`timescale 1ns/100ps
assign #2.5      y = a ^ b;    // 2.5 ns propagation delay

📐 Delay Types

#d
Single
Same for 0→1, 1→0, →z
#(r,f)
Rise / Fall
Separate 0→1 and 1→0
#(r,f,z)
+ Turn-off
Add delay for →z
#(min:typ:max)
Corners
Process variation

Fig 6 — Transport Delay Waveform

assign #(3,5) y = a & b — rise delayed 3ns, fall delayed 5ns
a b y 0 10 20 30 40 50 +3 rise +5 fall RHS changes → output delayed

Inertial Delay — Pulse Filtering

The assign statement uses transport delay by default — every transition is propagated, just delayed. Gate primitives use inertial delay — pulses shorter than the delay are swallowed.

⚙️ Gate Primitive — Inertial

// Pulse shorter than #5 → swallowed
and #5 g1(y, a, b);
// 3ns pulse on input → NO output

🔌 assign — Transport

// All pulses pass, just delayed
assign #5 y = a & b;
// 3ns pulse on input → appears at t+5

📏 Assignment to Vectors

A vector is a multi-bit signal declared with a range [MSB:LSB]. Continuous assignments work identically for scalar (1-bit) and vector (multi-bit) signals — the expression simply operates on all bits in parallel.

Fig 7 — 8-bit vector anatomy
bit 7MSB
bit 6
bit 5
bit 4
bit 3
bit 2
bit 1
bit 0LSB
wire [7:0] data;  |  data[7] = MSB  |  data[0] = LSB  |  8 bits total
Fig 8 — Vector assignments: full bus, slices, and concatenation
// ── Full vector assignment ─────────────────────────────────────
wire [7:0] a, b, y;
assign y = a & b;          // 8-bit bitwise AND — all bits simultaneously
assign y = a + b;          // 8-bit addition
assign y = ~a;             // bitwise invert all 8 bits

// ── Bit-select (access one bit) ───────────────────────────────
wire       msb, lsb;
assign msb = a[7];          // drive scalar from one bit of vector
assign lsb = a[0];

// ── Part-select (slice of bits) ───────────────────────────────
wire [3:0] upper, lower;
assign upper = a[7:4];       // upper nibble of a
assign lower = a[3:0];       // lower nibble of a

// ── Concatenation on LHS ──────────────────────────────────────
wire [8:0] result;
assign {cout, result[7:0]} = a + b;   // split 9-bit sum

// ── Concatenation on RHS ──────────────────────────────────────
wire [15:0] word;
assign word = {a, b};        // join two 8-bit signals into 16-bit word

// ── Replication ───────────────────────────────────────────────
wire [31:0] sign_ext;
assign sign_ext = {{24{a[7]}}, a};  // sign-extend 8-bit to 32-bit

// ── Indexed part-select (Verilog-2001) ────────────────────────
wire [7:0] byte_n;
assign byte_n = word[8*1 +: 8];   // byte 1 of word (bits [15:8])

✂️ Bit-Select and Part-Select

Verilog provides three ways to access a subset of bits from a vector, each suited to different scenarios:

SyntaxNameExampleResult
v[i]Bit-selectdata[3]1-bit — bit 3 of data
v[hi:lo]Constant part-selectdata[7:4]4-bit — upper nibble; indices must be constants
v[base +: width]Indexed part-select (ascending)data[4 +: 4]bits [7:4] — start at 4, count up by 4; base can be a variable
v[base -: width]Indexed part-select (descending)data[7 -: 4]bits [7:4] — start at 7, count down by 4
Fig 9 — Part-select with variable index (indexed part-select)
wire [31:0] data32;
wire [1:0]  byte_sel;    // selects which byte: 0,1,2,3
wire [7:0]  byte_out;

// ✅ Variable base with constant width — works in Verilog-2001+
assign byte_out = data32[8*byte_sel +: 8];
// byte_sel=0 → data32[7:0]
// byte_sel=1 → data32[15:8]
// byte_sel=2 → data32[23:16]
// byte_sel=3 → data32[31:24]

// ❌ Variable in constant part-select — NOT allowed
// assign byte_out = data32[8*byte_sel+7 : 8*byte_sel];  // illegal
Indexed part-select (+: / -:) is one of the most useful Verilog-2001 additions. It allows a variable base index with a constant width, which is essential for processing byte lanes and protocol fields in a loop or with a select signal.

🧮 Operators in Data Flow Modelling

All Verilog operators are available in assign expressions. They are categorised below with data-flow specific usage notes and examples for each group.

Arithmetic Operators + − * / % **

Result width equals the width of the wider operand (for +, −, *). Division and modulus are synthesizable but generate large hardware — use only with constants or when the tool supports it.

OpNameData Flow ExampleNotes
+Additionassign sum = a + b + cin;Adder chain — result may overflow if LHS too narrow
Subtractionassign diff = a – b;Subtractor — use signed if negative values expected
*Multiplicationassign product = a * b;Multiplier — result is 2×width; size LHS accordingly
/Divisionassign quot = num / den;Costly in hardware; prefer power-of-2 via shift
%Modulusassign rem = a % 8;Remainder — simple for power-of-2 divisor (= bit mask)
**Powerassign w = 2 ** N;Mostly used in constant/parameter expressions
// Width control for arithmetic
wire [7:0]  a, b;
wire [8:0]  sum9;    // 1 bit wider to capture carry-out
wire [15:0] prod16;  // 2× wider for full multiplication result

assign sum9   = a + b;   // no overflow — captures carry in bit[8]
assign prod16 = a * b;   // 8×8=16 bit product
Logical Operators && || !

Always return a 1-bit result. Any non-zero value is treated as TRUE. Commonly used in conditional (ternary) expressions and enable signals.

OpNameData Flow ExampleNotes
&&Logical ANDassign en = (a!=0) && (b!=0);TRUE if both sides non-zero
||Logical ORassign valid = (a||b||c);TRUE if any is non-zero
!Logical NOTassign idle = !busy;Inverts truthiness of a vector
Bitwise Operators & | ^ ~ ~^

Operate independently on each bit pair. Result is the same width as the operands. Most commonly used for bus masking and combinational logic.

OpNameData Flow ExampleResult
&ANDassign masked = data & 8’hF0;Masks lower nibble to 0
|ORassign flags = status | 8’h01;Sets bit 0
^XORassign toggled = data ^ mask;Flips bits where mask=1
~NOTassign inv = ~data;Invert all bits
~^XNORassign eq_bits = a ~^ b;1 where bits match
Reduction Operators &a |a ^a ~&a ~|a ~^a

Unary — collapse a multi-bit vector into a single bit by applying the gate across all bits. Essential for zero-detection, all-ones checks, and parity generation.

OpNameData Flow ExampleMeaning
&aReduction ANDassign all_ones = &data;1 if ALL bits are 1
~&aReduction NANDassign not_all = ~&data;0 if all bits are 1
|aReduction ORassign non_zero = |data;1 if ANY bit is 1 — zero detect
~|aReduction NORassign is_zero = ~|data;1 only if ALL bits are 0
^aReduction XORassign parity = ^data;Odd-parity bit across the whole bus
~^aReduction XNORassign even_par = ~^data;Even-parity bit
// Practical uses of reduction operators
assign zero_flag  = ~|result;         // ALU zero flag
assign parity_out = ^tx_byte;         // UART parity bit
assign all_valid  = &valid_vec;        // all channels valid?
assign any_error  = |error_flags;      // any error asserted?
Shift Operators << >> <<< >>>

Shift bits left or right. In hardware, a constant shift is free — just rewires bits. A variable shift becomes a barrel shifter, which is significant logic.

OpNameData Flow ExampleFill bits
<<Logical leftassign x2 = a << 1;Zero-fill right — equivalent to ×2
>>Logical rightassign d2 = a >> 1;Zero-fill left — equivalent to ÷2
<<<Arithmetic leftassign y = a <<< n;Same as logical left
>>>Arithmetic rightassign y = a >>> n;Sign-extends — preserves sign for signed types
// Constant shift — just wires in hardware (no logic cost)
assign mul2  = {a, 1'b0};       // left shift 1 via concatenation
assign mul4  = {a, 2'b00};      // left shift 2

// Variable shift — becomes a barrel shifter
assign shifted = data << shamt; // shamt bits, synthesises barrel shifter

// Arithmetic right shift for signed division
wire signed [7:0] s;
assign s_div2 = s >>> 1;        // preserves sign bit
Relational & Equality Operators < > <= >= == != === !==

Always return a 1-bit result. Critical distinction: == returns x if either operand has x/z bits; === compares x and z literally (testbench use only — not synthesizable).

OpNameExamplex/z result
<Less thanassign lt = (a < b);x if operand has x/z
>Greater thanassign gt = (a > b);x if operand has x/z
==Logical equalityassign eq = (a == b);x if any bit is x or z
!=Inequalityassign ne = (a != b);x if any bit is x or z
===Case equalityassign ex = (a === b);Never x — compares x/z literally
!==Case inequalityassign nx = (a !== b);Never x — compares x/z literally
Concatenation & Replication {a,b} {n{a}}

The most unique Verilog operators — exclusive to hardware description languages. Essential for routing bits, building wide buses, and sign-extension.

OpNameExampleResult
{a,b}Concatenationassign w = {a[3:0], b[3:0]};Join bits — {a_nibble, b_nibble} = 8-bit word
{n{a}}Replicationassign ones = {8{1’b1}};Repeat a n times — 8’hFF
// Common data flow concatenation patterns
assign {cout, sum}   = a + b;          // split carry and sum
assign word          = {byte_hi, byte_lo}; // byte assembly
assign sign_ext32    = {{24{data8[7]}}, data8}; // sign extend
assign zero_ext32    = {24'b0, data8};      // zero extend
assign all_ones      = {WIDTH{1'b1}};       // all-ones constant
assign rotated       = {a[6:0], a[7]};    // rotate left by 1
Conditional (Ternary) Operator cond ? a : b

The only 3-operand operator — the backbone of data-flow level mux design. In hardware it synthesizes to a 2-to-1 multiplexer.

FormExampleHardware
Simple muxassign y = sel ? a : b;2-to-1 MUX on sel
Enable / tri-stateassign bus = oe ? data : 8’bz;Tri-state buffer
Conditional logicassign out = en ? (a & b) : 8’b0;Gated logic
Priority muxassign y = s[1] ? (s[0]?d:c) : (s[0]?b:a);4-to-1 MUX

Operator Precedence Summary (Highest → Lowest)

PriorityOperatorsCategory
1 — Highest+ − ! ~ & ~& | ~| ^ ~^ (unary)Unary / Reduction
2**Power
3* / %Multiply / Divide / Modulus
4+ − (binary)Add / Subtract
5<< >> <<< >>>Shift
6< <= > >=Relational
7== != === !==Equality
8& (binary)Bitwise AND
9^ ~^ (binary)Bitwise XOR / XNOR
10| (binary)Bitwise OR
11&&Logical AND
12||Logical OR
13 — Lowest? :Conditional (Ternary)
Always use parentheses when mixing categories. While a & b | c is legal, it means (a & b) | c — but a reader might interpret it as a & (b | c). Explicit parentheses make code unambiguous for both compilers and engineers.

Fig 10 — Data Flow Level Complete Module Example

8-bit ALU at data flow level — all operations using assign
module alu_8bit (
  input  [7:0] a, b,
  input  [2:0] op,          // operation select
  output [7:0] result,
  output       zero,        // result is zero
  output       carry
);
  wire [8:0] add_result;

  // Intermediate — full adder with carry
  assign add_result = a + b;

  // Select operation using nested ternary (priority mux)
  assign result =
    (op == 3'b000) ? add_result[7:0] :   // ADD
    (op == 3'b001) ? (a - b)        :   // SUB
    (op == 3'b010) ? (a & b)        :   // AND
    (op == 3'b011) ? (a | b)        :   // OR
    (op == 3'b100) ? (a ^ b)        :   // XOR
    (op == 3'b101) ? ~a             :   // NOT a
    (op == 3'b110) ? (a << 1)       :   // SHL
                      (a >> 1);           // SHR (default)

  // Status flags using reduction and relational operators
  assign zero  = ~|result;              // reduction NOR — 1 if result=0
  assign carry = add_result[8];          // carry-out from adder
endmodule
Gate Arrays, Flip-Flops, Delays & Nets☰ Verilog Series IndexBehavioral Modelling
Scroll to Top