The Conditional Operator
Ask anyone who writes RTL for a 2-to-1 mux and they will type assign out = sel ? a : b;. That is the multiplexer of section 1.5 in three characters. It is not sugar over something more complicated, which is how a programmer sees a ternary; it is the hardware.
The 2-to-1 mux
condition ? value_if_true : value_if_false
wire [7:0] mux_out = sel ? input_1 : input_0;
sel is the select line, the first value is what comes out when it is 1, the second when it is 0. Eight bits wide, it is eight mux cells in parallel, or eight lookup tables on an FPGA, and the correspondence between the text and the gates is exact.
Any condition
The condition is any expression, and it is true when non-zero:
wire [7:0] larger = (a > b) ? a : b; // a comparator driving the select
wire [7:0] bypass = (a == 8'h00) ? b : a;
wire [7:0] safe_data = (|a) ? a : 8'hFF; // a reduction as the condition
wire [7:0] clamped = (a > 8'd200 && a < 8'd255) ? 8'd200 : a;
The tool builds the comparison, connects its one-bit output to the select, and routes the two values to the data inputs.
Where it turns up
A tri-state output, meaningful only at the pins:
assign bus = oe ? data : 8'bz;
Minimum and maximum:
assign min_val = (a < b) ? a : b;
assign max_val = (a > b) ? a : b;
Saturation, from the arithmetic article:
assign sum_wide = {1'b0, operand_a} + {1'b0, operand_b};
assign result = sum_wide[8] ? 8'hFF : sum_wide[7:0];
Absolute value of a two’s-complement number:
assign abs_a = a[7] ? (~a + 1'b1) : a; // negate when the sign bit is set
Chains
Chained conditionals make an N-way selection, and each link is another mux in series:
wire [7:0] out = (sel == 2'b00) ? a :
(sel == 2'b01) ? b :
(sel == 2'b10) ? c :
d;
A chain is priority logic: the first true condition wins. Here the conditions exclude each other, so the order does not matter. When they do not, it does:
wire [7:0] priority_out =
err_critical ? 8'hFF : // beats everything
err_warning ? 8'h80 : // only if no critical error
data_valid ? data :
8'h00;
That is exactly what you want for interrupt priorities and arbitration, and the order of the lines is the order of precedence, so write the most urgent first.
Conditional or always block
These two are the same hardware:
assign out_cond = (sel == 2'b00) ? a :
(sel == 2'b01) ? b :
(sel == 2'b10) ? c : d;
reg [7:0] out_always;
always @(*) begin
if (sel == 2'b00) out_always = a;
else if (sel == 2'b01) out_always = b;
else if (sel == 2'b10) out_always = c;
else out_always = d;
end
For one output and two or three alternatives, the conditional in an assign is shorter. For several outputs that change together, or more than three or four alternatives, an always block with if or case reads better. Beyond about four links a chain of conditionals is hard to read and a case statement, which Module 5 covers, says the same thing in a table:
always @(*) begin
case (s)
3'b000: out = a;
3'b001: out = b;
3'b010: out = c;
3'b011: out = d;
3'b100: out = e;
3'b101: out = f;
3'b110: out = g;
default: out = h;
endcase
end
The netlist is the same either way. One thing to know: ?: is the only conditional construct allowed inside an assign. There is no if in a continuous assignment; if you want one, you want an always block.
Hardware
| Expression | Hardware |
|---|---|
sel ? a : b, 1 bit |
one mux |
sel ? a : b, 8 bits |
eight muxes in parallel |
(a > b) ? a : b |
a comparator and a mux |
| a chain of $N$ links | $N$ muxes in series, or fewer after the tool merges them |
The delay of a chain grows with its length, and a long priority chain can end up on the critical path. FPGA tools merge chained 2-to-1 muxes into wider structures where the logic allows, so a chain is often cheaper than it reads, but keep long ones in mind when the timing report comes back.
Practice Exercise
Using only continuous assignments, write a 4-to-1 mux of 8-bit inputs with a 2-bit select, and a valid output that is high when the select chooses d0 or d1.
Solution
`timescale 1ns / 1ps
`default_nettype none
module mux4_with_valid (
input wire [1:0] sel,
input wire [7:0] d0, d1, d2, d3,
output wire [7:0] out,
output wire valid
);
assign out = (sel == 2'b00) ? d0 :
(sel == 2'b01) ? d1 :
(sel == 2'b10) ? d2 :
d3;
assign valid = ~sel[1]; // sel is 00 or 01 exactly when its top bit is 0
endmodule
valid could be (sel == 2'b00) || (sel == 2'b01), but noticing that the top bit alone separates the two halves turns it into one inverter. Spotting that kind of thing is most of what distinguishes good RTL from a transliteration.
That completes the operators. The last article of the section is about how they combine: precedence, the traps in it, and the width rules that bite when an expression is not in a plain assignment.