Module 4: Introduction to Verilog HDL ยท 4.3 Operators

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.

โ† Shift and Concatenation Operators Precedence and Width โ†’