An area‑optimized 8‑bit arithmetic unit for a single Sky130 Tiny Tapeout tile, driven over SPI and a parallel full‑duplex byte port from the demo‑board host.
This revision takes the original four‑page sketch and works it up into a build‑ready specification: a defined number format (bfloat16), a complete pin map, exact SPI and handshake timing at the chosen 40 MHz core / 10 MHz SCK operating point, a full instruction set, a host‑supplied control byte and FPU‑returned status byte, the datapath micro‑architecture, a clean control FSM, the CRC definition, an area budget, and a verification plan. Points where the original document was silent, ambiguous, or self‑contradictory are flagged inline as Design Decisions (a choice has been proposed — please confirm), Corrections (an error in an earlier revision was fixed), or Open Issues (still needs your input).
FPU‑130 is a small numeric coprocessor intended to be fabricated as a digital design on a Tiny Tapeout Sky130 shuttle. It receives operands and a command from a host (the demo board's RP2040, or any SPI controller), performs one arithmetic or logic operation, and returns the result. The whole design must fit the severe area budget of a Tiny Tapeout tile while still supporting multiply and divide, so the architecture trades latency for area: complex operations are iterative and take multiple clock cycles rather than being fully combinational.
This is an interface and micro‑architecture specification: it defines externally observable behavior (pins, protocols, instruction semantics, timing) and the internal block structure precisely enough to write RTL against. It does not include the Verilog source, the synthesis scripts, the physical floorplan, or the test‑bench code; those are downstream deliverables that must conform to this document.
| Term | Meaning |
|---|---|
| Tile | The unit of silicon area on Tiny Tapeout, ≈160×100 µm, ≈1000 logic gates. Multiple tiles can be purchased for one project. |
| Host | The controller driving FPU‑130 — normally the RP2040 on the demo board, scripted in MicroPython. |
| Frame | One fixed‑length unit of serial/parallel transfer (e.g. the 8‑bit instruction frame). |
| Burst | Mode in which operands arrive over the parallel port rather than over SPI. |
| Accumulate | Mode in which the previous result is used as the first operand. |
| op1 / op2 | First and second operand of a binary operation. |
Earlier revisions left several load‑bearing questions unanswered — most importantly what an operand actually is. These are the decisions now locked in; each is also explained where it is used.
uio[0]=CS uio[1]=MOSI uio[2]=MISO uio[3]=SCK. The four full‑duplex status signals take uio[4..7]. The dedicated input byte uses ui_in, the result byte uses uo_out. See §6.
0x3), validated first. The control byte and all operand bytes are then covered by a single CRC‑8/SMBUS (poly 0x07); the payload length is fixed by the opcode (and the acc bit), so no per‑operand CRC is needed to delimit anything. See §13.
DIV.
FPU‑130 targets a Tiny Tapeout Sky130 shuttle, mounted on the TT demo board and exercised by the on‑board RP2040. Tiny Tapeout exposes a fixed, standard interface to every project; the design must use exactly those pins.
The 24 user pins divide into three groups of eight: ui_in (input‑only), uo_out (output‑only), and uio (bidirectional, each pin's direction set by the matching bit of uio_oe). A global clk, an active‑low rst_n, and a tile‑enable ena are also provided. The host RP2040 can drive all of these and can read uo_out back, which is exactly the channel FPU‑130 uses to return results.
uio bus is configurable, but each individual uio wire has a single direction at a time, fixed by uio_oe. FPU‑130 assigns each uio pin a constant direction (e.g. MISO is always an output, MOSI always an input), so no wire is ever driven from both ends. The uio_oe mask is therefore a constant: 0x64 (outputs on bits 2, 5, 6 — see §6).
Every operand and result in FPU‑130 is a bfloat16 value: a 16‑bit floating‑point number with a 1‑bit sign, an 8‑bit exponent (bias 127, identical to fp32), and a 7‑bit mantissa with an implicit leading 1 for normal values. Because the fields match the top half of an IEEE‑754 fp32, a host produces a bf16 simply by truncating (or round‑to‑nearest‑even) the upper 16 bits of an fp32 — conversion is trivial and the dynamic range is the same as fp32 (~10±38).
A bf16 value does not fit in one byte, so every operand and result crosses the 8‑bit interface as two bytes. The 8‑bit exponent straddles the byte boundary: 7 of its bits sit in the high byte alongside the sign, and its least‑significant bit sits in the low byte alongside the mantissa. Bytes travel high byte first to stay consistent with MSB‑first SPI.
| Property | Value | Notes |
|---|---|---|
| Exponent bias | 127 | stored = actual + 127 (same as fp32) |
| Largest finite | ≈ 3.39 × 10³⁸ | S=0, E=11111110, M=1111111 |
| Smallest normal | ≈ 1.18 × 10⁻³⁸ | E=00000001, M=0000000 |
| Precision | ~2–3 decimal digits | 8-bit significand (7 stored + implicit 1) |
| Zero / Inf | ±0 · ±Inf | E=0 / E=255, M=0 (fp32-compatible) |
| NaN | E=255, M≠0 | fp32-compatible quiet/signalling encodings |
What it costs in the datapath. bf16's significand is 8 bits (7 stored + the implicit 1), so MUL is a full 8×8 significand multiply and DIV an 8‑bit iterative divide — there is no small‑mantissa shortcut. The exponent path is 8 bits wide and the aligner/normalizer work on 8‑bit significands. All of this is still sequenced through the one shared add/sub core (§12), just wider and a few more cycles than a minifloat would need.
| Property | Benefit |
|---|---|
| fp32-width exponent | Huge dynamic range; rarely overflows/underflows in practice — robust for the host's data. |
| Top 16 bits of fp32 | Host conversion is a truncation; no lookup tables or complex packing on either side. |
| Recognized ML format | Reference models and test vectors are widely available, simplifying §17 verification. |
| 8-bit significand | Multiply/divide stay 8‑bit — affordable iteratively, though larger than a 4-bit minifloat. |
FPU‑130 is five blocks: two transport handlers (SPI and the parallel I/O port), a register file, the iterative datapath, and a control unit that sequences everything. The redrawn block diagram below tightens up the Rev 0.1 sketch and shows the actual signal grouping on the pins.
| Block | Responsibility |
|---|---|
| SPI Handler | Clocks bits in/out on uio[3:0], deserializes 8‑bit frames, checks the 3‑bit (instruction) and 8‑bit (data) CRC, and serializes the result back on MISO. |
| I/O Handler | Runs the valid/ready handshake on uio[7:4], latches operand bytes from ui_in, and drives result bytes onto uo_out. |
| Register File | Holds operand A, operand B, and the accumulator (ACC) which doubles as the result register. |
| Datapath | One shared add/subtract core plus the sequencer logic that turns it into multiply, divide, negate, abs, and compare; includes the float normalizer. |
| Control Unit | The FSM (§13). Decodes the instruction frame, schedules operand fetches, drives the datapath cycle‑by‑cycle, and routes the result to the requested transport. |
FPU‑130 uses every group of the standard Tiny Tapeout interface. The dedicated input byte carries burst operands; the dedicated output byte carries results; the bidirectional bus carries SPI on its low nibble and the four flow‑control signals on its high nibble.
| Pin | Dir | Signal | Function |
|---|---|---|---|
| uio[7:0] — bidirectional, uio_oe = 0x64 | |||
| uio[0] | in | spi_cs_n | SPI chip select, active low. |
| uio[1] | in | spi_mosi | SPI host→FPU data (command & operands). |
| uio[2] | out | spi_miso | SPI FPU→host data (result read‑back). |
| uio[3] | in | spi_sck | SPI clock, Mode 0. |
| uio[4] | in | in_valid | Host asserts: input byte on ui_in is valid. |
| uio[5] | out | in_ready | FPU asserts: ready to accept an input byte. |
| uio[6] | out | out_valid | FPU asserts: result byte on uo_out is valid. |
| uio[7] | in | out_ready | Host asserts: ready to accept a result byte. |
| Dedicated buses | |||
| ui_in[7:0] | in | byte_in | Parallel operand byte (burst mode). |
| uo_out[7:0] | out | byte_out | Parallel result byte. |
| Housekeeping (provided by Tiny Tapeout) | |||
| clk | in | clk | Synchronous board clock; clocks the whole core. |
| rst_n | in | rst_n | Active‑low synchronous reset. |
| ena | in | ena | Tile enable; high when this design is selected. |
Spi_mosi appears twice. The fourth pin is the FPU‑to‑host line, spi_miso. Corrected above.
uio_oe is a constant, MISO is always an output even when spi_cs_n is high. It will drive 0 while deselected. If you want true tri‑state on a shared SPI bus, MISO's uio_oe bit must be driven dynamically from ~spi_cs_n — a small change, but it makes the OE mask non‑constant. Confirm which you want.
FPU‑130 is an SPI peripheral (slave). The host owns spi_sck and spi_cs_n. The link runs in Mode 0: clock idles low (CPOL=0) and data is sampled on the rising (leading) edge (CPHA=0). All frames are 8 bits, MSB‑first.
A transaction begins when the host pulls spi_cs_n low and clocks an instruction byte (its own CRC‑3 is validated first, which fixes how many bytes follow). When operand data travels over SPI (burst = 0), a control byte and the operand bytes follow, all covered by a single trailing CRC‑8. Each bf16 operand is two bytes (high byte first).
SPI is full‑duplex at the wire level: every byte the host clocks out on MOSI simultaneously clocks a byte in on MISO. While the FPU is busy it returns the status byte (§11) with BUSY=1, so the host polls; once BUSY clears, a read returns the two result bytes (high byte first). Each transaction's status byte echoes the control byte's TAG field for request/response correlation.
The core clocks at 40 MHz; SPI SCK runs at 10 MHz. FPU‑130 is an oversampled SPI peripheral: SCK is never used as a clock. The SPI inputs (spi_sck, spi_cs_n, spi_mosi) pass through 2‑flop synchronizers into the core domain; a one‑cycle edge‑detect pulse off the synchronized SCK enables MOSI sampling (rising edge) and MISO update (falling edge).
| Parameter | Value | Notes |
|---|---|---|
| Core clock | 40 MHz (25 ns) | clocks the entire design |
| SCK | 10 MHz (100 ns) | host-driven; sampled, not used as a clock |
| Oversampling ratio | 4× | minimum safe ratio; 2 core clocks per SCK phase |
| Max SCK | board ÷ 4 = 10 MHz | at the ceiling — no headroom without a faster core |
| Input sync latency | 2 core clocks (+1 edge-detect) | consistent, so MOSI sampling stays aligned |
For streaming workloads, operands can be pushed in over the dedicated 8‑bit input bus and results pulled out over the dedicated 8‑bit output bus, each governed by an independent valid/ready handshake. This is the "burst" path (instruction bit 7 = 1). It is faster than SPI because a whole byte moves per clock instead of one bit per SCK edge.
A byte transfers on the rising board‑clock edge where both valid and ready are high. Either side may assert its signal first; neither waits on the other to begin asserting. This is the standard AXI‑Stream‑style contract and is deadlock‑free as long as valid never waits for ready.
valid while the receiver asserts ready; the transfer happens on the cycle both are high. Steps corrected below.
ready when it can accept a byte.valid and drives the byte onto the bus. (Steps 1–2 may occur in either order.)valid & ready both high, the byte is transferred.A burst carries the same CRC‑protected payload as SPI, just moved over the parallel port one byte per handshake: control, then each bf16 operand as two bytes (high byte first), then the single CRC‑8 over the whole payload. Each operand therefore takes two in_valid/in_ready transfers. Results stream back over uo_out as two bytes (high first) with the FPU as sender (out_valid) and the host as receiver (out_ready); the status byte is available on the same path. The instruction byte and its CRC‑3 are always delivered over SPI first — burst only relocates the operand data.
The 3‑bit opcode selects one of eight operations, completed with exact semantics, operand count, and result for the bfloat16 format of §4. Operands are a = op1 (or ACC in accumulate mode) and b = op2; rounding follows the RND field of the control byte (§11).
| Opcode | Mnemonic | Operands | Result | Notes |
|---|---|---|---|---|
| 000 | ADD | a, b | a + b | float add; align exponents, add, normalize, round‑to‑nearest‑even. |
| 001 | SUB | a, b | a − b | add with b's sign flipped. |
| 010 | MUL | a, b | a × b | add exponents, multiply significands (4×4), normalize. |
| 011 | DIV | a, b | a ÷ b | subtract exponents, iterative significand divide; b=0 → NaN. |
| 100 | NEG | a | −a | unary; flip sign bit only. |
| 101 | ABS | a | |a| | unary; clear sign bit. |
| 110 | SLT | a, b | (a < b) ? 1.0 : 0.0 | set‑less‑than; returns a float 1.0 or 0.0 flag. |
| 111 | NOP | — | ACC unchanged | no operands, no write; used for status read‑back / padding. |
SLT returns bfloat16 1.0 (0x3F80) for true and +0.0 (0x0000) for false, so the flag can feed straight back into arithmetic in accumulate mode.
Each transaction carries a host‑supplied control byte as the first byte of the CRC‑protected payload (§7.1), and the FPU returns a status byte on read‑back. The two are symmetric and share a 2‑bit TAG that the FPU echoes, letting the host match each result to its request in a pipelined or burst stream.
| Field | Dir | Meaning |
|---|---|---|
| RND[1:0] | in | Rounding mode: 00 = round‑nearest‑even, 01 = toward zero, 10 = toward +∞, 11 = toward −∞. |
| CLRA | in | Clear ACC to +0.0 before this op (start a fresh accumulate chain). |
| CLRF | in | Clear sticky exception flags before this op. |
| SAT | in | On overflow, saturate to max‑finite instead of producing ±Inf. |
| TAG[1:0] | in/out | Host‑chosen transaction tag; echoed in the status byte. |
| BUSY | out | Computation in progress; result not yet valid. |
| NAN / OVF / UNF | out | Result is NaN / overflowed / underflowed. |
| INX | out | Inexact — rounding occurred (sticky until CLRF). |
| ERR | out | Last CRC check failed; the frame was dropped. |
NOP, where NOP + CLRA or NOP + CLRF becomes a useful control‑only operation, and a bare NOP is a status read).
The two mode bits in the instruction frame are orthogonal and can be combined freely.
burst = 0: operands and result travel over SPI (data frames on MOSI, read‑back on MISO). burst = 1: operands arrive over the parallel ui_in port and the result leaves over uo_out, each under valid/ready flow control (§8). The instruction frame itself is always delivered over SPI regardless of burst — burst only changes where the operand data flows.
acc = 0: both operands come from the incoming frames. acc = 1: the first operand is taken from the ACC register (the previous result), and only op2 is supplied. This lets the host chain operations — e.g. a running sum — without re‑transmitting the accumulator each time.
| burst | acc | Operand source | Result destination |
|---|---|---|---|
| 0 | 0 | op1 & op2 over SPI | SPI MISO read‑back |
| 0 | 1 | op1 = ACC; op2 over SPI | SPI MISO read‑back |
| 1 | 0 | op1 & op2 over ui_in | uo_out stream |
| 1 | 1 | op1 = ACC; op2 over ui_in | uo_out stream |
FPU‑130 uses two layered CRC scopes, both computed MSB‑first with a small LFSR. The instruction byte is protected on its own and validated first — that is what tells the receiver the payload length, so a single CRC over the rest is unambiguously placed.
| Field | Covers | Standard | Poly | Init |
|---|---|---|---|---|
| Instruction CRC‑3 | burst·acc·opcode (5 bits) | CRC‑3/GSM | 0x3 | 0x0 |
| Payload CRC‑8 | control byte + all operand bytes | CRC‑8/SMBUS | 0x07 | 0x00 |
The payload CRC‑8 spans the control byte plus (arity − acc) × 2 operand bytes — for a binary op that is the control byte and four operand bytes under one CRC, versus the two separate CRCs an earlier draft would have used. The LFSR is reset at the start of the payload, clocked through every payload byte, and checked once against the trailing byte. By CRC linearity an 8‑bit CRC over the whole block keeps the same ~1/256 random false‑accept and still catches errors that straddle the op1/op2 boundary; the only thing given up is per‑operand fault localization, which costs nothing because recovery resends the whole frame anyway.
On any mismatch the FPU sets the ERR status bit and drops the frame rather than computing on corrupt data; the host re‑sends. Both CRCs together are a few XOR gates and two short shift registers — negligible area for the integrity.
This is the only section that depends on the number‑format choice. The datapath is deliberately narrow and reused: one shared add/subtract core does the heavy lifting for every instruction, sequenced over several clocks by the control unit (Decision D2). For bfloat16 the core and the unpacked operands are 8‑bit significands (7 stored + the implicit 1) with an 8‑bit exponent path; MUL is a full 8×8 significand multiply and DIV an 8‑bit iterative divide, both shift‑sequenced through the one core.
| Op | Mechanism | ≈ cycles |
|---|---|---|
| ADD / SUB | Compare exponents, shift the smaller significand to align, add/subtract in the core, normalize, round. | 3–4 |
| MUL | Add exponents (exponent unit); multiply 8‑bit significands by shift‑add through the core; normalize, round. | 6–8 |
| DIV | Subtract exponents; non‑restoring division of significands by shift‑subtract through the core; normalize, round. | 6–10 |
| NEG / ABS | Pure bit manipulation of the sign field; no core pass. | 1 |
| SLT | Sign check, then compare exponent/mantissa magnitudes; emit 1.0 / 0.0. | 1–2 |
Latency is variable, which is exactly why the FPU exposes a busy status: the host either polls the status byte or watches out_valid, rather than assuming a fixed delay.
The Rev 0.1 state diagram was a dense, partly‑overlapping sketch. Redrawn as a single clean flow, the control unit is a straightforward request→decode→fetch→execute→writeback→return loop. The same FSM serves both transports; only the fetch and return states branch on the burst bit.
| State | Action | Exit condition |
|---|---|---|
| RESET | Clear ACC, registers, status; deassert all outputs. | rst_n high → IDLE |
| IDLE | Wait for spi_cs_n to fall. | CS asserted → RX_INSTR |
| RX_INSTR | Shift 8 SPI bits; verify CRC‑3. | CRC ok → DECODE; CRC bad → set err, IDLE |
| DECODE | Latch burst/acc/opcode; compute payload length = 1 (control) + (arity − acc) × 2 operand bytes. | → RX PAYLOAD (every op has at least a control byte) |
| RX PAYLOAD | Clock in the control byte, then each bf16 operand as two bytes (high first) over the selected transport, accumulating one CRC‑8; apply CLRA/CLRF from the control byte. | CRC ok → EXECUTE; CRC bad → set ERR, IDLE |
| EXECUTE | Assert BUSY; run the datapath for the op's cycle count with the selected rounding mode. | datapath done → WRITEBACK |
| WRITEBACK | Store 16‑bit result into ACC; update status (NAN/OVF/UNF/INX), echo TAG. | → RETURN |
| RETURN | Present the two result bytes + status on MISO (poll‑read) or stream on uo_out via out_valid. | host consumed → IDLE |
Two end‑to‑end examples make the protocol concrete.
spi_cs_n low.burst=0, acc=0, opcode=010 (MUL), CRC3.control · op1_hi · op1_lo · op2_hi · op2_lo · CRC8 (one CRC over all five data bytes).BUSY, runs the multiply (≈6–8 cycles), writes the 16‑bit ACC, sets any NAN/OVF/INX flags.BUSY=1) until done, then the two result bytes (high first); the status byte's TAG echoes the control byte's TAG.spi_cs_n.burst=1, acc=1, opcode=000 (ADD), then the control byte (e.g. CLRA=1 on the first to zero ACC).ui_in + raises in_valid; FPU raises in_ready; the two operand bytes (high first) transfer, then the CRC‑8 byte; FPU checks CRC.ACC ← ACC + op2 per value (running bf16 sum), updating ACC in place.out_ready; FPU raises out_valid and streams the two ACC bytes (high first) plus status.cs_n and re‑issues. Decide whether the control byte repeats per value or is sent once when arming.
A first‑order gate estimate (2‑input NAND equivalents, before place‑and‑route overhead). These are planning numbers, not synthesis results; routing and tap/antenna cells typically add 30–50%, which — with the bf16 datapath and 16‑bit registers — puts the design clearly into a 2‑tile budget.
| Block | Est. gates | Dominated by |
|---|---|---|
| Datapath (bf16) | ~480 | 8‑bit add/sub core, 8×8 iterative mul, 8‑bit exponent path, normalizer |
| Register file | ~150 | A, B, ACC (3 × 16 bits) |
| Control FSM | ~140 | state register, decode, payload/byte counters, control-byte handling |
| SPI handler | ~120 | byte receiver, bit pointer, MISO mux |
| I/O handler | ~100 | handshake logic, 2‑byte operand assembly |
| Synchronizers | ~50 | SPI CDC flops + edge detect |
| CRC + status/control | ~70 | CRC‑3 + CRC‑8 LFSRs, control decode, status flags |
| Subtotal | ~1110 | before place & route |
DIV (removes the divide sequencer); a deeper cut would be a narrower float, which shrinks the registers and the significand multiply.
clk at 40 MHz (25 ns). SPI signals are sampled into this domain through 2‑flop synchronizers at 4× oversampling (SCK = 10 MHz); the design never uses spi_sck as a true clock. This keeps the whole core synchronous and synthesis‑friendly.rst_n is active‑low and applied synchronously (the board clock free‑runs, so a sync reset is glitch‑immune and clean for STA/DFT). On reset: ACC = +0.0, A/B cleared, status cleared, all output enables driven to their idle values, FSM → IDLE.ena is low the tile is not selected; outputs hold their reset/idle values.uo_out / uio_out / uio_oe bit is tied to a defined value (0). FPU‑130 uses all output pins, but any reserved bits default to 0.clk = 40 MHz, SCK = 10 MHz, 4× oversampling. The worst‑case path is expected to be the normalizer's leading‑one detect over the 8‑bit bf16 significand — a short path that should close comfortably at 25 ns, but it must be confirmed against actual synthesis. SCK is capped at clk ÷ 4 = 10 MHz (§7.3).
Tiny Tapeout supports cocotb test benches that can later be replayed against the real chip via the demo board. The plan:
| Level | What it checks |
|---|---|
| Unit — datapath | Each op (ADD/SUB/MUL/DIV/NEG/ABS/SLT) against a golden software bfloat16 reference across a directed corner set (±0, ±Inf, NaN, max finite, smallest normal, div‑by‑0) plus randomized vectors; exercise all four rounding modes. |
| Unit — CRC | CRC‑3/CRC‑8 against published reference vectors; deliberate bit‑flips must raise err. |
| Protocol — SPI | Mode‑0 framing, instruction+data sequencing, busy polling, read‑back; bad‑CRC frames dropped. |
| Protocol — burst | valid/ready handshake including back‑to‑back transfer, stalls (ready low), and result streaming. |
| Mode matrix | All four burst×acc combinations from §10. |
| Integration | Full transactions from §14, then re‑run as hardware‑in‑the‑loop on the demo board. |
Consolidated list of everything flagged above that needs your sign‑off before RTL starts:
| ID | Decision | Status / recommendation |
|---|---|---|
| D1 | Number format | Resolved: bfloat16 (1·8·7). Drop to a narrower float only if area forces it. |
| D2 | Iterative vs combinational datapath | Resolved: iterative, shared add/sub core. |
| D5 | Tile count | Resolved: 2 tiles (2×2). Confirm against live TT pricing. |
| D6 | Operating point | Resolved: 40 MHz core / 10 MHz SCK (4× oversampling). |
| D7 | Control byte / status byte | Resolved (confirm bits): control byte in (RND/CLRA/CLRF/SAT/TAG), status byte out (BUSY/NAN/OVF/UNF/INX/ERR/TAG). |
| O2 | MISO tri‑state vs always‑driven | Open — recommend driving 0 when deselected (constant OE). |
| O4 | Burst stream framing | Open — recommend one instruction arms the mode until CS toggles; decide if control byte repeats per value. |
| O5 | Confirm current TT pricing / 2‑tile cost | Open — check the live TT pricing page. |
Resolved since Rev 0.2: the format (D1 → bf16), the single operand‑region CRC (D4), the operating point and oversampling margin (was O3, now D6), and the exception/status reporting (was O1, now D7 + §9.1).