step(m; inputs...) is the simplest: one call, one clock edge, a new state back. Everything is a value, so a run is a list of states, and a unit test is a loop:
@quartzstruct Blinker@in en::Bool =true@out led::Bool n::Bits{4} =0end@on Blinker posedge(clk) begin en && (n ← n +1) led ← n[3]endleds =Bool[]let m =Blinker()for _ in1:20 m =step(m)push!(leds, m.led)endendleds
For anything with real time in it — several clocks, peripherals, a stimulus that waits for things — a Simulation wraps the design with a clock plan and a recorder, and you drive it with @run.
clocks gives every pin clock a rate — a named tuple is enough for a simple plan, and @clocks handles several clocks (benches has the details); wiring connects the design to models around it, and is left out when there is nothing to connect, as here; watch says which nets to record. Any stubs — models of chips on the board — come as further keywords.
Every register, pad, input and clock is a net, named by its path:
sim["n"] reads a net, and sim.n is the same thing unquoted. Only an input, or a pad the wiring leaves alone, can be driven; everything can be read.
@run: a stimulus
A stimulus is a @run body. Inside it, sim.x = v drives a net, advance_by and advance_until let time pass, and times carry units:
out =@run sim begin sim.en =falseadvance_by(3µs) sim.en =trueadvance_until(sim.led) # a condition, looked at every slotadvance_until(20µs) # or an absolute timeadvance_until(!sim.led; timeout=50µs)end
@run returns what the body returned, or the capture when the body returns nothing. Outside a macro, using QuartzHDL.Units gives ms, µs, MHz and so on.
advance_until with a condition is a breakpoint: the simulation runs until the condition is true, or the timeout passes, which is an error.
Tasks
A @task inside a @run body runs alongside it and ends with it. Given on its own, @run sim @task ... end keeps running until stop!(sim, task):
t =@run sim @taskbeginwhiletrueadvance_by(7µs) sim.en = !sim.enendend@run sim advance_by(30µs)stop!(sim, t)float(time(sim))
5.0e-5
This is how a background model — a clock source, a device answering a bus — lives beside the main stimulus. The components chapter builds on it.
Reusable stimuli
A stimulus that will be used more than once is a function. @stimulus makes a function whose body is a @run body; an argument declared ::Net names a net by its path. It is called from inside a @run, or as one:
The capture — what @run returned above, or capture(sim) at any time — holds every watched net’s history, as values: a pin asserted low reads true while asserted, as the design sees it, and plots and VCDs show the wires. Query it by name or path, and by time:
out =capture(sim)out.led[5µs] # its value at 5 µs
false
changes(out.led)[1:4] # when it changed, and to what
An encoded register gives its state’s name rather than a number. sampled(signal) gives one value per clock, for signal-processing packages; slots(signal) the raw slot indices.
watch!(sim, "ctr.*") adds nets to record and unwatch! takes them away; recording every net costs a few percent. clear!(sim) forgets the capture; reset!(sim) puts the simulation back to time zero, as built.
The REPL prompt
At the REPL, simrepl(sim) switches to a sim> prompt where each line is a @run body and nets are named bare:
Backspace on an empty line returns to Julia. This is the fastest way to find out what a design does: poke it, look, poke again.
Logging
@info, @debug, @warn and @error work inside a block, with the simulation time and the module’s name added to each message:
@quartzstruct Talker n::Bits{4} =0end@on Talker posedge(clk) begin n ← n +1 n ==15&&@info"wrapping" n@check n ≤15endsim =Simulation(Talker(); clocks=(clk=1MHz,))@run sim advance_by(40µs)
┌ Info: wrapping
│ time = "15.0µs"
└ n = Bits{4}(0xf)
┌ Info: wrapping
│ time = "31.0µs"
└ n = Bits{4}(0xf)
Capture(0 signals, 40.0µs)
A message is part of the design’s meaning and stays in the code; what to show right now is chosen from outside and never touches the design: showlogs!(sim; from=10µs, to=12µs, modules=[:Talker], when=() -> sim.n > 3) narrows what is printed, and showlogs!(sim) shows everything again.
@check cond is the design’s own assumption: a simulation stops where it fails, with the time and the module in the error.
None of this reaches the Verilog unless asked for. Verilog(; debug=true) and cosim(T, stim; debug=true) emit messages as $display and a failed check as $error, for simulation-only builds; synthesis output is untouched.
Lower-level pieces
Under @run sit a few functions that components and harnesses use directly: spawn!(sim, f) starts a task, run!(sim, f) runs a function as a stimulus and returns its value, hook!(sim, f) registers a function called after every slot (for a model that must watch a bus every cycle) and unhook! removes it. They appear again in custom components.
Next
Benches: clocks with real rates, and wiring the design to models of what surrounds it.