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:


Practical case: LED Chaser on Raspberry Pi or Pico-ICE iCE40

Practical case: LED Chaser on Raspberry Pi or Pico-ICE iCE40 — hero

Objective and use case

What you’ll build: Build a “pattern-leds-chaser” on the Pico-ICE (Lattice iCE40UP5K), using the RP2040 side of the board to drive 8 LEDs in a clean, repeating chase pattern.

Why it matters / Use cases

  • Demonstrates the capabilities of the RP2040 microcontroller in real-time LED control.
  • Provides a foundation for understanding microcontroller programming and hardware interfacing.
  • Can be extended to include more complex patterns or integrate with other sensors for interactive displays.
  • Serves as an introductory project for those interested in FPGA development with the Lattice iCE40UP5K.

Expected outcome

  • Successful execution of the LED chase pattern with a clear visual output.
  • Validation of the setup through specific commands and expected responses during the flashing process.
  • Measurement of response time from input to LED output, aiming for less than 100ms latency.
  • Documentation of any errors encountered and their resolutions during the setup process.

Audience: Beginners to intermediate users interested in microcontrollers and FPGAs; Level: Basic.

Architecture/flow: The project involves setting up a Raspberry Pi host, configuring a Python environment, wiring LEDs, and flashing MicroPython to the Pico-ICE.

Pico-ICE (Lattice iCE40UP5K) LED Chaser (Basic Level)

Objective: Build a “pattern-leds-chaser” on the Pico-ICE (Lattice iCE40UP5K), using the RP2040 side of the board (Pico‑compatible) to drive 8 LEDs in a clean, repeating chase pattern. You will prepare a Raspberry Pi host (Raspberry Pi OS Bookworm 64‑bit) with Python 3.11, set up a Python virtual environment, install the necessary tools, wire the LEDs, flash MicroPython, copy the code, run it, and validate the behavior step by step.

This is a hands‑on practical with a focus on correctness and reproducibility. It includes exact commands, versions and file paths. No circuit drawings are used; all connections are explained with text and a table.

Note: Pico‑ICE combines a Raspberry Pi Pico‑compatible RP2040 microcontroller with a Lattice iCE40UP5K FPGA. In this basic exercise we focus on the RP2040 to immediately realize the LED chaser. In “Improvements,” you will find guidance to push the pattern into the FPGA fabric later.

Prerequisites

  • A Raspberry Pi 4/400/5 running:
  • Raspberry Pi OS Bookworm 64‑bit (fully updated).
  • Python 3.11 (default on Bookworm).
  • Internet access on the Raspberry Pi to install packages.
  • A spare USB port on the Raspberry Pi to connect the Pico‑ICE.
  • Basic breadboard and wiring skills (no soldering required if your Pico‑ICE is header‑equipped).
  • Familiarity with a terminal and the concept of a Python virtual environment.

Confirm OS and Python:

cat /etc/os-release
python3 --version

Expected: Bookworm and Python 3.11.x.

Materials

  • Device: “Pico‑ICE (Lattice iCE40UP5K)” — exact model as specified.
  • Host: Raspberry Pi 4/400/5 (running Raspberry Pi OS Bookworm 64‑bit).
  • USB data cable appropriate for your Pico‑ICE’s USB connector (ensure it is data‑capable).
  • 1 × solderless breadboard.
  • 8 × LEDs (5 mm or 3 mm; any color).
  • 8 × 330 Ω resistors (1/4 W).
  • ~20 × male–male jumper wires.
  • Optional (for later improvements): Digilent Pmod 8LD (8‑LED Pmod), but not required here.

We will assume the Pico‑ICE is pin‑compatible with the Raspberry Pi Pico. The RP2040 GPIOs we will use are GP2–GP9.

Setup/Connection

1) Enable interfaces on the Raspberry Pi

We follow the Raspberry Pi family defaults. Even though the LED chaser doesn’t require I2C/SPI/UART on the host, enabling them now saves time for future expansions.

Option A: Using raspi-config (interactive)

  • Open the TUI:
    sudo raspi-config
  • Interface Options:
  • I2C: Enable.
  • SPI: Enable.
  • Serial Port: Disable login shell over serial; Enable serial port hardware.
  • Finish and reboot when prompted.

Option B: Edit /boot/firmware/config.txt (manual)

  • Open the config file:
    sudo nano /boot/firmware/config.txt
  • Add (or ensure these lines exist and are uncommented):
    dtparam=i2c_arm=on
    dtparam=spi=on
    enable_uart=1
  • Save, exit, and reboot:
    sudo reboot

These steps do not interfere with USB communications to the Pico‑ICE.

2) Create a project directory and Python virtual environment

  • Create a working folder and venv:
    mkdir -p ~/pico-ice-led-chaser
    cd ~/pico-ice-led-chaser
    python3 -m venv .venv
    source .venv/bin/activate
    python -V

    Expected output: Python 3.11.x

  • Install host utilities and Python tooling:
    sudo apt update
    sudo apt install -y screen curl unzip
    pip install --upgrade pip
    pip install mpremote pyserial gpiozero smbus2 spidev

    Notes:

  • mpremote will be used to copy and run MicroPython files on the RP2040.
  • gpiozero, smbus2, spidev are installed per family defaults, to prepare for future projects on the Raspberry Pi itself.

3) Prepare MicroPython firmware for the Pico‑ICE (RP2040 side)

We will run the LED chaser using MicroPython on the RP2040. Download a known stable MicroPython build for the RP2040 (Raspberry Pi Pico). Example shown below uses MicroPython v1.21.0 for rp2-pico; you can substitute a newer stable version if preferred.

  • Create a firmware folder and download:
    mkdir -p ~/pico-ice-led-chaser/fw
    cd ~/pico-ice-led-chaser/fw
    curl -LO https://micropython.org/resources/firmware/rp2-pico-20230426-v1.21.0.uf2
    ls -lh

  • Put the Pico‑ICE into BOOTSEL mode (RP2040 bootloader):

  • Unplug the Pico‑ICE from USB.
  • Press and hold the BOOTSEL button on the Pico‑ICE.
  • While holding BOOTSEL, plug the USB cable into the Raspberry Pi.
  • Release BOOTSEL after the board enumerates as a mass storage device (RPI-RP2).

  • Copy the UF2 to the mounted volume:

  • Identify the mount path (it typically auto‑mounts under /media/pi/RPI-RP2 on Raspberry Pi OS desktop; on Lite you may need to mount it).
  • Copy:
    cp ~/pico-ice-led-chaser/fw/rp2-pico-20230426-v1.21.0.uf2 /media/pi/RPI-RP2/
    sync
  • The board will automatically reboot into MicroPython. The mass storage volume disappears; this is expected.

  • Verify the USB serial device appears:
    dmesg | tail -n 50 | grep -i tty
    ls -l /dev/ttyACM*

    Expected: something like /dev/ttyACM0

4) Wire the 8 LEDs to the Pico‑ICE

We’ll use GPIO pins GP2–GP9 to drive the LEDs. Each LED needs a current‑limiting resistor. We choose 330 Ω from GPIO to LED anode, LED cathode to GND.

  • Orientation reminder:
  • LED anode: longer lead → connect via resistor to the GPIO.
  • LED cathode: shorter lead/flat side → connect to GND.
  • Ground reference: use any GND pin on the Pico‑ICE header.

Use the standard Raspberry Pi Pico header pinout (Pico‑ICE is Pico‑compatible). The table below maps LEDs to GPIOs and the physical pin numbers on the Pico header.

LED # RP2040 GPIO Pico Header Pin Connection Instruction
1 GP2 Pin 4 GPIO → 330 Ω → LED anode; LED cathode → GND
2 GP3 Pin 5 GPIO → 330 Ω → LED anode; LED cathode → GND
3 GP4 Pin 6 GPIO → 330 Ω → LED anode; LED cathode → GND
4 GP5 Pin 7 GPIO → 330 Ω → LED anode; LED cathode → GND
5 GP6 Pin 9 GPIO → 330 Ω → LED anode; LED cathode → GND
6 GP7 Pin 10 GPIO → 330 Ω → LED anode; LED cathode → GND
7 GP8 Pin 11 GPIO → 330 Ω → LED anode; LED cathode → GND
8 GP9 Pin 12 GPIO → 330 Ω → LED anode; LED cathode → GND

Notes:
– Any GND pins may be used for the cathodes (common ground). Convenient ground pins near these are Pin 3 (GND), Pin 8 (GND), and Pin 13 (GND).
– Keep wiring short and neat to avoid shorts. Double‑check polarity of each LED before powering.

Full Code (MicroPython on RP2040)

Save the following as main.py. It implements a bidirectional “chaser” (Knight Rider‑style), with a configurable speed and clear pin mapping for GP2–GP9. It also performs a brief startup test lighting all LEDs.

# Tested with MicroPython v1.21.0 on RP2040 (Raspberry Pi Pico compatible).
#
# Wiring: See table in the tutorial.
# - Each LED anode goes to one GPIO via a 330 Ω resistor.
# - Each LED cathode goes to GND.

from machine import Pin
import time

# Ordered list of RP2040 GPIO numbers used for the chaser
LED_PINS = [2, 3, 4, 5, 6, 7, 8, 9]

# Convert into Pin objects set to OUTPUT, initially LOW
leds = [Pin(gp, Pin.OUT, value=0) for gp in LED_PINS]

# Chaser timing (seconds between steps); adjust as needed
STEP_DELAY_S = 0.08  # 80 ms per step ~ 12.5 steps/sec

def all_off():
    for led in leds:
        led.value(0)

def all_on():
    for led in leds:
        led.value(1)

def startup_test():
    # Light all briefly, then a walking 0 test, then off
    all_on()
    time.sleep(0.3)
    for i in range(len(leds)):
        for j, led in enumerate(leds):
            led.value(0 if i == j else 1)
        time.sleep(0.05)
    all_off()

def chase_loop():
    # Ping-pong pattern: 0..7 forward, then 6..1 backward
    n = len(leds)
    index = 0
    direction = 1  # 1 forward, -1 backward

    while True:
        # Light exactly one LED at position 'index'
        for i, led in enumerate(leds):
            led.value(1 if i == index else 0)

        time.sleep(STEP_DELAY_S)

        # Compute next index and direction
        if direction == 1 and index == n - 1:
            direction = -1
        elif direction == -1 and index == 0:
            direction = 1

        index += direction

def main():
    print("Pico-ICE LED chaser starting; pins:", LED_PINS)
    startup_test()
    chase_loop()

if __name__ == "__main__":
    main()

Behavior:
– On boot: all LEDs on briefly, then a quick walking‑zero test, then the chaser begins.
– The pattern moves from LED1 to LED8 and back continuously.
– Adjust STEP_DELAY_S to speed up or slow down.

Optional: You can create variants with different patterns (e.g., bounce with two LEDs, fading via PWM, etc.) in the “Improvements” section later.

Build/Flash/Run Commands

This section gives exact, copy‑pasteable commands to put the code onto the Pico‑ICE and run it.

1) Ensure MicroPython is flashed

If you followed the earlier “Prepare MicroPython firmware” step, the device is already running MicroPython. If not, return to that step.

2) Identify the USB serial path

List connected ACM devices:

ls -l /dev/ttyACM*

Typically /dev/ttyACM0. If multiple devices exist, use dmesg to confirm which one appeared after plugging in the Pico‑ICE:

dmesg | tail -n 50 | grep -i "ttyACM"

3) Copy and run main.py with mpremote

  • Go back to your project directory with the main.py you created:
    cd ~/pico-ice-led-chaser
  • Connect and copy:
    mpremote connect /dev/ttyACM0 fs cp main.py :main.py
    This copies main.py to the device filesystem as /main.py (colon syntax).

  • Run immediately (useful for testing without reboot):
    mpremote connect /dev/ttyACM0 run main.py
    You should see console output:
    Pico-ICE LED chaser starting; pins: [2, 3, 4, 5, 6, 7, 8, 9]
    And the LEDs should begin chasing.

  • Make it auto‑run on power‑up:

  • The file name main.py already causes auto‑run on boot for MicroPython. Power cycle the Pico‑ICE; the pattern should start without mpremote.

4) Optional: Open a MicroPython REPL for interactive tests

You can access a live REPL on the device to test individual GPIOs:

mpremote connect /dev/ttyACM0 repl

At the >>> prompt, try:

from machine import Pin
p=Pin(2, Pin.OUT); p.value(1)

LED1 should light. Turn it off:

p.value(0)

Exit REPL with Ctrl‑X (mpremote) or Ctrl‑] then q, if using screen:

screen /dev/ttyACM0 115200
# Exit: Ctrl-A, then K, then y

Step‑by‑Step Validation

Follow these precise validation steps to eliminate ambiguity:

1) Validate Raspberry Pi OS and Python stack
– Confirm Bookworm 64‑bit:
cat /etc/os-release
uname -m

Expect PRETTY_NAME with “Bookworm” and aarch64 for 64‑bit.
– Check Python version:
python3 --version
Expect Python 3.11.x.

2) Validate interfaces and configuration
– If you used raspi-config, ensure it saved:
grep -E 'i2c_arm|spi|enable_uart' /boot/firmware/config.txt
Expect:
– dtparam=i2c_arm=on
– dtparam=spi=on
– enable_uart=1

3) Validate MicroPython firmware and USB
– Unplug and replug the Pico‑ICE (not in BOOTSEL mode).
– Check device node:
ls -l /dev/ttyACM*
Expect /dev/ttyACM0 (or ACM1 if others are connected).
– If missing: try a different USB cable/port; verify it’s data‑capable.

4) Validate basic GPIO control
– Enter REPL:
mpremote connect /dev/ttyACM0 repl
– Turn on LED1 (GP2):
from machine import Pin
Pin(2, Pin.OUT).value(1)

LED1 should light. If not:
– Verify wiring: LED1 anode → 330 Ω → GP2 (header pin 4), LED1 cathode → GND (pin 3/8/13).
– Check LED polarity (long lead toward the resistor/GPIO; short lead to GND).

5) Validate the full chaser program
– Exit REPL and run:
mpremote connect /dev/ttyACM0 run main.py
– Observe:
– All LEDs briefly on (startup test), then chaser begins.
– Pattern marches LED1→LED8, then reverses back LED8→LED1.
– If the order is inverted, you may have swapped wires; compare with the wiring table.

6) Validate timing consistency
– The default STEP_DELAY_S is 0.08 s.
– To slow it down for visual inspection, edit main.py:
nano ~/pico-ice-led-chaser/main.py
Change STEP_DELAY_S to 0.2, save, re‑copy:
mpremote connect /dev/ttyACM0 fs cp main.py :main.py
mpremote connect /dev/ttyACM0 run main.py

7) Validate persistence on power cycle
– Unplug USB and plug back in (no BOOTSEL). The code auto‑runs. If it doesn’t, ensure the file is named main.py on the device:
mpremote connect /dev/ttyACM0 fs ls :
You should see /main.py.

Troubleshooting

  • No /dev/ttyACM0 appears:
  • Try another USB cable/port; some cables are charge‑only.
  • Check dmesg:
    dmesg | tail -n 100
  • If still absent, reflash MicroPython using BOOTSEL steps.

  • mpremote cannot connect:

  • Ensure correct device path:
    mpremote connect /dev/ttyACM0 ls
  • If permission errors, try:
    sudo usermod -aG dialout $USER
    newgrp dialout

    Then reconnect the board and try again.

  • LEDs do not light:

  • Polarity: The LED’s flat side/short lead must go to GND.
  • Resistors: Ensure each GPIO passes through a 330 Ω to the LED anode, not to GND.
  • Wrong pins: Confirm you used GP2..GP9 (Pico header pins 4,5,6,7,9,10,11,12).
  • Test single pin via REPL:
    from machine import Pin; Pin(2, Pin.OUT).value(1)
  • Try a slower delay to better see the steps (e.g., 0.2 s).

  • LEDs too dim or too bright:

  • Use 330 Ω to 1 kΩ. Lower values increase brightness but also current. Keep GPIO current per pin under 12 mA, and total under ~50 mA to be safe.

  • Device reboots or locks:

  • Avoid short circuits. If an LED is wired backward or GPIO tied directly to GND/VBUS, the microcontroller can brown‑out or be damaged. Disconnect power and inspect wiring.

  • After copying main.py, nothing runs:

  • Confirm the file exists on the device:
    mpremote connect /dev/ttyACM0 fs ls :
  • Ensure you used :main.py (colon indicates device path) when copying.

  • BOOTSEL drive never appears:

  • Hold BOOTSEL while plugging in.
  • Try a different USB port.
  • If the drive still doesn’t appear, test on another computer to isolate a cable/port issue.

Improvements

Here are concrete ways to extend the “pattern‑leds‑chaser” once the basic version works:

  • Pattern variety in MicroPython:
  • Implement multiple patterns (e.g., one‑hot, two‑dot bounce, “comet” tail).
  • Add a button input (e.g., GP14 with internal pull‑up and a pushbutton to GND) to cycle pattern or speed on press.
  • Use PWM for fading. In MicroPython, use machine.PWM on each Pin to create a fade‑in/fade‑out “comet”.

  • Host‑side controls over USB (MicroPython REPL):

  • Parse single‑character commands from sys.stdin to increase/decrease STEP_DELAY_S in real time. For example, ‘+’ speeds up, ‘-’ slows down. You can interact through mpremote repl.

  • Transition to the FPGA (iCE40UP5K) for hardware LED chaser:

  • Install open‑source FPGA tools on the Raspberry Pi:
    sudo apt update
    sudo apt install -y yosys nextpnr-ice40 fpga-icestorm openfpgaloader
  • Use a Pmod (e.g., Digilent Pmod 8LD) on the Pico‑ICE Pmod header and a simple HDL chaser clocked from an internal oscillator.
  • A minimal Verilog module could look like this (illustrative — adjust constraints for Pico‑ICE Pmod pins according to the Pico‑ICE reference design and your Pmod wiring):

    «`
    // led_chaser.v – simple 8-bit chaser for iCE40UP5K
    module led_chaser (
    input wire clk, // e.g., 12 MHz internal or external
    output reg [7:0] led // map to Pmod pins via .pcf
    );
    reg [23:0] div;
    reg dir;
    reg [2:0] idx;

    always @(posedge clk) begin
        div <= div + 1;
        if (div == 24'd0) begin
            // one-hot
            led <= 8'b0000_0001 << idx;
    
            // ping-pong index
            if (!dir && idx == 3'd7) dir <= 1'b1;
            else if (dir && idx == 3'd0) dir <= 1'b0;
            idx <= dir ? (idx - 1) : (idx + 1);
        end
    end
    

    endmodule
    «`

  • Build on the Raspberry Pi:
    mkdir -p ~/pico-ice-led-chaser/fpga && cd ~/pico-ice-led-chaser/fpga
    # Save led_chaser.v here, and create pico-ice.pcf with your exact pin mappings.
    yosys -p "read_verilog led_chaser.v; synth_ice40 -top led_chaser -json chaser.json"
    nextpnr-ice40 --up5k --json chaser.json --pcf pico-ice.pcf --asc chaser.asc
    icepack chaser.asc chaser.bin

  • Program via openFPGALoader (Pico‑ICE typically supports USB loading through the RP2040 bridge; consult Pico‑ICE docs for the exact target and mode):
    openFPGALoader -b pico-ice chaser.bin
  • This moves the chaser logic into hardware for precise timing and zero CPU load.

  • Power and safety improvements:

  • If you want higher LED current or more LEDs, use a transistor array (e.g., ULN2803A) or dedicated LED driver to keep RP2040 currents within limits.

  • Packaging:

  • 3D‑print a panel with 8 holes for the LEDs to create a neat light bar.

Final Checklist

  • Raspberry Pi OS Bookworm 64‑bit verified on host.
  • Python 3.11 verified; virtual environment created in ~/pico-ice-led-chaser/.venv.
  • Packages installed: mpremote, pyserial, gpiozero, smbus2, spidev.
  • Interfaces enabled (I2C/SPI/UART) via raspi-config or /boot/firmware/config.txt.
  • MicroPython v1.21.0 UF2 flashed onto Pico‑ICE RP2040 via BOOTSEL.
  • Wiring matches the table for GP2–GP9 with 330 Ω resistors and correct LED polarity.
  • main.py copied to device and runs via mpremote; auto‑runs on power‑up.
  • Step‑by‑step validation completed (single GPIO test, full chaser test, timing adjusted).
  • Troubleshooting steps understood (USB device path, permissions, wiring checks).
  • Optional improvements considered (alternate patterns, button control, FPGA implementation).

With the above, you have a reliable, basic “pattern‑leds‑chaser” on the Pico‑ICE (Lattice iCE40UP5K), using the Raspberry Pi host for setup and MicroPython on the RP2040 for control. This foundation prepares you for deeper exploration, including moving the pattern into the FPGA for fully hardware‑driven lighting effects.

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 Pico-ICE LED chaser project?




Question 2: Which version of Python is required for this project?




Question 3: What type of microcontroller is used in the Pico-ICE?




Question 4: What operating system must the Raspberry Pi run for this project?




Question 5: What is required to connect the Pico-ICE to the Raspberry Pi?




Question 6: What is the expected output when confirming the OS and Python version?




Question 7: Is soldering required for the Pico-ICE project?




Question 8: What skills are necessary for this project?




Question 9: What is the purpose of setting up a Python virtual environment?




Question 10: What is the maximum number of LEDs that can be driven in this project?




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

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

Follow me: