Chapter 8c — From Signal to Silicon

The Signal and Signal.reg you have been writing are not just Lean values — each one corresponds to a real piece of hardware. This chapter connects the abstraction to the physical element level: what a register, a clock, and a gate actually are, how a design's speed (static timing analysis) and size (gate count) are measured, and how the same design maps onto ASIC standard cells versus FPGA primitives (LUT4, DFF, BSRAM, DSP). It is the conceptual bridge between Ch 8 (Yosys netlists), Ch 8b (simulation), and Ch 9 (#verify_fpga + FPGA bring-up).

import Sparkle

open Sparkle.Core.Domain
open Sparkle.Core.Signal

namespace Notebooks.Ch08c

8c.1 The register is a D flip-flop

Every Signal.reg (and the lower-level Signal.register init d) becomes one D flip-flop (DFF) per stored bit. A DFF samples its D input on the rising clock edge and holds it on Q until the next edge:

DFF D Q clk Q(t+1) = D(t)

That defining behaviour is exactly the .val semantics of the register: the value at reset is init, and the value at cycle t+1 is the input sampled at cycle t. Both hold by rfl — the register literally is a DFF.

-- Q at reset is `init`.
example {dom : DomainConfig} (init : BitVec 8) (d : Signal dom (BitVec 8)) :
    (Signal.register init d).val 0 = init := rfl

-- Q at cycle t+1 is D sampled at cycle t.
example {dom : DomainConfig} (init : BitVec 8) (d : Signal dom (BitVec 8))
    (t : Nat) :
    (Signal.register init d).val (t + 1) = d.val t := rfl

The init is the DFF's reset value. On real hardware that reset is either synchronous (applied on a clock edge) or asynchronous (applied immediately) — a choice carried by the DomainConfig, not by the register itself.

8c.2 The clock and clock domains

There is no explicit clock wire in a Sparkle expression, and that is deliberate: the cycle index `t` is the clock. One rising edge advances t → t+1; every register samples its input at that edge.

fill="none" stroke="#333" stroke-width="1.5"/> <text x="10" y="62" text-anchor="end" fill="#555">clk</text> <g fill="#0366d6" text-anchor="middle"> <line x1="60" y1="30" x2="60" y2="95" stroke="#cbd5e0"/><text x="60" y="112">t=0</text> <line x1="140" y1="30" x2="140" y2="95" stroke="#cbd5e0"/><text x="140" y="112">t=1</text> <line x1="220" y1="30" x2="220" y2="95" stroke="#cbd5e0"/><text x="220" y="112">t=2</text> <line x1="300" y1="30" x2="300" y2="95" stroke="#cbd5e0"/><text x="300" y="112">t=3</text> </g> <text x="200" y="18" text-anchor="middle" fill="#555">each ↑ edge: every register samples, t advances by one</text> </svg>

A DomainConfig is a clock domain. A design with two domains has two independent clocks, and any signal crossing between them is a clock-domain crossing (CDC) that needs a synchroniser — the reason domains are tracked in the type. On the FPGA the clock is not free- running math: it comes from the board crystal, optionally through a PLL (the Tang Nano 20K's 27 MHz crystal → rPLL → 13.5 MHz in Ch 9 §9.4).

8c.3 Combinational logic is gates

Between any two registers sits a combinational cloud — pure logic with no state. Each Sparkle operator lowers to gates: &&& ||| ^^^ ~~~ to AND/OR/XOR/INV, Signal.mux to a 2:1 multiplexer, +/- to an adder built from those. The cloud has no clock and no memory: its output is a function of its inputs right now.

reg combinational cloud (gates) reg

8c.4 Static timing analysis (STA)

How fast can you clock the design? The critical path is the longest combinational path between two registers. The clock period must be longer than everything that path costs:

reg A logic depth reg B t_clk→q t_comb t_setup clock period ≥ t_clk→q + t_comb + t_setup ⇒ Fmax = 1 / period

Deeper logic between registers → longer t_comb → lower Fmax. The fix is pipelining: insert a register partway through the cloud, halving the depth (at the cost of one cycle of latency). Sparkle's cost model approximates the critical path with a depth metric, and #verify_fpga (Ch 9 §9.7) turns it into an Fmax_est ≈ 1 / (depth × picoSecPerUnit) — a "right order of magnitude" number, not a substitute for the vendor timing report, but enough to catch a design that clearly won't close timing before you run place-and-route.

8c.5 Gate count and area

Area is counted in cells. yosys stat (Ch 8) prints them after synthesis; for the Tang Nano 20K flow it reports LUT1..4, MUX2_LUT*, ALU, and DFF* counts. #verify_fpga estimates the same four pools (LUT4 / FF / BSRAM / DSP) straight off the IR, before synthesis — that is what its calibration against real Yosys numbers (Ch 9 §9.7) is for. FF count is exact (one flip-flop per register bit); LUT count is the estimate the optimiser-aware cost model produces.

8c.6 Mapping: ASIC vs FPGA

The same Signal netlist targets two very different fabrics. On an ASIC it is mapped to a standard-cell library; on an FPGA to a fixed set of configurable primitives. On the FPGA the basic logic element is a LUT4 feeding a DFF — a 4-input lookup table (which can implement any Boolean function of 4 inputs) with a flip-flop on its output:

LUT4 any 4-in fn 4 inputs DFF Q
Sparkle constructASIC (standard cells)FPGA (Gowin GW2A-18)
`&&& \\\^^^ ~~~, Signal.mux`NAND / NOR / INV / AOI cellsLUT4 — any ≤4-input function is one LUT4
+, - (arithmetic)adder cells + carry chaindedicated carry chain (ALU cells) + LUT4
* (multiply)synthesised Booth/Wallace tree, or a hard macroDSP block (18×18 multiplier)
Signal.reg (a register bit)DFF standard celldedicated DFF (one per LUT slice)
an array / memorySRAM compiler macroBSRAM (18 Kb block); tiny ones as LUT-RAM
the clockclock tree + PLLglobal clock net + PLL / rPLL
area unitgate-equivalents (NAND2) / µm²LUT4 + FF + BSRAM + DSP counts

So when #verify_fpga reports LUT 5966, FF 1032, BSRAM 0, DSP 0, it is literally counting how many of each of these fabric primitives your Signal design will occupy — the FPGA-side of this table, tallied without running the toolchain.

end Notebooks.Ch08c