Summary
An RT-domain VAR that is assigned in full is reported as a latch if it is later read through a slice whose bound is a design parameter. The same code with a local Int <> CONST bound elaborates fine, so the trigger is specifically that the bound depends on a parameter.
Reproduction
import dfhdl.*
class SliceD(val MB: Int <> CONST = 17) extends RTDesign:
val addr = Bits(32) <> IN
val o = Bit <> OUT
val v = Bits(32) <> VAR
v := h"32'f0040000" // assigned in full, unconditionally
o := addr(31, MB) == v(31, MB)
DFiant HDL connectivity/assignment error!
Hierarchy: SliceD
Message: Found a latch variable `v`. Latches are not allowed under RT domains.
v has exactly one unconditional whole-variable assignment, so there is no path on which it is unassigned.
What does and does not trigger it
A local constant bound is fine:
class SliceB extends RTDesign:
val W: Int <> CONST = 4
val src = Bits(32) <> IN
val o = Bits(4) <> OUT
val v = Bits(32) <> VAR
v := src
o := v(31, 32 - W) // OK
Making the bound depend on a parameter is what breaks it, whether it is used directly (SliceD above) or through a derivation:
class SliceC(val SIZE: Int <> CONST = 128) extends RTDesign:
val MB: Int <> CONST = 10 + clog2(SIZE)
...
o := addr(31, MB) == v(31, MB) // same latch error
So clog2 is not implicated; parameter-dependence is.
Reading a port or a parameter through the same slice is fine -- only a VAR trips it:
class SliceE(val MB: Int <> CONST = 17, val SADR: Bits[32] <> CONST = h"32'f0040000")
extends RTDesign:
val addr = Bits(32) <> IN
val o = Bit <> OUT
o := addr(31, MB) == SADR(31, MB) // OK: assign o = addr[31:MB] == SADR[31:MB];
That last form is the workaround where the variable can be eliminated.
Why it matters
It pushes ports away from a generic parameterized design. VeeR-EH1's rvrangecheck is
module rvrangecheck #(CCM_SADR = 32'h0, CCM_SIZE = 128) (...);
localparam MASK_BITS = 10 + $clog2(CCM_SIZE);
logic [31:0] start_addr;
assign start_addr = CCM_SADR;
assign in_range = (addr[31:MASK_BITS] == start_addr[31:MASK_BITS]);
and is instantiated 8 times at three different CCM_SIZE values. The direct transcription hits this, and the fallback -- reading the parameter with .toScalaInt so the bounds become literals -- makes the elaboration read the parameter, which pins it and emits one specialised module per instantiation instead of one generic module.
Environment
DFHDL v0.22.0+116-9b313a7e-SNAPSHOT, Scala 3.8.4.
Summary
An RT-domain
VARthat is assigned in full is reported as a latch if it is later read through a slice whose bound is a design parameter. The same code with a localInt <> CONSTbound elaborates fine, so the trigger is specifically that the bound depends on a parameter.Reproduction
vhas exactly one unconditional whole-variable assignment, so there is no path on which it is unassigned.What does and does not trigger it
A local constant bound is fine:
Making the bound depend on a parameter is what breaks it, whether it is used directly (
SliceDabove) or through a derivation:So
clog2is not implicated; parameter-dependence is.Reading a port or a parameter through the same slice is fine -- only a
VARtrips it:That last form is the workaround where the variable can be eliminated.
Why it matters
It pushes ports away from a generic parameterized design. VeeR-EH1's
rvrangecheckisand is instantiated 8 times at three different
CCM_SIZEvalues. The direct transcription hits this, and the fallback -- reading the parameter with.toScalaIntso the bounds become literals -- makes the elaboration read the parameter, which pins it and emits one specialised module per instantiation instead of one generic module.Environment
DFHDL v0.22.0+116-9b313a7e-SNAPSHOT, Scala 3.8.4.