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.
assign statement is always active. The moment any input changes, the output re-evaluates — just like a physical wire.assign expression.assign expressions are directly synthesizable — tools map them to gates using the target library.// 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
assignkeyword - 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
♾️ 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.
wire (or tri, wand, etc.) — never a reg. The RHS can be any expression.assign statements in a module run simultaneously — order of declaration does not matter.📐 assign Syntax
The assign statement has a clear and consistent structure:
// ── 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
// 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
| Rule | Detail |
|---|---|
| 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. |
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.
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.
// ── 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
Fig 6 — Transport Delay Waveform
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.
// ── 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:
| Syntax | Name | Example | Result |
|---|---|---|---|
| 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 |
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
+: / -:) 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.
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.
| Op | Name | Data Flow Example | Notes |
|---|---|---|---|
| + | 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
Always return a 1-bit result. Any non-zero value is treated as TRUE. Commonly used in conditional (ternary) expressions and enable signals.
| Op | Name | Data Flow Example | Notes |
|---|---|---|---|
| && | Logical AND | assign 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 NOT | assign idle = !busy; | Inverts truthiness of a vector |
Operate independently on each bit pair. Result is the same width as the operands. Most commonly used for bus masking and combinational logic.
| Op | Name | Data Flow Example | Result |
|---|---|---|---|
| & | 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 |
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.
| Op | Name | Data Flow Example | Meaning |
|---|---|---|---|
| &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 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.
| Op | Name | Data Flow Example | Fill 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
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).
| Op | Name | Example | x/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 |
The most unique Verilog operators — exclusive to hardware description languages. Essential for routing bits, building wide buses, and sign-extension.
| Op | Name | Example | Result |
|---|---|---|---|
| {a,b} | Concatenation | assign 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
The only 3-operand operator — the backbone of data-flow level mux design. In hardware it synthesizes to a 2-to-1 multiplexer.
| Form | Example | Hardware |
|---|---|---|
| Simple mux | assign y = sel ? a : b; | 2-to-1 MUX on sel |
| Enable / tri-state | assign 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)
| Priority | Operators | Category |
|---|---|---|
| 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) |
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
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
