First Lights: LEDs, Buttons & Switches
Blinky from a 125 MHz clock, counters as clock dividers, synchronising and debouncing buttons, and programming the board for the first time.
1. The goal: make the board do something
You have written HDL, simulated it, and walked through the Vivado flow. Now we put a real design on the PYNQ-Z2. By the end of this chapter four green LEDs will run as a light chaser whose speed you set with a switch, and a second mode will count presses of a push-button. It sounds modest, but it forces you to solve the three problems every FPGA design meets on day one: making slow events from a fast clock, bringing in signals that are not synchronised to that clock, and taming mechanical contacts that bounce.
2. Slow events from a fast clock
The PYNQ-Z2 feeds the programmable logic a 125 MHz clock on pin H16, so each clock period lasts 8 ns. An LED blinking at 125 MHz just looks dimly lit. The simplest way to get something visible is a free-running binary counter: each bit toggles half as often as the bit below it.
Bit 0 changes on every clock edge, so it completes one period every 2 cycles. Bit 1 changes every 2 cycles and completes a period every 4. In general bit \(k\) of an \(N\)-bit counter flips every \(2^k\) cycles, so
For a blink of about 1 Hz: \(\log_2(125\times10^6) - 1 \approx 25.9\). Bit 26 gives\(125\,\text{MHz}/2^{27} = 0.93\ \text{Hz}\), close enough to see. That needs a 27-bit counter, which the synthesiser builds from 27 flip-flops and a fast carry chain (Chapter 5).
module blinky (
input wire clk, // 125 MHz on the PYNQ-Z2
output wire led // LD0
);
reg [26:0] count = 27'd0;
always @(posedge clk)
count <= count + 1'b1;
// Bit 26 toggles every 2^26 cycles:
// f = 125 MHz / 2^27 = 0.93 Hz
assign led = count[26];
endmoduleNotice the initial value on the counter (= 27'd0 and := (others => '0')). On Xilinx FPGAs every flip-flop is loaded with a defined value when the bitstream is configured, and the synthesiser honours these initialisers. That is why this design needs no reset at all.
3. Exact rates and the clock-enable pattern
Powers of two rarely give the rate you want. For an exact period, count up to a chosen limit and wrap around. A tick generator does this and outputs a pulse that is high for exactly one clock cycle each time it wraps:
module tick_gen #(
parameter DIV = 125_000_000 // one tick per DIV clock cycles
) (
input wire clk,
input wire rst,
output reg tick = 1'b0 // high for exactly one cycle
);
localparam W = $clog2(DIV);
reg [W-1:0] count = 0;
always @(posedge clk) begin
if (rst) begin
count <= 0;
tick <= 1'b0;
end else if (count == DIV - 1) begin
count <= 0;
tick <= 1'b1;
end else begin
count <= count + 1'b1;
tick <= 1'b0;
end
end
endmoduleIn VHDL the counter is a natural range 0 to DIV - 1; the synthesiser works out the width from the range. In Verilog, $clog2 computes it. Either way, DIV is a parameter (Verilog) or generic (VHDL): one module serves for 8 Hz on the board and for a tick every 5 cycles in a testbench.
tick is high — not by creating a slower clock. Clock enables are built into every FPGA flip-flop, so they cost nothing, and the timing tools can check a single-clock design completely.always @(posedge count[24]) or rising_edge(count(24)) creates a derived clock that travels through ordinary routing instead of the dedicated clock network. Its edges arrive at different flip-flops at different times, and data passing between it and the main clock is not timed. It may work on your desk and fail on a warm day. Use a tick and an enable instead.4. Buttons: asynchronous and bouncy
A push-button has two problems. First, you can press it at any moment, so its edges have no relation to the clock. A flip-flop that samples an input changing inside its setup/hold window can go metastable — hover between 0 and 1 before falling one way. The standard cure is two flip-flops in series: the first may wobble, but it has a full clock period to settle before the second one samples it.
Second, the metal contacts physically bounce for a few milliseconds, producing a burst of pulses. At 125 MHz each of those pulses lasts hundreds to thousands of clock cycles or more — the FPGA sees every one of them as a separate press. A debouncer only accepts a new level after it has stayed unchanged for a set time. With a 10 ms window:
Finally, most logic wants an event — “the button was just pressed” — rather than a level. Comparing the clean level with its value one cycle earlier gives a one-cycle pulse on each rising edge.
module debounce #(
parameter STABLE = 1_250_000 // 10 ms at 125 MHz
) (
input wire clk,
input wire btn_in, // raw, bouncing, asynchronous
output reg btn_out, // clean level
output wire press // one-cycle pulse on each press
);
// 1. Two-flip-flop synchroniser
reg sync0 = 1'b0, sync1 = 1'b0;
always @(posedge clk) begin
sync0 <= btn_in;
sync1 <= sync0;
end
// 2. Accept a new level only after it has held for STABLE cycles
localparam W = $clog2(STABLE + 1);
reg [W-1:0] count = 0;
initial btn_out = 1'b0;
always @(posedge clk) begin
if (sync1 == btn_out)
count <= 0;
else if (count == STABLE - 1) begin
btn_out <= sync1;
count <= 0;
end else
count <= count + 1'b1;
end
// 3. Rising-edge detector
reg btn_prev = 1'b0;
always @(posedge clk)
btn_prev <= btn_out;
assign press = btn_out & ~btn_prev;
endmoduleThe VHDL keeps the clean level in an internal signal level and copies it to the output port. That is because in VHDL-93 — the default language version in Vivado — you cannot read an out port inside the architecture. Verilog has no such rule, so btn_out is used directly.
5. Check it in simulation first
Never send an untested design to the board. Setting STABLE to 4 cycles lets a testbench feed the debouncer a burst of short glitches and check that exactly one press comes out. Both versions below print PASS at 488 ns — the two languages describe the same hardware, cycle for cycle.
`timescale 1ns/1ps
module debounce_tb;
reg clk = 0, btn = 0;
wire level, press;
integer presses = 0;
debounce #(.STABLE(4)) dut (.clk(clk), .btn_in(btn), .btn_out(level), .press(press));
always #4 clk = ~clk; // 125 MHz: 8 ns period
always @(posedge clk) if (press) presses = presses + 1;
// Hold the raw button at value v for n clock cycles
task hold(input v, input integer n);
begin btn = v; repeat (n) @(negedge clk); end
endtask
initial begin
hold(0, 10);
hold(1, 1); hold(0, 2); hold(1, 1); hold(0, 1); // bounce on press
hold(1, 20); // settled: pressed
hold(0, 2); hold(1, 1); hold(0, 1); hold(1, 2); // bounce on release
hold(0, 20); // settled: released
if (presses == 1 && level == 0)
$display("PASS: %0d press detected, level back to 0", presses);
else
$display("FAIL: presses=%0d level=%b", presses, level);
$finish;
end
endmodule# Verilog with Icarus iverilog -g2012 -o tb debounce.v debounce_tb.v && vvp tb # VHDL with GHDL ghdl -a --std=08 debounce.vhd debounce_tb.vhd ghdl -e --std=08 debounce_tb && ghdl -r --std=08 debounce_tb
6. The complete design
The top level connects the pieces. Note that the slide switches also pass through a two-flip-flop synchroniser: they do not bounce much, but they are just as asynchronous as the buttons.
module first_lights #(
parameter TICK_DIV = 15_625_000, // 125 MHz / 8 = 8 ticks per second
parameter DEBOUNCE = 1_250_000 // 10 ms
) (
input wire clk, // 125 MHz
input wire [1:0] btn, // BTN0 = count, BTN1 = clear
input wire [1:0] sw, // SW0 = fast/slow, SW1 = mode
output wire [3:0] led
);
// Switches are asynchronous too: synchronise them
reg [1:0] sw_s0 = 2'b00, sw_s1 = 2'b00;
always @(posedge clk) begin
sw_s0 <= sw;
sw_s1 <= sw_s0;
end
// 8 Hz tick, and a 2 Hz tick derived from it
wire tick8;
tick_gen #(.DIV(TICK_DIV)) u_tick (.clk(clk), .rst(1'b0), .tick(tick8));
reg [1:0] pre = 2'd0;
always @(posedge clk)
if (tick8) pre <= pre + 1'b1;
wire step = sw_s1[0] ? tick8 : (tick8 && pre == 2'd3);
// Clean button presses
wire inc, clr;
debounce #(.STABLE(DEBOUNCE)) u_b0 (.clk(clk), .btn_in(btn[0]), .btn_out(), .press(inc));
debounce #(.STABLE(DEBOUNCE)) u_b1 (.clk(clk), .btn_in(btn[1]), .btn_out(), .press(clr));
// Mode 0: running light. Mode 1: press counter.
reg [3:0] runner = 4'b0001;
reg [3:0] presses = 4'd0;
always @(posedge clk) begin
if (step)
runner <= {runner[2:0], runner[3]}; // rotate left
if (clr)
presses <= 4'd0;
else if (inc)
presses <= presses + 1'b1;
end
assign led = sw_s1[1] ? presses : runner;
endmodule| SW1 | SW0 | LEDs show | Buttons |
|---|---|---|---|
| 0 | 0 | light chaser, 2 steps/s | ignored |
| 0 | 1 | light chaser, 8 steps/s | ignored |
| 1 | x | press count in binary | BTN0 +1, BTN1 clear |
7. Constraints: telling Vivado where the pins are
The HDL only names the ports. The constraint file maps each one to a physical pin of the Zynq package, sets its voltage standard, and declares the clock period so the timing analyser knows the target.
## PYNQ-Z2 constraints for first_lights (all pins are 3.3 V banks)
## 125 MHz system clock
set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -name sys_clk -period 8.000 -waveform {0 4} [get_ports clk]
## Push-buttons BTN0, BTN1
set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports {btn[0]}]
set_property -dict { PACKAGE_PIN D20 IOSTANDARD LVCMOS33 } [get_ports {btn[1]}]
## Slide switches SW0, SW1
set_property -dict { PACKAGE_PIN M20 IOSTANDARD LVCMOS33 } [get_ports {sw[0]}]
set_property -dict { PACKAGE_PIN M19 IOSTANDARD LVCMOS33 } [get_ports {sw[1]}]
## Green LEDs LD0-LD3
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports {led[0]}]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports {led[1]}]
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports {led[2]}]
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports {led[3]}]- Set the PYNQ-Z2 boot jumper to JTAG, connect the micro-USB cable (PROG/UART) and switch the board on.
- In Vivado, create an RTL project for part
xc7z020clg400-1. Addtick_gen,debounceandfirst_lights(all Verilog or all VHDL) and the XDC file. Setfirst_lightsas top. - Click Generate Bitstream. Vivado runs synthesis, implementation and bitstream generation; open the implemented design's timing summary and check that WNS is positive.
- Open Hardware Manager → Open target → Auto connect → Program device. LD0 should start chasing within a second.
- Flip SW0 to change the speed. Flip SW1 and press BTN0 a few times: the LEDs count in binary. BTN1 clears.
Already running the PYNQ Linux image from the SD card? You can skip JTAG: copy first_lights.bit to the board and run pynq.Bitstream("first_lights.bit").download() in a notebook. Part VII uses this route all the time.
xc7z020clg400-1; that the port names in the XDC match the HDL exactly (led[0], not LED0); and that the Messages tab has no critical warning about unplaced ports. A port with no pin assignment makes bitstream generation fail on purpose, to protect the hardware.8. Python: watching a button bounce
This model runs the debouncer cycle by cycle on a simulated bouncing contact. It uses a 1 MHz clock and a 2 ms window so the plot stays readable. Each loop iteration is one clock edge, and all new values are computed from the old ones before any is updated — exactly what non-blocking assignment means in hardware.
Click Run to execute the Python code
Code will be executed with Python 3 on the server
Practice Problems
tick_gen. What value of DIV do you use, and how wide is the internal counter?always @(posedge count[24]) to make a slow process. It works on the bench but Vivado prints a timing warning. Explain what went wrong and rewrite the idea correctly.STABLE = 1_250_000. A button bounces for at most 4 ms. (a) What is the delay between the end of the bounce and the clean output changing? (b) Could you make STABLE smaller, and what is the risk?