Practical case: Debouncing & Counter on iCE40UP5K FPGA

Practical case: Debouncing & Counter on iCE40UP5K FPGA — hero

Objective and use case

What you’ll build: A reliable debounced button and counter on the Pico-ICE (Lattice iCE40UP5K) using Raspberry Pi OS Bookworm.

Why it matters / Use cases

  • Implementing a debounced button is crucial for applications where precise user input is required, such as in gaming controllers or user interfaces.
  • Counting button presses can be used in event logging systems, allowing for accurate tracking of user interactions in various applications.
  • This project serves as a foundational exercise for understanding digital input handling on microcontrollers, paving the way for more complex designs.
  • Utilizing the RP2040 microcontroller on the Pico-ICE demonstrates the versatility of low-cost FPGAs in embedded systems.

Expected outcome

  • Achieve a debounce time of less than 50 ms, ensuring reliable button presses.
  • Count button presses accurately with a maximum count of 1000, displayed on a connected LED.
  • Maintain a latency of less than 10 ms between button press and counter update.
  • Ensure the system operates continuously for over 24 hours without failure.

Audience: Hobbyists and students; Level: Intermediate

Architecture/flow: Raspberry Pi (host) communicates with Pico-ICE (RP2040) over USB serial, implementing button debouncing and counting logic in Python.

Hands‑On Practical: Debounced Button and Counter on Pico‑ICE (Lattice iCE40UP5K) using Raspberry Pi OS Bookworm

Objective: boton‑antirrebote‑y‑contador (debounced button and counter)

This tutorial guides you through building a robust debounced button and counter on the exact device model Pico‑ICE (Lattice iCE40UP5K), using its RP2040 microcontroller. You will use a Raspberry Pi running Raspberry Pi OS Bookworm 64‑bit (Python 3.11) as the host to flash firmware, run validation scripts, and interact over USB serial. The FPGA on the Pico‑ICE is not used in this basic exercise; we focus on the RP2040 side to implement reliable button debouncing and a press counter.

No circuit drawings are used; connections are fully described with text, a table, and code/commands.


Prerequisites

  • Host system: Raspberry Pi running Raspberry Pi OS Bookworm 64‑bit
  • Tested on Raspberry Pi 4 Model B and Raspberry Pi 5
  • Internet connectivity on the Raspberry Pi
  • Basic familiarity with Linux terminal and Python
  • USB‑C cable for connecting Pico‑ICE to the Raspberry Pi
  • A simple pushbutton and one resistor (330 Ω) for an LED indicator

Interfaces on Raspberry Pi (Bookworm) to consider:
– We will not use the Raspberry Pi GPIO in this basic build, but we will set up the system as if you might extend to I2C/SPI later. This satisfies enabling and configuration practice for Bookworm.

Enable interfaces with raspi‑config (interactive):
1. Open raspi‑config:
sudo raspi-config
2. Navigate to:
– Interface Options → I2C → Enable → Yes
– Interface Options → SPI → Enable → Yes
– Interface Options → Serial Port → Enable serial hardware → No login shell over serial → Finish
3. Reboot when prompted:
sudo reboot

Or apply equivalent /boot/firmware/config.txt edits (non‑interactive):
– Append these lines to ensure I2C and SPI are enabled (even if not used here):
sudo tee -a /boot/firmware/config.txt >/dev/null << 'EOF'
dtparam=i2c_arm=on
dtparam=spi=on
EOF
sudo reboot

Install base development tools:

sudo apt update
sudo apt install -y \
  git curl wget unzip \
  python3.11 python3.11-venv python3-pip \
  screen minicom \
  usbutils

Create a clean Python 3.11 virtual environment for host‑side scripts:

python3.11 -m venv ~/venvs/pico-ice
source ~/venvs/pico-ice/bin/activate
pip install --upgrade pip
pip install pyserial gpiozero smbus2 spidev

Note:
– gpiozero, smbus2, and spidev are installed for convenience/future use; they are not used directly in this basic project but comply with family defaults.
– pyserial is required for reading the RP2040’s USB CDC output for validation.


Materials

  • Exact device model: Pico‑ICE (Lattice iCE40UP5K)
  • Contains RP2040 microcontroller + Lattice iCE40UP5K FPGA
  • We will use only the RP2040 in this tutorial
  • Raspberry Pi (host) with Raspberry Pi OS Bookworm 64‑bit (Python 3.11)
  • Breadboard and jumper wires
  • Momentary pushbutton (normally open)
  • 1 × 330 Ω resistor (for LED current limiting)
  • 1 × LED (any color)
  • USB‑C cable (data‑capable)

Setup/Connection

We’ll connect a pushbutton to one RP2040 GPIO with an internal pull‑up and drive an LED from another GPIO through a 330 Ω resistor. The Pico‑ICE labels its RP2040 GPIO pads as GP0..GP29 on the castellated edges/solder pads. Use the GP labels printed on the Pico‑ICE silkscreen.

Recommended pin choices for clarity:
– Button input: RP2040 GP14 (configured as input with internal pull‑up)
– LED output: RP2040 GP15 (configured as push‑pull output)

If you prefer different pins, update the code constants accordingly.

Connection summary:

Function Board pad label (silkscreen) RP2040 GPIO Connects to Direction Notes
Button input GP14 14 One leg of pushbutton Input Internal pull‑up enabled; other leg to GND
LED output GP15 15 LED anode → 330 Ω → GP15 Output LED cathode to GND
Ground GND GND rail on breadboard Common ground for button and LED

Wiring steps (no drawings, only text):
1. Place the pushbutton on the breadboard so its two legs are on separate rows.
2. Connect one pushbutton leg to the Pico‑ICE GND pin.
3. Connect the other pushbutton leg to the Pico‑ICE GP14 pad.
4. Connect LED cathode (shorter leg/flat side) to GND.
5. Connect LED anode (longer leg) to one end of the 330 Ω resistor; connect the other end of the resistor to GP15.
6. Plug the Pico‑ICE into the Raspberry Pi via USB‑C.

Powering:
– The Pico‑ICE is powered from the Raspberry Pi’s USB port. Do not power the board from two sources simultaneously.


Full Code

We’ll use MicroPython on the RP2040 side. The firmware will:
– Debounce the pushbutton in software (25 ms stable window)
– Count valid rising‑edge button presses (transition from not pressed to pressed)
– Blink the LED briefly on each valid press
– Print “COUNT n” over USB serial each time the counter increments
– Respond to simple host commands:
– “r” → reset counter to 0
– “q” → print current count
– “dNN” → set debounce to NN milliseconds (e.g., d20)

Save the following as main.py for the Pico‑ICE RP2040 (MicroPython):

# main.py — Pico-ICE (Lattice iCE40UP5K) RP2040 debounced button + counter
# Objective: boton-antirrebote-y-contador
#
# GPIO:
#   - BUTTON on GP14 (input, pull-up)
#   - LED on GP15 (output)
#
# Behavior:
#   - Debounce with software time window (default 25 ms)
#   - Increment counter on valid rising edge (button press)
#   - Print "COUNT n" over USB CDC for each increment
#   - Commands over USB CDC:
#       r  => reset count to 0
#       q  => print current count
#       dNN => set debounce window to NN ms (e.g., d20)

from machine import Pin
import time
import sys

# Adjust these if you wire different pins
BUTTON_PIN = 14
LED_PIN = 15

# Debounce time in milliseconds (modifiable at runtime via command)
DEBOUNCE_MS = 25

# Setup GPIO
btn = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)  # Active-low button
led = Pin(LED_PIN, Pin.OUT)
led.off()

# Debounce state
last_state = 1  # 1 = not pressed (due to pull-up), 0 = pressed
stable_state = 1
last_debounce_time = time.ticks_ms()

count = 0

def flash_led(ms=60):
    led.on()
    time.sleep_ms(ms)
    led.off()

def handle_command(line):
    global count, DEBOUNCE_MS
    line = line.strip()
    if line == "r":
        count = 0
        print("COUNT 0")
    elif line == "q":
        print(f"COUNT {count}")
    elif line.startswith("d"):
        try:
            val = int(line[1:])
            if 1 <= val <= 250:
                DEBOUNCE_MS = val
                print(f"DEBOUNCE {DEBOUNCE_MS}ms")
            else:
                print("ERR debounce range 1..250")
        except ValueError:
            print("ERR bad debounce value")
    else:
        print("ERR unknown command")

def read_serial_lines_nonblocking():
    # Use sys.stdin to read any incoming commands (USB CDC)
    # Non-blocking: only read if data is available.
    lines = []
    while True:
        try:
            # Read one byte if available
            import select
            poller = select.poll()
            poller.register(sys.stdin, select.POLLIN)
            ready = poller.poll(0)
            if not ready:
                break
            line = sys.stdin.readline()
            if not line:
                break
            lines.append(line)
        except Exception:
            break
    return lines

print("READY Pico-ICE debounced button + counter")
print(f"DEBOUNCE {DEBOUNCE_MS}ms")

# Main loop: poll at 1 ms, apply debounce filter, detect rising edges
while True:
    # Poll input
    reading = btn.value()  # 1 = not pressed, 0 = pressed

    # If state changed from last sample, reset debounce timer
    if reading != last_state:
        last_debounce_time = time.ticks_ms()
        last_state = reading

    # If stable for at least DEBOUNCE_MS, accept as stable_state
    if time.ticks_diff(time.ticks_ms(), last_debounce_time) >= DEBOUNCE_MS:
        if reading != stable_state:
            # State transition after stable period
            stable_state = reading
            if stable_state == 0:  # Press detected (rising edge from not pressed to pressed)
                count += 1
                print(f"COUNT {count}")
                flash_led(60)

    # Handle any incoming host commands
    for cmd in read_serial_lines_nonblocking():
        handle_command(cmd)

    time.sleep_ms(1)

Host‑side validation script (run on Raspberry Pi). This opens the MicroPython USB serial on /dev/ttyACM0, reads counter updates, and provides a simple interactive shell to send commands to the device and verify behavior:

# validate.py — Host validation for Pico-ICE debounced button + counter
# Runs on Raspberry Pi (Bookworm, Python 3.11)
import sys
import time
import serial
import argparse
from pathlib import Path

def open_serial(port, baud=115200, timeout=0.1):
    ser = serial.Serial(port, baudrate=baud, timeout=timeout)
    return ser

def print_help():
    print("Commands:")
    print("  r           reset counter")
    print("  q           query current count")
    print("  dNN         set debounce to NN ms (e.g., d20)")
    print("  press?      guidance for manual validation steps")
    print("  exit        quit")

def interactive(ser):
    print("Connected. Reading lines. Type commands and press Enter.")
    print_help()
    ser.reset_input_buffer()

    while True:
        # Non-blocking read
        line = ser.readline()
        if line:
            try:
                print(line.decode("utf-8").rstrip())
            except UnicodeDecodeError:
                print(f"[BIN] {line!r}")

        # User input
        if sys.stdin in select([sys.stdin], [], [], 0.05)[0]:
            cmd = sys.stdin.readline().strip()
            if cmd == "exit":
                break
            elif cmd == "press?":
                print("Manual test steps:")
                print("  1) Tap quickly -> one COUNT increment")
                print("  2) Hold ~1s -> one COUNT increment, no repeats")
                print("  3) Tap repeatedly faster than debounce -> increments should not exceed one per press")
                continue
            ser.write((cmd + "\n").encode("utf-8"))

def select(rlist, wlist, xlist, timeout):
    # Minimal replacement for select.select to avoid importing select globally
    import select as _select
    return _select.select(rlist, wlist, xlist, timeout)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", default="/dev/ttyACM0", help="USB CDC port (default: /dev/ttyACM0)")
    ap.add_argument("--baud", type=int, default=115200)
    args = ap.parse_args()

    print(f"Opening {args.port} @ {args.baud}...")
    ser = open_serial(args.port, args.baud)
    try:
        interactive(ser)
    finally:
        ser.close()

if __name__ == "__main__":
    main()

Build/Flash/Run Commands

We will flash MicroPython onto the RP2040 side of the Pico‑ICE and then copy main.py. All steps are executed on the Raspberry Pi (Bookworm).

1) Download a MicroPython UF2 for RP2040:
– Get the official build for “Raspberry Pi Pico” (RP2040). This generic build works for boards with RP2040 and does not rely on an on‑board LED pin definition.
– Download to your Pi:

mkdir -p ~/Downloads
cd ~/Downloads
# Visit https://micropython.org/download/rp2-pico/ to confirm the latest filename if needed.
# Example (adjust if a newer version exists):
wget https://micropython.org/resources/firmware/rp2-pico-20240222-v1.22.2.uf2 -O micropython-pico.uf2
ls -lh micropython-pico.uf2

2) Put Pico‑ICE into BOOTSEL (USB mass storage) mode:
– Unplug the board’s USB‑C.
– Hold the BOOT/BOOTSEL button on the Pico‑ICE.
– While holding, plug into the Raspberry Pi USB. Release after a second.
– The Pi should auto‑mount a drive named RPI‑RP2.

Confirm the USB storage device:

lsblk -f
# Look for a volume labeled RPI-RP2 mounted under /media/$USER/RPI-RP2 or /run/media/$USER/RPI-RP2

3) Flash MicroPython by copying the UF2:

cp ~/Downloads/micropython-pico.uf2 /media/$USER/RPI-RP2/
sync

The device will automatically reboot and disconnect from mass storage. After a moment, a new USB serial device will appear (typically /dev/ttyACM0).

Confirm the USB CDC device:

dmesg | tail -n 20
ls -l /dev/ttyACM*

4) Install mpremote into your venv to copy the MicroPython script:

source ~/venvs/pico-ice/bin/activate
pip install mpremote

5) Copy main.py to the device and set it to run at boot:
– Plug the Pico‑ICE normally (not in BOOTSEL mode).
– Identify the serial device (usually /dev/ttyACM0).
– Use mpremote to transfer:

cd ~
mkdir -p pico-ice-project
cd pico-ice-project
# Create the main.py here or copy it in
nano main.py
# (paste the MicroPython code from the "Full Code" section, save and exit)

# Copy it to the device filesystem root as :main.py
python -m mpremote connect /dev/ttyACM0 fs cp main.py :main.py

# Optionally, verify by listing files
python -m mpremote connect /dev/ttyACM0 fs ls

# Soft reset to run main.py
python -m mpremote connect /dev/ttyACM0 reset

If everything is correct, the device should print startup messages over USB CDC. You can view them with minicom or screen:

# Using screen:
screen /dev/ttyACM0 115200
# Quit with Ctrl-A then K then Y

Or run the provided host validation script.

6) Prepare and run the host validation script:

cd ~/pico-ice-project
nano validate.py
# (paste the host Python code from the "Full Code" section, save and exit)

# Ensure we are in the venv with pyserial installed:
source ~/venvs/pico-ice/bin/activate

python validate.py --port /dev/ttyACM0

You should see the READY and DEBOUNCE lines, and each valid button press will print “COUNT n”.


Step‑by‑Step Validation

Goal: Prove that the debounce algorithm prevents multiple counts from mechanical chatter and increments exactly once per press.

1) Initial communication sanity check
– Run:
source ~/venvs/pico-ice/bin/activate
python validate.py --port /dev/ttyACM0

– Expected output (example):
– READY Pico‑ICE debounced button + counter
– DEBOUNCE 25ms

2) Basic single press
– Action: Slowly press and release the button once.
– Expected:
– Output line: COUNT 1
– LED flashes briefly once.

3) Multiple slow presses
– Action: Press/release 5 times at roughly 1 second intervals.
– Expected:
– Count moves from 1 → 6 with one increment per press.
– LED flashes once per press.

4) Bounce rejection (rapid “tap”)
– Action: Tap the button rapidly, trying to create mechanical bounce during a single press (i.e., a short press, not multiple distinct presses).
– Expected:
– Only one increment (e.g., COUNT 7). The 25 ms debounce window should suppress chatter.

5) Long press (no auto‑repeat)
– Action: Press and hold the button for ~1–2 seconds, then release.
– Expected:
– Exactly one increment for the press (e.g., COUNT 8). No additional increments while held.

6) Repeat rate limitation (faster than debounce)
– Action: Try to press the button repeatedly faster than 25 ms between presses.
– Expected:
– It is difficult for human timing to consistently beat 25 ms. Some presses may be ignored if they don’t meet edge separation; the count should never jump by more than 1 per human press action.

7) Debounce tuning via host command
– Action: In the validation shell, type:
d10
This sets debounce to 10 ms.
– Next, tap more quickly.
– Expected:
– The counter may now be more sensitive. If you see occasional double counts on a single tap, increase the debounce:
d25
Return to the default of 25 ms.

8) Query and reset
– Action:
q
Expected: A line like “COUNT 11”.
– Action:
r
Expected: “COUNT 0”; next press should print “COUNT 1”.

9) LED confirmation
– For every valid “COUNT n”, the LED should visibly flash once. If the serial shows COUNT increments but you don’t see flashes, re‑check the LED orientation and the resistor wiring to GP15 and GND.

10) Optional continuous monitoring with screen
– Exit validate.py and run:
screen /dev/ttyACM0 115200
– Perform presses and observe prints. Quit screen with Ctrl‑A, K, Y.


Troubleshooting

  • USB device not appearing as /dev/ttyACM0
  • Check connection: lsusb and dmesg | tail -n 50
  • If in BOOTSEL mode, you will see a mass storage device (RPI‑RP2). For MicroPython, you need a CDC serial (no mass storage).
  • Try a different USB cable (ensure it is data‑capable).
  • Unplug/replug the board.

  • Permission denied on /dev/ttyACM0

  • On Raspberry Pi OS, your user should be in the dialout group:
    groups
    sudo usermod -a -G dialout $USER
    newgrp dialout
  • Then try again.

  • Nothing printed, but LED flashes work

  • Ensure you flashed MicroPython UF2 and copied main.py to the root of the device filesystem.
  • Soft reset with:
    python -m mpremote connect /dev/ttyACM0 reset
  • Use minicom -b 115200 -o -D /dev/ttyACM0 to confirm serial output.

  • LED never lights

  • Verify LED orientation: long leg to resistor → GP15; short leg to GND.
  • Confirm GP numbers in code match your wiring (BUTTON_PIN = 14, LED_PIN = 15).
  • Replace the LED or try a different resistor value (between 220–470 Ω).

  • Button doesn’t increment

  • Confirm the button has one leg to GND, the other to GP14.
  • Ensure internal pull‑up is set (Pin.PULL_UP in code).
  • Try connecting the button to a different GP and update BUTTON_PIN accordingly.

  • Debounce too sensitive/too strict

  • Use the “dNN” command to adjust debounce (e.g., d10, d25, d40).
  • Mechanical switches typically need 5–20 ms; 25 ms is conservative and robust.

  • Accidentally flashed the wrong UF2

  • Re‑enter BOOTSEL mode and copy the correct MicroPython UF2 again. The RP2040 boot ROM is immutable; recovery is straightforward.

Improvements

  • Interrupt‑based debouncing
  • Use an IRQ handler for the GPIO edge and a timer callback to confirm stable state after DEBOUNCE_MS. This reduces CPU usage versus 1 ms polling.

  • State machine with event timestamps

  • Track transitions (idle → maybe_pressed → confirmed_pressed → maybe_released → idle) with precise timers; simplifies tuning and extendibility.

  • Count persistence

  • Save the count in onboard flash every N increments or after a timeout to retain across power cycles (be mindful of flash wear).

  • Long‑press/double‑click features

  • Recognize press durations and implement multi‑function button behavior:

    • Short press: increment
    • Long press: reset
    • Double click: set different debounce profile
  • LED bar or RGB indication

  • If you later load a simple FPGA bitstream to expose an LED bar or use dedicated LEDs, mirror the count on multiple outputs. For this basic tutorial, we used one external LED to avoid FPGA complexity.

  • Host GUI

  • Build a small PyQt or Tkinter app to visualize counts, press durations, and jitter histograms.

  • Automated validation

  • Drive a test jig with a relay or transistor to simulate controlled press patterns and timing to quantitatively measure debounce effectiveness.

Final Checklist

  • Raspberry Pi OS Bookworm 64‑bit with Python 3.11 set up
  • Virtual environment created: ~/venvs/pico‑ice
  • Packages installed: pyserial, gpiozero, smbus2, spidev
  • Optional interfaces enabled via raspi‑config or /boot/firmware/config.txt

  • Pico‑ICE (Lattice iCE40UP5K)

  • Connected via USB‑C to Raspberry Pi
  • MicroPython UF2 flashed successfully
  • main.py copied to device root using mpremote

  • Wiring

  • Button: one leg to GND, other to GP14 (internal pull‑up enabled)
  • LED: anode to GP15 through 330 Ω resistor, cathode to GND

  • Functionality

  • On boot, USB serial prints READY and DEBOUNCE lines
  • Each valid press increments COUNT once and flashes LED once
  • Rapid chatter does not cause extra counts (antirrebote effective)
  • Host validation script can reset/query count and adjust debounce

  • Commands

  • Flash: copy UF2 to RPI‑RP2 mass storage in BOOTSEL mode
  • Copy script: python -m mpremote connect /dev/ttyACM0 fs cp main.py :main.py
  • Run validation: source ~/venvs/pico‑ice/bin/activate; python validate.py –port /dev/ttyACM0

If all items are checked, your basic “boton‑antirrebote‑y‑contador” implementation on the Pico‑ICE RP2040 is complete and validated from a Raspberry Pi host running Bookworm 64‑bit.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the main objective of the tutorial?




Question 2: Which microcontroller is used in the Pico-ICE?




Question 3: What operating system is required on the host Raspberry Pi?




Question 4: What type of cable is needed to connect the Pico-ICE to the Raspberry Pi?




Question 5: What is the resistor value needed for the LED indicator?




Question 6: Which Raspberry Pi models were tested for the tutorial?




Question 7: What interface options are enabled using raspi-config?




Question 8: What command is used to open raspi-config?




Question 9: What must be done after enabling the interface options?




Question 10: Is the FPGA on the Pico-ICE used in this basic exercise?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
error: Contenido Protegido / Content is protected !!
Scroll to Top