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.
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.
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.
Modules and ports
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.
@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.
@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.
@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
a ⊞ b
bits(a, b): the two values side by side, a in the more significant half.
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.
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.
@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.
@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.
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.
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.
@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.
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.
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
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
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.
clocksgives 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_iis pulsed in every slot,clk_slow_iin every fourth. Each count must divide the largest; clocks scheduled in the same slot are pulsed in the order given.referenceco-simulates against a hand-written Verilog file instead of the generated one – the way a ported design is checked against its original.ref_initputs 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.scaledivides a black box’s clock faster than the board would, so a few thousand cycles cover many periods of a slow domain.debugemits the design’s log statements as$displayin the Verilog.name,dir,suffix,extra_sourcesandtimeoutname the module, say where the files go, put_i/_oon 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.
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!.