Quickstart
This page walks through a complete, minimal design: two registers updated in
sequence, compiled to Verilog. It is the tc1_seq_simple test case from the
Kathryn repository, trimmed down to the model and the build call.
The whole program:
from kathryn import *
class tc1_seq_simple(Module): @init def com_declare(self): self.x = reg(8, "x") self.y = reg(8, "y") self.simple_val = val(8, 48, "simple_val")
self.x.mark_output("my_x") self.y.mark_output("my_y")
@flow def my_flow(self): with seq(): self.x |= self.simple_val self.y |= self.x
def build(output_folder: str) -> None: reset() module = tc1_seq_simple() build_model(module) emit_verilog(output_folder)Let’s take it apart.
1. A design is a Module subclass
Section titled “1. A design is a Module subclass”class tc1_seq_simple(Module):Every piece of hardware in Kathryn lives inside a module. You describe one by
subclassing Module and tagging methods with two decorators:
@initmethods declare hardware. They run eagerly, when the module is instantiated, inside the module’s scope — so everyreg(...),wire(...), orval(...)you create there is attached to this module.@flowmethods describe behavior. They are deferred: instantiating the module only registers them, and they run later during the global build.
The two decorators split a module into a declare phase and a behavior phase:
flowchart TB
M["Module subclass<br/>(tc1_seq_simple)"] --> I["@init com_declare<br/>declare hardware"]
M --> F["@flow my_flow<br/>describe behavior"]
I --> IH["reg / wire / val<br/>(runs eagerly on instantiation)"]
F --> FD["seq / |= assignments<br/>(deferred to global build)"]
2. @init: declare the hardware
Section titled “2. @init: declare the hardware”@initdef com_declare(self): self.x = reg(8, "x") self.y = reg(8, "y") self.simple_val = val(8, 48, "simple_val")
self.x.mark_output("my_x") self.y.mark_output("my_y")Three components are declared:
reg(8, "x")andreg(8, "y")— two 8-bit clocked registers.val(8, 48, "simple_val")— an 8-bit constant with value 48.
The name argument is optional everywhere; Kathryn auto-generates names
(reg0, reg1, …) if you omit it. Explicit names just make the emitted
Verilog easier to read.
mark_output("my_x") promotes a signal to a module output port named my_x.
Without it, x would remain an internal register. See
Signals for the full catalog of components and I/O
marking.
3. @flow: describe behavior with a seq block
Section titled “3. @flow: describe behavior with a seq block”@flowdef my_flow(self): with seq(): self.x |= self.simple_val self.y |= self.xTwo things are happening here:
with seq():opens a sequential flow block. Statements inside it execute as consecutive steps, one per clock cycle — a small sequencer is built out of real state registers.|=is Kathryn’s clocked assignment operator: “on this step’s clock edge, load the right-hand side into the register.” (Wires use*=for combinational assignment instead — see Assignment.)
So the behavior is: on step 1, x latches 48; on step 2, y latches x.
4. Build and emit
Section titled “4. Build and emit”def build(output_folder: str) -> None: reset() module = tc1_seq_simple() build_model(module) emit_verilog(output_folder)reset()gives you a fresh model arena (useful in scripts and tests that build more than once per process).- Instantiating
tc1_seq_simple()runs its@initmethods and registers its@flowmethods. build_model(module)is the one-shot build: it setsmoduleas the top of the design, constructs every registered flow block, then runs the host build pass (sequencer state, update events, clock and master-reset wiring) over the whole module tree. It is equivalent toset_top(module); gen_flow(); build_flow().emit_verilog(output_folder)runs the Verilog backend. The output directory must already exist; one<name>.vis written per module, with the top module written totop.v(the file name is the optional second argument).
The four calls form the build pipeline, from a fresh arena to emitted Verilog:
flowchart LR
R["reset()<br/>fresh model arena"] --> N["tc1_seq_simple()<br/>run @init, register @flow"]
N --> B["build_model(module)<br/>set_top, gen_flow, build_flow"]
B --> E["emit_verilog(output_folder)<br/>write top.v per module"]
5. The emitted Verilog
Section titled “5. The emitted Verilog”Here is the actual top.v Kathryn emits for this design (abbreviated):
module MODULE_tc1_seq_simple0_0( output reg [7:0] my_x, output reg [7:0] my_y, input wire [0:0] clk, input wire [0:0] mrst);
// ---- REG declarations ----reg [7:0] REG_x_1;reg [7:0] REG_y_2; // ---- SR_ST declarations ----reg [0:0] SR_ST_start_ST_16;reg [0:0] SR_ST_seq_state_4_0_ST_36;reg [0:0] SR_ST_seq_state_4_1_ST_40; // ---- VAL declarations ----wire [7:0] VAL_simple_val_3 = 8'h30;
always @(posedge WIRE_clk_12) begin if (SR_ST_seq_state_4_0_ST_36) begin REG_x_1[7:0] <= VAL_simple_val_3[7:0]; endend
always @(posedge WIRE_clk_12) begin if (SR_ST_seq_state_4_1_ST_40) begin REG_y_2[7:0] <= REG_x_1[7:0]; endend
always @(posedge WIRE_clk_12) begin SR_ST_seq_state_4_1_ST_40[0:0] <= VAL_seq_state_4_1_ST_UNSET_39[0:0]; if (SR_ST_seq_state_4_0_ST_36) begin SR_ST_seq_state_4_1_ST_40[0:0] <= VAL_seq_state_4_1_ST_SET_38[0:0]; end if (WIRE_mrst_13) begin SR_ST_seq_state_4_1_ST_40[0:0] <= VAL_seq_state_4_1_ST_UNSET_39[0:0]; endendWhat to notice:
- Your components are there, by name.
REG_x_1,REG_y_2, andVAL_simple_val_3 = 8'h30(48 in hex) are exactly theregandvalobjects you declared — nothing was inferred or optimized away. - The
seqblock became state registers. TheSR_ST_seq_state_*registers are the sequencer: one state bit per step, each enabling its step’s assignment for one cycle and then handing off to the next. This is the “control flow is hardware” principle made visible. - Each
|=became a guarded always block.x’s update fires only whileseq_state_4_0is high;y’s only whileseq_state_4_1is high. clkandmrstports appeared automatically. Kathryn wires a clock and a master reset into every module. Assertingmrstsets the sequencer’sstartstate and holds the step states cleared; whenmrstdeasserts, the sequence launches and advances one step per clock.- Outputs are driven from the internals.
my_xandmy_yare combinationally driven fromREG_x_1andREG_y_2(inalways @(*)blocks further down the file).
6. Simulating it (optional)
Section titled “6. Simulating it (optional)”In the Kathryn repository, each test/model/tc*.py file pairs a model like
this one with a cocotb testbench that drives clk
and mrst and checks the outputs cycle by cycle. For this design the
testbench asserts mrst for two cycles, releases it, and then observes
my_x latch 48 one step before my_y does — matching the two-step seq
exactly.
Where next
Section titled “Where next”- Signals — everything you can declare in
@init. - Expressions — building logic with operators and slices.
- Assignment — the
|=/*=rules in full. - Seq & Par — sequential and parallel flow blocks in depth.
- Building & Emitting — the build
pipeline beyond
build_model.