@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
@board: pin assignments 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 PORT "clk_i" 12.000000 MHz ;
LPF writes the Lattice constraint file: pin sites, buffer settings, the frequency of every clock the logic runs on (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.
A clock that arrives on a pin needs care, because synthesis puts a buffer behind the pin and names the net after it clk_i_c: a constraint on the net clk_i would match nothing, and the tool would drop it with only a warning. So its frequency is written against the port, FREQUENCY PORT "clk_i", and its global buffer against the buffered net, USE PRIMARY NET "clk_i_c" — the name Synplify gives it, the synthesiser a Diamond workspace uses. A clock that nothing runs on, such as one only read as data, gets no frequency line, since there is no logic for it to time.
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; - a black box that makes a clock sits below the top module, where the constraints cannot reliably name its clocks (see black boxes).
@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, overconstrain::Float64) @ QuartzHDL ~/Projects/QuartzHDL.jl/src/emitters/lpf.jl:11 [3] write(io::Base.DevNull, T::Type{Top}, f::LPF) @ QuartzHDL ~/Projects/QuartzHDL.jl/src/emitters/lpf.jl:7 [4] top-level scope @ ~/Projects/QuartzHDL.jl/qdocs/boards.qmd:93
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.