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.