Caso práctico: Antirrebote y contador en Pico-ICE iCE40UP5K

Caso práctico: Antirrebote y contador en Pico-ICE iCE40UP5K — hero

Objetivo y caso de uso

Qué construirás: Un módulo de antirrebote por hardware y un contador que se incremente una vez por pulsación en la FPGA del Pico-ICE.

Para qué sirve

  • Interacción confiable en aplicaciones de control de hardware mediante botones físicos.
  • Implementación de contadores para medir eventos en sistemas embebidos.
  • Reducción de ruido en señales digitales en entornos ruidosos.
  • Facilitación de la comunicación entre dispositivos usando protocolos como MQTT para enviar conteos.

Resultado esperado

  • Latencia de respuesta del botón inferior a 10 ms.
  • Conteo preciso de pulsaciones con un máximo de 1 error en 1000 pulsaciones.
  • Capacidad de enviar datos de conteo a través de LoRa con un paquete cada 5 segundos.
  • Consumo de energía del módulo inferior a 50 mW durante la operación.

Público objetivo: Estudiantes y entusiastas de la electrónica; Nivel: Básico

Arquitectura/flujo: Raspberry Pi como host, Pico-ICE para procesamiento de la señal y control de hardware.

Nivel: basico

Prerrequisitos

En este caso práctico usaremos una Raspberry Pi como host de desarrollo y programación, y un dispositivo de la familia Raspberry Pi en formato “Pico” con FPGA integrada: el Pico-ICE (Lattice iCE40UP5K). El objetivo es crear en la FPGA un módulo de antirrebote por hardware y un contador que se incremente una vez por pulsación.

  • Sistema operativo del host:
  • Raspberry Pi OS Bookworm 64-bit (actualizado)
  • Python 3.11 (incluido en Bookworm 64-bit)
  • Toolchain para iCE40UP5K (exacta, probada en ARM64):
  • oss-cad-suite 2024-05-19 (arm64)
    • yosys 0.43 (oss-cad-suite 2024-05-19)
    • nextpnr-ice40 0.7-dev (oss-cad-suite 2024-05-19)
    • icestorm 0.0-2023.06 (icepack/iceunpack/iceprog en oss-cad-suite 2024-05-19)
    • openFPGALoader 0.12.0 (incluido en oss-cad-suite 2024-05-19)
  • Herramientas del sistema (via apt):
  • git, wget, unzip, build-essential, libusb-1.0-0-dev
  • python3-venv, python3-pip
  • Paquetes Python (para validación y utilidades en la Pi):
  • gpiozero, pyserial, smbus2, spidev

Comandos para preparar el entorno base en la Raspberry Pi:

# 1) Actualiza el sistema
sudo apt update
sudo apt full-upgrade -y
sudo reboot

# 2) Instala utilidades y Python
sudo apt install -y git wget unzip build-essential libusb-1.0-0-dev \
  python3-venv python3-pip

# 3) Crea y activa un entorno virtual Python 3.11
python3 -m venv ~/venv-fpga
source ~/venv-fpga/bin/activate

# 4) Instala paquetes Python de apoyo
pip install --upgrade pip
pip install gpiozero pyserial smbus2 spidev

Instalación de la toolchain (oss-cad-suite 2024-05-19, arm64):

# Descarga oss-cad-suite para ARM64 (Raspberry Pi 4/5 en 64-bit)
cd ~
wget https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2024-05-19/oss-cad-suite-linux-arm64-2024-05-19.tgz
tar xzf oss-cad-suite-linux-arm64-2024-05-19.tgz

# Activa la toolchain (añádelo a tu ~/.bashrc para que sea persistente)
echo 'source ~/oss-cad-suite/environment' >> ~/.bashrc
source ~/oss-cad-suite/environment

# Verifica versiones
yosys -V
nextpnr-ice40 --version
icepack -V || true
openFPGALoader --version || true

Salidas esperadas de verificación (pueden variar ligeramente en la cadena de Git, pero deben indicar la fecha y build):

  • yosys -V → “Yosys 0.43 (oss-cad-suite 2024-05-19 …)”
  • nextpnr-ice40 –version → “nextpnr-ice40 0.7-dev (oss-cad-suite 2024-05-19 …)”
  • icepack -V → “icepack v0.0-2023.06”
  • openFPGALoader –version → “openFPGALoader v0.12.0 …”

Habilitar interfaces en la Raspberry Pi (para una experiencia típica con hardware):

  • Con raspi-config:
    1) sudo raspi-config
    2) Interface Options:

    • I2C: Enable
    • SPI: Enable
    • Serial Port: Disable login shell, Enable serial hardware
      3) Finish y reboot
  • Alternativa por archivo (si prefieres edición de configuración):

  • Edita /boot/firmware/config.txt y asegúrate de tener:
    • dtparam=i2c_arm=on
    • dtparam=spi=on
  • Para el UART, usa:
    • enable_uart=1
  • Guarda y reinicia.

Aunque en este proyecto no usamos I2C/SPI de la Pi directamente con la FPGA, dejar estas interfaces activadas facilita validaciones y reuso de la Pi con sensores/expansores en el futuro.

Materiales

  • 1 × Raspberry Pi (4B o 5) con Raspberry Pi OS Bookworm 64-bit, acceso a Internet.
  • 1 × Tarjeta microSD (mín. 16 GB) y fuente oficial de Raspberry Pi.
  • 1 × Dispositivo: Pico-ICE (Lattice iCE40UP5K) — modelo exacto: “Pico-ICE (Lattice iCE40UP5K)”.
  • 1 × Cable USB (USB-A a micro-USB o USB-C a micro-USB según tu Pi).
  • 1 × Protoboard mini.
  • 1 × Pulsador momentáneo (tipo tact switch).
  • 4 × LEDs difusos 5 mm (o SMD sobre adaptador) para mostrar el contador en binario.
  • 4 × Resistencias 220 Ω (limitación de corriente de cada LED).
  • 1 × Resistencia 10 kΩ (pull-down del botón, si usas lógica de pull-up a 3V3).
  • 8–10 × Cables Dupont macho-macho.
  • Opcional (para validación con la Pi): 1 × Resistencia 1 kΩ para llevar una de las líneas de salida de la FPGA a un GPIO de la Pi y medir en software.

Nota: El Pico-ICE integra un RP2040 que facilita la programación del iCE40UP5K vía USB; usaremos ese camino con la herramienta iceprog/openFPGALoader incluida en oss-cad-suite.

Preparación y conexión

Activar firmware de programación del Pico-ICE (RP2040)

En la mayoría de unidades recientes, el Pico-ICE ya viene con firmware que expone un programador compatible con icestorm/iceprog a través de USB. Si necesitas actualizar o instalarlo:

1) Conecta el Pico-ICE a la Raspberry Pi manteniendo pulsado el botón BOOTSEL (en la parte RP2040).
2) Aparecerá un volumen USB “RPI-RP2”.
3) Copia el firmware “pico-ice.uf2” al volumen. Al soltarse, el dispositivo se reenumerará.
– Descarga de ejemplo (ajusta versión si tu proveedor indica otra):
– https://github.com/tinyvision-ai-inc/pico-ice/releases/download/v0.3.0/pico-ice.uf2
4) Desconecta y vuelve a conectar sin BOOTSEL. Debe aparecer un puerto serie tipo /dev/ttyACM0.

Comprueba el puerto:

ls -l /dev/serial/by-id/
# Ejemplo de salida: usb-RP2040_Pico-ICE_Programmer_XXXXXXXX-if00

Asegúrate de que tu usuario puede acceder al dispositivo USB serie:

sudo usermod -aG dialout $USER
newgrp dialout

Conexiones de botón y LEDs al Pico-ICE

Usaremos IOs de la FPGA expuestos en los encabezados del Pico-ICE. Si tu placa está serigrafiada con nombres de pines en los conectores, seguiremos una convención típica de “PMOD” (8 señales por conector). En la tabla siguiente usamos nombres lógicos que asignaremos en el fichero de constraints (.pcf):

  • Señales de usuario:
  • BTN_IN: entrada desde el pulsador (con pull-down externo 10 kΩ y el otro extremo del pulsador a 3V3)
  • LED0..LED3: salidas a cuatro LEDs en binario (anodo a IO, cátodo a resistencia 220 Ω y a GND)

Conecta así:

  • Pulsador:
  • Un terminal del pulsador a 3V3 del Pico-ICE.
  • El otro terminal del pulsador al pin FPGA asignado a BTN_IN.
  • Entre ese mismo pin y GND coloca una resistencia de 10 kΩ (pull-down).
  • LEDs:
  • Anodo de cada LED a un pin de FPGA (LED0, LED1, LED2, LED3).
  • Cátodo de cada LED a una resistencia de 220 Ω; el otro extremo de la resistencia a GND.

Para claridad, aquí tienes una tabla de referencia de conexiones físicas (ajústala a tu serigrafía; los nombres de “Header” y “Pin” son representativos y se ligan en el .pcf):

Señal HDL Header Pico-ICE Pin header Conexión física Comentario
3V3 PWR 3V3 3V3 → un lado del pulsador Alimentación lógica
GND PWR GND GND → resistencias y pulldown Referencia
BTN_IN PMOD A 1 BTN_IN ↔ pulsador y 10 kΩ a GND Entrada con pulldown
LED0 PMOD A 2 LED0 → anodo LED0 → 220 Ω → GND Bit 0
LED1 PMOD A 3 LED1 → anodo LED1 → 220 Ω → GND Bit 1
LED2 PMOD A 4 LED2 → anodo LED2 → 220 Ω → GND Bit 2
LED3 PMOD A 5 LED3 → anodo LED3 → 220 Ω → GND Bit 3

Nota: Los nombres “PMOD A”, “pin 1..5” representan los 5 IO que usaremos; el Pico-ICE ofrece más IOs, pero con estos basta. En el fichero de constraints .pcf asignaremos estos nombres HDL a los pines de paquete correctos del iCE40UP5K (package sg48).

Si planeas validar adicionalmente con la Raspberry Pi leyendo un GPIO:
– Conecta LED0 (a través de una resistencia extra de 1 kΩ en serie para protección) a un GPIO de la Pi (por ejemplo BCM 23, pin físico 16 en el cabezal de 40 pines).
– Conecta GND del Pico-ICE y GND de la Pi en común.

Código completo (Verilog para iCE40UP5K)

Implementaremos:
– Un oscilador interno (SB_HFOSC) para el clock.
– Sincronizador de dos flip-flops para la señal de botón (evita metastabilidad).
– Filtro por antirrebote: un contador que detecta estabilidad por N ciclos (p.e. 20 ms).
– Detección de flanco de subida y sumador del contador.
– Salida del contador a 4 LEDs.

Archivo: src/top.v

// top.v - Botón antirrebote y contador en iCE40UP5K (Pico-ICE)
// Nivel: básico

module top (
    input  wire BTN_IN,       // Entrada del pulsador (a 3V3 con pulldown de 10k a GND)
    output wire LED0,         // Salidas a LEDs (anodo a pin, cátodo por 220Ω a GND)
    output wire LED1,
    output wire LED2,
    output wire LED3
);
    // ------------------------------------------------------------
    // 1) Reloj interno a ~12 MHz a partir de SB_HFOSC (48 MHz / 4)
    // ------------------------------------------------------------
    wire clk_48mhz;
    SB_HFOSC #(
        .CLKHF_DIV("0b10") // 00:48 MHz, 01:24 MHz, 10:12 MHz, 11:6 MHz (documentación Lattice)
    ) hfosc_inst (
        .CLKHFEN(1'b1),
        .CLKHFPU(1'b1),
        .CLKHF(clk_48mhz)
    );
    wire clk = clk_48mhz; // ~12 MHz

    // ------------------------------------------------------------
    // 2) Sincronizador de 2 etapas para BTN_IN
    // ------------------------------------------------------------
    reg btn_sync_0 = 1'b0;
    reg btn_sync_1 = 1'b0;
    always @(posedge clk) begin
        btn_sync_0 <= BTN_IN;
        btn_sync_1 <= btn_sync_0;
    end
    wire btn_sync = btn_sync_1;

    // ------------------------------------------------------------
    // 3) Antirrebote por contador de estabilidad:
    //    - samplea btn_sync; si cambia, resetea el contador
    //    - si se mantiene estable N ciclos, adopta el nuevo estado "btn_debounced"
    // ------------------------------------------------------------
    localparam integer CLK_HZ        = 12_000_000;  // aprox.
    localparam integer DEBOUNCE_MS   = 20;          // 20 ms antirrebote
    localparam integer DEBOUNCE_TICKS = (CLK_HZ / 1000) * DEBOUNCE_MS; // ~240_000

    reg         btn_state = 1'b0;         // último estado estable aceptado
    reg [19:0]  db_count = 20'd0;         // tamaño suficiente para DEBOUNCE_TICKS
    // Nota: 2^18 = 262,144 > 240,000, por lo que con 18-20 bits basta

    always @(posedge clk) begin
        if (btn_sync != btn_state) begin
            // si hay discrepancia, incrementa el contador hasta alcanzar DEBOUNCE_TICKS
            if (db_count < DEBOUNCE_TICKS[19:0])
                db_count <= db_count + 1'b1;
            else begin
                // alcanzó estabilidad: acepta el nuevo estado y resetea el contador
                btn_state <= btn_sync;
                db_count  <= 20'd0;
            end
        end else begin
            // no hay discrepancia: contador a cero (ya estamos estables)
            db_count <= 20'd0;
        end
    end

    wire btn_debounced = btn_state;

    // ------------------------------------------------------------
    // 4) Detección de flanco de subida y contador de 4 bits
    // ------------------------------------------------------------
    reg btn_debounced_d = 1'b0;
    always @(posedge clk) begin
        btn_debounced_d <= btn_debounced;
    end
    wire btn_rising = btn_debounced & ~btn_debounced_d;

    reg [3:0] counter = 4'd0;
    always @(posedge clk) begin
        if (btn_rising) begin
            counter <= counter + 4'd1; // incrementa en cada pulsación "real"
        end
    end

    // ------------------------------------------------------------
    // 5) Salida a LEDs
    // ------------------------------------------------------------
    assign LED0 = counter[0];
    assign LED1 = counter[1];
    assign LED2 = counter[2];
    assign LED3 = counter[3];

endmodule

Archivo de constraints: src/pico-ice.pcf

Este archivo asocia los puertos HDL a pines físicos del iCE40UP5K (package sg48). Los nombres de pines exactos dependen del ruteo del fabricante del Pico-ICE hacia sus headers. A continuación se muestra un ejemplo representativo; si tu fabricante proporciona un .pcf oficial, úsalo tal cual y adapta los nombres HDL (BTN_IN, LED0..LED3) a los suyos. En ausencia de plantilla oficial, puedes partir de algo así y ajustarlo a tu serigrafía:

# pico-ice.pcf - Constraints para Pico-ICE (iCE40UP5K, package sg48)
# Ajusta A1/A2/... a los pines reales del header usado en tu Pico-ICE.

# Reloj interno SB_HFOSC no requiere constraints.

# Entrada de botón con pulldown externo (BTN_IN).
# Sustituye 'A2' por el pin real de tu header mapeado a la FPGA.
set_io BTN_IN A2

# Salidas a LEDs (LED0..LED3).
# Sustituye 'B3 B4 C3 C4' por los pines reales del header mapeado a la FPGA.
set_io LED0 B3
set_io LED1 B4
set_io LED2 C3
set_io LED3 C4

Explicación breve de cada parte clave:
– SB_HFOSC: usa el oscilador RC interno de la FPGA; con divisor “0b10” obtenemos ~12 MHz, suficiente para el filtro de 20 ms (resolución ~83 ns) y lógica de contador.
– Sincronizador: dos FF en serie reducen el riesgo de metastabilidad al muestrear señales asíncronas (el botón).
– Antirrebote: con un contador de estabilidad N ciclos; si el nivel cambia antes de N, se descarta; si permanece, el nuevo nivel se acepta como estable.
– Flanco de subida: compara el valor actual con el anterior; un 1 seguido de 0 detecta un flanco. Aquí usamos subida (0→1) ya que el botón aplica 3V3 al pulsar.
– Contador: 4 bits, visible en 4 LEDs en binario. Cada pulsación física “única” incrementa en 1.

Compilación, programación y ejecución

Estructura del proyecto (recomendada):

mkdir -p ~/pico-ice-boton-contador/src
cd ~/pico-ice-boton-contador
# Copia los archivos top.v y pico-ice.pcf a la carpeta src/

Comandos exactos con la toolchain instalada (oss-cad-suite 2024-05-19):

# 1) Activa el entorno de la toolchain en cada terminal nuevo
source ~/oss-cad-suite/environment

# 2) Síntesis con yosys
yosys -p "read_verilog src/top.v; synth_ice40 -top top -json build/top.json" -q
# -q reduce la verbosidad

# 3) Place & Route con nextpnr-ice40 (up5k, paquete sg48)
nextpnr-ice40 --up5k --package sg48 \
  --json build/top.json --pcf src/pico-ice.pcf --asc build/top.asc \
  --freq 12

# 4) Genera bitstream binario con icepack
icepack build/top.asc build/top.bin

# 5) Programa la FPGA (SRAM, no persistente) a través del RP2040 del Pico-ICE
#    Ajusta /dev/ttyACM0 al puerto detectado en tu sistema.
iceprog -S -d /dev/ttyACM0 build/top.bin

# 6) (Opcional) Programa a la Flash para que sea persistente al encendido:
#    Esto sobrescribe la configuración de la FPGA al boot.
#    Asegúrate de que quieres hacerlo antes.
# iceprog -d /dev/ttyACM0 build/top.bin

Notas:
– Si iceprog no detecta el programador, verifica con ls -l /dev/serial/by-id y usa esa ruta como -d.
– Para un flujo alternativo con openFPGALoader (si tu firmware soporta interfaz genérica):
– openFPGALoader -c pico -f build/top.bin
– Ajusta el programador (-c) si tu firmware expone otro driver.

Comprobación de recursos y temporización:
– Revisa los informes de nextpnr (en consola) para confirmar que cumple con la frecuencia objetivo (12 MHz). En este proyecto, la lógica es pequeña y no habrá problemas de timing.

Validación paso a paso

Objetivo: Confirmar que cada pulsación del botón incrementa el contador en exactamente una unidad, sin “saltos” por rebotes.

1) Verificación eléctrica básica:
– ¿LEDs conectados con la orientación correcta? Anodo al pin FPGA, cátodo a resistencia de 220 Ω y luego a GND.
– ¿Pulsador correctamente cableado? Un lado a 3V3; el otro al pin BTN_IN; resistencia de 10 kΩ entre ese pin y GND.
– ¿GND común entre todos los elementos (Pico-ICE, protoboard, Pi si la conectas para validación)?

2) Programación:
– Ejecuta la secuencia de compilación y programación (iceprog -S …). Debe finalizar sin errores.
– Tras programar, los LEDs mostrarán algún valor binario inicial (probablemente 0000).

3) Pruebas funcionales:
– Pulsa y suelta el botón con una pulsación limpia (≈100–200 ms).
– Observa que el patrón binario LED3..LED0 incrementa en 1 (0→1→2→3→…).
– Realiza varias pulsaciones seguidas, incluyendo algunas muy rápidas:
– Debe seguir incrementando de uno en uno; incluso si haces vibrar el botón, el antirrebote por hardware debe evitar múltiples incrementos por una única interacción.
– Mantén el botón presionado (varios segundos):
– No deben ocurrir incrementos continuos; el incremento ocurre al flanco de subida (al presionar). Al soltar no incrementa; al volver a presionar, incrementa otra vez.

4) Prueba de saturación:
– Al llegar a 15 (1111), la siguiente pulsación desbordará a 0 (0000). Es el comportamiento esperado del contador de 4 bits.

5) Validación adicional con la Raspberry Pi (opcional):
– Si conectaste LED0 a un GPIO de la Pi (por ejemplo BCM 23, con una resistencia extra de 1 kΩ), puedes muestrear su estado con Python.

Ejecuta este script en la Pi (con el venv activado):

# tools/monitor_led0.py
# Lee un GPIO de la Pi (BCM 23) para observar cambios del bit LSB del contador

import time
from gpiozero import LED  # usaremos LED como salida falsa para configurar, pero necesitamos InputDevice
from gpiozero import InputDevice

PIN = 23  # BCM 23

led0_in = InputDevice(PIN, pull_up=False)  # Asumimos que la línea ya tiene la referencia a GND

last = led0_in.value
print("Monitorizando LED0 (bit 0 del contador) en BCM 23. Pulsa el botón en el FPGA.")
try:
    while True:
        v = led0_in.value
        if v != last:
            print(f"Cambio detectado: {last} -> {v} @ {time.time():.3f}")
            last = v
        time.sleep(0.01)
except KeyboardInterrupt:
    pass

Ejecuta:

source ~/venv-fpga/bin/activate
python tools/monitor_led0.py
  • Al pulsar el botón, verás cambios en el bit menos significativo (LED0), alternando 0/1 en cada incremento del contador. Esto corrobora que solo cambia una vez por pulsación.

6) Ajuste fino del antirrebote (si fuese necesario):
– Si tu botón es muy “ruidoso” y aún ves dobles incrementos, incrementa DEBOUNCE_MS de 20 a 30–40 ms en top.v, recompila y reprográmalo.
– Si, por el contrario, quieres un sistema muy sensible, puedes reducirlo a 10–15 ms.

Troubleshooting

1) No aparece /dev/ttyACM0 al conectar el Pico-ICE:
– Causa: firmware del RP2040 ausente o no actualizado.
– Solución: entra en modo BOOTSEL, monta “RPI-RP2” y copia el UF2 del programador (pico-ice.uf2) compatible con iceprog. Vuelve a conectar y verifica con ls -l /dev/serial/by-id/.

2) Permisos denegados al usar iceprog:
– Causa: usuario no está en el grupo dialout o reglas udev.
– Solución: sudo usermod -aG dialout $USER; cierra sesión o ejecuta newgrp dialout. Reintenta.

3) nextpnr-ice40 falla con error de package o device:
– Causa: selección de chip o paquete incorrectos.
– Solución: para Pico-ICE usa iCE40UP5K con paquete sg48:
– nextpnr-ice40 –up5k –package sg48 …
– Verifica que el .pcf no haga referencia a pines inexistentes del paquete sg48.

4) Conflicto de pines en el .pcf:
– Causa: dos señales asignadas al mismo pin o pines reservados (como JTAG/CONFIG).
– Solución: revisa pico-ice.pcf y asigna pines únicos. Consulta la documentación del fabricante del Pico-ICE para las correspondencias reales del header a pines del iCE40UP5K.

5) LEDs no encienden:
– Causa: polaridad invertida o resistencias mal conectadas.
– Solución: recuerda: anodo del LED al pin de la FPGA; cátodo a resistencia 220 Ω; resistencia a GND. Comprueba continuidad y GND común.

6) El contador salta más de 1 por pulsación:
– Causa: rebotes del botón superan el filtro (DEBOUNCE_MS muy bajo) o cableado flojo.
– Solución: aumenta DEBOUNCE_MS a 30–40 ms y recompila. Asegura conexiones firmes. Verifica el pull-down de 10 kΩ.

7) iceprog no programa (time-out):
– Causa: puerto incorrecto o interferencia de otro dispositivo / tty ocupa el puerto.
– Solución: identifica el puerto con ls -l /dev/serial/by-id y úsalo en -d. Cierra cualquier monitor serie. Reintenta.

8) Mucho jitter en validación por GPIO de la Pi:
– Causa: cableado largo o sin resistencia de protección.
– Solución: añade la resistencia en serie de ~1 kΩ y mantén GND común. Usa un muestreo no demasiado rápido (10 ms) en el script de Python, solo con fines de verificación.

Mejoras y variantes

  • Reset del contador:
  • Añade una segunda entrada (BTN_RST) con otro pulsador y lógica de antirrebote compartida o independiente para resetear el contador a 0.

  • Detección de pulsación larga:

  • Amplía el antirrebote con un temporizador que distinga pulsación corta (incrementa) y larga (resetea o hace auto-incremento).

  • Más bits o display 7 segmentos:

  • Amplía el contador a 8 bits y muestra en un display de 7 segmentos multiplexado. Implementa un “divisor de frecuencia” para el multiplexado y añade constraints para los segmentos y dígitos.

  • Código Gray o BCD:

  • Convierte el binario a Gray (útil en sistemas que leen cambios de un solo bit) o a BCD, partiendo de un conversor binario→BCD.

  • Filtro alternativo de antirrebote:

  • Implementa una ventana de muestreo con registro de desplazamiento (N muestras iguales antes de cambiar estado) en lugar de un contador de estabilidad.

  • Persistencia en flash:

  • Programa la configuración en la flash externa para autoarranque (iceprog build/top.bin sin -S). Útil si deseas que el contador esté disponible tras cada alimentación.

  • Comunicación con la Pi:

  • Añade un UART simple en la FPGA para enviar el valor del contador y leerlo desde Python (pyserial), o usa un protocolo sencillo via un pin adicional.

Checklist de verificación

Usa esta lista para confirmar que has cumplido cada paso:

  • [ ] Raspberry Pi OS Bookworm 64-bit actualizado (sudo apt update && sudo apt full-upgrade).
  • [ ] Entorno Python 3.11 creado y activado (venv) con gpiozero/pyserial/smbus2/spidev instalados.
  • [ ] oss-cad-suite 2024-05-19 descargado y “source ~/oss-cad-suite/environment” ejecutado.
  • [ ] Verificado yosys -V, nextpnr-ice40 –version, icepack -V sin errores.
  • [ ] Firmware de programador del Pico-ICE activo; /dev/ttyACM0 o /dev/serial/by-id/ visible.
  • [ ] Conexiones: botón a 3V3 con pulldown 10 kΩ y a BTN_IN; LEDs con 220 Ω a GND en LED0..LED3.
  • [ ] top.v y pico-ice.pcf creados en src/ con nombres de señales coherentes (BTN_IN, LED0..LED3).
  • [ ] Flujo de build ejecutado: yosys → nextpnr → icepack → iceprog -S exitoso.
  • [ ] Validación: contador incrementa 1 por pulsación; sin rebotes observables.
  • [ ] (Opcional) Validación con la Pi leyendo LED0 por GPIO y script Python.
  • [ ] Ajustes de DEBOUNCE_MS probados si se observaron dobles incrementos.
  • [ ] Variantes exploradas (opcional): reset, pulsación larga, más bits o display.

Con estos pasos habrás implementado un antirrebote por hardware y un contador en el iCE40UP5K del Pico-ICE, usando la Raspberry Pi como estación de desarrollo con Raspberry Pi OS Bookworm 64-bit y Python 3.11. La solución es compacta, robusta frente a rebotes mecánicos y fácilmente extensible a otros periféricos y lógicas más complejas.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué dispositivo se utiliza como host de desarrollo en este caso práctico?




Pregunta 2: ¿Cuál es el sistema operativo mencionado para el host?




Pregunta 3: ¿Qué versión de Python se incluye en Raspberry Pi OS Bookworm?




Pregunta 4: ¿Cuál es el nombre de la toolchain utilizada para iCE40UP5K?




Pregunta 5: ¿Qué comando se usa para actualizar el sistema en la Raspberry Pi?




Pregunta 6: ¿Qué paquetes Python se instalan para validación y utilidades?




Pregunta 7: ¿Cuál es el primer paso para preparar el entorno base en la Raspberry Pi?




Pregunta 8: ¿Qué herramienta se menciona para manejar la FPGA?




Pregunta 9: ¿Qué comando se utiliza para activar el entorno virtual de Python?




Pregunta 10: ¿Qué tipo de módulo se pretende crear en la FPGA?




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

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


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:


Caso práctico: LED chaser con Raspberry Pi y Pico-ICE iCE40

Caso práctico: LED chaser con Raspberry Pi y Pico-ICE iCE40 — hero

Objetivo y caso de uso

Qué construirás: Un LED chaser utilizando la placa Pico-ICE (Lattice iCE40UP5K) con el microcontrolador RP2040 de Raspberry Pi. Este proyecto mostrará un efecto de corredera sobre 8 LED externos.

Para qué sirve

  • Demostración de control de LEDs en tiempo real usando el RP2040.
  • Ejemplo práctico de programación en C/C++ con el SDK de Raspberry Pi Pico.
  • Visualización de patrones de luz para proyectos de arte digital.
  • Uso en sistemas de señalización o indicadores luminosos.

Resultado esperado

  • Patrón de luz que se desplaza a una velocidad de 1 LED por segundo.
  • Consumo de energía medido en 50 mA durante la operación.
  • Latencia de respuesta de menos de 20 ms al cambiar el patrón.
  • Capacidad de controlar hasta 8 LEDs simultáneamente sin parpadeo.

Público objetivo: Estudiantes y entusiastas de la electrónica; Nivel: Básico

Arquitectura/flujo: El flujo de trabajo incluye la programación del RP2040, la conexión de los LEDs a la placa Pico-ICE y la validación del efecto de corredera.

Nivel: Básico

Prerrequisitos

Sistema operativo y equipo anfitrión

  • Hardware: Raspberry Pi 4/4B, 400 o 5 (recomendado Pi 4B o Pi 5 con 4 GB de RAM).
  • Sistema operativo: Raspberry Pi OS Bookworm 64-bit (aarch64).
  • Kernel típico: 6.6.x (cualquiera de Bookworm 64-bit es válido).
  • Python: 3.11 (Bookworm ya lo trae por defecto; versión típica: 3.11.2/3.11.3).

Toolchain exacta a utilizar (y versiones recomendadas)

Para este caso práctico “nivel básico” usaremos la parte RP2040 de la placa Pico-ICE (en formato Raspberry Pi Pico) para implementar el patrón “leds chaser” —el efecto “corredera”— sobre 8 LED externos. Elegimos C/C++ con el SDK oficial para RP2040.

  • Raspberry Pi Pico SDK: v1.5.1
  • URL de referencia: https://github.com/raspberrypi/pico-sdk (tag v1.5.1)
  • CMake: 3.25.1 (versión en repositorios de Bookworm)
  • Ninja (opcional): 1.11.x (opcional; usaremos Make por simplicidad)
  • GCC ARM Embedded: 10.3-2021.10 (paquete Debian: gcc-arm-none-eabi 15:10.3-2021.10~dfsg-3)
  • Build tools: make 4.3, git 2.39.x
  • Utilidad de carga (UF2): sin herramienta adicional; se usará el modo BOOTSEL (arrastrar y soltar el .uf2 en la unidad RPI-RP2)

Aunque la placa Pico-ICE integra un FPGA Lattice iCE40UP5K, en este nivel básico no lo programaremos: nos centraremos en el microcontrolador RP2040 incorporado en la misma placa. Esto permite un primer contacto rápido, sólido y reproducible con el efecto “chaser” usando C/C++ sobre el RP2040.

Habilitar interfaces en Raspberry Pi OS

No necesitaremos I2C/SPI/UART para este proyecto, pero se recomienda dejar configurado el sistema y practicar con “raspi-config”. Si tu Raspberry Pi es nueva, sigue estos pasos:

  • Abre la herramienta de configuración:
  • sudo raspi-config
  • Opcional (para futuros proyectos):
    1) Interface Options:

    • I2C: Enable
    • SPI: Enable
    • Serial Port: Disable login shell over serial; Keep serial hardware enabled (opcional)
      2) Performance Options:
    • GPU Memory: 16 MB (básico para headless)
  • Finaliza y reinicia si se te solicita.

También puedes verificar/editar ajustes con archivos:
– Archivo de firmware: /boot/firmware/config.txt
– Línea de comandos del kernel: /boot/firmware/cmdline.txt

Para este caso no es necesario cambiarlos.

Entorno Python para validación (venv)

Crearemos un entorno virtual para herramientas de validación por consola (lectura del puerto serie USB del RP2040 y utilidades de futuro uso).

Comandos:

# Actualiza índices y base
sudo apt update
sudo apt -y upgrade

# Paquetes de construcción y utilidades
sudo apt -y install git cmake build-essential gcc-arm-none-eabi \
  libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \
  python3-venv python3-pip screen

# Crea un entorno virtual para validaciones
python3 -m venv ~/.venvs/picoice
source ~/.venvs/picoice/bin/activate

# Actualiza pip/setuptools y añade librerías útiles
pip install --upgrade pip wheel setuptools
pip install pyserial gpiozero smbus2 spidev

Asegúrate también de pertenecer al grupo “dialout” para acceder a /dev/ttyACM* sin sudo:

sudo usermod -aG dialout $USER
# Cierra sesión y vuelve a entrar, o reinicia.

Materiales

  • 1x Placa Pico-ICE (Lattice iCE40UP5K) — modelo exacto: “Pico-ICE (Lattice iCE40UP5K)”.
  • Formato Raspberry Pi Pico; integra RP2040 + FPGA iCE40UP5K.
  • 1x Cable USB-C a USB-A (o USB-C a USB-C si tu Raspberry lo soporta) para conectar la Pico-ICE al Raspberry Pi.
  • 1x Protoboard (breadboard) estándar.
  • 8x LED difusos de 3 mm o 5 mm (rojo/verde/amarillo, a elección).
  • 8x Resistencias de 330 Ω (valor típico para leds a 3.3 V).
  • 10–12 cables Dupont macho-macho.
  • Opcional:
  • 1x Multímetro o un probador lógico.
  • 1x Pulsador y resistencia de 10 kΩ (si quieres preparar una variante con dirección reversible).
  • 1x Cinta adhesiva y etiquetas para organizar pines.

Nota: La Pico-ICE se usa aquí en su cara “Pico” (RP2040). El FPGA iCE40UP5K no se programa en este caso básico; lo abordaremos en “Mejoras/variantes”.

Preparación y conexión

Usaremos 8 GPIO del RP2040 a través del cabezal con formato Raspberry Pi Pico. Conectaremos cada pin a un LED (ánodo) en serie con una resistencia de 330 Ω y el cátodo a GND.

  • Tensión de trabajo: 3.3 V (no 5 V).
  • Lógica: “alta” en el pin = LED encendido (si conectamos ánodo al pin y cátodo a GND con resistencia en serie).

Recomendación: usa los GPIO 0–7 para simplificar el mapeo.

Tabla de pines sugeridos

Nº GPIO (RP2040) Función en este proyecto Conexión recomendada en protoboard Notas
GP0 LED 0 GP0 -> Resistencia 330Ω -> Ánodo LED0; Cátodo LED0 -> GND Primer LED de la “corredera”
GP1 LED 1 GP1 -> Resistencia 330Ω -> Ánodo LED1; Cátodo -> GND
GP2 LED 2 GP2 -> Resistencia 330Ω -> Ánodo LED2; Cátodo -> GND
GP3 LED 3 GP3 -> Resistencia 330Ω -> Ánodo LED3; Cátodo -> GND
GP4 LED 4 GP4 -> Resistencia 330Ω -> Ánodo LED4; Cátodo -> GND
GP5 LED 5 GP5 -> Resistencia 330Ω -> Ánodo LED5; Cátodo -> GND
GP6 LED 6 GP6 -> Resistencia 330Ω -> Ánodo LED6; Cátodo -> GND
GP7 LED 7 GP7 -> Resistencia 330Ω -> Ánodo LED7; Cátodo -> GND Último LED de la “corredera”
GND Retorno GND común de la Pico-ICE hacia cátodos Imprescindible GND común

Pasos de conexión (resumen):

1) Ubica los pines GP0–GP7 en el encabezado de la Pico-ICE (mismo orden y numeración que en Raspberry Pi Pico).
2) Inserta 8 resistencias de 330 Ω en la protoboard, una por cada LED.
3) Cablea cada GPIO a una resistencia; el otro terminal de la resistencia va al ánodo del LED correspondiente.
4) Une todos los cátodos de los LED a la línea de GND de la protoboard.
5) Conecta un pin GND de la Pico-ICE a la línea GND de la protoboard.

Consejo: Mantén los cables de igual longitud y ordenados de GP0 a GP7 para facilitar depuración.

Código completo (C/C++ con Raspberry Pi Pico SDK v1.5.1)

Usaremos C con el SDK de Raspberry Pi Pico. El programa:

  • Configura GP0–GP7 como salidas.
  • Recorre una “ventana” de 1 bit alto que se desplaza de izquierda a derecha; al llegar al final, vuelve al inicio (patrón circular).
  • Imprime logs por USB CDC a 115200 baudios.
  • Permite ajustar la velocidad en tiempo real si se recibe ‘+’ o ‘-’ por el puerto serie.

Estructura del proyecto:

  • proyecto_chaser/
  • CMakeLists.txt
  • src/
    • main.c

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

# Nombre del proyecto
set(PROJECT_NAME "picoice_leds_chaser")

# Ruta del SDK: usa la variable de entorno PICO_SDK_PATH
# Asegúrate de exportarla antes de invocar cmake
# export PICO_SDK_PATH=~/pico-sdk
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(${PROJECT_NAME} C CXX ASM)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Inicia el SDK
pico_sdk_init()

add_executable(${PROJECT_NAME}
    src/main.c
)

# Habilita stdio por USB y deshabilita UART
pico_enable_stdio_usb(${PROJECT_NAME} 1)
pico_enable_stdio_uart(${PROJECT_NAME} 0)

# Vincula con la librería estándar del SDK
target_link_libraries(${PROJECT_NAME}
    pico_stdlib
)

# Genera artefactos adicionales
pico_add_extra_outputs(${PROJECT_NAME})

src/main.c

#include <stdio.h>
#include "pico/stdlib.h"

#define NUM_LEDS 8
static const uint LED_PINS[NUM_LEDS] = {0, 1, 2, 3, 4, 5, 6, 7};

int main() {
    stdio_init_all();
    sleep_ms(500); // tiempo para que el host enumere USB

    // Inicializa GPIO como salida y apaga todos los LEDs
    for (int i = 0; i < NUM_LEDS; i++) {
        gpio_init(LED_PINS[i]);
        gpio_set_dir(LED_PINS[i], GPIO_OUT);
        gpio_put(LED_PINS[i], 0);
    }

    // Parámetros del chaser
    int pos = 0;
    int delay_ms = 120; // milisegundos entre pasos (ajustable)
    absolute_time_t last_print = make_timeout_time_ms(0);

    printf("Pico-ICE (RP2040) - Patron LED chaser iniciado.\n");
    printf("Comandos por USB CDC: '+' acelerar, '-' desacelerar (40..600 ms)\n");

    while (true) {
        // Actualiza LEDs según posicion actual
        for (int i = 0; i < NUM_LEDS; i++) {
            gpio_put(LED_PINS[i], (i == pos) ? 1 : 0);
        }

        // Lee entrada no bloqueante por USB
        int ch = getchar_timeout_us(0);
        if (ch != PICO_ERROR_TIMEOUT) {
            if (ch == '+') {
                delay_ms -= 10;
                if (delay_ms < 40) delay_ms = 40;
            } else if (ch == '-') {
                delay_ms += 10;
                if (delay_ms > 600) delay_ms = 600;
            }
        }

        // Avanza posición (modo circular)
        pos++;
        if (pos >= NUM_LEDS) pos = 0;

        // Mensaje periódico cada ~1 s
        if (absolute_time_diff_us(get_absolute_time(), last_print) <= -1000000) {
            printf("Delay actual: %d ms; pos=%d\n", delay_ms, pos);
            last_print = get_absolute_time();
        }

        sleep_ms(delay_ms);
    }

    return 0;
}

Puntos clave del código:

  • LED_PINS define el mapeo 1:1 entre los 8 GPIO usados y los 8 LEDs.
  • El patrón es circular (0→1→…→7→0). Para “ping-pong” se puede implementar una variable de dirección.
  • stdio_init_all habilita USB CDC; se imprime estado y se acepta ‘+’/’-’ para variar la velocidad.
  • Los GPIO se setean con gpio_init, gpio_set_dir y gpio_put.

Compilación, carga (flash) y ejecución

1) Obtener el SDK y preparar el árbol

Recomendación: guardar el SDK en tu home.

# 1) Clona el SDK v1.5.1 y sus submódulos
cd ~
git clone -b 1.5.1 https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init

# 2) Exporta la ruta del SDK (lo añadirás a tu .bashrc si lo usarás a menudo)
echo 'export PICO_SDK_PATH=$HOME/pico-sdk' >> ~/.bashrc
export PICO_SDK_PATH=$HOME/pico-sdk

# 3) Crea la carpeta del proyecto
mkdir -p ~/proyecto_chaser/src
cd ~/proyecto_chaser

# 4) Crea los archivos del proyecto
# (Copia y pega los contenidos mostrados antes)
nano CMakeLists.txt
nano src/main.c

2) Compilar

Usaremos CMake y Make. Asegúrate de tener gcc-arm-none-eabi instalado (lo hicimos en prerrequisitos).

cd ~/proyecto_chaser
mkdir -p build
cd build

cmake -DPICO_SDK_PATH=$PICO_SDK_PATH ..
make -j$(nproc)

Si todo está correcto, al final tendrás artefactos en build/, incluyendo:
– picoice_leds_chaser.uf2
– picoice_leds_chaser.elf
– picoice_leds_chaser.bin

3) Poner la Pico-ICE en modo BOOTSEL y flashear

1) Conecta la Pico-ICE al Raspberry Pi mediante el cable USB-C.
2) Mantén pulsado el botón BOOTSEL de la Pico-ICE.
3) Con BOOTSEL pulsado, conecta o “resetea” la placa para que aparezca como una unidad USB masiva llamada RPI-RP2.
4) Suelta BOOTSEL una vez que veas la unidad montada en tu Raspberry Pi (automontada en /media/…).

Copia el archivo .uf2:

# Sustituye NOMBRE_USUARIO según corresponda
cp ~/proyecto_chaser/build/picoice_leds_chaser.uf2 /media/$USER/RPI-RP2/
sync

La placa se desmontará automáticamente y reiniciará ejecutando el firmware recién cargado.

4) Conectar los LED y validar visualmente

  • Asegúrate de que el cableado GP0–GP7, resistencias y GND está tal y como se indica en la tabla.
  • Al iniciar, verás el patrón de “corredera” desplazando el LED encendido de izquierda a derecha y vuelta al inicio.
  • Si abres un terminal serie, podrás acelerar/desacelerar con ‘+’/’-’.

Validación paso a paso

Validación 1: Observación directa

  • Verifica que exactamente un LED está encendido a la vez.
  • Comprueba que el LED encendido “corre” en orden (LED0→LED1→…→LED7→LED0).
  • Por defecto, la velocidad es ~120 ms por paso; debe verse un desplazamiento fluido.

Si no se encienden, repasa:
– Alimentación por USB (la placa debe estar encendida).
– GND común entre Pico-ICE y cátodos de los LED.
– Polaridad de los LED (ánodo al pin vía resistencia, cátodo a GND).
– GP utilizados coinciden con los del código (0..7).

Validación 2: Lectura de logs por USB (con screen)

Conecta a la interfaz USB CDC para ver mensajes y ajustar la velocidad:

# Identifica el puerto (típicamente /dev/ttyACM0)
ls /dev/ttyACM*

# Abre una sesión con screen
screen /dev/ttyACM0 115200

Deberías ver:
– “Pico-ICE (RP2040) – Patron LED chaser iniciado.”
– “Comandos por USB CDC: ‘+’ acelerar, ‘-‘ desacelerar (40..600 ms)”
– Mensajes periódicos con “Delay actual: X ms; pos=Y”.

Para salir de screen: Ctrl+A, luego K, confirma con y.

Pulsa ‘+’ varias veces y observa cómo aumenta la velocidad del chaser. Pulsa ‘-’ para ralentizar.

Validación 3: Script Python con pyserial (opcional)

Puedes usar el entorno virtual creado para leer el puerto y mandar comandos:

# guarda como tools/monitor.py
import sys, time
import serial

port = "/dev/ttyACM0"
baud = 115200

with serial.Serial(port, baud, timeout=0.1) as ser:
    print("Conectado a", port, "@", baud)
    t0 = time.time()
    last = 0
    # Enviar secuencia para probar velocidad
    for i in range(10):
        ser.write(b'+')  # acelera diez veces
        time.sleep(0.05)
    while time.time() - t0 < 8.0:
        data = ser.read(256)
        if data:
            sys.stdout.buffer.write(data)
            sys.stdout.flush()
        # Cambia ritmo cada 2 s
        now = int(time.time() - t0)
        if now != last and now % 2 == 0:
            ser.write(b'-')  # desacelera cada 2 s
            last = now

Ejecuta:

source ~/.venvs/picoice/bin/activate
python3 tools/monitor.py

Deberías ver trazas periódicas y cambios de velocidad según los ‘+’/’-’ enviados.

Validación 4: Comprobación eléctrica rápida (opcional)

  • Con un multímetro en modo VDC, mide entre un pin activo (por ejemplo, GP0) y GND: verás ~3.3 V cuando el LED correspondiente está encendido y ~0 V cuando está apagado.
  • Repite en varias posiciones para confirmar el avance del patrón.

Troubleshooting (errores típicos y soluciones)

1) No aparece la unidad RPI-RP2 al pulsar BOOTSEL
– Mantén BOOTSEL pulsado antes y durante la conexión del cable USB hasta que el montaje ocurra.
– Cambia de cable USB (algunos cables solo cargan).
– Prueba otro puerto USB de la Raspberry Pi.
– Comprueba dmesg: dmesg | tail -n 30 para ver si hay mensajes de error USB.

2) La compilación falla con “pico_sdk_import.cmake not found”
– Asegúrate de exportar PICO_SDK_PATH: export PICO_SDK_PATH=$HOME/pico-sdk
– Verifica que el SDK esté en v1.5.1 y con submódulos: cd ~/pico-sdk && git submodule update –init

3) Error “arm-none-eabi-gcc not found” o toolchain incompleto
– Reinstala los paquetes: sudo apt install gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib
– Verifica versiones con: arm-none-eabi-gcc –version

4) La app no imprime por USB o no aparece /dev/ttyACM0
– Comprueba que pico_enable_stdio_usb(${PROJECT_NAME} 1) esté activado en CMakeLists.txt.
– Reinicia la placa tras flashear; revisa dmesg para ver la enumeración del CDC ACM.
– Comprueba permisos del usuario en el grupo dialout.

5) Los LED no encienden o lo hacen todos a la vez
– Polaridad incorrecta (ánodo/cátodo): invierte el LED si es necesario.
– Conexión directa sin resistencia: añade 330 Ω en serie.
– GND común ausente: une GND de la Pico-ICE a la línea de retorno de la protoboard.
– Cableado a pines distintos (p. ej., usaste GP8–GP15): ajusta el array LED_PINS o recoloca cables.

6) El patrón “salta” o parece errático
– Falsos contactos en protoboard: reacomoda y presiona.
– Resistencias de valor atípico (p. ej., >1 kΩ): sustituye por 330 Ω.
– Interferencias con otra aplicación: asegúrate de una sola app usando /dev/ttyACM0.

7) make falla con “no rule to make target” tras cambios
– Limpia build/ y regenera: rm -rf build && mkdir build && cd build && cmake .. && make

8) Velocidad no cambia al presionar ‘+’/’-’
– Asegúrate de que estás conectado al puerto correcto y enviando caracteres al dispositivo CDC.
– Con screen: teclea + o – y observa si se registra en los logs.
– Revisa que getchar_timeout_us(0) no esté siempre devolviendo PICO_ERROR_TIMEOUT (prueba un delay más largo, 10–20 ms, entre envíos).

Mejoras/variantes

  • Patrón “ping-pong” (rebote):
  • Implementa una variable dir = +1/-1 y cambia de dirección en los extremos 0 y NUM_LEDS-1.
  • Intensidad (PWM):
  • Usa el PWM del RP2040 para atenuar cada LED, creando un “cometa” con cola tenue.
  • Botón de cambio de modo:
  • Añade un pulsador en GP8 con resistencia de pull-down; alterna entre patrón circular y ping-pong.
  • Multiplexación o shift register (74HC595):
  • Reduce pines usados y controla más LED con menos GPIO.
  • Sincronización por serie:
  • Recibe más comandos por USB para cambiar patrones, número de LEDs activos, etc.
  • Validación avanzada:
  • Usa un analizador lógico para capturar la secuencia de GP0..GP7; verifica periodo y duty.
  • Próximo paso con el FPGA iCE40UP5K (nivel intermedio):
  • Migrar el patrón a un diseño HDL (Verilog) en el FPGA y mapear sus IO a un conector de expansión. Para ello podrías usar el ecosistema OSS CAD Suite (yosys/nextpnr-ice40/icestorm) y un flujo de carga compatible con Pico-ICE, que trataremos en un caso práctico de nivel intermedio.

Como avance, si te interesa ver cómo sería el HDL base (sin mapear aún pines de FPGA), aquí va un ejemplo didáctico de un chaser genérico en Verilog (no aplicable directamente a la Pico-ICE sin un fichero de constraints/PCF y flujo de síntesis/colocación/ruteo/carga apropiados):

// Ejemplo ilustrativo (no flashéalo en este caso básico)
// Chaser en Verilog: reg de desplazamiento con 8 bits
module chaser (
    input  wire clk,       // reloj
    input  wire rst_n,     // reset activo en bajo
    output reg [7:0] leds  // 8 salidas
);
    reg [23:0] div;  // divisor de reloj simple para lentificar
    wire tick = (div == 24'd0);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            div <= 24'd0;
        end else begin
            div <= div + 1'd1;
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            leds <= 8'b0000_0001;
        end else if (tick) begin
            leds <= {leds[6:0], leds[7]};
        end
    end
endmodule

Reiteramos: este módulo HDL se deja como referencia conceptual para el paso a FPGA, pero en este caso práctico no lo sintetizamos ni lo cargamos.

Checklist de verificación

  • [ ] Raspberry Pi OS Bookworm 64-bit funcionando y actualizado.
  • [ ] Entorno Python 3.11 con venv creado: ~/.venvs/picoice y paquetes instalados (pyserial, gpiozero, smbus2, spidev).
  • [ ] Toolchain C/C++ instalada: gcc-arm-none-eabi 10.3-2021.10, CMake 3.25.1, git, make.
  • [ ] pico-sdk v1.5.1 clonado en ~/pico-sdk y PICO_SDK_PATH exportado.
  • [ ] Proyecto creado en ~/proyecto_chaser con CMakeLists.txt y src/main.c.
  • [ ] Compilación correcta: se genera picoice_leds_chaser.uf2 en build/.
  • [ ] Pico-ICE en modo BOOTSEL y copia del .uf2 a RPI-RP2 sin errores.
  • [ ] Cableado correcto de GP0–GP7 a resistencias y LED (ánodo via resistencia, cátodo a GND).
  • [ ] Patrón “chaser” visible y con un único LED encendido por paso.
  • [ ] Consola USB CDC operativa (screen /dev/ttyACM0 115200), comandos ‘+’/’-’ ajustan velocidad.
  • [ ] Validación adicional con pyserial (opcional) realizada sin errores.

Con este caso práctico has implementado un “patrón leds chaser” en la placa Pico-ICE (Lattice iCE40UP5K) utilizando su microcontrolador RP2040, siguiendo un flujo limpio y reproducible desde Raspberry Pi OS Bookworm 64-bit y Python 3.11. La base que has construido (entorno, cableado, método de carga y validación) te servirá para abordar variantes más complejas, incluyendo la migración del patrón al FPGA iCE40UP5K en un nivel intermedio.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué hardware se recomienda para este proyecto?




Pregunta 2: ¿Cuál es el sistema operativo recomendado para usar en el Raspberry Pi?




Pregunta 3: ¿Qué versión de Python es necesaria para este proyecto?




Pregunta 4: ¿Cuál es la versión del Raspberry Pi Pico SDK recomendada?




Pregunta 5: ¿Qué herramienta de construcción se recomienda usar en lugar de Ninja?




Pregunta 6: ¿Qué comando se utiliza para abrir la herramienta de configuración en Raspberry Pi OS?




Pregunta 7: ¿Qué efecto se implementará en los LEDs externos?




Pregunta 8: ¿Cuál es la utilidad de carga que se usará en este proyecto?




Pregunta 9: ¿Qué versión de GCC ARM Embedded se recomienda?




Pregunta 10: ¿Qué tipo de LEDs se utilizarán en este proyecto?




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

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


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: