Beginner · Lesson 10 of 10

From RTL to board: the full flow

Constraints files for AMD, Altera, Lattice and Gowin, pin assignment and I/O standards, what the timing report tells you, and a worked blinky on the Basys 3 and the Tang Nano 9K.

BeginnerAMDGowinOpen source45 minUpdated 13 Nov 2026

You will learn

  • Write a constraints file that assigns pins, sets I/O standards and defines the clock
  • Read a timing report and explain what met timing and worst negative slack mean
  • Build and program a design on the Basys 3 with Vivado and on the Tang Nano 9K with Yosys, nextpnr and openFPGALoader
  • Diagnose the common first-time errors: unconstrained pins, wrong voltage standard, missing clock constraint and a board that will not program

Before this: memories block ram and fifos

You have written modules, simulated them and learned what the synthesiser does with them. This lesson closes the loop: it takes a design from source files to a blinking LED on two real boards, and explains the one file you have not written yet, the constraints file. It uses the Digilent Basys 3 with Vivado and the Sipeed Tang Nano 9K with the open-source flow, so you see a vendor tool and a free tool doing the same job. Lesson 02 described the flow in outline. Here you run it.

The flow, once more, with files

top.vsynthesisplace, routebitstreamboardnetlistJTAGconstraints.xdc .sdc .cst .pcftiming reportslack, WNS, fmax
The implementation flow. The constraints file is read at every stage after synthesis, and the timing report is produced after routing.

Synthesis turns your HDL into a netlist of the device’s primitives: LUTs, flip-flops, block RAMs, I/O buffers. Place and route decides which physical LUT and flip-flop each one becomes, and which wires connect them. Bitstream generation encodes the result into the configuration data, and a programmer pushes it into the chip over JTAG. Simulation is not on this path at all. It happened before, and it should have happened before.

The constraints file is the only input other than your HDL. It tells the tool two things it cannot find out on its own: which package pin each top-level port is attached to, with what electrical standard, and how fast the clock runs.

Constraints files by vendor

Every vendor has its own format for the same information.

Vendor Tool File Clock constraint Pin constraint
AMD Vivado .xdc create_clock set_property PACKAGE_PIN, IOSTANDARD
Altera Quartus Prime .sdc for timing, .qsf for pins create_clock set_location_assignment, set_instance_assignment
Lattice Radiant, Diamond .pdc or .lpf, plus .sdc create_clock ldc_set_location, LOCATE COMP
Lattice iCE40 Yosys and nextpnr-ice40 .pcf --freq or a .py constraint set_io
Gowin Gowin EDA, nextpnr-himbaechel .cst for pins, .sdc for timing create_clock IO_LOC, IO_PORT

The clock side is nearly universal. AMD, Altera and Lattice all use the Synopsys Design Constraints language, where create_clock -period 10.000 declares a 100 MHz clock. The pin side varies, but it always amounts to a port name, a pin name and an I/O standard.

Pins and I/O standards

An FPGA pin can be configured for many electrical standards: 3.3 V CMOS, 2.5 V, 1.8 V, 1.5 V, several differential standards and more. The standard must match what the pin is wired to on the board, and all the pins in one I/O bank must agree with the bank’s supply voltage. On the Basys 3 every pin you will use is in a bank powered at 3.3 V, so the standard is LVCMOS33. On the Tang Nano 9K the banks are also 3.3 V. Choosing the wrong standard is at best a tool error and at worst damage to the device, so take it from the board’s documentation, not from memory.

Board makers publish a master constraints file listing every pin on the board. The usual way to work is to copy it, delete everything you are not using, and rename the ports to match your module. The pin facts for both boards in this lesson come from those files.

Worked example: Basys 3 with Vivado

The design is a blinky with a button. LED0 blinks at about 1.5 Hz, LED1 mirrors the centre button, and SW0 freezes the blink. On the Basys 3 the 100 MHz oscillator is on pin W5, LED0 is on U16, LED1 on E19, the centre button BTNC on U18 and SW0 on V17.

// Blinky plus a button for the Digilent Basys 3.
// LED0 blinks at about 1.5 Hz, LED1 shows the centre button, SW0 pauses the blink.
module top_basys3 (
  input  wire clk,     // 100 MHz
  input  wire btnc,
  input  wire sw0,
  output wire led0,
  output wire led1
);
  reg [25:0] count = 26'd0;

  always @(posedge clk)
    if (!sw0)
      count <= count + 1'b1;

  assign led0 = count[25];   // 100 MHz / 2^26 = 1.49 Hz
  assign led1 = btnc;
endmodule

The counter is initialised with = 26'd0 rather than reset, which the bitstream honours on AMD parts. The switch is read directly as an enable. For a blinky that is fine. In anything with a state machine you would put the button and switch through the synchroniser from lesson 08.

The constraints file, basys3.xdc:

## Clock: 100 MHz oscillator on W5
set_property -dict { PACKAGE_PIN W5 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -add -name sys_clk -period 10.000 -waveform {0 5} [get_ports clk]

## LEDs
set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports led0]
set_property -dict { PACKAGE_PIN E19 IOSTANDARD LVCMOS33 } [get_ports led1]

## Centre button and switch 0
set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports btnc]
set_property -dict { PACKAGE_PIN V17 IOSTANDARD LVCMOS33 } [get_ports sw0]

## Configuration bank voltage, needed to silence a DRC warning on this family
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]

XDC is Tcl. Each set_property line attaches a pin and a standard to a port found by get_ports. The create_clock line declares that the signal on clk is a clock with a 10 ns period and a rising edge at 0 ns and a falling edge at 5 ns. Without it the timing tool has nothing to check against and will report that the design met timing while telling you nothing. The last two lines stop a warning about the configuration bank voltage on this family.

In Vivado, create a project for part xc7a35tcpg236-1, add top_basys3.v as a design source and basys3.xdc as a constraint, and click Generate Bitstream. Vivado runs synthesis, implementation and bitstream generation in turn. The same steps in Tcl, for a script you can keep in version control:

read_verilog top_basys3.v
read_xdc basys3.xdc
synth_design -top top_basys3 -part xc7a35tcpg236-1
opt_design
place_design
route_design
report_timing_summary -file timing.rpt
write_bitstream -force top_basys3.bit

Plug in the board, open Hardware Manager, Open Target, Auto Connect, then Program Device and choose top_basys3.bit. The Basys 3 has its JTAG programmer on the same USB connector as its power, so one cable does both. LED0 should blink, LED1 should follow the button, and SW0 up should stop the blink.

Worked example: Tang Nano 9K with the open-source flow

The Tang Nano 9K carries a Gowin GW1NR-LV9QN88PC6/I5, a 27 MHz crystal on pin 52, six LEDs on pins 10, 11, 13, 14, 15 and 16, and two buttons, with S1 on pin 4. The LEDs and buttons are active low, and the I/O banks are at 3.3 V. The module changes to suit.

// Blinky plus a button for the Sipeed Tang Nano 9K.
// LEDs and the button are active low on this board.
module top_tangnano9k (
  input  wire       clk,    // 27 MHz
  input  wire       btn_n,  // S1, low when pressed
  output wire [5:0] led_n   // low lights the LED
);
  reg [24:0] count = 25'd0;

  always @(posedge clk)
    count <= count + 1'b1;

  // LED0 blinks at 27 MHz / 2^25 = 0.8 Hz. LED1 follows the button.
  // The remaining LEDs are driven high, which turns them off.
  assign led_n = {4'b1111, btn_n, ~count[24]};
endmodule

The pin constraints go in a Gowin CST file, tangnano9k.cst:

IO_LOC "clk" 52;
IO_PORT "clk" PULL_MODE=UP;
IO_LOC "btn_n" 4;
IO_PORT "btn_n" PULL_MODE=UP;
IO_LOC "led_n[0]" 10;
IO_LOC "led_n[1]" 11;
IO_LOC "led_n[2]" 13;
IO_LOC "led_n[3]" 14;
IO_LOC "led_n[4]" 15;
IO_LOC "led_n[5]" 16;
IO_PORT "led_n[0]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[1]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[2]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[3]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[4]" IO_TYPE=LVCMOS33 DRIVE=8;
IO_PORT "led_n[5]" IO_TYPE=LVCMOS33 DRIVE=8;

The open-source flow for Gowin is three tools: Yosys for synthesis, nextpnr with the himbaechel backend and the Apicula device database for place and route, and gowin_pack from Apicula to write the bitstream. openFPGALoader programs it.

yosys -p "read_verilog top_tangnano9k.v; synth_gowin -top top_tangnano9k -json top.json"
nextpnr-himbaechel --json top.json --write top_pnr.json \
  --device GW1NR-LV9QN88PC6/I5 \
  --vopt family=GW1N-9C --vopt cst=tangnano9k.cst
gowin_pack -d GW1N-9C -o top.fs top_pnr.json
openFPGALoader -b tangnano9k top.fs

synth_gowin is the Yosys script that maps to Gowin primitives. nextpnr prints its own timing estimate at the end of routing, including the maximum frequency it believes the clock can run at. The openFPGALoader line loads the bitstream into SRAM, so it is lost at power-off. Add -f to write it to the on-board flash instead.

The same flow with Gowin’s own tool, Gowin EDA, uses the same CST file and an SDC file with create_clock -name clk -period 37.037 [get_ports {clk}]. The free licence covers this device.

The timing report

After routing, the tool checks every path from a flip-flop output, through logic and wires, to the next flip-flop input, and compares the delay against the clock period you declared. The result is a slack for each path: the period minus the delay, after setup time and clock uncertainty are subtracted. Positive slack means the signal arrives in time. Negative slack means it does not, and the design will misbehave at that frequency even though it simulated perfectly.

Vivado’s summary leads with three numbers. WNS, worst negative slack, is the slack of the single worst setup path. TNS, total negative slack, is the sum of all negative slacks, a measure of how far from passing you are. WHS, worst hold slack, is the same idea for hold time, which is about a signal changing too soon rather than too late. “Met timing” means all three are zero or positive. For the blinky the counter’s carry chain is the longest path and WNS will be several nanoseconds positive.

Quartus Prime presents the same information in its Timing Analyzer, with a Fmax summary per clock and a list of failing paths. nextpnr prints a per-clock Fmax line and marks it PASS or FAIL against the target. The timing topic and the intermediate track go deeper into what to do when a path fails.

Common first-time errors

An unconstrained port. Vivado by default refuses to write a bitstream if any port has no pin assignment, with DRC errors named UCIO-1 (unconstrained pin) or NSTD-1 (no I/O standard). The fix is to add the missing line to the XDC. The port name in the constraint must match the HDL exactly, including bus indices such as led[0].

A port that does not exist. If the XDC names a port the module does not have, Vivado warns and carries on, so a typo in the constraints produces a design that builds and does nothing. Read the critical warnings before you program.

Wrong I/O standard for the bank. The tool reports a voltage conflict between pins in the same bank. On the beginner boards here, everything is LVCMOS33.

Active-low confusion. On the Tang Nano 9K an LED driven with 1 is off and a button reads 1 until pressed. The _n suffix in the port names is there to remind you.

A clock pin that is not a clock-capable pin. Dedicated clock inputs sit on particular pins. The board designers have put the oscillator on one, but if you ever feed a clock in through a general-purpose header pin the tool will complain or route it badly. Use the pin the board provides.

The board does not program. The usual causes, in order: the USB cable is power-only, the driver or udev rule for the programmer is missing, another program has the port open, or the board is in a flash-boot mode. openFPGALoader --detect and Vivado’s Auto Connect both tell you whether they can see the device at all, which separates cable problems from design problems.

The build passed and the LED does nothing. Check that the clock constraint matches the real oscillator, that the counter is wide enough to produce a visible frequency, and that the design was actually programmed rather than an older bitstream. Then put the top-level module in a testbench with the counter width reduced and watch it blink in simulation. Most “the board is broken” reports end there.

Exercise

Add the debouncer from lesson 08 to the Basys 3 design so that each press of BTNC changes the blink rate between four settings, and show the setting on LED1 to LED4. You will need to look up the extra LED pins in the Digilent master XDC. Build it, open the timing summary and write down the WNS. Then change the clock constraint to 5 ns, rebuild, and see whether the design still meets timing at 200 MHz. On the Tang Nano 9K, do the same with S2 and the remaining LEDs, and compare the Fmax that nextpnr reports against the 27 MHz you asked for.

Summary

  • The constraints file supplies what the HDL cannot: which pin each port uses, its I/O standard, and the clock period. Every vendor has a format, and the clock side is nearly always SDC.
  • Take pin numbers and I/O standards from the board’s master constraints file, and keep the bank voltage consistent.
  • The timing report compares every register-to-register path against the clock period. Met timing means worst negative slack is zero or positive, and it means nothing without a clock constraint.
  • Vivado and the Yosys, nextpnr and Apicula flow do the same job. One is a GUI and a Tcl script, the other is four commands.
  • When the board does nothing, check the critical warnings, the constraint names and the programmer connection before suspecting the hardware.

Further reading