A for loop in SystemVerilog repeats a set of statements as long as a condition stays true. The starting value, the exit condition and the step are all written in the loop header. That makes it the most common loop in both RTL design and testbenches. When the body has more than one statement, wrap them in begin and end, just like any other procedural block.

7 min read | Intermediate Level

What You'll Learn

  • The syntax of a for loop and the order in which its parts run
  • How to declare loop variables in the header and use several of them at once
  • How a for loop in always_comb synthesizes into hardware
  • The loop variable mistakes that cause infinite loops and compile errors

Syntax

A for loop runs in three steps:

  • Initialization runs once, before the first pass. It usually declares and sets the loop variable.
  • Condition is checked before every pass, including the first. If it is false, the loop ends.
  • Modifier (the step) runs at the end of every pass, and then the condition is checked again.

for ( [initialization]; <condition>; [modifier])
	// Single statement

for ( [initialization]; <condition>; [modifier]) begin
	// Multiple statements
end
  • initialization (optional): for example int i = 0. Variables declared here exist only inside the loop.
  • condition: for example i < 8. Leaving it out, as in for (;;), creates an infinite loop.
  • modifier (optional): for example i++, i += 2 or i--.

SystemVerilog adds C-style increment and decrement operators, which are the usual way to write the step. They can be used as a pre-increment or a post-increment:

StatementDescription
j = i++Post-increment: assign i to j, then increment i by 1
j = ++iPre-increment: increment i by 1, then assign i to j
j = i--Post-decrement: assign i to j, then decrement i by 1
j = --iPre-decrement: decrement i by 1, then assign i to j

Used on its own as a loop step, i++ and ++i do the same thing.

Using the for Loop in Digital Design

A for loop is synthesizable when its bounds are constants (literals or parameters) and its body has no timing controls such as # or @.

The synthesis tool unrolls the loop. It makes one copy of the body for each iteration and turns all the copies into logic. A loop that runs eight times in simulation becomes eight slices of hardware that all work in parallel.

A priority encoder is a good example. It outputs the position of the highest-priority active input. Unlike a plain encoder, which assumes only one input is active, it handles several active inputs at once and picks the one with the highest priority, here the highest index.


module priority_encoder #(parameter WIDTH = 8) (
    input  logic [WIDTH-1:0] req,
    output logic [$clog2(WIDTH)-1:0] grant,
    output logic valid
);

    always_comb begin
        grant = '0;
        valid = 1'b0;

        // Process from LOW to HIGH priority, so higher index overwrites lower
        for (int i = 0; i < WIDTH; i++) begin
            if (req[i]) begin
                grant = i;
                valid = 1'b1;
            end
        end
    end

endmodule

The loop scans req from bit 0 upwards. Every set bit overwrites grant, so after the last iteration grant holds the index of the highest set bit. For example, req = 8'b0010_0110 gives grant = 5. Because WIDTH is a parameter, the loop unrolls into a chain of 2-to-1 muxes, one per request bit. Changing WIDTH resizes the hardware with no other code changes.

The order of the iterations matters. In unrolled hardware, a later iteration's assignment sits closer to the output, so it wins. That is how this loop gives the highest index priority.

Where is the for loop used in RTL?

Arbiters, leading-zero counters, parity and CRC generators, and population counters are all commonly written as parameterized for loops in always_comb. A single module can then serve a 4-requester arbiter in one block and a 32-requester arbiter in another.

Using the for Loop in Verification

In a testbench there are no synthesis limits. Bounds can come from variables or $size(), and the body can wait on clocks. The example below prints every element of a string array of fruit names.

The initialization declares a local variable i that holds the current index. The condition keeps i below the size of the array, and the step moves i to the next index on every pass.


module tb;
	string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};

	initial begin
		for (int i = 0; i < $size(array); i++)
			$display ("array[%0d] = %s", i, array[i]);
	end
endmodule
Output
ncsim> run
array[0] = apple
array[1] = orange
array[2] = pear
array[3] = blueberry
array[4] = lemon
ncsim: *W,RNQUIE: Simulation is complete.

Use $size(array) instead of a hard-coded 5. The loop then stays correct if the array size changes. To visit every element of an array, foreach is even shorter.

Quick Check

Q: How many times does the body of for (int i = 10; i > 0; i -= 3) run?

Show Answer

Four times, with i = 10, 7, 4 and 1. After the fourth pass i becomes -2, the condition i > 0 fails, and the loop ends.

Multiple Initializations

The initialization part can set up more than one variable, separated by commas. Below, i and j are both declared as int when the loop starts. Each pass replaces the character at index j = 2 of the current string with "0".


module tb;
	string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};

	initial begin
      for (int i = 0, j = 2; i < $size(array); i++) begin
			array[i][j] = "0";
        $display ("array[%0d] = %s, %0dth index replaced by 0", i, array[i], j);
		end
	end
endmodule
Output
ncsim> run
array[0] = ap0le, 2th index replaced by 0
array[1] = or0nge, 2th index replaced by 0
array[2] = pe0r, 2th index replaced by 0
array[3] = bl0eberry, 2th index replaced by 0
array[4] = le0on, 2th index replaced by 0
ncsim: *W,RNQUIE: Simulation is complete.

String characters are indexed from 0, so index 2 is the third character: the first "p" in "apple" and the "a" in "orange".

Multiple Modifiers

The modifier part can also hold several comma-separated expressions. Here i counts up while j counts down. j starts at the last index of the first string ("apple", so 4) and decreases by one on every pass.


module tb;
	string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};

	initial begin
      for (int i = 0, j = array[i].len() - 1; i <  $size(array); i++, j--) begin
			array[i][j] = "0";
        $display ("array[%0d] = %s, %0dth index replaced by 0", i, array[i], j);
		end
	end
endmodule
Output
ncsim> run
array[0] = appl0, 4th index replaced by 0
array[1] = ora0ge, 3th index replaced by 0
array[2] = pe0r, 2th index replaced by 0
array[3] = b0ueberry, 1th index replaced by 0
array[4] = 0emon, 0th index replaced by 0
ncsim: *W,RNQUIE: Simulation is complete.

j is calculated only once, from the first string. After that it just counts down 4, 3, 2, 1, 0. It is not the last index of each string, which is why "orange" has its fourth character replaced rather than its last.

Common Beginner Mistakes

Mistake #1: A loop variable too narrow for the condition

Wrong: for (bit [2:0] i = 0; i < 8; i++)
Right: for (int i = 0; i < 8; i++)

Why? A 3-bit variable can only hold 0 to 7. After 7 it wraps back to 0, so i < 8 is always true and the loop never ends.

Mistake #2: Using the loop variable after the loop

A variable declared in the header, as in for (int i = 0; ...), exists only inside the loop. Reading i after end is a compile error. Why? The header creates a new local scope. If you need the final value, declare the variable before the loop and write for (i = 0; ...).

Mistake #3: A non-constant bound in RTL

Wrong (RTL): for (int i = 0; i < num_req; i++), where num_req is a signal
Right (RTL): for (int i = 0; i < MAX_REQ; i++) if (i < num_req) ...

Why? Synthesis must know how many copies of the body to build. Loop to a constant maximum and use an if inside for the run-time limit.

Mistake #4: Forgetting begin and end

Without begin and end, only the first statement after the header belongs to the loop. Why? Indentation means nothing to the compiler. The remaining statements run once, after the loop finishes.

Practice Exercise

Challenge: Count the ones

Write a synthesizable module that takes an 8-bit input data and outputs ones, the number of bits in data that are 1. Use a for loop inside always_comb.

Hint

Start ones at zero before the loop, then add each bit of data to it. The largest possible count is 8, so how many bits does ones need?

Solution

module popcount (
  input  logic [7:0] data,
  output logic [3:0] ones     // 0 to 8 needs 4 bits
);
  always_comb begin
    ones = '0;                 // default before the loop
    for (int i = 0; i < 8; i++)
      ones += data[i];         // add 1 for every bit that is set
  end
endmodule

The loop unrolls into eight adder stages, which synthesis typically optimizes into an adder tree. For data = 8'b1011_0010, ones = 4.

Summary

In this tutorial, you learned:

  • A for loop runs the initialization once, checks the condition before every pass, and runs the modifier after every pass
  • Variables declared in the header are local to the loop and cannot be used after it
  • The header can initialize and update several variables, separated by commas
  • ++ and -- come in pre and post forms; as a loop step they behave the same
  • With constant bounds and no timing control, a for loop unrolls into parallel hardware, and iteration order sets priority
  • A loop variable that is too narrow wraps around and makes the loop infinite
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5