Signals
Signals are the atoms of a Kathryn design. Every one you construct is a real hardware component registered in the model, and every one appears in the emitted Verilog. There are five user-constructible kinds:
| Factory | Hardware | Assignment | Emitted as |
|---|---|---|---|
reg(width) | clocked register | |= | Verilog reg, written in always @(posedge clk) |
wire(width) | combinational signal | *= | Verilog reg, driven from always @(*) |
val(width, value) | constant | read-only | wire ... = <sized literal>; |
mem_blk(width, index_width) | addressable memory | via mem_ele | reg [W-1:0] name [0:depth]; |
mem_ele(blk, index, width, is_read) | memory access port | |= (writes) | assign (reads) / clocked write |
The five kinds and their assignment operators at a glance:
flowchart TB
Signal["Signal (hardware component)"]
Signal --> reg["reg(width) - clocked register - assigned with |="]
Signal --> wire["wire(width) - combinational - assigned with *="]
Signal --> val["val(width, value) - constant - read-only"]
Signal --> blk["mem_blk(width, index_width) - addressable memory"]
blk --> ele["mem_ele - access port - |= writes / assign reads"]
All factories share two conventions:
- Width comes first and is a plain bit count:
reg(8)is an 8-bit register. - The name is optional and last. If you omit it, Kathryn auto-generates
one per kind (
reg0,reg1,wire0, …), and the Rust core additionally appends a globally unique id to every emitted name — so name collisions are impossible either way. Explicit names exist purely to make the Verilog readable.
reg — clocked registers
Section titled “reg — clocked registers”class counter(Module): @init def declare(self): self.count = reg(8, "count") # 8-bit register named "count" self.tmp = reg(16) # auto-named ("reg0")A reg is a flip-flop-based register: it holds its value across clock cycles
and only changes on a clock edge, when a clocked assignment (|=) to it
fires. In the emitted Verilog it becomes a reg declaration updated inside
always @(posedge clk) blocks:
reg [7:0] REG_count_1;
always @(posedge WIRE_clk_12) begin if (SR_ST_seq_state_4_0_ST_36) begin REG_count_1[7:0] <= ...; endendRegisters can be given a reset value with .reset(value) — see
Reset & Defaults.
wire — combinational signals
Section titled “wire — combinational signals”self.sum = wire(9, "sum")A wire carries a value combinationally within a cycle: it has no storage,
and is assigned with the combinational operator *=. In the emitted Verilog,
a Kathryn wire is declared as a Verilog reg and driven from an
always @(*) block (a standard idiom for procedural combinational logic):
reg [7:0] WIRE_w_1546;
always @(*) begin WIRE_w_1546[7:0] <= REG_src_1544[7:0];endWires can be given a fallback value with .default(value) for cycles when
nothing drives them — see
Reset & Defaults.
val — constants
Section titled “val — constants”self.limit = val(8, 48, "limit") # 8-bit constant, value 48self.big = val(128, 1 << 100) # arbitrary-precision values workA val is an immutable constant of a given width. It can be read anywhere a
signal can, but it is not an assignment destination — neither |= nor
*= accepts it. The initial value is an ordinary Python integer of any
magnitude; it is wrapped two’s-complement style into the declared width, so
val(8, -1) is 0xFF and values wider than 64 bits are handled exactly.
In Verilog a val becomes a continuously driven wire with a sized literal:
wire [7:0] VAL_limit_3 = 8'h30;mem_blk and mem_ele — memory
Section titled “mem_blk and mem_ele — memory”A mem_blk declares a block of addressable storage:
self.buf = mem_blk(8, 4, "buf") # 2**4 = 16 entries, 8 bits eachThe first argument is the data width per entry, the second the index
width — the block holds 2**index_width entries. In Verilog it becomes a
two-dimensional register array:
reg [7:0] buf [0:15];A mem_blk is not read or written directly; it has no assignment operator of
its own. Instead you create mem_ele access ports bound to an index signal:
self.addr = reg(4, "addr")self.rdata = mem_ele(self.buf, self.addr, 8, True, "rd") # read portself.wdata = mem_ele(self.buf, self.addr, 8, False, "wr") # write portThe arguments are: the master mem_blk, the index signal, the element bit
width, and is_read (True for a read element, False for a write
element), plus the usual optional name.
- A read element behaves as a combinational view of
blk[index]— emitted asassign rd = buf[addr];. - A write element is a clocked destination: assign to it with
|=, and the write is emitted asbuf[addr] <= ...;inside a clocked always block.
Inspecting a signal
Section titled “Inspecting a signal”Every signal handle exposes its kind and identity:
r = reg(8)r.hw_type # "REG" (also "WIRE", "VAL", "MEM_BLOCK", ...)r.global_id # unique integer id in the modelMarking I/O ports
Section titled “Marking I/O ports”Any signal can be promoted to a port of its module:
self.din = wire(8, "din")self.din.mark_input("data_in") # input port named data_in
self.dout = reg(8, "dout")self.dout.mark_output("data_out") # output port named data_out
self.dout.is_io # -> Truemark_input(name) / mark_output(name) stamp the component as an I/O port
with the given direction and external name; both return the signal, so they
chain. Marked signals appear in the module header of the emitted Verilog:
module MODULE_top0_0( output reg [7:0] data_out, input wire [7:0] data_in, input wire [0:0] clk, input wire [0:0] mrst);The clk and mrst (master reset) ports are added automatically to every
module — you never declare them.
Where next
Section titled “Where next”- Expressions — combining signals with operators and slices.
- Assignment — which operator goes with which signal kind.
- Karray — when a handful of signals grows into register files and structured arrays.