Verilog Case Inside Statement

Posted on Mon 29 June 2020 in Logic

A Wild SystemVerilog Syntax Appeared!

I recently found something unexpected in the latest SystemVerilog specification. The case_statement language construct has a form that includes the inside operator keyword:

<case_statement>
    ::= ...
      | [ <unique_priority> ] 'case' '(' <case_expression> ')' 'inside'
            <case_inside_item> { <case_inside_item> }
        'endcase'

I have never used this syntax, and I do not recall reading or hearing about it from any training material. We need to get to the bottom of this mystery 😃.

I assumed the case-inside statement must have been added in the 2017 standard, but it actually dates back to IEEE 1800-2009, the first SystemVerilog standard.

Here is the description, from 1800-2017, Section 12.5.4:

The keyword inside can be used after the parenthesized expression to indicate a set membership (see 11.4.13). In a case-inside statement, the case_expression shall be compared with each case_item_expression (open_range_list) using the set membership inside operator. The inside operator uses asymmetric wildcard matching (see 11.4.6). Accordingly, the case_expression shall be the left operand, and each case_item_expression shall be the right operand. The case_expression and each case_item_expression in braces shall be evaluated in the order specified by a normal case, unique-case, or priority-case statement. A case_item shall be matched when the inside operation compares the case_expression to the case_item_expressions and returns 1'b1 and no match when the operation returns 1'b0 or 1'bx. If all comparisons do not match and the default item is given, the default item statement shall be executed.

So, for example, a highly verbose implementation of the Boolean Algebra "OR" function using a Verilog case statement might look like either of the following:

always_comb begin
    // Method #1: enumerate all cases
    unique case ({x1, x0})
        2'b00: y0 = 1'b0;
        2'b01: y0 = 1'b1;
        2'b10: y0 = 1'b1;
        2'b11: y0 = 1'b1;
        default:
            y0 = 1'bx;
    endcase

    // Method #2: group equivalent cases
    unique case ({x1, x0})
        2'b00:
            y1 = 1'b0;
        2'b01, 2'b10, 2'b11:
            y1 = 1'b1;
        default:
            y1 = 1'bx;
    endcase
end

The case-inside syntax accomplishes the same task using either a range expression (eg [1:3]), or wildcard equality matching:

always_comb begin
    // Method #1: Compress equivalent cases into a range expression
    unique case ({x1, x0}) inside
        2'b00:
            y0 = 1'b0;
        [2'b01:2'b11]:
            y0 = 1'b1;
        default:
            y0 = 1'bx;
    endcase

    // Method #2: use wildcards
    unique case ({x1, x0}) inside
        2'b00:
            y1 = 1'b0;
        2'b01, 2'b1?:
            y1 = 1'b1;
        default:
            y1 = 1'bx;
    endcase
end

Note that all of these implementations use pessimistic X propagation. The Verilog bit-wise OR function would evaluate (1'b1 | 1'bx) == 1'b1, because the 1'b1 value dominates the other OR inputs. Verilog X-propagation is a big topic, so for our purposes we will simply stipulate that being pessimistic about propagating Xes is a more risk-averse coding style.

Anybody who has used Verilog seriously will be familiar with its Perl-like adherence to the "there is more than one way to do it" philosophy. In the spirit of the Zen of Python, I would argue that it is easier and—more importantly—cheaper for teams with large code bases to have "one—and preferably only one—obvious way to do it".

The case-inside statement is a good candidate for "the one true way" to write selection logic in Verilog for at least three reasons:

  1. It eliminates the need for a casez statement.
  2. It provides a more elegant way to enumerate cases.
  3. It makes X-propagation easier to implement correctly.

Eliminating casez

For background on the various types of Verilog case statements, see 1800-2017 section 12.5.2: unique-case, unique0-case, and priority-case. Furthermore, the SNUG 2005 paper SystemVerilog's priority & unique - A Solution to Verilog's "full_case" & "parallel_case" Evil Twins!, by Cliff Cummings, is also an excellent reference.

There are three types of Verilog cases: case, casex, and casez. A good, modern style guide forbids the use of casex due to the danger of simulator/synthesis behavior mismatches. The casez keyword, on the other hand, is difficult to dispense with. It is a variant of case that allows wildcards in the case items. These wildcards are treated as "don't care", and can lead to concise selection logic. For example, you can code a 4-input priority encoder with casez:

always_comb begin
    // Note: casez allows wildcards
    unique casez (sel[3:0])
        3'b1???: {v, y} = {1'b1, 2'b11};
        3'b01??: {v, y} = {1'b1, 2'b10};
        3'b001?: {v, y} = {1'b1, 2'b01};
        3'b0001: {v, y} = {1'b1, 2'b00};
        3'b0000: {v, y} = {1'b0, 2'bxx};
        default: {v, y} = 3'bxxx;
    endcase
end

But as we already have demonstrated with the OR gate example, the case-inside variant also supports wildcards. So you could write the previous priority encoder as:

always_comb begin
    // Note: case-inside also allows wildcards
    unique case (sel[3:0]) inside
        3'b1???: {v, y} = {1'b1, 2'b11};
        3'b01??: {v, y} = {1'b1, 2'b10};
        3'b001?: {v, y} = {1'b1, 2'b01};
        3'b0001: {v, y} = {1'b1, 2'b00};
        3'b0000: {v, y} = {1'b0, 2'bxx};
        default: {v, y} = 3'bxxx;
    endcase
end

This subtle change might not seem like a big deal, but we have just compressed the number of keywords an engineer needs to know about from three down to two. Designers should already be familiar with the inside operator, as it is commonly used in Verilog expressions. The key benefit is that designers will never again have to consult the LRM about the semantics of casez. In addition to reduced cognitive overhead of humans, it is cheaper and easier to write lint tools and other static checkers that enhance overall productivity. With a large enough team working over a long enough timeline, small simplifications can lead to appreciable time/money savings.

The Inside Set Notation is Elegant

If the input to a case statement has \(N\) bits, there will be \(2^N\) possible cases. Sometimes, those cases are easy to collapse using wildcards, but not always. When the input is either large, or parameterized, enumerating all the cases can be challenging.

For example, consider a counter with minimum value \(0\), maximum value \(1023\), and +1/-1 increment/decrement inputs. Let's also add a couple safety measures:

  • When the counter is at \(0\), ignore the decrement (never underflow)
  • When the counter is at \(1023\), ignore the increment (never overflow)

You need \(ceil(log_2(1023)) = 10\) bits to encode the number \(1023\), so the select logic has \(2^{10}\) cases. This is tedious and error-prone to write by hand, but relatively easy to specify using a case-inside range:

logic cnt_en;
logic [9:0] cnt_next;
logic [9:0] cnt;

always_comb begin
    unique case (cnt) inside
        // Note: ignore decrement
        10'd0: begin
            cnt_en   = inc;
            cnt_next = cnt + 10'd1;
        end

        [10'd1:10'd1022]: begin
            cnt_en   = dec ^ inc;
            cnt_next = cnt - dec + inc;
        end

        // Note: ignore increment
        10'd1023: begin
            cnt_en   = dec;
            cnt_next = cnt - 10'd1;
        end

        default: begin
            cnt_en   = 1'bx;
            cnt_next = 'x;
        end
    endcase
end

always_ff @(posedge clock) begin
    cnt <= cnt_en ? cnt_next : cnt;
end

Furthermore, this method extends nicely to parameterized inputs. If the counter from our example had a maximum value of MaxCnt instead of \(1023\), the three cases would be 0, [1:MaxCnt-1], and MaxCnt. It is not obvious to me how to accomplish this with a regular case statement without further breaking out the decode logic for min and max values into a separate expression. For case statements involving parameters, this appears to be a huge win.

X Propagation is Easier to Implement

As noted previously, the Verilog case default statement can be used to propagate Xes pessimistically from the inputs of a case statement to its outputs. This is easy and straightforward when you can enumerate all the cases. However, for cases that either have a large or parameterized input, it is common to resort to hacks to propagate Xes.

For example, consider a one-hot state machine with a zero reset state, and \(6\) active states. The state encoding will have \(6\) bits, which requires \(2^6 = 64\) cases. For simplicity, let's say this state machine just goes around in a circle. Also, if the FSM ever finds itself in an undesirable state, it will reset back to the initial state of zero. Ignoring for a moment some of the details of assertions, typedefs, enums, and parameters, one might code the next-state logic like:

always_comb begin
    unique case (state)
        // reset state
        0:  state_next = 1;
        // hot states
        1:  state_next = 2;
        2:  state_next = 4;
        4:  state_next = 8;
        8:  state_next = 16;
        16: state_next = 32;
        32: state_next = 1;

        default:
`ifdef SIMULATION
            if (^(state) === 1'bx)
                state_next = 'x;
            else
`endif
                state_next = 0;
    endcase
end

To avoid hand-coding the 64 states with two or more "hot" bits, we have resorted to using the default statement to both redirect non-X inputs back to zero, and propagate Xes. Furthermore, to avoid giving the synthesis tool X-checking code, we are using the conventional but non-standard method of using the preprocessor to separate code meant for simulation and synthesis.

This type of coding style is both ugly and a potential source of simulation/synthesis behavior mismatches. It is difficult to explain, and error-prone to write. It will likely lead to engineers refusing to implement X-checking code. If you believe that Xes in simulation are valuable for debug, this is a complete disaster.

One way to work around this limitation is with our newly-learned case-inside statement. We can rewrite this block as:

always_comb begin
    unique case (state) inside
        // reset state
        0:   state_next = 1;
        // hot states
        1:   state_next = 2;
        2:   state_next = 4;
        4:   state_next = 8;
        8:   state_next = 16;
        16:  state_next = 32;
        32:  state_next = 1;
        // error states
        3, [5:7], [9:15], [17:31], [33:63]:
            state_next = 0;
        default:
            state_next = 'x;
    endcase
end

The case-inside statement's superpower is its ability to specify large ranges of integer values with a simple and elegant range syntax. There is no longer any need to work around the difficulty of enumerating all case items when the state space is large. Range expressions are perfectly capable of handling this.

Summary

The SystemVerilog LRM defines a case-inside statement to implement selection logic. This particular case variant is more than just arcane knowledge for hardware designers and EDA language lawyers. With its wildcard matching and range expression capabilities, this author believes it has the potential to be the Verilog case statement "über alles". That is, it can elegantly replace all other selection logic, improve readability, enhance debuggability, and reduce simulation/synthesis mismatch risks.

Add it to your Verilog toolbox today.