Verilog · Chapter 10.10 · Operators & Operands
Concatenation Operator in Verilog — Joining and Splitting Vectors
The concatenation operator joins several values into one wider vector. You list them in braces separated by commas, and they are bundled end to end, with the leftmost value becoming the most-significant bits. This is free wiring, no gates, just bundled connections, and it does the everyday work of building a wide bus from smaller fields, packing a header and payload into one word, or swapping byte order. Used on the left side of an assignment, concatenation also splits a value across several targets, which is how a carry-out and a sum are captured in a single line. Two rules govern it. The result width equals the sum of the operand widths, and every operand must have a known width, so unsized constants are not allowed. This lesson drills joining, splitting, and the common idioms.
Foundation17 min readVerilogConcatenation OperatorBus AssemblyCarry CaptureRTL Design
Chapter 10 · Section 10.10 · Operators & Operands
1. The Engineering Problem
Back in 10.2, an 8-bit add overflowed because the carry-out had nowhere to go. You can capture both the sum and that carry in one clean line — if you can join two targets into one. The concatenation operator does exactly that:
reg [7:0] a, b, sum;
reg carry;
// ...
{carry, sum} = a + b; // 9-bit result split: bit 8 → carry, bits 7:0 → sumThe left-hand side `{carry, sum}` is a 9-bit target built by joining the 1-bit carry (as the MSB) with the 8-bit sum (as the low bits). The 9-bit result of a + b is split across them — the carry-out lands in carry, the low eight bits in sum. No overflow, no lost bit, one assignment. This is concatenation used on the left side to split a value; on the right side it joins values into a wider one.
The concatenation operator joins values into a wider vector (leftmost = most-significant) — and on the left-hand side, splits a value across several targets. It is the operator of structure: assembling buses, packing fields, swapping bytes, and capturing carries.
Concatenation is, with replication (10.9), one of the two construction operators — it builds and breaks apart vectors rather than computing with them. This page teaches joining and splitting, the idioms that make it indispensable, and the one rule that surprises everyone: you cannot put an unsized constant in a concatenation.
2. Mental Model — Join Values End to End; Split on the Left
Visual A — concatenation joins values
3. The Syntax
{a, b, c} // join a (MSB) … c (LSB); width = sum of widths
{4'hA, 4'h5} // = 8'hA5
{byte3, byte2, byte1, byte0} // assemble a 32-bit word from four bytes
{1'b1, addr[14:0]} // set the top bit, keep the low 15
{carry, sum} = a + b; // LHS: split the result across carry and sum
{a, {4{1'b0}}, b} // concatenation with a nested replication (10.9)The rules:
- Comma-separated operands in braces, leftmost most-significant.
- Result width = sum of operand widths — and each operand must have a definite width (§7).
- Operands can be signals, slices, literals (sized), or nested replications/concatenations. Part-selects (
addr[14:0]) and bit-selects are common operands. - On the LHS, the target widths must sum to the source width (or the split mis-aligns — DebugLab 3).
4. Building and Splitting
The two directions of concatenation are its whole power:
// BUILD (RHS) — assemble a wide value from parts
assign packet = {header, payload}; // join header and payload
assign word = {byte_hi, byte_lo}; // two bytes → one word
assign tagged = {1'b1, addr}; // prepend a flag bit
// SPLIT (LHS) — disassemble a value into parts
{carry, sum} = a + b; // capture carry + sum (§1)
{opcode, operand} = instruction; // decode an instruction word
{byte_hi, byte_lo} = word; // unpack a word into bytes- Building on the right joins fields into a bus — packets, instruction words, tagged addresses, multi-byte words.
- Splitting on the left unpacks a value into named parts — capturing a carry, decoding an instruction into opcode and operand, separating a word into bytes. The LHS split is concatenation's most distinctive feature, and the cleanest way to route one value to several destinations in a single assignment.
5. The Key Idioms
A handful of concatenation patterns appear constantly:
// CARRY CAPTURE — keep the carry-out of an add (the §1 / 10.2 fix)
{cout, sum} = a + b;
// BYTE / WORD ASSEMBLY — build a wide word from bytes
word32 = {b3, b2, b1, b0};
// BYTE SWAP — reverse byte order (endianness conversion)
swapped = {word[7:0], word[15:8]}; // swap the two bytes of a 16-bit word
// ZERO / SIGN EXTENSION (with replication, 10.9)
zext = {8'b0, val}; // zero-extend
sext = {{8{val[7]}}, val}; // sign-extend (replicate sign bit)
// FIELD PACK / UNPACK
packed = {flags, addr, data}; // pack
{flags, addr, data} = packed; // unpack- Carry capture (
`{cout, sum} = a + b`) solves the 10.2 overflow directly — the carry-out gets its own bit instead of being dropped. - Byte assembly and swap build and reorder multi-byte words — endianness conversion is a byte-order concatenation.
- Extension uses concatenation with replication (10.9) — zero-extend by prepending
0s, sign-extend by prepending replicated sign bits. - Field pack/unpack is the same concatenation read both ways: build a packed word on the right, unpack it on the left.
Visual B — left-hand-side split
6. Nesting With Replication
Concatenation and replication (10.9) are the two brace operators, and they nest freely — replication is just a concatenation of copies, and it slots into a larger concatenation:
{a, {4{1'b0}}, b} // a, then four 0 bits, then b
{{8{val[7]}}, val} // sign extension: replicated sign bit + value
{hdr, {2{8'hFF}}, crc} // a marker of two 0xFF bytes between header and crc`{n{x}}` (replication) is shorthand for n copies of x joined by concatenation, so dropping a replication into a concatenation is natural and common — most often to insert a fill or an extension between real fields. Sign extension (`{{8{val[7]}}, val}`) is the canonical example of the two operators composing.
7. The Unsized-Constant Rule
The rule that surprises everyone: an unsized constant cannot appear in a concatenation. Because the result width is the sum of operand widths, every operand needs a definite width — and an unsized literal like `1` or `'hA` has no fixed width:
{1, addr} // ILLEGAL — '1' is an unsized constant (no width)
{1'b1, addr} // OK — '1'b1' is a 1-bit sized constant
{8'd0, data} // OK — sized zero
{'h0, data} // ILLEGAL — unsizedA bare `1` is an unsized integer (32 bits wide by default, but treated as unsized in this context), so Verilog cannot determine how many bits it should contribute to the concatenation and forbids it. Always size every literal in a concatenation: `1'b1` not `1`, `8'd0` not `0`, `4'hF` not `'hF`. This is the most common concatenation error (DebugLab 1), and it is caught at compile time — a definite-width discipline the operator enforces.
Visual C — the unsized-constant trap
Every concatenation operand needs a known width
data flow8. Worked Examples
8.1 Example 1 — capture the carry of an adder
module add_with_carry (
input [7:0] a, b,
output [7:0] sum,
output cout
);
assign {cout, sum} = a + b; // 9-bit result split: carry + 8-bit sum
endmoduleThe 9-bit result of the 8-bit add is split by `{cout, sum}`: the carry-out into cout, the low eight bits into sum. This is the 10.2 overflow fix in its cleanest form — one assignment captures the full result with the carry in its own bit. The LHS concatenation sizes everything correctly.
8.2 Example 2 — byte swap (endianness)
module swap16 (
input [15:0] data_in,
output [15:0] data_out
);
// swap the two bytes: low byte to high, high byte to low
assign data_out = {data_in[7:0], data_in[15:8]};
endmoduleReversing byte order is a concatenation of part-selects in swapped order: the low byte data_in[7:0] becomes the high byte of the output, and vice versa. This is how endianness conversion is done — pure wiring, no logic. Extending to a 32-bit swap is `{d[7:0], d[15:8], d[23:16], d[31:24]}`.
8.3 Example 3 — pack and unpack a field word
// PACK — assemble a 16-bit control word from fields
assign ctrl_word = {2'b00, mode, enable, addr}; // pad + mode + enable + addr
// UNPACK — split a received control word back into fields
wire [1:0] r_pad; wire [2:0] r_mode; wire r_en; wire [9:0] r_addr;
assign {r_pad, r_mode, r_en, r_addr} = ctrl_word;The same concatenation read both directions: build the packed word by joining fields (with a sized 2'b00 pad), and recover the fields by splitting the word on the LHS. The field widths must sum to 16 in both directions, or the packing mis-aligns. This pack/unpack pattern is how structured words (control registers, packet headers) are assembled and decoded.
9. Industry Perspective
- Bus and packet assembly is concatenation. Building wide buses, packet headers, and instruction words from fields — and decoding them back — is everyday RTL, done with concatenation on the right (pack) and left (unpack).
- Carry and flag capture use the LHS split.
`{cout, sum} = a + b`(and`{carry, borrow, result} = ...}`style flag capture) is the idiomatic way to keep arithmetic side-results, directly addressing the overflow of 10.2. - Endianness conversion is byte-order concatenation. Byte swaps for big/little-endian interfaces are concatenations of reordered part-selects — pure wiring, common at protocol boundaries.
- The unsized-literal error is a frequent compile failure. Forgetting to size a constant in a concatenation (
{1, ...}) is a routine first-compile error; teams size every literal as a habit, and lint reinforces it.
10. Common Mistakes
- Unsized constant in a concatenation —
`{1, x}`is illegal; size it as`{1'b1, x}`(§7, DebugLab 1). - Wrong operand order — leftmost is MSB; reversing fields swaps high and low (byte-order bugs, DebugLab 2).
- LHS target widths not summing to the source — the split mis-aligns; sizes must match (§4, DebugLab 3).
- Miscomputing the result width — it is the sum of operand widths, not the count.
- Zero-extending a signed value with concatenation — prepend replicated sign bits, not zeros (10.9 carryover).
11. Debugging Lab
Three concatenation-operator debug post-mortems
Pitfall 1 — unsized constant in a concatenation
module tag_addr (
input [14:0] addr,
output [15:0] tagged
);
// Intent: prepend a single '1' bit to the 15-bit address.
assign tagged = {1, addr}; // BUG: '1' is an UNSIZED constant
endmodule
// '1' has no definite width, so the concatenation width is undefined.
// Per the LRM, unsized constants are ILLEGAL in concatenations — this is a
// compile error (or, in lax tools, '1' is taken as 32 bits, blowing the
// width far past 16).A concatenation meant to prepend a single bit fails to compile with an error about an unsized constant or an illegal concatenation operand — or, in a permissive tool, produces a result far wider than expected because the literal was treated as 32 bits.
A concatenation's result width is the SUM of its operands' widths, so every operand must have a definite width. A bare '1' is an UNSIZED constant — it has no fixed bit width in this context — so Verilog cannot determine how many bits it contributes, and the LRM forbids unsized constants in concatenations. The intent was a single 1 bit, but '1' does not express that; '1'b1' does.
The fix is to SIZE every literal in a concatenation: '1'b1' for a single 1 bit.
module tag_addr (
input [14:0] addr,
output [15:0] tagged
);
assign tagged = {1'b1, addr}; // sized 1-bit constant + 15-bit addr = 16 bits
endmodule
// Rule: always size literals in a concatenation — 1'b1 not 1, 8'd0 not 0,
// 4'hF not 'hF. The result is then a well-defined 16 bits.Pitfall 2 — reversed operand order swaps high and low
module make_word (
input [7:0] hi_byte, lo_byte,
output [15:0] word
);
// Intent: hi_byte in the high 8 bits, lo_byte in the low 8 bits.
assign word = {lo_byte, hi_byte}; // BUG: order reversed
endmodule
// Leftmost is MSB, so {lo_byte, hi_byte} puts lo_byte in the HIGH byte and
// hi_byte in the LOW byte — the two bytes are swapped relative to intent.
// Every value comes out byte-swapped.A 16-bit word assembled from a high byte and a low byte comes out with the bytes swapped — the high byte is in the low position and vice versa. Values look byte-reversed downstream (e.g. 0x12 and 0x34 give 0x3412 instead of 0x1234).
Operand order. In a concatenation, the LEFTMOST operand occupies the most-significant bits. The intent was hi_byte in the high 8 bits, so it must be written first: {hi_byte, lo_byte}. The code writes {lo_byte, hi_byte}, which places lo_byte in the high byte — swapping the two. The widths are correct (8 + 8 = 16), so nothing flags it; only the byte order is wrong.
The fix is to order the operands most-significant first.
module make_word (
input [7:0] hi_byte, lo_byte,
output [15:0] word
);
assign word = {hi_byte, lo_byte}; // hi_byte = high 8 bits, lo_byte = low 8
endmodule
// Leftmost is MSB: hi_byte first puts it in the high byte. 0x12, 0x34 →
// 0x1234 as intended. (A deliberate swap IS {lo_byte, hi_byte} — order is
// the whole meaning.)Pitfall 3 — LHS split target widths do not sum to the source
reg [7:0] opcode; // 8 bits
reg [3:0] operand; // 4 bits
reg [15:0] instruction;
// Intent: split a 16-bit instruction into an 8-bit opcode and an 8-bit
// operand. But 'operand' is only 4 bits, so the targets sum to 12, not 16.
always @(*)
{opcode, operand} = instruction; // BUG: 8 + 4 = 12 != 16
// The 16-bit source is split across 12 bits of target: the top 12 bits of
// 'instruction' land in opcode+operand and the LOW 4 BITS ARE DROPPED.An instruction decoder loses the low 4 bits of every instruction — the opcode looks right but the operand is wrong, taking bits from the wrong position. The 16-bit instruction is not fully captured by the split.
On the left-hand side, a concatenation splits the source across the target operands by WIDTH, most-significant first. The targets here are opcode (8) and operand (4), summing to 12 bits — but the source 'instruction' is 16 bits. So the split aligns the top 12 bits of the instruction to the targets and DROPS the low 4 bits. The intended 8-bit operand field is declared only 4 bits wide, causing the misalignment.
The target widths in an LHS concatenation must sum to the source width, or bits are dropped or misaligned.
The fix is to size 'operand' to 8 bits so the targets sum to 16.
reg [7:0] opcode; // 8 bits
reg [7:0] operand; // 8 bits — now targets sum to 16
reg [15:0] instruction;
always @(*)
{opcode, operand} = instruction; // 8 + 8 = 16 = source width ✓
// Targets now sum to the 16-bit source: opcode = instruction[15:8],
// operand = instruction[7:0]. No bits dropped.12. Interview Q&A
13. Exercises
Exercise 1 — Evaluate the concatenations
Give the value and width of each: (a) {4'h3, 4'hC}; (b) {2'b10, 3'b011}; (c) {8'hFF, 8'h00}; (d) {1'b1, 7'h00}; (e) {2'b01, {3{1'b1}}}.
Exercise 2 — Build and split
Write the concatenation for each: (a) capture the carry and 8-bit sum of a + b; (b) swap the two bytes of a 16-bit w; (c) build a 12-bit word from a 4-bit tag (high) and an 8-bit data (low); (d) split a 16-bit pkt into an 8-bit hdr (high) and an 8-bit body (low).
Exercise 3 — Fix the concatenations
Each line states intent. Identify the bug and fix it.
y = {1, data}; // intent: prepend a single 1 bit to 7-bit data
word = {lo, hi}; // intent: hi in high byte, lo in low byte
{op, arg} = instr; // intent: split 12-bit instr into 4-bit op + 8-bit argExercise 4 — Reason about width and order
(a) What is the width of {3'b101, 5'h0A, 1'b1}? (b) Why does {cout, sum} = a + b avoid the overflow that sum = a + b (with narrow sum) suffers? (c) For an LHS split {x, y} = z where z is 12 bits, x is 4 bits, and y is 4 bits, what happens to z's low 4 bits?
14. Summary
The concatenation operator joins values into a wider vector (leftmost most-significant) and, on the left-hand side, splits a value across several targets. With replication, it is one of the two construction operators.
The core ideas:
{a, b, c}joins — leftmost is the MSB, result width = sum of operand widths, free wiring.- On the LHS,
{a, b}splits the source across the targets — the basis of carry capture and field unpacking. - Key idioms: carry capture (
{cout, sum} = a + b), byte assembly and swap, field pack/unpack, and extension (with replication). - Every operand needs a known width — unsized constants are illegal; size every literal (
1'b1, not1). - Concatenation and replication compose —
{a, {n{b}}, c}; sign extension is the canonical pairing.
The discipline this page instils:
- Order operands most-significant first — order is the meaning; reversing swaps high and low.
- Size every literal in a concatenation — the unsized-constant error is the most common one.
- Make LHS target widths sum to the source — or bits are dropped or misaligned.
You can now assemble and disassemble vectors. One operator family remains in the chapter — the selector: Chapter 10.11 Conditional Operator drills ?: — the ternary that builds a multiplexer, choosing between two values based on a condition, and closing the operator vocabulary of combinational logic.
Related Tutorials
- Replication Operator — Chapter 10.9; the other brace operator, which nests inside concatenation for fills and extension.
- Arithmetic Operators — Chapter 10.2; the addition overflow that the carry-capture concatenation idiom solves.
- Scalar vs Vector vs Arrays — Chapter 5.2.4; the vectors and part-selects concatenation joins and splits.
- Verilog Operators & Operands — Chapter 10 overview; the operator-as-hardware model.