VLSIChaps Academy · Track 01

Loops: for, while, repeat, foreach

~12 min Hands-on lab 1 exercise Verilator
Page 13 of 50
01

The Four Loop Forms and When Each Fits

SystemVerilog gives you four loop forms, and each one exists for a distinct shape of repetition. for is bounded iteration with an explicit counter — a start value, a comparison, and a step, all visible on one line — and it is the workhorse of the language, the loop you reach for by default. while is condition-driven: it has no counter of its own, it runs as long as a condition holds, and it stops the moment that condition fails, which makes it the right tool when you don't know the count in advance. repeat(N) runs a fixed number of times with nothing else to manage — no condition to check on every pass, no loop variable to declare — it simply repeats the body N times and stops. foreach walks the dimensions of an array, and because the language derives the bounds from the array itself, a foreach loop cannot produce an off-by-one; there is no index to get wrong. The one-line rule that covers all four: if you know the count, use for or repeat; if you don't know the count, use while; if you're walking an array, use foreach.

02

Hardware Loops Versus Testbench Loops

This is the divide that separates a loop that becomes silicon from a loop that only ever runs in simulation. In synthesis, a loop is unrolled: the compiler copies the loop body N times at elaboration and produces N instances of hardware, one per iteration. That unrolling is only possible if the bound is a constant the tool can resolve before it starts building gates — a variable bound, one that depends on a runtime signal, is not a hardware description at all, and a synthesis tool will reject it as a compile error. In a testbench, none of that applies. A testbench loop executes sequentially through simulation time, one iteration after another, and it is free to use runtime variables, because a testbench never has to become a netlist. The mental model to carry forward: a synthesisable loop generates hardware, a testbench loop generates time. This is exactly the boundary you saw on the always_comb page — always_comb is where synthesisable loops live, and every loop inside one must resolve to a static, elaboration-time bound.

03

Foreach Walks Arrays Cleanly

foreach(arr[i]) declares i for you, iterates every valid index of the array, and produces no off-by-one, because the bounds come from the array's own declared size rather than a number you typed by hand. It is particularly powerful in testbenches, where dynamic arrays and queues change size at runtime and a hand-written bound would need to track that size on every pass. In synthesis, foreach works cleanly on fixed-size arrays and unpacks to identical hardware as a correctly bounded for loop — you lose nothing by using it. Foreach is the professional's default whenever the task is "do something to every element": it removes an entire category of indexing mistakes by construction, and it reads, at a glance, as exactly what it does.

04

Key Insight: A Loop That Never Terminates Is a Simulation Catastrophe

A while(1) with no path out is not a slow loop, it is a hung simulator. Unlike hardware, where separate blocks run in parallel regardless of what any one of them is doing, simulation is single-threaded: one stuck loop freezes the entire design, every clock, every other block, everything, because the simulator can never advance past it. And tools have no general way to catch this at compile time — deciding whether an arbitrary loop will terminate is the halting problem, and no static check can solve it for every case. The discipline that has to replace the missing safety net: always ensure visible progress toward the exit condition on every single pass, and prefer for or repeat over while whenever the count is actually known in advance, because a bound you can see is a bound you can't lose track of. In synthesis this entire failure mode disappears by construction — the static-bound requirement from section two means an infinite hardware loop cannot exist, the tool simply won't build it. In a testbench, no such guarantee exists; discipline replaces the compiler, the same discipline you first saw matter with unknowns on Page 03.

Lab: Count Every Bit

Write a module that counts the number of 1-bits in an 8-bit input. Your module receives data_in [7:0] and must produce ones_count [3:0], the total number of bits that are high. Use a for loop inside always_comb to walk every bit position and accumulate the count. Initialise ones_count to zero before the loop. Submit and the engine runs eight test patterns covering edge cases — all zeros, all ones, single bits, alternating patterns. Get the loop bound wrong by one and the engine will name the exact bit position the loop never reached and the test pattern that exposed it.

Eight bits, eight iterations. Start the count at zero, loop i from zero to seven inclusive, and add data_in[i] to the count on every pass. If the bound stops at seven exclusive, bit 7 never gets counted.
Compiling, linting, and simulating...
Correct. Your loop walks all eight bit positions, the count matches on every test pattern, and there is no bit the loop skipped. A for loop with a correct bound and an initialised accumulator — that is the pattern behind every popcount, CRC, and parity check in hardware.
Not quite. Your loop ran, but at least one test pattern produced the wrong count. This usually means the loop bound is off by one — the loop stops early and misses a bit position, or overshoots. The engine output below names the test pattern, the expected count, and the count your loop produced.
Compile error.
Something went wrong reaching the engine. Try again in a moment.
AI Mentor
Thinking...
📲 Join 20K+ Engineers