Loops
DFHDL supports loops in several contexts with different semantics depending on the domain and placement.
Elaboration-Time Loops (Generate Loops)
By default, for loops in a concurrent scope (a design or domain body, not within a function, procedure, or process) run at elaboration time: their range is an ordinary Scala range, even when the range arguments are DFHDL Int values (such as Int <> CONST parameters, whose values are read during elaboration). The loop unrolls into repeated hardware, equivalent to Verilog generate for, so the generated HDL contains no loop; each iteration produces distinct instances.
A range bound is the one place a DFHDL constant is read implicitly. Anywhere else that Scala needs the number (a List size, an index computation, a plain method argument) the constant has to be read explicitly with .toScalaInt. See reading a constant into Scala.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
When a design containing an elaboration-time loop is instantiated with different parameter values, DFHDL creates distinct elaborated designs (with enumerated names), each with a different number of unrolled instances.
Elaboration-Time Conditionals
An if whose condition is a constant resolves during elaboration, so only the taken branch produces hardware. Both branches are still ordinary Scala code, though, so Scala type-checks both. Whether that rejects an untaken branch depends on whether the widths involved are literal or parameterized.
Which if you get depends on the domain
In an ED design body, an implicit .toScalaBoolean is applied to a constant condition, so the if is a Scala if and resolves at elaboration.
In an RT or DF design body, an if is a DFHDL (hardware) if unless its condition is a Scala Boolean. Force that with .toScalaBoolean when you want the elaboration-time behavior:
1 | |
A DFHDL if elaborates both branches, so a width that is invalid in either one is an error regardless of which is taken:
1 | |
What decides the Scala-level check is the type ascription on the width, not the value. These two declarations look almost identical and behave differently:
1 2 | |
A plain Scala Int gives Bits(4) the bounded type Bits[4], so Scala tracks the width and rejects an invalid untaken branch at compile time:
1 2 3 4 5 6 7 8 | |
1 2 | |
An Int <> CONST gives Bits(WIDTH) the unbounded type Bits[Int], which the Scala type level does not track, so there is nothing for it to reject. The width check moves to elaboration, and elaboration only ever visits the taken branch:
1 2 3 4 5 6 7 8 | |
The same holds for a width that arrives as a design parameter (class narrow(val WIDTH: Int <> CONST = 4)), which is the usual case when translating a Verilog parameter. This is why a generate if whose branches are each valid only for their own parameter value translates directly, with no .resize guard and no .toScalaInt. If you do need both branches valid at the Scala level, use .resize or guard the index computations, as in the plain-Int example above.
The ascription has a second, visible consequence: an Int <> CONST survives into the generated HDL as a localparam, while a plain Scala Int is inlined away. See localparam for that side of the same distinction.
ED Domain Loops
In ED designs, for and while loops inside processes produce combinational or sequential logic depending on the process type. Unlike a design-scope loop, a loop inside a process stays a loop: it is elaborated once and emitted as a real for in the generated HDL, as the OnesCount example below shows.
That difference decides what its iterator is. A design-scope iterator is an ordinary Scala Int, so it can index Scala collections and be used anywhere Scala needs a number. A process-scope iterator is a hardware value, and cannot be read out into Scala at all, .toScalaInt included:
1 2 3 4 | |
1 2 | |
The reported position is the i in the for binding rather than the use that actually needs a Scala value, so read the message as "something in this loop body wanted a Scala Int" and look at the uses, not the range.
To write per-index slices of a packed bus, do the work in a design-scope loop and give each iteration its own small process. The i is then a Scala value captured by closure, and no loop exists inside a process:
1 2 3 4 5 6 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
lane instances and three separate always_comb blocks, each writing one static slice. No loop remains.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
out_bus.
Contrast this with the OnesCount example above, where the loop is inside the process and survives into the generated HDL as a real for.
Arithmetic on the iterator that stays inside DFHDL is fine either way: a process-scope i is a valid part-select base (see Bit Selection and Slicing), and emits a variable-base part-select.
Loops Accumulation Example
A loop that stays a hardware loop is elaborated once, not once per iteration, so a Scala var cannot accumulate across it. Reassigning the var in the body only rebinds the Scala name to a value built inside the loop, and reading it after the loop reaches the loop's own iterator, which does not exist outside it. Declaring a var in a process is a compile error for this reason, and the elaboration rejects the shape as a scope error wherever it is laundered in through a helper def. See Scala var with DFHDL values for the full permission list.
Accumulate into a DFHDL variable instead, driven with := inside the loop:
| Accumulating in hardware | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
for loop, and sum is hoisted to a module-scope logic, since Verilog has no notion of a declaration local to an always block. DFHDL's := becomes Verilog's blocking =, which is what makes the accumulation work: each iteration reads the value the previous one wrote.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
variable rather than a signal. That distinction is not cosmetic: a signal assignment is scheduled and would not take effect until the process suspended, so every iteration would read the same stale value. A variable assignment (:=) takes effect immediately, which is the semantics DFHDL's := carries. The output port stays a signal, so the final count <= sum is a signal assignment.
Declaring the accumulator in the design body rather than inside the process would make it a VHDL signal, and the accumulation would then be wrong. Keep a loop accumulator local to its process.
Contrast this with the elaboration-time accumulation a Scala var is for, where the loop is a Scala loop and nothing of it survives into the generated code.
RT Domain Loops
In RT designs, for and while loops inside processes create synthesizable procedural FSMs. The compiler transforms the loop body into state machine transitions. Loop iterators become registers, and the loop boundaries (entry, exit, and loop-back) consume zero extra cycles: each executed iteration costs exactly the cycles its body consumes, so a flat wait, a loop of waits, and nested loops of waits with the same total time are cycle-identical. See Processes for the full RT cycle semantics.
Combinational Loops (COMB_LOOP)
Wrapping a loop with a COMB_LOOP block marks it combinational: the whole loop executes within a single cycle. At RT design scope (outside processes), the wrapper also keeps the for range as a hardware range, so the loop is emitted as a single procedural loop in the generated HDL instead of unrolling at elaboration time. Like FALL_THROUGH, this annotation is allowed under RT domains only; applying it elsewhere is a compile-time error.
1 2 3 4 5 6 7 8 | |
Inside an RT process, where loops are sequential (multi-cycle) by default, the same wrapper keeps a loop combinational; its body must not consume cycles.
COMB_LOOP is a block wrapper because it marks a whole region: a loop nested inside a combinational loop cannot consume cycles either, so it is combinational too. The other RT annotation, FALL_THROUGH, marks a single loop or condition wait and is written on that construct's own condition or range instead (see Processes).