SystemVerilog Procedural Statements and Operators for RTL Design

SystemVerilog Procedural Statements and Operators for RTL Design

SystemVerilog introduces several procedural statements and operators that enhance the Verilog language, enabling more concise and expressive synthesizable RTL code. These additions convey designer intent more clearly, reducing ambiguity and helping EDA tools interpret procedural statements consistently. This article examines the synthesizable features that improve RTL modeling productivity.

New Operators in SystemVerilog

Increment and Decrement Operators

SystemVerilog adds the ++ increment operator and -- decrement operator from the C language. These operators provide a shorthand for adding or subtracting one from a variable:

for (byte idx = 0; idx <= 31; idx++) begin
    // Process each element
end

Pre-increment versus Post-increment

Both operators support pre and post forms. The post-increment evaluates the current value before incrementing, while pre-increment increments first:

while (counter++ < MAX_LIMIT) begin: iteration_one
    // counter's final value will be MAX_LIMIT
end

while (++index < MAX_LIMIT) begin: iteration_two
    // index's final value will be MAX_LIMIT - 1
end

In the first loop, the comparison occurs before incrementing, so the final counter value equals the limit. In the second loop, incrementing happens first, resulting in a value one less than the limit when the loop terminates.

Race Condition Considerations

Verilog distinguishes between blocking assignments (=) and nonblocking assignments (<=). Blocking assignments execute immediately, while nonblocking assignments schedule updates for the end of the current simulation timestep. This distinction is critical for modeling combinational and sequential logic accurately.

data_out = data_in;    // Blocking: immediate assignment
data_out <= data_in;   // Nonblocking: scheduled assignment

The increment and decrement operators behave as blocking assignments. Consider this problematic example:

always_ff @(posedge clk) begin
    if (!reset_n) counter <= 0;
    else counter++;  // Behaves like: counter = counter + 1;
end

always_ff @(posedge clk)
    case (current_state)
        WAIT: if (counter == THRESHOLD) state <= ACTIVE;
        // Counter might be read before or after increment
    endcase

This code contains a race condition because both blocks trigger simultaneously. The simulator may execute the counter read before or after the increment, producing inconsistent results.

Pre-increment and pre-decrement do not resolve this issue because they only affect the order of read and write within a single statement, not between concurrent processes.

To model sequential logic correctly, use nonblocking assignments:

always_ff @(posedge clk)
    if (!reset_n) counter <= 0;
    else counter <= counter + 1;  // Correct for sequential modeling

always_ff @(posedge clk)
    case (current_state)
        WAIT: if (counter == THRESHOLD) state <= ACTIVE;
    endcase

Synthesis Considerations

Both pre and post forms synthesize correctly. However, some synthesis tools require these operators as standalone statements:

idx++;                    // Synthesizable
if (--remaining)          // May not synthesize
    result = idx++;       // May not synthesize

Compound Assignment Operators

SystemVerilog provides compound assignment operators that combine arithmetic operations with assignment:

accumulator += input_value;     // Equivalent to: accumulator = accumulator + input_value
product    *= multiplier;        // Equivalent to: product = product * multiplier

Table 1 summarizes the available operators:

Operator Description
+= Add and assign
-= Subtract and assign
*= Multiply and assign
/= Divide and assign
<<= Logical shift left and assign
>>= Logical shift right and assign

These operators behave as blocking assignments and share similar race condition risks with ++ and --.

Synthesis Guidelines

Compound assignment operators synthesize correctly, though some tools restrict their use in compound expressions:

value += 5;                     // Synthesizable
result = (operand += 5);        // May not synthesize

Example: Arithmetic Logic Unit

package alu_types;
    typedef enum logic [2:0] {OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_SHL, OP_SHR} operation_t;
    typedef enum logic {MODE_UNSIGNED, MODE_SIGNED} sign_mode_t;
    
    typedef union packed {
        logic [23:0] unsigned_data;
        signed [23:0] signed_data;
    } data_union_t;

    typedef struct packed {
        operation_t opcode;
        sign_mode_t sign_mode;
        data_union_t operand_a;
        data_union_t operand_b;
    } instruction_t;
endpackage

import alu_types::*;

module arithmetic_unit (
    input instruction_t instr,
    output data_union_t result
);
    always_comb begin
        if (instr.sign_mode == MODE_SIGNED) begin
            result.signed_data = instr.operand_a.signed_data;
            unique case (instr.opcode)
                OP_ADD: result.signed_data += instr.operand_b.signed_data;
                OP_SUB: result.signed_data -= instr.operand_b.signed_data;
                OP_MUL: result.signed_data *= instr.operand_b.signed_data;
                OP_DIV: result.signed_data /= instr.operand_b.signed_data;
                OP_SHL: result.signed_data <<= 2;
                OP_SHR: result.signed_data >>= 2;
            endcase
        end
        else begin
            result.unsigned_data = instr.operand_a.unsigned_data;
            unique case (instr.opcode)
                OP_ADD: result.unsigned_data += instr.operand_b.unsigned_data;
                OP_SUB: result.unsigned_data -= instr.operand_b.unsigned_data;
                OP_MUL: result.unsigned_data *= instr.operand_b.unsigned_data;
                OP_DIV: result.unsigned_data /= instr.operand_b.unsigned_data;
                OP_SHL: result.unsigned_data <<= 2;
                OP_SHR: result.unsigned_data >>= 2;
            endcase
        end
    end
endmodule

Wildcard Equality Operators

Verilog provides == logical equality and === case equality operators. The == operator treats X and Z values as unknown, while === requires exact matching including X and Z values.

SystemVerilog adds wildcard equality operators (==? and !=?) that treat X and Z values in the right-hand operand as don't-care bits:

logic [7:0] operation_code;
...
if (operation_code ==? 8'b11011???) begin
    // Lower 3 bits are don't-care
end

In wildcard comparisons, X or Z bits in the right operand serve as masks, but X or Z bits in the left operand are treated as literal four-state values.

Synthesis Guidelines

For synthesis, masked bits must be constant expressions:

logic [3:0] operand_a, operand_b;
logic match_result;

assign match_result = (operand_a ==? 4'b1??1);   // Synthesizable
assign match_result = (operand_a ==? operand_b); // Not synthesizable

Set Membership Operator

The inside operator tests whether a value matches any element in a set:

logic [2:0] selector;
if (selector inside {3'b001, 3'b010, 3'b100}) begin
    // Match found
end

This is more concise than multiple equality comparisons:

// Equivalent without inside:
if ((selector == 3'b001) || (selector == 3'b010) || (selector == 3'b100))

The set can contain other signals or array elements:

if (data_bus inside {bus_x, bus_y, bus_z, bus_w}) ...

int data_array [0:1023];
if (13 inside {data_array}) ...

X and Z values in the set act as don't-care masks:

logic [2:0] value;
if (value inside {3'b1?1})  // Matches 3'b101, 3'b111, 3'b1x1, 3'b1z1

The inside operator works with case statements:

always_comb begin
    case (instruction) inside
        4'b0???: operation = instruction[2:0];
        4'b1000, 4'b1100: operation = 3'b000;
        default: operation = 3'b111;
    endcase
end

Unlike casex which treats both sides as don't-care, inside only masks the right-hand side. Synthesis requires constant expressions for masked values.

Operand Enhencements

Operations on Two-State and Four-State Types

Verilog defines operation rules for various operand type combinations. SystemVerilog extends these rules to two-state types (bit, logic). Most operations can return 0, 1, or X for each bit. Two-state operations rarely produce X, except in cases like division by zero.

Type Casting

Verilog performs implicit type conversion during assignment. SystemVerilog adds explicit type casting using the syntax type'(expression):

longint accumulator, result;
real floating_value;
result = accumulator + longint'(floating_value ** 3);

This differs from C's (type)expression syntax to maintain Verilog compatibility and enable additional casting capabilities.

Size Casting

SystemVerilog allows explicit size casting to control expression width:

logic [15:0] operand_x, operand_y, sum_output;
logic carry_out;

sum_output = operand_x + 16'(5);           // Cast operand
{carry_out, sum_output} = 17'(operand_x + 3);  // Cast result
sum_output = operand_x + 16'(operand_y - 2) / operand_z;  // Cast intermediate

Truncation removes leftmost bits when casting to smaller sizes. Zero extension extends unsigned values; sign extension extends signed values.

Sign Casting

SystemVerilog provides explicit sign casting:

signed_result = signed'(value_a) + signed'(value_b);
if (unsigned'(value_a - value_b) <= 5) ...

These operators perform the same conversion as $signed and $unsigned system functions and are synthesizable.

Enhanced For Loops

Local Variable Declaration

In Verilog, loop control variables must be declared outside the loop. This can cause conflicts when multiple concurrent blocks use the same variable name:

module processing_unit (...);
    reg [7:0] loop_index;
    integer iter_a, iter_b;
    
    always_ff @(posedge clk) begin
        for (loop_index = 0; loop_index <= 15; loop_index = loop_index + 1)
            for (iter_a = 511; iter_a >= 0; iter_a = iter_a - 1) begin
                // Process data
            end
    end
    
    always_ff @(posedge clk) begin
        for (iter_b = 1; iter_b <= 1024; iter_b = iter_b + 2) begin
            // Different loop
        end
    end
endmodule

SystemVerilog allows declaring loop variables within the loop header:

module processing_unit (...);
    always_ff @(posedge clk) begin
        for (bit [4:0] idx = 0; idx <= 15; idx++)
            // Processing
    end
    
    always_ff @(posedge clk) begin
        for (int idx = 1; idx <= 1024; idx += 1)
            // Different loop, no conflict
    end
endmodule

Automatic Storage

Loop-declared variables have automatic storage—they exist only during loop execution and cannot be referenced hierarchically:

always_comb begin
    for (int bit_pos = 0; bit_pos <= 63; bit_pos++) begin
        if (data_word[bit_pos]) break;
    end
    if (bit_pos > 7)  // Error: bit_pos not visible here
        ...
end

To reference a variable outside the loop, declare it in a block:

always_comb begin
    int bit_position;
    for (bit_position = 0; bit_position <= 63; bit_position++) begin
        if (data_word[bit_position]) break;
    end
    if (bit_position > 7)  // Valid: bit_position exists
        ...
end

Multiple Initialization and Step Statements

SystemVerilog allows multiple initialization and step expressions in for loops:

for (int i = 1, j = 0; i * j < 128; i++, j += 3)
    // Loop body

for (int i = 1, byte j = 0; i * j < 128; i++, j += 3)
    // Different types for each variable

Do-While Loop

The while loop tests at the beginning, potentially executing zero times. SystemVerilog adds do...while which tests at the end, guaranteeing at least one execution:

always_comb begin
    if (address < 128 || address > 255) begin
        complete = 0;
        invalid_range = 1;
        output_data = memory[128];
    end
    else while (address >= 128 && address <= 255) begin
        if (address == 128) begin
            complete = 1;
            invalid_range = 0;
        end
        else begin
            complete = 0;
            invalid_range = 0;
        end
        output_data = memory[address];
        address -= 1;
    end
end

The do...while version consolidates logic:

always_comb begin
    do begin
        complete = 0;
        invalid_range = 0;
        output_data = memory[address];
        if (address < 128 || address > 255) begin
            invalid_range = 1;
            output_data = memory[128];
        end
        else if (address == 128) complete = 1;
        address -= 1;
    end
    while (address >= 128 && address <= 255);
end

Synthesis requires loops with statically determinable iteration counts.

Jump Statements

Verilog's disable statement can exit loops or tasks. SystemVerilog provides C-style jump statements that are more intuitive.

Continue Statement

The continue statement skips to the next iteration:

logic [15:0] data_array [0:255];
always_comb begin
    for (int idx = 0; idx <= 255; idx++) begin : filter_loop
        if (data_array[idx] == 0)
            continue;  // Skip zero entries
        process_value(data_array[idx]);
    end
end

Break Statement

The break statement terminates the loop immediately:

always_comb begin
    first_bit = 0;
    for (int idx = 0; idx <= 63; idx++) begin
        if (idx < start_range) continue;
        if (idx > end_range) break;  // Exit loop
        if (data_word[idx]) begin
            first_bit = idx;
            break;  // Found target, exit
        end
    end
    // Process results
end

Return Statement

The return statement exits functions or tasks immediately:

task compute_maximum (
    input [5:0] max_value,
    output [63:0] result
);
    result = 1;
    if (max_value == 0) return;  // Early exit
    for (int idx = 1; idx <= 63; idx++) begin
        result = result + result;
        if (idx == max_value) return;  // Exit task
    end
endtask

function automatic int calculate_log2 (input int n);
    if (n <= 1) return 1;  // Early function exit
    calculate_log2 = 0;
    while (n > 1) begin
        n /= 2;
        calculate_log2++;
    end
    return calculate_log2;
endfunction

These jump statements apply only to the current execution flow, unlike disable which affects all running invocations.

Named Blocks and Statement Labels

Named End Blocks

SystemVerilog allows naming end statements to clarify block boundaries:

always_ff @(posedge clk, posedge reset)
    begin: state_machine_fsm
        logic break_flag;
        if (reset) begin: reset_handler
            // Reset logic
        end: reset_handler
        else begin: state_sequencer
            unique case (current_state)
                WAIT_FOR_VALID: begin: rx_wait_state
                    ready <= '1;
                    break_flag = 1;
                    for (int outer = 0; outer < num_receivers; outer += 1) begin: outer_loop
                        for (int inner = 0; inner < num_receivers; inner += 1) begin: inner_loop
                            if (valid[inner] && round_robin[inner] && break_flag)
                                begin: match_condition
                                    cell_data <= receiver_cell[inner];
                                    ready[inner] <= 0;
                                    current_state <= WAIT_INVALID;
                                    break_flag = 0;
                                end: match_condition
                        end: inner_loop
                    end: outer_loop
                end: rx_wait_state
                // Other states
            endcase
        end: state_sequencer
    end: state_machine_fsm

Statement Labels

Individual statements can have labels for documentation:

always_comb begin : decoder_logic
    decoder_select: case (opcode)
        2'b00:
            outer_iteration: for (int row = 0; row <= 15; row++)
                inner_iteration: for (int col = 0; col <= 15; col++)
                    // Process element
        // Other opcodes
    endcase
end : decoder_logic

Labels help document code and enible referencing statements for debugging or coverage analysis.

Enhanced Case Decisions

Verilog case statements evaluate items in order, implying priority. SystemVerilog adds unique and priority modifiers to clarify intent.

Unique Case

The unique modifier indicates items are mutually exclusive and complete:

always_comb
    unique case (opcode)
        2'b00: result = operand_a + operand_b;
        2'b01: result = operand_a - operand_b;
        2'b10: result = operand_a * operand_b;
        2'b11: result = operand_a / operand_b;
    endcase

Tools generate warnings if multiple items match or no item matches. This enables parallel evaluation optimization.

With wildcards:

logic [2:0] bus_request;
always_comb
    unique casez (bus_request)
        3'b1??: grant_device1 = 1;
        3'b?1?: grant_device2 = 1;
        3'b??1: grant_device3 = 1;
    endcase

Priority Case

The priority modifier maintains ordered evaluation:

always_comb
    priority case (1'b1)
        interrupt0: interrupt_vector = 4'b0001;
        interrupt1: interrupt_vector = 4'b0010;
        interrupt2: interrupt_vector = 4'b0100;
        interrupt3: interrupt_vector = 4'b1000;
    endcase

This explicitly documents that multiple items might match and the first match should win.

Comparison with Pragmas

Verilog synthesis pragmas like parallel_case and full_case inform synthesis behavior but don't affect simulation. SystemVerilog unique and priority modifiers are part of the language semantics, ensuring consistent behavior across all tools including simulators, synthesizers, and formal verification tools.

Unique case combines paralel_case and full_case semantics plus runtime checking. Priority case provides full_case semantics with additional verification.

Enhanced If-Else Decisions

The unique and priority modifiers also apply to if-else chains:

logic [2:0] select;
always_comb begin
    unique if (select == 3'b001) multiplexer_output = input_a;
    else if (select == 3'b010) multiplexer_output = input_b;
    else if (select == 3'b100) multiplexer_output = input_c;
end

Priority if-else:

always_comb begin
    priority if (interrupt0) vector = 4'b0001;
    else if (interrupt1) vector = 4'b0010;
    else if (interrupt2) vector = 4'b0100;
    else if (interrupt3) vector = 4'b1000;
end

Both modifiers generate warnings for overlapping conditions or missing matches, helping catch design errors early.

Tags: SystemVerilog rtl-design synthesis Operators procedural-statements

Posted on Sun, 27 Sep 2026 16:04:55 +0000 by DarkJamie