@quartz struct Loop
@in rx::Bool = true
@out tx::Bool = true
end
@on Loop posedge(clk) tx ← rx
sim = Simulation(Loop(); clocks = (clk = 48MHz,))
u = UART(sim; rx = "tx", tx = "rx", baud = 115200)UART(tx ← design, design ← rx, 115200 baud)
The chips around the design, ready to attach
What sits on the board beside the design — a USB controller, a UART, an SPI flash, an I2C sensor, a block RAM — comes from the library. A component is bound to the design’s nets by role, and then talked to the way a host would talk to the hardware.
A component needs no wiring and no entry in the clock plan: it reads and drives nets the way a stimulus does, from a task of its own or, for a bus it must watch every cycle, a hook run after every slot. Pins are asserted as true, as everywhere in the design; a pin declared active=:low is inverted at the pin, not here. close(c) takes a component off the bench.
Let’s build a loopback and talk to it:
UART(tx ← design, design ← rx, 115200 baud)
The roles are the UART’s own: its rx listens on the design’s tx. A UART is a stream, and a stream is talked to like a serial port:
"hello"
2-element Vector{UInt8}:
0x01
0x02
A stream is an IO, so print, readuntil, read and the rest work on it. A blocking call suspends its task inside a @run, or runs the simulation itself at top level, and gives up after the link’s timeout. frame = 8 or frame = "\n" groups what arrives into units for take!, read(u) and on. UART(sim; ..., parity = :even, stop = 2) sets the framing.
The FT2232H is a USB-to-parallel FIFO, driven on the design’s byte bus in FT245 asynchronous mode:
It is a stream like the UART, and it watches the bus every slot, so the design’s read and write strobes are honoured whenever they come.
A master is a transaction link: transfer(m, bytes) selects the chip, clocks the bytes out and returns the bytes that came back. A slave answers each byte with a closure:
mode is the usual 0–3 (clock polarity and phase).
The master’s verbs are write(i, address, bytes) and read(i, address, n). A slave answers a transaction with a closure, the way a device would — here, a register file:
The bus is open-drain, so the design’s scl and sda are pads with ext_pull=:up. Clock stretching is not modelled.
A simulation keeps one drive per net from outside the design, so a pad can carry one library component: a slave on the bus the design’s master runs, or a master on the bus a design’s slave listens to. Two library components on the same pad — two slaves on one bus, or a library master and a library slave with no design between them — overwrite each other’s drive, and the transaction fails. A second device on a shared bus has to be a module of the design for now.
A pulse train on a net — a clock, a strobe, a gated burst:
A memory that stands in for a vendor RAM block behind a black box. Its ports are named by role, in the lowercase names Julia sees; any number of each:
A read port holds the word its address named before the edge, and a read of the address being written sees the old word. The storage is changed in place, and reset! does not clear it.
The pattern is the same across the library: on(component) do unit ... end is called with each unit as it arrives — a transaction for I2C, a byte or a configured frame for an SPI slave or a stream — and what it returns is queued to send. A unit given to a closure is not kept for take!. A slave with nothing queued sends 0xff.