@quartz struct Top
@in rst::Bool = false active=:low
@out led::Bits{4} = 0
@io sda::Pad{1} = Pad{1}(:pullup)
n::Bits{26} = 0
end
@on Top posedge(clk) begin
@reset(rst)
n ← n + 1
led ← n[22:25]
end
@wire Top sda ← release()
@board Rev2 begin
"revision 2 of the evaluation board"
device = "LFE5U-25F-6BG381C"
io = :LVCMOS33 # every pin below, unless it says otherwise
clk => (pin="G2", osc=12MHz) # a rate belongs with the pin it arrives on
rst => (pin="R1", pull=:up)
sda => (pin="T2", drive=8, ext_pull=:up)
led => (pins=["A9", "B9", "A10", "B10"], io=:LVCMOS25)
endBoards and constraints
Where each port goes: @board and the constraint file
The design says nothing about pins, and the board says nothing about logic. A @board is the pin map of one physical board; the constraint file is written from the two together, and refuses if they disagree.
@board
Every line is either a setting — key = value — or a binding of a port to its pin. Ports are named as the design names them; the constraint file names the pins as the emitted Verilog does. A port of an instance is addressed by path, uart.tx. A site is written the way the datasheet writes it: "G2" on a BGA, pin = 27 on a numbered package.
A setting that is a pin attribute (io, pull, ext_pull, drive) applies to every binding; a binding overrides it, nothing included — (pin=27, io=nothing) says “left to the tool, on purpose”. pins=[...] takes nothing for a bit with no pin, and an attribute may vary by bit: pull=(0:2 => :down,).
Two of those are pulls, and they say who provides one. pull is the FPGA’s own weak pull, configured in the bitstream, so it goes into the constraint file. ext_pull is a resistor on the board: it goes nowhere, but it is how the board says a net is pulled without the FPGA’s help. A pad that relies on a pull — Pad{1}(:pullup) in the design, which is what its simulation models — must get one from the board, either way; that is checked when the constraint file is written.
Attribute names are a closed set, checked where they are written, since a misspelt one that was quietly ignored would leave a pin with the wrong buffer. A string at the top documents the board; one before a binding documents the pin.
LPF: the constraint file
write(stdout, Top, LPF(Rev2));// generated by QuartzHDL for Top on Rev2 (LFE5U-25F-6BG381C)
LOCATE COMP "clk_i" SITE "G2" ;
LOCATE COMP "rst_ni" SITE "R1" ;
LOCATE COMP "sda_io" SITE "T2" ;
LOCATE COMP "led_o[0]" SITE "A9" ;
LOCATE COMP "led_o[1]" SITE "B9" ;
LOCATE COMP "led_o[2]" SITE "A10" ;
LOCATE COMP "led_o[3]" SITE "B10" ;
IOBUF PORT "clk_i" PULLMODE=NONE IO_TYPE=LVCMOS33 ;
IOBUF PORT "rst_ni" PULLMODE=UP IO_TYPE=LVCMOS33 ;
IOBUF PORT "sda_io" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=8 ;
IOBUF PORT "led_o[0]" PULLMODE=NONE IO_TYPE=LVCMOS25 ;
IOBUF PORT "led_o[1]" PULLMODE=NONE IO_TYPE=LVCMOS25 ;
IOBUF PORT "led_o[2]" PULLMODE=NONE IO_TYPE=LVCMOS25 ;
IOBUF PORT "led_o[3]" PULLMODE=NONE IO_TYPE=LVCMOS25 ;
FREQUENCY NET "clk_i" 12.000000 MHz ;
LPF writes the Lattice constraint file: pin sites, buffer settings, the frequency of every clock (from the oscillators and the clock tree, through every black box), the nets that want a global buffer, and the timing exceptions — one for every source and sink of a Multicycle wire, and any declared by hand with @multicycle.
Which clocks ride the chip’s global distribution is a choice about the design, made once on the top module:
@primary Top clk, clk_fastEach name must be a clock the design has, and LPF writes a USE PRIMARY NET line for each. A design that says nothing leaves the choice to the tool.
What it refuses
The constraint writer checks the design against the board and refuses to write anything if:
- a port has no pin, or a pin names a port that does not exist;
- two ports sit on one site;
- a port’s width does not match its pin count;
- a pad relies on a pull and the board provides none, neither
pullnorext_pull, or pulls the other way.
@board Wrong begin
device = "LFE5U-25F-6BG381C"
clk => (pin="G2", osc=12MHz)
rst => (pin="R1")
led => (pins=["A9", "B9", "A10"]) # three pins for a 4-bit port
sda => (pin="T2") # and no pull-up for an open-drain pad
end
write(devnull, Top, LPF(Wrong))Wrong and Top do not agree: led is 4 bits and Wrong gives it 3 pin(s) sda relies on a pull-up and Wrong provides none, neither its own (pull) nor one on the board (ext_pull) Stacktrace: [1] error(s::String) @ Base ./error.jl:44 [2] _lpf(io::IOBuffer, T::Type{Top}, b::Board) @ QuartzHDL ~/Projects/QuartzHDL.jl/src/emitters/lpf.jl:10 [3] write(io::Base.DevNull, T::Type{Top}, f::LPF) @ QuartzHDL ~/Projects/QuartzHDL.jl/src/emitters/lpf.jl:6 [4] top-level scope @ ~/Projects/QuartzHDL.jl/qdocs/boards.qmd:90
Each of those would otherwise be found only on the board.
@board plus LPF replaces the hand-maintained .lpf and keeps it in step with the design: rename a port, and the constraint line follows; add a Multicycle wire, and its exception appears. Only Lattice’s format is written today; the @board description itself holds nothing vendor-specific except the device name.
The command line
quartz design.jl --top Top --board Rev2 --outdir build
writes Top.v and Rev2.lpf together, which is what a build script calls.
Next
The appendices: a side-by-side with Verilog, the rules, a look under the hood, and the reference.