3. RTL Contract
The first RTL target is a single Honeycomb cell. It is intentionally small: one
64-bit program counter, one 64-bit accumulator, eight 64-bit scalar registers,
four 256-entry local memories, 32-bit resident weights, 32-bit streamed
operands, and debug counters. The host loads memories through 16-bit local
indexes while the cell is halted, pulses start, and then observes halted,
pc, acc, cycles, and retired. Instruction, data, resident-weight,
stream, and debug-data accesses use the low eight bits of the presented local
address or program counter.
The four hot memories no longer live as bare inferred arrays inside the cell.
They sit behind a honeycomb_sram adapter submodule boundary: a synchronous-write,
combinational-read model with a single write port and one read port for the
program, weight, and stream memories, and two read ports for the scalar data
memory (concurrent load and debug read). The adapter is exactly the interface a
technology SRAM macro is bound onto during synthesis. The abstract read timing
is still combinational, so the simulation and golden behavior are unchanged from
the inferred-array cell.
This chapter fixes the external boundary used by the generated SystemVerilog.
The cell is generated from a Lean RTL DSL value, not a raw SystemVerilog
template. The execute path of that DSL value is proved to refine step; the
plain-text SystemVerilog file is the renderer output checked by CI.
namespace Honeycomb
inductive RtlDir where
| input
| output
deriving Repr, DecidableEq
structure RtlPort where
name : String
dir : RtlDir
width : Nat
deriving Repr, DecidableEq
def honeycombCellPorts : List RtlPort :=
[ { name := "clk", dir := .input, width := 1 },
{ name := "rst_n", dir := .input, width := 1 },
{ name := "start", dir := .input, width := 1 },
{ name := "instr_we", dir := .input, width := 1 },
{ name := "instr_addr", dir := .input, width := 16 },
{ name := "instr_wdata", dir := .input, width := 32 },
{ name := "data_we", dir := .input, width := 1 },
{ name := "data_addr", dir := .input, width := 16 },
{ name := "data_wdata", dir := .input, width := 64 },
{ name := "weight_we", dir := .input, width := 1 },
{ name := "weight_addr", dir := .input, width := 16 },
{ name := "weight_wdata", dir := .input, width := 32 },
{ name := "stream_we", dir := .input, width := 1 },
{ name := "stream_addr", dir := .input, width := 16 },
{ name := "stream_wdata", dir := .input, width := 32 },
{ name := "dbg_reg_sel", dir := .input, width := 3 },
{ name := "dbg_data_addr", dir := .input, width := 16 },
{ name := "halted", dir := .output, width := 1 },
{ name := "busy", dir := .output, width := 1 },
{ name := "pc", dir := .output, width := 64 },
{ name := "acc", dir := .output, width := 64 },
{ name := "wptr", dir := .output, width := 16 },
{ name := "sptr", dir := .output, width := 16 },
{ name := "cycles", dir := .output, width := 64 },
{ name := "retired", dir := .output, width := 64 },
{ name := "dbg_reg_value", dir := .output, width := 64 },
{ name := "dbg_data_value", dir := .output, width := 64 } ]
def rtlXlen : Nat := 64
def rtlElemBits : Nat := 32
def rtlAccBits : Nat := 64
def rtlAddrBits : Nat := 64
def rtlLocalAddrBits : Nat := 16
def rtlMemIndexBits : Nat := 8
def rtlMemDepth : Nat := 256
def rtlRegCount : Nat := 8
def rtlInstrBits : Nat := 32
def rtlOpcodeBits : Nat := 4
def rtlImmBits : Nat := 16
theorem rtlConfigMatchesDefault :
rtlXlen = defaultConfig.xlen
/\ rtlElemBits = defaultConfig.elemBits
/\ rtlAccBits = defaultConfig.accBits
/\ rtlAddrBits = defaultConfig.addrBits
/\ rtlLocalAddrBits = defaultConfig.localAddrBits
/\ rtlMemIndexBits = defaultConfig.memIndexBits
/\ rtlRegCount = defaultConfig.regCount := ⊢ rtlXlen = defaultConfig.xlen ∧
rtlElemBits = defaultConfig.elemBits ∧
rtlAccBits = defaultConfig.accBits ∧
rtlAddrBits = defaultConfig.addrBits ∧
rtlLocalAddrBits = defaultConfig.localAddrBits ∧
rtlMemIndexBits = defaultConfig.memIndexBits ∧ rtlRegCount = defaultConfig.regCount
All goals completed! 🐙
theorem rtl_memory_depth_matches_default :
rtlMemDepth = memDepth defaultConfig := ⊢ rtlMemDepth = memDepth defaultConfig
All goals completed! 🐙
The instruction word is fixed at thirty-two bits:
-
bits
[31:28]: opcode -
bits
[27:24]: destination register -
bits
[23:20]: first source register or branch-test register -
bits
[19:16]: second source register or store source register -
bits
[15:0]: signed immediate, branch offset, or pointer address
The branch target follows the architectural contract: when blz is taken, the
new PC is pc + off, not pc + 1 + off.
def rtlOpcode {cfg : Config} : Instr cfg -> Nat
| .mac _ _ => 0
| .kmac => 1
| .clracc => 2
| .movacc _ => 3
| .li _ _ => 4
| .ld _ _ => 5
| .st _ _ => 6
| .setwptr _ => 7
| .setsptr _ => 8
| .blz _ _ => 9
| .halt => 10
def rtlOpcodeMax : Nat := 10
theorem rtl_opcode_fits : rtlOpcodeMax < 2 ^ rtlOpcodeBits := ⊢ rtlOpcodeMax < 2 ^ rtlOpcodeBits
All goals completed! 🐙
theorem rtl_halt_opcode (cfg : Config) :
rtlOpcode (cfg := cfg) .halt = 10 := rfl
theorem rtl_kmac_opcode (cfg : Config) :
rtlOpcode (cfg := cfg) .kmac = 1 := rfl
end Honeycomb
The low-eight-bit physical index is part of this initial standard-cell target,
not an accidental truncation. The honeycomb_sram adapter submodules are the
macro-adapter contract: larger local memories and a fully technology-mapped
macro are later physical integration steps that reuse this same boundary.
A first OpenLane2 sky130 run was made before the adapter existed, against the bare inferred-array cell. It is kept as a recorded warning about that earlier boundary: because the cell memories were inferred arrays, Yosys lowered the program, scalar-data, resident-weight, stream, and register memories into registers and muxes. The run reached Yosys synthesis and DFF legalization with 0 Yosys check problems, 0 remaining memories, 318547 cells, and 40994 mapped sky130 flip-flops. The final ABC mapping did not complete within the bounded run, so the book records no pre-PNR timing number for that inferred-memory target.
That result motivated the adapter. A single generated unit could be simulated
and synthesized, but four 256-entry hot memories should not be implemented as
flip-flop arrays in a useful physical tile. The honeycomb_sram boundary now lets
synthesis bind a technology memory macro in place of those arrays; the abstract
cell keeps combinational-read timing, so a bound synchronous macro is a
synthesis-shape experiment rather than a cycle-faithful implementation of the
proved cell.