VERILOG SERIES · MODULE 09

Modelling at Data Flow Level — VLSI Trainers
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 net The left-hand side of assign must be a wire, tri, wand, wor, etc. — never a reg or integer.
RHS can be any expression The right-hand side can use any combination of nets, regs, constants, and operators. It can include function calls.
Always active Continuous assignments are evaluated whenever any operand on the RHS changes value — not just at specific trigger events.
Runs in parallel All assign statements in a module run concurrently. Execution order is not sequential — order of declaration doesn’t matter.
No procedural context assign cannot appear inside an always or initial block. It is a module-level statement only.
Single driver per wire A plain wire should be driven by at most one assign. Multiple drivers cause strength resolution (or x for equal-strength conflict).
Width matching If LHS and RHS widths differ, the RHS is zero-padded (narrower) or truncated from the MSB (wider). Signed expressions sign-extend.
No delay in synthesis Delay 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-select data[3] 1-bit — bit 3 of data
v[hi:lo] Constant part-select data[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
+ Addition assign sum = a + b + cin; Adder chain — result may overflow if LHS too narrow
Subtraction assign diff = a – b; Subtractor — use signed if negative values expected
* Multiplication assign product = a * b; Multiplier — result is 2×width; size LHS accordingly
/ Division assign quot = num / den; Costly in hardware; prefer power-of-2 via shift
% Modulus assign rem = a % 8; Remainder — simple for power-of-2 divisor (= bit mask)
** Power assign 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 OR assign 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
& AND assign masked = data & 8’hF0; Masks lower nibble to 0
| OR assign flags = status | 8’h01; Sets bit 0
^ XOR assign toggled = data ^ mask; Flips bits where mask=1
~ NOT assign inv = ~data; Invert all bits
~^ XNOR assign 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
&a Reduction AND assign all_ones = &data; 1 if ALL bits are 1
~&a Reduction NAND assign not_all = ~&data; 0 if all bits are 1
|a Reduction OR assign non_zero = |data; 1 if ANY bit is 1 — zero detect
~|a Reduction NOR assign is_zero = ~|data; 1 only if ALL bits are 0
^a Reduction XOR assign parity = ^data; Odd-parity bit across the whole bus
~^a Reduction XNOR assign 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 left assign x2 = a << 1; Zero-fill right — equivalent to ×2
>> Logical right assign d2 = a >> 1; Zero-fill left — equivalent to ÷2
<<<Arithmetic left assign y = a <<< n; Same as logical left
>>>Arithmetic right assign 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 than assign lt = (a < b); x if operand has x/z
> Greater than assign gt = (a > b); x if operand has x/z
== Logical equality assign eq = (a == b); x if any bit is x or z
!= Inequality assign ne = (a != b); x if any bit is x or z
=== Case equality assign ex = (a === b); Never x — compares x/z literally
!== Case inequality assign 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}} Replication assign 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 mux assign y = sel ? a : b; 2-to-1 MUX on sel
Enable / tri-stateassign bus = oe ? data : 8’bz; Tri-state buffer
Conditional logic assign out = en ? (a & b) : 8’b0; Gated logic
Priority mux assign 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

Leave a Comment

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

Scroll to Top