Introduction

Write FPGA logic in Julia, simulate it in Julia, compile it to Verilog.

What QuartzHDL is

QuartzHDL is a small language for describing synchronous digital hardware, embedded in Julia. You write a module as a struct whose fields are the registers, and a block that says what those registers become on each clock edge. The same source then does two things:

  • it runs as ordinary Julia, one function call per clock edge, so you can poke at it, print it, plot it and test it like any other Julia code; and
  • it compiles to Verilog, ready for your FPGA vendor’s tools.

And because both come from one source, QuartzHDL can drive the Julia model and the generated Verilog with the same stimulus and check, cycle by cycle, that they agree.

NoteWhat QuartzHDL is not

QuartzHDL does not turn arbitrary Julia code into hardware, and it is not a replacement for Verilog or VHDL. It is a hardware description language that happens to be written in Julia, with the restrictions that hardware needs. If you know how to write an always @(posedge clk) block, you already know most of what QuartzHDL lets you say.

Why you might want it

It reads like the design, not like the wiring. Registers are typed fields with defaults, ports are declared with their direction and polarity, a state machine is a @fsm, a multi-step transaction is a @sequence, a handshake bit is a Pulse. Each of these is one line where Verilog needs several, and each is checked at the declaration rather than found in the lab.

You debug it in Julia. A module is a value. Step it, inspect a field, set a breakpoint on a condition, log from inside a block with @info, and look at waveforms in a viewer or in a plot. There is a REPL prompt for talking to a running simulation. None of this needs a simulator licence or a testbench in another language.

Porting a signal-processing algorithm becomes a sequence of small steps. If the reference implementation is Julia — a filter, a correlator, a demodulator — you can move it into fixed-width hardware one piece at a time, running the hardware model and the floating-point reference side by side on the same data and comparing the two at every stage. The helper functions you write along the way are plain Julia, traced into the Verilog without annotations.

The surrounding chips are in the library. A UART, a USB FIFO, SPI and I2C masters and slaves, a PWM source and a block RAM come as ready-made models that attach to your design’s pins, so a system test talks to the design the way a host would: write(uart, "AT\r\n"), readline(uart).

Testing and CI are just Julia testing and CI. A hardware test is a @testset. The Verilog co-simulation runs under Icarus Verilog on any CI machine. A pull request that changes a module runs the whole system test before anyone looks at it.

A first look

Here is a complete module: an 8-bit counter with an enable input and a tick output that fires when it wraps around.

using QuartzHDL

@quartz struct Counter
  @in  en::Bool = true
  @in  rst::Bool = false
  count::Bits{8} = 0
  @out tick::Bool
end

@on Counter posedge(clk) begin
  @reset(rst)
  if en
    count ← count + 1
    tick ← count == 255
  else
    tick ← false
  end
end

The @quartz struct declares the registers and ports. @on says what happens on each rising edge of clk: ← is a register write that lands at the end of the cycle, exactly like Verilog’s <=. To simulate, we make a module and step it:

m = Counter()
for i in 1:300
  m = step(m; en = true)
  m.tick && println("wrapped after $i clocks")
end
m.count
wrapped after 256 clocks
Bits{8}(2ch)

And to get Verilog, we write the module type out:

write(stdout, Counter, Verilog());
module Counter (
  input wire clk_i,
  input wire en_i,
  input wire rst_i,
  output wire tick_o
);

  wire clk = clk_i;
  wire en = en_i;
  wire rst = rst_i;
  reg [7:0] count;
  reg tick;

  wire [7:0] w6 = count + 8'h1;
  wire w8 = count == 8'hff;

  always @(posedge clk_i) begin
    if (rst) begin
      count <= 8'h0;
    end else begin
      if (en) begin
        count <= w6;
        tick <= w8;
      end else begin
        tick <= 1'h0;
      end
    end
  end

  assign tick_o = tick;
endmodule

That is the whole loop: describe, simulate, compile. The rest of this manual walks through it slowly.

Installation

QuartzHDL needs Julia 1.10 or later.

using Pkg
Pkg.add("QuartzHDL")

Some features need tools on your path, and the manual says so where it uses them: Icarus Verilog (iverilog, vvp) for co-simulation, and Surfer for live waveforms. On a Mac with Homebrew, brew install icarus-verilog surfer gets both.

Where to go next

TipIf you know Verilog

Boxes like this one appear throughout the manual to point out where QuartzHDL differs from what a Verilog author would expect.