Skip to content

Backings

A Karray’s first constructor argument is its backing — the kind of hardware each (element, field) pair becomes. There are three:

BackingHwComponentTypeHardware per fieldAssign with
RegisterREGone clocked reg per (element, field)|= (clocked)
WireWIREone combinational net per (element, field)*= (combinational)
Memory blockMEM_BLOCKone addressable memory block per field|= (clocked)

Each backing maps to a different hardware and a different assignment operator:

flowchart LR
    REG["REG"] --> R1["one clocked reg<br/>per (element, field)"] --> RO["|= (clocked)"]
    WIRE["WIRE"] --> W1["one combinational net<br/>per (element, field)"] --> WO["*= (combinational)"]
    MEM["MEM_BLOCK"] --> M1["one memory block<br/>per field"] --> MO["|= (clocked)"]

The backing is fixed at construction:

class RobEntry(Karray):
valid = kaf(1)
reg_idx = kaf(5)
self.rob = RobEntry(HwComponentType.REG, (5, 3), "rob") # registers
self.bus = RobEntry(HwComponentType.WIRE, (2, 2), "bus") # wires
self.kmem = RobEntry(HwComponentType.MEM_BLOCK, (5, 3), "kmem") # memory blocks

Reg backing materializes one register per (element, field). A 5×3 array of {valid:1, reg_idx:5} yields 15 × 2 = 30 registers, each written by its own clocked always-block. Assign with |=, whether field-wise or whole-element:

with seq():
self.rob[2][1] |= {"valid": self.vsrc, "reg_idx": self.isrc} # whole element
self.rob[0][0].valid |= self.vbit # single field

Reg backing is the most capable: it is the only backing that supports dynamic writes (non-selected elements need a register to hold their value), and it supports dynamic reads and reduce.

Wire backing is combinational — each field is a net with no storage. Assign with *=:

class BusEntry(Karray):
data = kaf(8)
self.bus = BusEntry(HwComponentType.WIRE, (2, 2), "bus")
with seq():
self.bus[1][0] *= {"data": self.s} # combinational drive

Wire-backed Karrays can be read dynamically and reduced, but they cannot be the target of a dynamic write (a wire cannot hold the non-selected elements).

MemBlock backing folds the whole array onto one addressable memory block per field, instead of discrete components per element. A static element write becomes a memory write at the constant flattened address:

self.kmem = RobEntry(HwComponentType.MEM_BLOCK, (5, 3), "kmem")
with seq():
# write element (2,1) -> flat address 7, one write per field's block
self.kmem[2][1] |= {"valid": self.vsrc, "reg_idx": self.isrc}

The emitted Verilog declares one memory per field (kmem_valid_MEM, kmem_reg_idx_MEM), which maps naturally onto block RAM. Assign with |= (clocked), like a reg.

Kathryn checks the operator against the backing before mutating the model, and raises a TypeError from Python on a mismatch:

self.rk = VEntry(HwComponentType.REG, (2,), "rk")
self.wk = VEntry(HwComponentType.WIRE, (2,), "wk")
self.wk[0] |= {"v": self.s} # TypeError: |= needs a reg/mem backing
self.rk[0] *= {"v": self.s} # TypeError: *= needs a wire backing

The rules match plain signals: |= declares clocked intent and requires a reg or mem backing; *= declares combinational intent and requires a wire backing.

A bare = on a Karray element carries no intent of its own — it is resolved from the destination’s backing (reg/memblock → clocked, wire → combinational). It works for static writes on any backing, but keep in mind that on a dynamic write it inherits the backing’s semantics and a wire-backed target is rejected (see Dynamic Writes).

  • REG — the default choice. Random access, per-element writes, dynamic reads/writes, reduce. Costs one register per (element, field), so it scales to small/medium tables (register files, ROB-style structures).
  • WIRE — combinational interconnect shaped like an array; useful for fan-in/fan-out buses where values are recomputed every cycle.
  • MEM_BLOCK — large storage where one address is accessed at a time and a block RAM is the right physical target.