@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 files
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 files are written from the two together, and refuse 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.
QSF and SDC: the Quartus files
A part Quartus builds for, such as a MAX 10 or a Cyclone, takes two files, because Quartus keeps settings apart from timing. QSF writes the settings: the family and the device, the pin of every port, the I/O standard, weak pull-up and drive strength of each, and a global clock assignment for every net named by @primary. A board-wide io also becomes the device’s default standard, which sets the voltage of the banks no port lands in; without it Quartus supplies those at 2.5 V, and warns when the configuration pins then sit across two voltages. Quartus also warns of an output with no drive, so a board gives its outputs one, from the drives the standard allows on that part; on a MAX 10 the 3.3 V LVCMOS standard allows only 2 mA, and an LED wants LVTTL. SDC writes the timing constraints: a create_clock for every clock that comes in on a pin, derive_pll_clocks for the clocks a vendor part makes, and for every multicycle path a setup exception of N cycles paired with a hold exception of N-1, which is where Quartus expects the hold check to stay. A clock a part makes is a pin of an atom inside the part’s netlist, which the constraint cannot name after synthesis, so Quartus derives it from the part’s own settings; the period the design expects for each is written beside the line as a comment, to check against the timing report. That covers a PLL, which is what makes clocks on these parts. A part that makes a clock some other way gets nothing from derive_pll_clocks, and its create_generated_clock, on the pin inside its netlist, is written by hand.
@board Kit begin
"the MAX 10 10M08S evaluation kit"
device = "10M08SAE144C8G"
io = :LVCMOS33
clk => (pin=27, osc=50MHz)
rst => (pin=120, ext_pull=:up)
sda => (pin=74, ext_pull=:up)
led => (pins=[132, 134, 135, 140])
end
write(stdout, Top, QSF(Kit));
write(stdout, Top, SDC(Kit));# generated by QuartzHDL for Top on Kit (10M08SAE144C8G)
set_global_assignment -name FAMILY "MAX 10"
set_global_assignment -name DEVICE 10M08SAE144C8G
set_global_assignment -name STRATIX_DEVICE_IO_STANDARD "3.3-V LVCMOS"
set_location_assignment PIN_27 -to clk_i
set_location_assignment PIN_120 -to rst_ni
set_location_assignment PIN_74 -to sda_io
set_location_assignment PIN_132 -to led_o[0]
set_location_assignment PIN_134 -to led_o[1]
set_location_assignment PIN_135 -to led_o[2]
set_location_assignment PIN_140 -to led_o[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to clk_i
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to clk_i
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to rst_ni
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to rst_ni
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sda_io
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to sda_io
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to led_o[0]
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to led_o[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to led_o[1]
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to led_o[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to led_o[2]
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to led_o[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to led_o[3]
set_instance_assignment -name WEAK_PULL_UP_RESISTOR OFF -to led_o[3]
# generated by QuartzHDL for Top on Kit; the clocks and timing exceptions the design declares
create_clock -name {clk_i} -period 20.0 [get_ports {clk_i}]
derive_clock_uncertainty
The board is written the same way whichever vendor made the part. The device decides which files it takes: LPF refuses a part Quartus builds for, and QSF and SDC refuse any other, so a board cannot be given to the wrong tool by mistake. Two attributes have a vendor’s flavour. An I/O standard is written as :LVCMOS33 and spelt for Quartus as "3.3-V LVCMOS"; a standard QuartzHDL has no spelling for is given as a string in Quartus’s own words, io = "3.3-V SCHMITT TRIGGER". And these parts have no weak pull-down, so pull = :down is refused on them; a resistor on the board is ext_pull = :down on any part.
What they refuse
The constraint writers check the design against the board and refuse 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:14 [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:115
Each of those would otherwise be found only on the board.
@board plus LPF, or QSF and SDC, replaces the hand-maintained .lpf or .qsf and .sdc and keeps them in step with the design: rename a port, and the constraint line follows; add a Multicycle wire, and its exception appears. Lattice’s and Quartus’s formats are written today; the @board description itself holds nothing vendor-specific except the device name and the spelling of an I/O standard given as a string.
The command line
quartz design.jl --top Top --board Rev2 --outdir build
quartz design.jl --top Top --board Kit --outdir build
writes Top.v and Rev2.lpf together, or Top.v, Kit.qsf and Kit.sdc, 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.