Boards 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

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

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

TipIf you know Verilog

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