Boards 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

@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)
end

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_fast

Each 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 pull nor ext_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.

TipIf you know Verilog

@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.