using QuartzHDL
@quartz struct Pwm
@in duty::Bits{8} = 128
count::Bits{8} = 0
@out pwm::Bool
end
@on Pwm posedge(clk) begin
count ← count + 1
pwm ← count < duty
endQuickstart
A small design, end to end
Let’s build something small but real: a pulse-width modulator. It takes an 8-bit duty and produces a square wave whose high time is duty out of every 256 clocks. Along the way we will simulate it, look at its waveform, check the generated Verilog against the Julia model, and write the Verilog out.
Describe it
Three things to notice:
- A field with
@inis an input: it has no storage, and the block reads it by name.= 128is its default, so we may leave it out when stepping. - A field with
@outis an output register, and a field with neither is an internal register.count::Bits{8} = 0is an 8-bit unsigned register that powers up at zero. count ← count + 1schedules the new value for the end of the cycle.count + 1wraps at 8 bits, as hardware does, so this counter runs from 0 to 255 and round again with no extra code.
@on Pwm posedge(clk) begin ... end is always @(posedge clk) begin ... end, and ← is <=. Every write in the block sees the register values from before the edge; the writes land together afterwards. There is no blocking assignment.
Step it by hand
A module is a value. step takes one and returns the next, one clock edge later:
m = Pwm()
m = step(m; duty = Bits{8}(3))
m = step(m; duty = Bits{8}(3))
mPwm(Bits{8}(02h), true, (duty = Bits{8}(80h),))
Let’s run a full period and count the high cycles:
m = Pwm()
highs = 0
for _ in 1:256
global m = step(m; duty = Bits{8}(64))
global highs += m.pwm
end
highs64
64 of 256, as promised. step is all you need for a unit test — we will see a nicer way to drive a longer run next.
Simulate it
For anything longer than a few cycles, a Simulation wraps the module with a clock and a recorder. We give the clock a real rate, and say which nets to watch ("*" means all of them):
sim = Simulation(Pwm(); clocks = (clk = 1MHz,), watch = "*")Simulation(Pwm, 4 nets, 4 watched, t = 0s)
Then we drive it with a @run block. Inside, sim.duty = v drives an input, and advance_by lets time pass:
out = @run sim begin
sim.duty = 64
advance_by(1ms)
sim.duty = 192
advance_by(1ms)
endCapture(4 signals, 2.0ms)
What comes back is a capture of every watched net. Ask it what a net was at some time, or list its changes:
out.pwm[0.5ms], out.pwm[1.5ms](false, false)
changes(out.pwm)[1:6]6-element Vector{Pair{Float64, Union{Missing, Bool}}}:
0.0 => 0
1.0e-6 => 1
6.5e-5 => 0
0.000257 => 1
0.000321 => 0
0.000513 => 1
And with Plots loaded, plot it:
using Plots
plot(out, "pwm"; xlims = (0.9ms, 1.1ms))The duty steps from 25% to 75% at 1 ms, which is what we asked for. Simulation covers the rest of this: watching specific nets, waiting on conditions, running tasks alongside the stimulus, and a REPL prompt for interactive poking.
Check it against the Verilog
Now the part that makes all of this trustworthy. cosim writes the Verilog for the module, runs it under Icarus Verilog with the same inputs as the Julia model, and compares every output after every clock:
stimulus = [(duty = Bits{8}(i < 300 ? 64 : 192),) for i in 1:600]
r = cosim(Pwm, stimulus)
r.oktrue
If r.ok were false, r.mismatches would list the first cycles where the two disagreed. Because the two came from one source, a mismatch means a bug in QuartzHDL rather than in your design — and that is the kind of bug that gets fixed first.
cosim needs iverilog and vvp on your path. On a Mac, brew install icarus-verilog.
Write the Verilog
write(stdout, Pwm, Verilog());module Pwm (
input wire clk_i,
input wire [7:0] duty_i,
output wire pwm_o
);
wire clk = clk_i;
wire [7:0] duty = duty_i;
reg [7:0] count;
reg pwm;
wire [7:0] w13 = count + 8'h1;
wire w14 = count < duty;
always @(posedge clk_i) begin
begin
count <= w13;
pwm <= w14;
end
end
assign pwm_o = pwm;
endmodule
Passing a path instead of stdout writes a file. The ports carry direction suffixes (duty_i, pwm_o, clk_i) so the pin list reads clearly in the vendor tools; Verilog output shows how to change the naming, and how to generate the constraint file that puts each port on a pin.
There is also a command line, for when you want a .v file without writing a driver script:
julia -m QuartzHDL pwm.jl --top Pwm -o pwm.v
What next
You have now seen every part of the workflow. The chapters that follow take each part in turn, starting with modules and registers — the types, the ports, and what a default means.