Reference

Every exported name, from its docstring

Types

QuartzModule

The supertype of every @quartz struct: a hardware module, whose fields are its registers and instances and whose blocks are declared with @on and @wire.

Bits{N}

An unsigned hardware integer N bits wide, the type most registers and ports have. It wraps on overflow and truncates on a slice, as the wire does.

@quartz struct Ctr
  n::Bits{8} = 0
end
SBits{N}

A signed (two’s complement) hardware integer N bits wide. Signed and unsigned values never mix in one expression: convert one of them first.

Pulse

A one-cycle flag: a field declared Pulse is a Bool its own block clears at the top of every cycle, so a write of true is high for exactly one cycle.

@quartz struct Rx
  got::Pulse
end
Timeout{N}

A countdown: a field declared Timeout{N} is a Bits{N} its own block decrements each cycle and holds at zero. Write it to arm it and ask expired when it is done.

@quartz struct Rx
  wait::Timeout{4} = 0
end
Step

The step of a @sequence: a field declared Step is sixteen bits in the Julia model and starts at START; the emitted register is as wide as the steps need. It takes no default and no width.

@quartz struct Tx
  step::Step
end
Edge

A Bool register that also remembers the level it held before, so its transitions can be asked about. Write it like any register, e ← x, and read it bare for the level; rose(e) and fell(e) are then true for exactly one cycle.

@quartz struct Sync
  sck::Edge
end
MetaGuard{K}

A K-stage synchroniser for a signal that crosses into this clock domain. Feed it with g ← x and read it bare for the value that has settled through all K stages.

@quartz struct Top
  @in async::Bool
  sync::MetaGuard{2}
end
Pipeline{K,T}

A K-deep pipeline of T values. A write pushes a value in; reading it bare gives what has come out the far end, or missing before anything has. isnew says a fresh result arrived this cycle and isready says nothing is still inside.

@quartz struct Mac
  acc::Pipeline{3,Bits{16}}
end
Multicycle{K,T}

A combinational value whose logic is allowed K clock cycles to settle, on the promise that its sources hold still that long. Drive it from a @wire block, read it bare, and guard the read with isready; the tool is told to relax the path.

@quartz struct Corr
  sum::Multicycle{3,Bits{32}}
end
Pad{N}

An inout pin, or a bus of N of them, declared with @io. Write drive(x) or release() to it and read it bare for the level the net settles to – the design’s drive, the outside world’s, and the pull together.

@quartz struct I2C
  @io sda::Pad{1} = Pad(:pullup)
end

Modules and ports

@quartz struct Name ... end

Declares a hardware module: a plain Julia struct whose fields are its registers, with @in, @out and @io lines declaring what crosses its boundary. The blocks that drive it are written separately, with @on and @wire.

@quartz struct Ctr
  @in en::Bool
  @out n::Bits{8} = 0
end
@in name::T = default

Declares an input of a @quartz module. An input has no storage: the blocks receive it as an argument, and a parent wires it from a @wire block.

@in en::Bool = false
@out name::T = default

Declares an output of a @quartz module. The field is an ordinary register that also reaches a pin. Attributes follow the declaration: active=:low for a pin asserted low, verilog="name" for the name the emitted port takes.

@out ready::Bool = false  active=:low
@io name::Pad{N} = Pad(:pullup)

Declares an inout pin, or a bus of them, of a @quartz module. The field is a Pad, written with drive/release and read for the level the net settles to.

@io sda::Pad{1} = Pad(:pullup)
interface(T)

The declared ports of module T, in declaration order, as PortDecl values.

portdoc(T, name)

The string that documents port name of module T, or nothing if it has none. A string written above an @in/@out/@io line documents that port.

static(v)

Marks a field’s default as delivered by the bitstream rather than by reset: x::Bits{8} = static(5) powers up at 5 and is left alone by @reset. A field whose block has no @reset is static without saying so; static is how a field opts out of a reset its block does have. In the emitted Verilog a static field keeps its initializer; reset-delivered fields lose theirs, which frees the synthesiser to use the flip-flops’ enable and clear pins.

Blocks

@on Module posedge(clk) begin ... end

Declares clocked logic: the body runs on every rising (or negedge, falling) edge of clock net clk, and its register writes take effect together at the end of the edge, as Verilog’s non-blocking assignment does. Leading @reset, @only_when and @clockout clauses say when it resets, when it runs, and which pin its clock reaches.

@on Ctr posedge(clk) begin
  @reset(rst)
  n  n + 1
end
@wire Module begin ... end

Declares continuous logic: the body has no clock and no storage, so every path through it must write every field it drives. It may write @out ports, pads, Multicycle wires, and the inputs and clocks of the module’s instances.

@wire Top begin
  ctr.en  run
  led  drive(ctr.n[7])
end
@reset(cond)
@reset(cond; field = value)

Says when the @on block it heads resets: while cond holds, every field with a default goes back to it. A named override holds a field at a value of its own instead. It belongs at the top of the body, before any statement.

@only_when(sig)

Says when the @on block it heads runs: an edge on which sig is false leaves every field the block owns alone. It belongs at the top of the body, before any statement.

@clockout(name; invert = false, gate = signal)

Forwards the clock of the @on block it heads out of the module as name, so it can reach a pin – an SPI clock, say. invert sends the opposite phase and gate names a signal that must hold for the clock to leave. It belongs at the top of the body, before any statement.

@method f(a, b) = ...

Declares a helper written as if inside a module: bare field names, inputs and register writes all work. It belongs to no one module – the same method serves every module that calls it – and is inlined where it is called. One that writes is a statement; one that only computes a value may be used as one.

@method arm(t, n) = t  n
@check cond

In a block: the design’s own assumption, checked every time the block runs. A simulation stops where it fails; Verilog emitted with debug = true raises $error.

Values

bits(x...)

Join hardware values into one word, most significant piece first, as Verilog’s {a, b} does. Every piece must have a known width, so a plain Int is refused.

bits(true, Bits{4}(0x5), false)    # Bits{6}(0b101010)
a ⊞ b

bits(a, b): the two values side by side, a in the more significant half.

bitwidth(x)

How many bits a hardware value, or a hardware type, takes on the wire.

bitwidth(Bits{12})        # 12
bitwidth(SBits{8}(-1))    # 8
part(i, Bits{N})

Part number i of a word, N bits wide, numbered like bits: from zero at the least significant end, so x[part(0, Bits{8})] is the low byte of x and x[part(1, Bits{8})] the one above it. A computed index is kept as the part number rather than multiplied out, so the emitter can decode it over the parts that fit.

firstset(a)

The lowest set bit of a, as a value of the same width; zero if a is zero.

onehot(Bits{N}, i)

An N-bit value with bit i set and the rest clear.

popcount(a)

How many bits of a are set. The result is just wide enough to hold the count.

drive(x)
drive(x, en)

What a block writes to a pad to hold it at x. With en only the bits en marks are driven; the rest are left to whatever else is on the net.

release()

What a block writes to a pad to let go of it, so the net is left to the outside world and the pad’s pull.

expired(t)

Whether a Timeout field has counted down to zero.

rose(e)

Whether the Edge field e went from low to high at the last clock edge. True for exactly one cycle.

fell(e)

Whether the Edge field e went from high to low at the last clock edge. True for exactly one cycle.

isrising(e, x)

Whether the incoming sample x is high while the Edge field e still holds low, so the rise is seen in the cycle it happens rather than the one after.

isfalling(e, x)

Whether the incoming sample x is low while the Edge field e still holds high, so the fall is seen in the cycle it happens rather than the one after.

isnew(p)

Whether the Pipeline field p delivered a fresh result at the last clock edge, as opposed to still holding the previous one.

isready(x)

Whether nothing is in flight, so that what x gives now is the result of its latest input: a Pipeline that has a result out and no write still inside it, or a Multicycle whose sources have been still for the cycles its path is declared.

Encodings, state machines and sequences

@encoding Name begin ... end
@encoding Name::W encoding=:onehot begin ... end

Declares a named set of values over a Bits – FSM states, protocol tags, command codes. A field declared with the encoding as its type is a register of that width that knows its names, so a bare name resolves where the field is written or compared. W fixes the width and encoding picks :binary, :onehot or :gray.

@encoding State begin
  IDLE
  RUN
end
encname(e)
encname(e, v)

With one argument, the name of encoding e. With two, the name e gives the value v, or nothing if it names no such value – which is what logs and waveform dumps show in place of the number.

encname(State)          # :State
encname(State, s)       # :IDLE
statedoc(e, s)

What value s of encoding e was declared to mean – the string written above its line – or nothing if it has none.

@fsm reg begin ... end

Dispatches on an encoded register, expanding to the if/elseif chain a person would otherwise write by hand. What it adds is the checking that chain cannot have: every state accounted for, no state named twice, and no name the encoding does not define. Branches are labelled @state NAME, and @otherwise takes the rest.

@fsm state begin
  @state IDLE
    go && (state  RUN)
  @state RUN
    state  IDLE
end
@state NAME

Labels a branch of an @fsm block: the statements after it run while the register holds NAME.

@otherwise

Labels the default branch of an @fsm block, covering every state that has no @state branch of its own.

@sequence Name field begin ... end

Runs statements across cycles inside an @on block: @then divides the body into steps, one per clock edge, and field is the register that holds which step is next. @when guards a step, @delay n inserts idle steps, @repeat n unrolls a group, and @goto LABEL jumps to a named step.

@sequence Xfer step begin
  cs  false
  @then SEND
  shift  data
end
@then
@then NAME
@then NAME @when cond

Divides one step of a @sequence from the next. NAME labels the step so @goto can reach it, and @when guards it: the step holds until the condition is true.

@when cond

Guards the step of a @sequence it heads: the sequence holds there until cond is true. It belongs at the head of the step, right after @then.

@delay n

Inserts n idle steps into a @sequence, so the next step runs n cycles later. n is a literal count.

@repeat n begin ... end

Unrolls the steps in its body n times into a @sequence. n is a literal count, and the steps inside cannot be named, since each copy would take the same name.

Black boxes and clocks

@blackbox Name verilog="VNAME" begin ... end

Declares a Verilog module QuartzHDL does not define – a vendor PLL, a RAM block, an oscillator. The body lists its ports (input, output, clock, clockout) using the datasheet’s names; a clockout also says which clock input it divides, by how much, and under what condition it runs. Nothing is emitted for the module itself, and QuartzHDL.standin supplies whatever behaviour a simulation needs.

@blackbox PLL verilog="EHXPLLL" begin
  clock(CLKI)
  clockout(CLKOP, from=CLKI, divide=2)
end
QuartzHDL.standin(::Type{Part}) = Model()

What a black box does in simulation, supplied by the harness rather than the design. The model is stepped by the simulator on every edge of each clock input, as step(model, :clockport; inputs...), and must return the model after the edge; an output of the part reads as the model’s property of the same (lowercase) name. A part whose outputs are all clockouts needs none: its recipes are its behaviour.

clocklevel(m, net)
clocklevel(net)

The level a clock net is resting at, for a design that samples a slow clock as data – a microsecond tick, say. In Verilog this is the net name in an expression; here it is the square wave the tree produces. Inside a block the one-argument form reads the block’s own module.

@clocks begin ... end

Names the rate of every clock a run has, in absolute units, and builds the ClockPlan a Bench takes. A clock whose rate does not divide the slot grid may be marked dithered; grid sets the slot rate by hand.

@clocks begin
  clk = 48MHz
  rtc = 32768Hz, dithered
end

A simple plan needs no macro: Bench and Simulation also take a named tuple of rates, clocks = (clk = 1MHz,) (with using QuartzHDL.Units), with (rate, :dithered) for a dithered clock and grid for the slot rate.

ClockPlan

What a run’s clocks do: every clock’s rate, and the slot grid they share. Built by @clocks and handed to a Bench, which advances one slot at a time.

@multicycle Module n from => to

Declares that the logic between two registers of Module may take n clock cycles to settle, so the tool need not route it for one. The endpoint names are checked against the module and the cell pattern generated, which a hand-written constraint cannot do.

@multicycle Corr 3 acc => out
@primary Module net, net, ...

Names the clock nets of Module that ride the chip’s global low-skew distribution. Stated once, on the top module.

stages(T)
stages(T, name)

How each Pipeline of module T, or the one called name, is cut into stages: what each stage takes in, computes, and registers, with the cost of its longest path, and the registers the whole pipeline costs. It is the plan the Verilog emitter follows.

simmodels(io, T; scale=NamedTuple())
simmodels(path, T; scale=NamedTuple())

Write a simmodel for every black box in the design T that declares a clock tree, in declaration order and each one once. scale is per black box, giving the clock overrides simmodel takes.

simmodels("standins.v", Top; scale=(PLL = (clk_slow = 4,),))

Benches and boards

Bench{D,S,W,K}

A design plus the models of the things around it – a USB controller, an I2C slave, an ADC chip – advanced together. A bench is a value: step returns a new one, so a run is a list of states to look through afterwards.

b = Bench(dut; clocks, wiring, rtc = I2CSlave())
b = step(b, 1000)
Bench(dut; clocks, wiring = Wiring(), stubs...)

clocks names the rate of every clock that comes from a pin, and of every stub: an @clocks plan, or for a simple plan a named tuple, clocks = (clk = 1MHz,) with using QuartzHDL.Units. wiring is an @wiring function, and defaults to nothing connected. Each remaining keyword is a model of something the design is wired to.

history(b, n)

Run bench b for n slots and return every state along the way, the starting one first, so a run can be looked through afterwards.

padnet(m, name)

What the design as a whole drives on pad net name, as a (value, enable) pair, so a peripheral model can resolve the net against its own drive.

netlevel(dut, pad, drive)

What a shared net reads: what the design drives on pad, resolved against what the outside drives, with the pad’s pull deciding any bit neither of them holds. A net with no pull reads zero where nobody holds it.

@wiring begin ... end

Says what a bench connects to what, with the same arrow a block uses: the left side is an input of a model (or of the design), the right side a value computed from the others’ outputs. A pad of the design names its net, so writing to it is what the outside drives and reading it is what the net settles to.

@wiring begin
  dut.rx  uart.tx
  dut.sda  drive(0, rtc.pull)
end
Wiring

What a @wiring block compiles to: the function that computes each model’s inputs, and which inputs of the design it drives – so a stimulus knows what is left for it to drive. Wiring() is the empty wiring, with nothing connected, and is what a bench or simulation uses when no wiring is given.

@board Name begin ... end

Declares a board: the settings it has (device, raw, and any pin attribute that applies to every pin) and a binding of each port of a design to its site. A port below the top is named by its instance path.

@board Rev2 begin
  device = "LFE5U-25F-6BG381C"
  clk => (pin = "P3", osc = 25MHz, io = :LVCMOS33)
end
Board

What a board provides: the part it is built around, which pin each port of a design lands on, and what the buffers there are told. Built by @board, and read by the constraint emitters and by QuartzHDL.problems.

Simulation

Simulation(dut; clocks, wiring = Wiring(), watch = nothing, stubs...)

A design with the models around it, ready to be driven. Takes what Bench takes: clocks is an @clocks plan or, for a simple plan, a named tuple of rates (clocks = (clk = 1MHz,) with using QuartzHDL.Units), and wiring defaults to nothing connected, which is all a single-module simulation needs. watch names the nets to record, as a path or pattern ("usb1.*", "*" for every net) or a list of them; nothing is recorded until something is watched. See watch! and unwatch!.

sim["name"] and sim.name read a net, sim.name = v drives one, and @run sim begin ... end runs a stimulus against it.

nets(sim, patterns...)

The nets of a simulation: its design’s registers, pads and inputs, the fields of its stubs, and its clocks. A pattern is a path, a bare name (matching any net that ends in it, the way sim["name"] reads it), or a prefix ending in *.

watch!(sim, patterns...)

Record these nets from now on, as well as those already watched. A pattern is a path, a bare name, or a prefix ending in *; "*" is every net. Capture costs time in proportion to the nets watched, so a large design runs faster with fewer.

unwatch!(sim, patterns...)

Stop capturing these nets. What they did so far stays in the capture.

capture(sim)

What a simulation has captured so far, queried by name: capture(sim).x is the signal net x recorded.

clear!(sim)

Forget the capture so far; the design keeps its state and the time keeps counting.

reset!(sim)

Back to time zero: the design and its stubs as they were built, nothing driven, every task stopped, and the capture cleared.

changes(signal)

When a signal changed and what it changed to, as time => value pairs with the time in seconds.

sampled(signal)

A signal as frames, one per slot of the clock grid, from the first slot captured to the last: the value held in each, as a number (NaN where there was none). Reads from the changes as it goes, so nothing is built until it is asked for; collect gives the plain vector. With SignalBase loaded it has a frame rate.

slots(capture)

The last slot a capture holds, so slots(c) * c.grid is how long it is in seconds.

advance_by(sim, t)

Let the simulation run for t seconds before the stimulus continues.

advance_until(sim, f; timeout = nothing, what = "for a condition")

Let the simulation run until f() holds, or until the time it gives if it is a number. A predicate is looked at every slot; with a timeout in seconds, an error naming what was waited for is raised if it never held.

@run sim begin ... end
@run sim expr

Run a stimulus against sim. Inside it sim.x reads a net and sim.x = v drives one, advance_by(t) and advance_until(cond) let time pass, @task expr starts a concurrent stimulus, and times are written with units: 1ms, 10us. A @task given on its own keeps running after the call returns.

@stimulus function name(sim, net::Net, args...) ... end

Define a reusable stimulus. Its first argument is the simulation; an argument declared ::Net names a net (as a path, or a Net) that the body reads and drives by that name. The body is written as a @run body.

spawn!(sim, f; persistent = false)

Start f() as a task of the stimulus, advancing with the simulation. A task made inside a @run block ends with the block; a persistent one keeps going until stop!(sim, task).

stop!(sim, task)

End a task of the stimulus. stop!(sim) ends every one.

run!(sim, f) -> value

Run f() as the stimulus: the simulation advances while it delays, alongside the tasks it and earlier stimuli started. Returns what f returned, or the capture when that is nothing.

hook!(sim, f)

Call f() after every slot, for a model that must react every cycle (a bus peripheral) and would be too slow as a task. f reads nets and drives them like any stimulus; what it drives applies at the next slot. unhook!(sim, f) removes it.

unhook!(sim, f)

Stop calling f after every slot; it undoes hook!(sim, f).

simrepl(sim; key = ')')

Switch the REPL to a sim> prompt that drives sim. Lines are @run bodies with bare net names: reset = true, advance_by(1ms), advance_until(busy), result. Backspace on an empty line returns to the Julia prompt, and key at the start of an empty Julia line comes back, as ] does for Pkg.

A bare name is a net, read or written; sim.name always is. A name that is not a net becomes a variable of the session, gone when simrepl is next called, and a variable of the session shadows a net of the same name; global x = 1 (or Main.x = 1 for one that exists) makes a variable of Main instead. Ambiguous net names are written with their path, ctrl.state.

showlogs!(sim; from = nothing, to = nothing, modules = nothing, when = nothing)

Choose which of sim’s log messages are shown: only between times from and to (seconds), only from the named module types, or only while when() holds. With nothing given, every message of sim is shown again. Each simulation has its own choice, and one simulation’s choice says nothing about another’s.

Formats and tools

Verilog(; name=nothing, suffix=true, debug=false, inits=:static)

Verilog for a design: write(path, T, Verilog()). name is the module’s name, the type’s if not given, and suffix puts _i/_o on the ports. debug emits the design’s log statements as $display. inits = :static initializes only the registers whose value the bitstream delivers – what synthesis needs, and nothing that would cost it a flip-flop’s enable pin; :all initializes every register, for a simulator that would otherwise start them at x.

VCD()

A capture as a value change dump: write(path, capture, VCD()), the format a waveform viewer reads.

LPF(board)

Lattice constraints for a design on a board: write(path, T, LPF(board)). Pin sites, buffer options, clock rates and timing exceptions come from the same declarations the Verilog does, so the two agree.

Diamond(board; vendor = String[], implementation = "impl")

A Lattice Diamond workspace for a design on a board: write(dir, T, Diamond(board)) fills dir with the Verilog under src/, the constraint file, a project file (.ldf) with a default strategy (.sty), and a build.sh and Makefile that run Diamond from synthesis to the bitstream – and the JEDEC file on a MachXO part – so make there builds the design where Diamond is installed. vendor lists the netlists of the design’s black boxes, copied into src/ and added to the project; a black box with no netlist is listed as src/<Name>.v for the user to supply.

Surfer()

The Surfer waveform viewer (https://surfer-project.org), as a viewer for view and an IO a capture can be written to: what is written to it is what it shows, through a scratch VCD file it is told to load again. One window per Surfer().

Icarus()

Icarus Verilog (iverilog/vvp) as the simulator a cosim runs the generated Verilog in. It is the default, and needs iverilog on the PATH.

cosim(T, stimulus; kwargs...)

Run the Julia model of module T and the Verilog generated from it on the same per-cycle inputs, and compare every output after every clock.

stimulus is a vector of NamedTuples, one per cycle, with one entry per input of the module and per pad the outside world drives. A pad entry is a value, or a (value, enable) pair for driving part of a bus.

  • clocks gives the edges per cycle of each clock of a multi-clock module, e.g. clocks = (clk_i = 4, clk_slow_i = 1): one stimulus entry is one slot, clk_i is pulsed in every slot, clk_slow_i in every fourth. Each count must divide the largest; clocks scheduled in the same slot are pulsed in the order given.
  • reference co-simulates against a hand-written Verilog file instead of the generated one – the way a ported design is checked against its original.
  • ref_init puts that original in the state the struct defaults put the Julia model in, as hierarchical assignments ("tx_data64" => "64'd0"), since its registers power up undefined.
  • scale divides a black box’s clock faster than the board would, so a few thousand cycles cover many periods of a slow domain.
  • debug emits the design’s log statements as $display in the Verilog.
  • name, dir, suffix, extra_sources and timeout name the module, say where the files go, put _i/_o on the ports, add Verilog sources to compile, and bound the run in seconds.

Returns (; ok, mismatches, julia, verilog, dir): whether the two agree, the differing cycles as (cycle, julia line, verilog line), the two sets of output lines, and the directory the files were written to.

r = cosim(Blinker, [(en_i = true,) for _ in 1:100])
r.ok || println(r.mismatches[1])
write(path_or_io, x, format)

Write x in a format: a design as Verilog(), a capture as VCD(), a design on a board as LPF(board). Returns the path when given one.

view([viewer], capture_or_sim)

Show a capture in a waveform viewer, Surfer() unless another is given. Given a simulation, the viewer follows it: refreshed after every @run, and now and then during a long one. close(viewer) closes it.

Library

UART(sim; rx = nothing, tx = nothing, baud = 115200, stop = 1, parity = :none,
     frame = nothing, timeout = 1)

A UART on the design’s pins: rx is the net it listens to (the design’s transmit), tx the net it drives (the design’s receive). frame groups what is received into units for take!, read and on: a byte count, or a delimiter (0x0a, "\r\n"). timeout bounds a blocking read or write, in seconds. close(u) stops it.

FT2232H(sim; data, rd, wr, rxf = nothing, txe = nothing, frame = nothing, timeout = 1)

An FT2232H USB FIFO on the design’s pins, in FT245 asynchronous mode. data is the bus pad, rd and wr the design’s strobes (as declared, so asserted means true whatever the pin’s polarity), rxf and txe the flags the design reads: rxf holds while there is a byte to read, txe while there is room to write. write(ft, bytes) is the host sending to the design; read(ft, n) is what the design sent to the host. frame = 8 makes take! and on work in 8-byte words. close(ft) takes it off the bus.

SPIMaster(sim; sclk, mosi = nothing, miso = nothing, cs = nothing, rate = 1MHz,
          mode = 0, timeout = 1)

An SPI master on the design’s pins, driving sclk, mosi and cs and reading miso. transfer(m, bytes) exchanges bytes, holding cs for the whole transfer, and returns what came back; write and read are one-way forms of it.

SPISlave(sim; sclk, mosi = nothing, miso = nothing, cs = nothing, mode = 0, idle = 0xff,
         frame = nothing, timeout = 1)

An SPI slave on the design’s pins, following the design’s sclk and cs. Bytes queued with put! go out on miso as the master clocks, idle when none is queued; what arrives on mosi is taken with take!, or answered by on – per byte, so a reply to a command byte goes out with the next byte, as a device does. close(sl) takes it off the bus.

transfer(master, bytes) -> bytes

Clock bytes out and return the bytes that came in, one per byte sent.

I2CMaster(sim; scl, sda, rate = 100kHz, timeout = 1)

An I2C master on the design’s scl and sda pads. write(m, address, bytes) sends a transaction and returns whether every byte was acknowledged; read(m, address, n) returns n bytes. A register read is write(m, a, [reg]); read(m, a, n), with stop = false on the write for a repeated start.

I2CSlave(sim; scl, sda, address, frame = nothing, timeout = 1)

An I2C slave at address on the design’s scl and sda pads. What the master writes arrives as one unit per transaction, to take! or to on; what the master reads comes from the bytes queued with put! (0xff when none), so a register map is an on closure that answers a write with the bytes to read next. close(sl) takes it off the bus.

PWM(sim; out, rate, duty = 0.5, phase = 0, count = Inf)

A pulse train on net out at rate, high for duty of each period, starting phase of a period late, for count periods. duty = 0.5 is a clock; count = 1 a single pulse. pwm.duty and pwm.rate may be changed as it runs; close(pwm) stops it with the output low.

RAM(depth, width; read = (clock, addr, data, en), write = (clock, addr, data, we, en))

A memory of depth words of width bits as a black-box stand-in: QuartzHDL.standin(::Type{Part}) = RAM(8192, 16; read = (...), write = (...)). Each port names the part’s pins by role, in lowercase as Julia sees them; en and we may be left out. Several ports of a kind are given as a vector. ram[a] reads a word, ram[a] = v stores one, fill!(ram, 0) clears it; an address counts from zero, as the design’s does. A read port’s output is read under the name its declaration gives it, ram.q.

on(f, link)

Answer what the link receives: f(unit) is called with each frame (each byte, with no frame set; each transaction, for a bus), and what it returns is sent back – bytes, a string, or nothing. A unit given to f is not queued for take!.