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}  ext_pull=:up
  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
Main.Notebook.Rev2

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, drive, ext_pull) 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,).

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;
  • the design declares ext_pull on a pad and the board does not provide it.
@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 needs an external pull-up and Wrong gives none
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:87

Each of those is a bug that would otherwise be found in the lab, with a scope.

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.