Verilog · Chapter 13.2 · Data-Flow Modeling
Dataflow Practical Examples in Verilog — Muxes, Decoders, Adders & Comparators
With the operators and the continuous assignment in hand, you can now build the circuits every datapath is made of. This is the dataflow cookbook: the canonical combinational building blocks, including multiplexers, decoders, priority encoders, adders, and comparators, each written as a clean continuous assignment of an operator expression. The point of this page is fluency, seeing that a mux is a conditional operator, a decoder is a shift, a priority encoder is a chain of ternaries, an adder is an addition with a concatenated carry, and a comparator is a relational operator, so that when you need one of these blocks you write it in one line of dataflow rather than wiring gates. Every example is a complete, synthesizable combinational module. By the end you can express any standard combinational function in dataflow on sight.
Foundation20 min readVerilogDataflowMultiplexerDecoderAdderComparatorRTL Design
Chapter 13 · Section 13.2 · Data-Flow Modeling
1. The Engineering Problem
Every datapath is built from a small set of combinational blocks — muxes to select, decoders to translate, encoders to prioritize, adders to compute, comparators to decide. You now know the operators that are these blocks and the assign that wires them. The remaining step is fluency:
How do you express each standard combinational building block — mux, decoder, encoder, adder, comparator — as a single, clean dataflow assignment?
The answer is a small cookbook, and it is mostly recognition: a multiplexer is the conditional operator (?:), a decoder is a shift (1 << sel), a priority encoder is a chain of ternaries, an adder is + with the carry captured by concatenation, and a comparator is a relational operator. Once you see each block as its operator expression, you write it in one line instead of assembling gates. This page is that cookbook — each building block as a complete dataflow module, applying everything from Chapters 10 and 13.1.
2. Mental Model — Every Combinational Block Is a Dataflow Expression
Visual A — the combinational building blocks
Building blocks → operator expressions
data flow3. Multiplexers
A multiplexer selects one of several inputs. In dataflow it is the conditional operator (10.11):
// 2:1 mux
module mux2 #(parameter integer W = 8)(
input [W-1:0] d0, d1,
input sel,
output [W-1:0] y
);
assign y = sel ? d1 : d0;
endmodule
// 4:1 mux — nested ternaries select on a 2-bit sel
module mux4 #(parameter integer W = 8)(
input [W-1:0] d0, d1, d2, d3,
input [1:0] sel,
output [W-1:0] y
);
assign y = sel[1] ? (sel[1:0] == 2'b11 ? d3 : d2)
: (sel[0] ? d1 : d0);
// equivalently, a flat ternary chain:
// assign y = (sel == 2'd0) ? d0 : (sel == 2'd1) ? d1 : (sel == 2'd2) ? d2 : d3;
endmoduleThe 2:1 mux is one ternary; the 4:1 is nested or chained ternaries selecting on the 2-bit sel. Both are width-independent (the ?: selects the whole bus, 10.11). The flat chain reads as "if sel is 0 then d0, else if 1 then d1, …" — the priority-ladder form. Selection is the most common combinational operation, and ?: is its dataflow form.
4. Decoders
A decoder translates an N-bit code into a one-hot output (exactly one bit set). In dataflow it is elegantly a shift — 1 shifted left by the code (10.7):
// 2:4 decoder with enable
module decoder2to4 (
input [1:0] sel,
input en,
output [3:0] onehot
);
assign onehot = en ? (4'b0001 << sel) : 4'b0000;
endmodule`4'b0001 << sel` shifts a single 1 to position sel, producing the one-hot output: sel = 0 gives 0001, sel = 1 gives 0010, sel = 2 gives 0100, sel = 3 gives 1000. The enable gates it to all-zeros when off (a ternary, 10.11). This one-line decoder is width-independent in spirit and far cleaner than enumerating each output bit. (A 3:8 decoder is the same with 8'b1 << sel.)
Visual B — the decoder as a shift
5. Priority Encoders
A priority encoder is the inverse of a decoder: given several request bits, output the index of the highest-priority set bit (plus a valid flag). In dataflow it is a chain of ternaries (10.11) with a reduction (10.5) for valid:
// 4-to-2 priority encoder: req[3] highest priority
module priority_enc (
input [3:0] req,
output [1:0] code,
output valid
);
assign valid = |req; // any request? (reduction-OR, 10.5)
assign code = req[3] ? 2'd3 :
req[2] ? 2'd2 :
req[1] ? 2'd1 :
2'd0; // req[0] (or default when none)
endmoduleThe ternary chain encodes priority directly: req[3] is checked first (highest priority), down to req[0]. The chain's top-to-bottom order is the priority order (10.11) — req[3] wins over req[2] if both are set. valid (an OR-reduction, 10.5) flags whether any request is present, so the consumer can distinguish "code 0, request on bit 0" from "no request." Priority encoders appear in arbiters, interrupt controllers, and find-first logic.
6. Adders
Addition is the + operator (10.2), with the carry captured by concatenation (10.10):
// half adder: a + b, 1-bit operands, 2-bit result
module half_adder (
input a, b,
output sum, carry
);
assign {carry, sum} = a + b; // 1+1 can be 2 → needs 2 bits
endmodule
// full adder: a + b + cin
module full_adder (
input a, b, cin,
output sum, cout
);
assign {cout, sum} = a + b + cin;
endmodule
// N-bit adder: the + operator builds the whole adder
module adderN #(parameter integer W = 8)(
input [W-1:0] a, b,
input cin,
output [W-1:0] sum,
output cout
);
assign {cout, sum} = a + b + cin; // (W+1)-bit result split: carry + sum
endmoduleThe pattern is the same at every width: the + operator builds the adder, and `{carry, sum}` on the left captures the carry-out in its own bit (the 10.2 overflow fix via 10.10). The N-bit adderN is a single assignment — the synthesizer builds an optimized adder of the right width. You rarely wire full-adders structurally; the + operator and a sized result do it.
7. Comparators
Comparison is the relational and equality operators (10.6, 10.8):
// equality comparator
module eq_cmp #(parameter integer W = 8)(
input [W-1:0] a, b,
output eq
);
assign eq = (a == b); // 10.8
endmodule
// magnitude comparator: greater / equal / less
module mag_cmp #(parameter integer W = 8)(
input [W-1:0] a, b,
output gt, eq, lt
);
assign gt = (a > b); // 10.6
assign eq = (a == b);
assign lt = (a < b);
endmoduleEquality is == (10.8); magnitude is the relational operators <, > (10.6). The three outputs of the magnitude comparator are three concurrent continuous assignments. Remember the signedness rule (10.6): if the operands are signed, declare them signed so negatives compare correctly — a magnitude comparator on signed data versus unsigned builds different (and differently-correct) hardware.
Visual C — building blocks in a datapath
The blocks composing a small datapath
data flow8. Worked Example — A Small ALU
The blocks compose into a small combinational ALU — a mux selecting among arithmetic and logic results, all in dataflow:
module alu #(parameter integer W = 8)(
input [W-1:0] a, b,
input [1:0] op, // 00:add 01:sub 10:and 11:or
output [W-1:0] result,
output carry,
output zero
);
wire [W-1:0] sum = a + b; // adder
wire [W-1:0] diff = a - b; // subtractor
wire [W:0] add_full = a + b; // capture carry for the add case
assign result = (op == 2'b00) ? sum : // mux of results (?: chain)
(op == 2'b01) ? diff :
(op == 2'b10) ? (a & b) :
(a | b);
assign carry = (op == 2'b00) ? add_full[W] : 1'b0; // carry only for add
assign zero = ~|result; // result == 0 (reduction-NOR, 10.5)
endmoduleThe ALU is pure dataflow: continuous assignments computing each operation (adder, subtractor, bitwise), a ternary chain selecting the result by op (a mux), the carry extracted from the captured full sum, and a reduction-NOR for the zero flag. Every line is an operator expression from Chapter 10 wired with assign — the building blocks of this page, composed. This is how combinational datapaths are written.
9. Industry Perspective
- Dataflow building blocks are everyday RTL. Muxes, decoders, encoders, adders, and comparators are written as continuous assignments of operator expressions — concise, readable, and directly synthesizable.
- The operator is the block. Experienced engineers see
?:and read "mux,"1 << seland read "decoder,"+with a captured carry and read "adder" — fluency this page builds. - Parameterized blocks scale. Real building blocks are parameterized by width (
#(parameter W = 8)) so one module serves every size — the width-independence of the operators (10.5, 10.9, 10.11) makes this natural. - Synthesis optimizes the operators. Writing
+lets the synthesizer pick an optimal adder for the timing target; writing the block as an expression, not a gate structure, gives the tool the freedom to optimize.
10. Common Mistakes
- Dropping the adder carry —
sum = a + binto a same-width net loses the carry; capture it with{carry, sum}(10.2/10.10, DebugLab 2). - Wrong decoder width or non-one-hot logic — the one-hot output must match the decode width;
1 << selmust be sized correctly (DebugLab 1). - Priority-encoder order reversed — the ternary chain's order is the priority order; highest priority first (10.11).
- Comparator signedness — signed data compared unsigned (or vice versa) flips on negatives (10.6, DebugLab 3).
- Mux branch width mismatch —
?:branches should be the same width (10.11).
11. Debugging Lab
Three dataflow-building-block debug post-mortems
12. Interview Q&A
13. Exercises
Exercise 1 — Write the block
Write a one-line continuous assignment for each: (a) an 8:1 mux selecting d[0..7] on a 3-bit sel; (b) a 3:8 decoder with enable; (c) an equality comparator for two 16-bit values; (d) a zero flag for an 8-bit result.
Exercise 2 — Build a priority encoder
Write a dataflow 8-to-3 priority encoder: req[7] highest priority, output a 3-bit code and a valid flag. Show the ternary chain and the valid reduction.
Exercise 3 — Fix the building blocks
Each has a bug from §10. Identify and fix it.
assign onehot = 4'b1 << sel; // 3-bit sel, 8-output decoder
assign sum = a + b; // 8-bit a,b; need the carry too
assign code = req[1] ? 2'd1 : req[2] ? 2'd2 : 2'd0; // req[2] should be higher priorityExercise 4 — Compose a datapath
Sketch (in dataflow assignments) a block that: selects one of two 8-bit operands with sel, adds it to a third operand, and asserts overflow if the add carries out. Name the operators used for each step.
14. Summary
The dataflow cookbook: every canonical combinational building block is an operator expression wired with a continuous assignment.
The building-block-to-operator map:
- Multiplexer →
?:—assign y = sel ? d1 : d0;; N:1 is a ternary chain (priority order = chain order). - Decoder →
1 << sel—assign onehot = (1 << sel);, sized to the full one-hot width; gate with an enable ternary. - Priority encoder →
?:chain + reduction — ternary chain ordered by priority,|reqfor valid. - Adder →
+with{carry, sum}—assign {cout, sum} = a + b + cin;captures the carry at any width. - Comparator →
==/</>—assign gt = (a > b);; mind signedness.
The discipline this page instils:
- See the operator as the block —
?:is a mux,1 << sela decoder,+with carry capture an adder,<a comparator. - Carry the Chapter 10 rules — capture adder carries, size decoders to the one-hot width, match comparator signedness, align mux branch widths.
- Parameterize by width so one block serves every size.
You can now build any standard combinational block in dataflow on sight. The final dataflow page goes further: Chapter 13.3 Advanced Techniques covers larger and parameterized dataflow patterns — generated structures, complex selection networks, and the point where dataflow gives way to procedural always modeling (Chapter 14) for sequential logic.
Related Tutorials
- Continuous Assignments — Chapter 13.1; the
assignmechanics every block here uses. - Conditional Operator — Chapter 10.11; the mux at the heart of selection and the ALU.
- Shift Operators — Chapter 10.7; the shift that builds a decoder.
- Arithmetic Operators — Chapter 10.2; the
+that builds adders, with the carry rules.