Tamal: Everything, Wired

The pads exist now, and they are wired to nothing.

That is the odd position the series has arrived in. We have read a CRC unit, a baud generator, a transmitter, a receiver and the umbrella over them; a COBS codec and the loader that streams it; two block RAMs; an engine, in eight instalments, from its eight-phase map down to the two bits of a lane; two pure format models for the bytes that go in and the words that come out; and, last week, the tri-state buffers that turn a (value, enable) pair into a pin that floats. Every piece is open. Not one of them is connected to another.

Tamal.Top connects them. It is a hundred and fifteen lines, and it contains exactly one function that matters — system, which takes a serial line and some sampled bits and hands back a serial line, a bus drive and a blinking LED — and four small pure helpers that exist so the parts of the wiring that could have been logic are not hidden inside it. Reading it is less like reading a module and more like reading a table of contents, because every import is a post.

Six names, and the series behind them

module Tamal.Top
  ( system
  , stepM
  , ringWrite
  , RigState (..)
  , rigState
  , ledPattern
  ) where

import Clash.Prelude

import Tamal.Bus.Serdes (Lanes)
import Tamal.Engine (BusIn (..), BusOut (..), Ring (..), State, initState, step)
import Tamal.Loader (LoaderIn (..), LoaderOut (..), loader)
import Tamal.Mem (instrRam, ringRam)
import Tamal.Params (RW)
import Tamal.Uart (uart)

Six exports, and the asymmetry between them is the design. One of the six is the machine. The other five are pure functions the machine happens to use, hoisted to the door so they can be tested without a clock: stepM, an adapter; ringWrite, a projection; and RigState with rigState and ledPattern, a three-state enum and the two total functions over it that decide how an LED blinks. Nothing in that list computes anything the design needs at speed. They are there because a wiring module that quietly grew logic inside it would be a wiring module you could not check, and the author would rather export four small things than hide them.

The imports are the more striking half. Six modules, and every one of them has a post: Serdes for the Lanes type, Engine for the state machine and the four record types that plug into it, Loader for the frame FSM, Mem for the two block RAMs, Params for the RW address width, Uart for the serial umbrella. The shape post opened with eight closed boxes and called that the exact inversion of the leaves-first UART — a map drawn before any of its territories had been walked. This import list is the same map with every box now open, and it is the first module in the project whose header you cannot read without having read the project.

The moment the engine gets its clock

The engine’s keystone type has been sitting still since the shape post:

step :: State -> BusIn -> (State, BusOut, Maybe Ring)

A pure function. Give it a state and this cycle’s inputs; get back the next state, the pins to drive, and maybe a trace word. Eight posts have described what it computes, and in all eight it has never once run in time — it is a transition, not a machine, and a transition needs something to iterate it.

That something is mealy, which the primer introduced and the transmitter first put at the top of a module. Its type wants a transition of a particular shape:

mealy :: (HiddenClockResetEnable dom, NFDataX s) =>
  (s -> i -> (s, o)) -> s -> Signal dom i -> Signal dom o

A pair. State and output, two elements. step returns three, because the shape post argued at length that splitting the output into pins and trace-word was worth doing — BusOut is what the wires carry right now and Maybe Ring is a thing to remember, and conflating them would have been a smaller type and a worse one. So the two shapes do not quite meet, and three lines bridge them:

stepM :: State -> BusIn -> (State, (BusOut, Maybe Ring))
stepM s i = (s', (bo, mr))
 where
  (s', bo, mr) = step s i

One pair of parentheses moved. (State, BusOut, Maybe Ring) becomes (State, (BusOut, Maybe Ring)), and now the second element is a single o and mealy will take it. That is the entire function, and it is worth stopping on precisely because it is nothing, because the line that uses it is the most consequential in the file:

(busOut, maybeRing) = unbundle (mealy stepM initState busInS)

Read that slowly. mealy stepM initState takes the pure transition and gives back a function from a Signal of inputs to a Signal of outputs: it allocates the register that holds State, initialises it to initState, and arranges for the transition to be applied once per clock edge for the rest of the design’s life. Eight posts of arithmetic acquire a clock in one application. unbundle then splits the paired output back into two signals, so the rest of the module can use busOut and maybeRing separately — the pairing existed only to satisfy mealy, and it is undone the moment it has.

step describes what happens next. mealy stepM initState is what makes next happen.

The loader was called the first block that owns a clock, and it earned the phrase: it had a mealy at its top and eighteen fields of state turning underneath. The engine is bigger than the loader by every measure — seventeen state fields, eight phases, thirty-six opcodes — and it does not own a clock at all. It never did. It borrows one here, in the top, on one line, and that is the whole reason the shape post’s property tests could hammer step with random inputs and no simulation: a function you can call is a function you can test a hundred thousand times a second.

stepM gets a test of its own, and the test is a statement that the adapter is not a place where behaviour can hide:

testProperty "stepM = step re-associated" $ H.property $ do
  i <- H.forAll genBusIn
  let (s', bo, mr) = step initState i
  stepM initState i H.=== (s', (bo, mr))

For a random BusIn — random instruction word, four random sampled IO bits, a random ALERT#, a random start flag — stepM must return exactly what step returned, only re-parenthesised. It looks like testing that a tuple is a tuple. It is really a guard on a seam: stepM is the last thing that touches the engine’s output before the clock does, and a swapped element or a dropped Maybe here would be invisible in every engine test and fatal on the board.

No BiSignal, on purpose

Now the signature that the whole module is arranged around:

system ::
  (HiddenClockResetEnable dom) =>
  Signal dom Bit ->            -- uart RX line
  Signal dom (Vec 4 Bit) ->    -- ioIn
  Signal dom Bit ->            -- alertIn
  ( Signal dom Bit             -- uart TX line
  , Signal dom Lanes           -- lanesOut
  , Signal dom Bit             -- csOut
  , Signal dom Bit             -- sckOut
  , Signal dom Bit             -- rstOut
  , Signal dom Bit             -- led
  )

Three in, six out, and not a BiSignal anywhere. The module’s own doc comment says why in half a sentence — no BiSignal, so the whole integration is cosim-testable — and the previous post is the long version of that half-sentence.

Recall the shape of the trouble. A bidirectional net’s value is a function of every driver attached to it, so a driver derived from a read of the same net is a value defined in terms of itself, and Clash’s simulator does not resolve it — it diverges. The pad post’s test harnesses had to be split into two single-driver directions to say anything at all about four lanes and one synchroniser. That is a tolerable amount of ceremony for a hundred and four lines. It would be an intolerable amount for a test that wants to serialise a program onto a UART, watch a loader parse it, watch an engine execute it, and read the result back out.

So the boundary is drawn to keep the hard part small. system speaks entirely in ordinary unidirectional Signals: ioIn arrives already sampled, four plain Bits, and lanesOut leaves as a plain Lanes, four (value, enable) pairs that are still just numbers. Everything bidirectional lives outside, in espiPads, and the board shell is what puts the two together. The result is a partition with a pleasing property: the part of the design that cannot be simulated conveniently is a hundred and four lines with nine tests, and the part that is the actual machine — fifteen hundred lines of UART, loader, memories and engine — can be driven end to end with a list of Bits.

Draw the boundary where the tooling gets hard, not where the diagram looks tidy.

There is a second thing hiding in that signature, quieter than the first: it is generic in dom. system does not name a clock frequency, a board, or a domain. It says HiddenClockResetEnable dom and lets whoever instantiates it supply the clock — which is what makes one system serve two board shells, and what leaves the whole question of which silicon for the post after this one.

The confluence

The body is thirty lines, and it is the entire design:

system rxLine ioIn alertIn = (txLine, lanesO, csO, sckO, rstO, ledOut)
 where
  -- UART @ 2MBaud
  (rxByte, _rxErr, txLine, txReady) = uart (SNat @2_000_000) rxLine txByteL

  -- Loader FSM
  lOut = loader (LoaderIn <$> rxByte <*> txReady <*> halted <*> ringPtrO <*> ringData)
  txByteL = txByte <$> lOut
  instrWrL = instrWr <$> lOut
  ringAddrL = ringAddr <$> lOut
  startO = startOut <$> lOut

  -- Memories
  instrWord = instrRam pcO instrWrL
  ringData = ringRam ringAddrL (ringWrite <$> maybeRing)

  -- Engine
  (busOut, maybeRing) = unbundle (mealy stepM initState busInS)
  busInS = BusIn <$> instrWord <*> ioIn <*> alertIn <*> startO
  pcO = pcOut <$> busOut
  lanesO = lanesOut <$> busOut

Four comments, four stages, and the four stages are the series in order.

The UART comes first, and it is the umbrella read whole: uart (SNat @2_000_000) rxLine txByteL returns four things, and the design uses three of them. rxByte is a Maybe (BitVector 8) strobing once per received byte, txLine is the outgoing wire, txReady is the transmitter’s back-pressure. The baud rate is passed as a type-level number in the SNat the umbrella demanded, and everything the baud generator did with a fractional 3.125 follows from it and from whatever dom turns out to be.

And _rxErr is dropped. The UART top post noted that the framing error had no consumer yet and left it unwired, and here at the very top — the last place it could have found one — it still has none. That underscore is the project being honest about an unfinished edge rather than inventing a use for it.

The loader is next, and its input is built by applying a constructor across signals:

lOut = loader (LoaderIn <$> rxByte <*> txReady <*> halted <*> ringPtrO <*> ringData)

LoaderIn is an ordinary five-field record. <$> and <*> lift its constructor over Signal, so what comes out is a Signal dom LoaderIn — one record per cycle, its five fields taken from five separate signals at the same instant.[1] The four outputs come back the same way, each a field projection mapped over lOut: txByte <$> lOut for the byte to transmit, instrWr <$> lOut for a write into the instruction memory, ringAddr <$> lOut for the drain’s read address, startOut <$> lOut for the trigger. The loader’s three lives — receive a frame, hold it, drain the ring — are all behind that one call.

The memories are two lines, and both are the mem post’s four-line leaf instantiated:

instrWord = instrRam pcO instrWrL
ringData = ringRam ringAddrL (ringWrite <$> maybeRing)

instrRam is read by the engine’s program counter and written by the loader. ringRam is read by the loader’s drain and written by the engine’s trace emitter. Each memory has exactly one reader and one writer, and in both cases they are different modules pointing in opposite directions — which is the shape a rig wants: the host writes programs and reads results; the engine reads programs and writes results.

And the engine closes it. busInS assembles a BusIn from four sources — instrWord from the instruction memory, ioIn and alertIn from the caller (and thence from the pads), startO from the loader — and mealy stepM initState runs it. busOut is then fanned out one field at a time: pcOut back to the memory, lanesOut/csOut/sckOut/rstOut out to the caller, haltedOut to the loader and the LED, ringPtrOut to the loader so the drain knows how far to sweep.

Three of those wires are loops.

The whole Tamal design as system wires it, with its three feedback loopsA dashed boundary labelled system encloses six blocks. Along the top row, left to right: uart, loader, instrRam, and an accented block labelled mealy stepM initState. The rxLine enters uart from outside the boundary and txLine leaves it. Between uart and loader, rxByte runs right and txByte runs left. The loader sends instrWr to instrRam, which sends instrWord to the engine. A feedback wire labelled pcO runs from the engine's top, left along a corridor above the row, and down into instrRam. Below, a ringRam block sits under the loader, exchanging ringAddr downward and ringData upward with it; a wire labelled ringWrite runs from the engine's bottom, left, and down into ringRam's right side. A ledPattern block sits below the engine, fed by a wire labelled halted, and drives led out through the boundary. On the right, lanesOut and the cs, sck and rst sidebands leave the engine through the boundary, and ioIn with alertIn enter it.systemuartloaderinstrRammealy stepMinitStateringRamledPatternrxLinetxLinerxBytetxByteinstrWrinstrWordpcOringWriteringAddrringDatahaltedledlanesOutcs, sck, rstioIn, alertIn
The whole design, as system wires it. The dashed boundary is the signature: everything inside speaks in ordinary unidirectional Signals, and the four bidirectional IO lanes are somebody else's problem — ioIn arrives already sampled and lanesOut leaves as four (value, enable) pairs. The three accented wires are the feedback loops: pcO into the instruction memory whose answer comes back a cycle later, ringWrite into the trace memory, and ringData back out of it into the loader's drain. Each of the three is a definition that mentions a name defined further down the where block, and each is legal because these are nets, not values.

Those loops are the reason this module reads strangely on a first pass. pcO is defined from busOut, which comes from busInS, which contains instrWord, which comes from instrRam pcOpcO appears on both sides. ringData feeds the loader, whose lOut produces ringAddrL, which the memory needs to produce ringData. halted is read by the loader four lines above the line that defines it.

None of that is a problem, and the UART top post already explained why: Clash elaborates, it does not evaluate. A where-bound name in a hardware description is the name of a net, and a netlist has no notion of “before”. Writing instrWord = instrRam pcO instrWrL above the line that defines pcO states that the memory’s address port is connected to the engine’s pcOut port, which is a fact about wires and is as true read upwards as downwards. What would be a problem is a loop with no register in it, and there is none: the instruction memory’s one-cycle read latency breaks the fetch loop, the engine’s own state register breaks the trace loop, and the Fetch phase exists precisely to spend the cycle that latency costs.[2]

One line that keeps a promise

The projection between the engine and the trace memory is a single line, and it is the smallest thing in the file with an argument behind it:

ringWrite :: Maybe Ring -> Maybe (Unsigned RW, BitVector 32)
ringWrite = fmap (\(Ring a d) -> (a, d))

The engine emits Maybe Ring, a record with named fields rAddr and rData. The memory’s write port takes Maybe (Unsigned RW, BitVector 32), a bare tuple. ringWrite unwraps one into the other, and fmap carries it through the Maybe so a cycle with nothing to write stays a cycle with nothing to write.

The mem post spent a section on why the write port is a bare tuple, and gave the reason as an aphorism: a memory that imports the engine is a memory that knows what a trace record is. ringRam is deliberately ignorant. It stores thirty-two-bit words at addresses and has never heard of Ring, or Capture, or the two-bit tag that tells a host which record shape follows. Keeping it that way requires somebody, somewhere, to do the unwrapping — and this is the somebody. The adapter lives in the top, which is the one module that is supposed to know about everything, and so the ignorance the memory was designed to have is paid for in one line by the module that can afford it.

Its tests are two, and they are exactly the two cases: ringWrite Nothing is Nothing, and ringWrite (Just (Ring a d)) is Just (a, d) for random a and d. Trivial — and the sort of trivial that catches a swapped pair.

A status LED, made a truth table

The last eight lines are the user interface, and they are the only part of the design a person looks at directly:

data RigState = Waiting | Running | Done

rigState :: Bool -> Bool -> RigState
rigState _ True = Done
rigState True False = Running
rigState False False = Waiting

ledPattern :: RigState -> Unsigned 26 -> Bit
ledPattern Waiting c = msb c
ledPattern Running c = msb (c `shiftL` 3)
ledPattern Done _ = high

And in system, the three lines that give them time:

running = register False (mux startO (pure True) (mux halted (pure False) running))
ledCnt = register (0 :: Unsigned 26) (ledCnt + 1)
ledOut = ledPattern <$> (rigState <$> running <*> halted) <*> ledCnt

Split that in half and the split is the point. The stateful part is two registers: a one-bit running latch that sets on the loader’s trigger and clears when the engine halts, and a free-running twenty-six-bit counter that does nothing but increment forever. The decision part is two pure total functions, exported and tested, that never touch a clock.

rigState is a truth table with three rows and a wildcard, and the wildcard is a priority: halted wins. A rig that has run and stopped shows Done whether or not the latch is still set, so the terminal state is genuinely terminal. Its tests are the truth table, written out — both halted cases, the running case, the idle case — which is what a three-line function deserves and rarely gets.

ledPattern is the blink, and it says a great deal in three lines. msb c is the top bit of the counter, which at 100 MHz toggles every 2²⁵ cycles: about a third of a second on and a third off, a slow, patient pulse.[3] msb (c `shiftL` 3) reads bit 22 instead — shifting left by three brings a lower bit into the top position — so Running blinks eight times faster on exactly the same counter, no second divider, no second register. And Done is high with the counter ignored: solid on, and the underscore in ledPattern Done _ is the whole statement that a finished rig does not blink.

Its tests check the two rates against the same count, which is the sharp way to do it:

ledPattern Running 0x400000 @?= high
ledPattern Waiting 0x400000 @?= low   -- same count => Running is faster

At count 2²², Running is on and Waiting is still off. One assertion, and the relationship — faster — is pinned rather than the absolute rates. That is the property that actually matters to a person squinting at a board: not that the LED blinks at 1.5 Hz, but that a running rig looks visibly different from a waiting one.

There is a general point here worth naming, because it is why these functions are exported at all. A status indicator is the least testable part of most designs — it is inherently about what something looks like over time, and it usually ends up as three lines of ad-hoc logic buried in a top-level module where nothing can reach it. Splitting it into a state derivation and a pattern function turns the question “does the LED do the right thing” into two questions with yes-or-no answers, and moves the only untestable part — how fast a third of a second feels — into a constant.

The tests: the first time the whole thing runs

Everything so far has been checked in pieces. Test.Top is the first time the pieces run together, and the harness it needs is instructive.

cyclesPerBit :: Int
cyclesPerBit = 50

100 MHz over 2 Mbaud is fifty cycles a bit — the same arithmetic the baud generator did with a fractional accumulator, done here in whole numbers because the test bench drives the line rather than recovering it. From that, serialize turns a list of bytes into a list of Bit samples: fifty low for the start bit, fifty per data bit LSB-first, and then two bit-times high.

That last detail is an admission, and the comment makes it:

-- one idle bit-time between bytes (a realistic transmitter's inter-byte gap ---
-- the RX needs it to resync; truly back-to-back bytes drop on the falling-edge
-- resync).

A stop bit and one idle bit before the next start. That is what a real UART transmitter emits and what a real host sends, so the bench is not cheating — but it is recording that the receiver’s falling-edge resynchronisation wants the gap, and that a stream with literally none would lose bytes. The kind of thing you learn when the pieces meet.

The decoder in the other direction is the nicer trick:

deserialize samples =
  [ b | Just b <- sampleN (L.length samples)
          (fst (uartRx (oversampleTick (SNat @2_000_000)) (fromList ))) ]

It does not reimplement UART framing. It runs the captured txLine back through the real uartRx, fed by the real oversampleTick, and collects the bytes that strobe out. The receiver post closed on a byte-exact TX-to-RX loopback and called it the keystone; this is that keystone used as laboratory equipment. The transmitter under test and the receiver reading it are the two halves the loopback already proved agree, so a decoding failure here is a system failure and not a bench artefact.

runSystem then drives the machine, and its one subtlety is at the front:

leadN = cyclesPerBit

Fifty idle-high cycles before the real stream, so that the domain’s cycle-zero reset settles while the line is idle rather than in the middle of the first start bit. It is the same sampleN idiom the memory tests needed, applied to a whole design.

And then the payoff. Two test cases, and the first is the smallest complete run the rig can perform:

testCase "cosim: load [HALT], trigger -> drain = REVISION + HALT terminator"
  $ loadRunDrain [encode (Halt 0)] 20000
  @?= Right [0x0001_0000, 0xC000_0000]

Follow what that sentence asks for. encode (Halt 0) produces one thirty-two-bit instruction word. encodeControl (LoadProgram …) wraps it in a CRC, COBS-stuffs it and appends a zero delimiter; encodeControl Trigger adds a second frame. serialize lays both out as ten thousand-odd line samples. Then system runs: the UART receives the bytes, the loader peels the frames and writes the word into the instruction memory, the trigger raises startOut, the engine leaves Idle, stamps its REVISION preamble, fetches, executes a HALT, and pushes a terminator record. The loader sees haltedOut, drains the ring, re-frames the words, and clocks them out of the transmitter. deserialize reads them back with the real receiver and decodeResult unwraps the frame.

What comes out is two words. 0x0001_0000 is the REVISION preamble the shape post said Preamble stamps. And 0xC000_0000 is a HALT record, which you can read straight off the trace post’s field ruler: the top two bits are the tag 0b11, and every other field — seventeen reserved zeros, a three-bit reason, the trap flag, the overflow flag, the status byte — is zero. A clean stop, no trap, no dropped records, status nought. Exactly what Halt 0 should leave behind.

That single assertion exercises the wire format, the COBS codec, the CRC, the baud generator, the transmitter and receiver, the loader’s three lives, both memories, the instruction decoder, the engine’s phase machine, the trace records and the ring discipline. Fourteen posts, one @?=.

The second case adds the pins:

let prog = [encode CsAssert, encode (PutByteImm 0xA5), encode CsDeassert, encode (Halt 0)]

assertBool "cs_n asserts low" (low `L.elem` cs)
assertBool "sck toggles" (low `L.elem` sck && high `L.elem` sck)
decodeResult (deserialize tx) @?= Right [0x0001_0000, 0xC000_0000]

A real, if minimal, eSPI transaction: assert chip select, put a byte, deassert, halt. The assertions are deliberately weak — CS# goes low at some point, SCK takes both values at some point — because the bus post already pinned the five-cycle beat and the rising edge at the 2→3 boundary with sharp unit tests against step. What this test adds is not precision but reach: it shows that a byte typed into a serial port at one end of the design comes out the other end as motion on the eSPI wires, and that the trace still drains cleanly afterwards. The sharp tests prove the waveform; this one proves the waveform is connected to anything.

What we read

A hundred and fifteen lines, and the project stops being a collection of modules. Tamal.Top exports one machine and four pure helpers, and imports six modules that are six posts. stepM re-associates the engine’s three-element output into the pair mealy wants, and that one application — mealy stepM initState — is where eight posts of pure transition acquire a clock, a register and a life; a property test holds the adapter to being nothing but parentheses. system is the whole design over plain Signals, with BiSignal deliberately excluded so the pad post’s simulation knot stays confined to a hundred and four lines while everything larger stays drivable from a list of bits. Its body is four stages — UART, loader, memories, engine — and three feedback loops, each written as a definition that mentions a later name, each legal because these are nets and each broken by a register that was placed for other reasons a long time ago. ringWrite unwraps a Ring into a bare tuple so the memory can go on not knowing what a trace record is. And the LED is a latch, a counter, and two total functions that turn “is it working” into a truth table.

Then the tests run the whole thing, which nothing before now could. A program is framed, serialised bit by bit onto a wire, received, parsed, stored, triggered, executed, traced, drained, re-framed, and read back with the project’s own receiver — and it comes back as a REVISION word and a HALT terminator with every field zero. The introduction described a rig that loads a program over a serial link and reports what happened on the bus. Two hundred lines of test say it does.

What is still missing is small and absolute. system is generic in its domain: it never says how fast its clock runs, never names a pin, and its four IO lanes are plain signals that no bonded pad has ever seen. It is a machine with no address. The next post gives it one — fifty-one lines that tie a real oscillator to a real domain, wrap system in espiPads, and name every port on the package so the place-and-route tool knows which ball of solder is IO[0]. Below that there is no more Haskell.

Footnotes

  1. LoaderIn <$> rxByte <*> txReady <*> halted <*> ringPtrO <*> ringData looks like effectful code and is nothing of the sort. Signal dom is an applicative functor over time: <$> maps a pure function across every cycle of a signal at once, and <*> applies a signal of functions to a signal of arguments cycle by cycle. Feed a five-argument constructor into that machinery and what comes back is Signal dom LoaderIn — a record assembled fresh every cycle from whatever its five sources carry at that moment. In hardware it is not an assembly at all: a record is a bundle of wires, so the expression describes five groups of wires being routed into one named bundle, and it costs exactly nothing. The field projections on the other side (txByte <$> lOut and friends) are the same move reversed — selecting a sub-bundle out of a wider one. The primer’s framing of Signal as a stream you map over, not a value you inspect is what makes this style read naturally: you never write down a cycle, so you never have to say which one.

  2. A where block in Clash is a set of simultaneous equations over nets, not a sequence of assignments, so mutual reference between its bindings is ordinary. What is not ordinary — and what the compiler will reject or the simulator will hang on — is a cycle with no state element in it, since that describes a combinational loop: a value that must be known in order to compute itself. All three loops here are broken by registers that were placed for independent reasons. The fetch loop pcO → instrRam → instrWord → engine → pcO crosses the instruction memory’s one-cycle read latency, which is a register inside the block RAM primitive and the reason the engine has a Fetch phase at all: it spends a cycle doing nothing so the word has time to arrive. The trace loop maybeRing → ringRam → ringData → loader → ringAddr → ringRam crosses the ring memory’s read latency and the loader’s own state. And halted and startO, which appear above their definitions, are both projections of registered outputs — haltedOut from the engine’s state register, startOut from the loader’s. The design was never arranged to make this module typecheck; it typechecks because a machine that talks to memories has registers in all the right places anyway.