Caso práctico: Semáforo con FSM y LEDs en iCEBreaker

Caso práctico: Semáforo con FSM y LEDs en iCEBreaker — hero

Objetivo y caso de uso

Qué construirás: Implementar una máquina de estados finitos (FSM) de un semáforo simple que controle las luces rojo, amarillo y verde utilizando el LED RGB integrado en la iCEBreaker.

Para qué sirve

  • Control de señales de tráfico en un entorno simulado.
  • Demostración de la funcionalidad de un semáforo en un proyecto educativo.
  • Prueba de la capacidad de la FPGA para manejar múltiples estados y salidas.
  • Implementación de un sistema de control simple para la enseñanza de FSM.

Resultado esperado

  • Luces del semáforo cambiando cada 4 segundos para rojo y verde, 2 segundos para amarillo.
  • Funcionamiento continuo sin necesidad de reloj externo, utilizando el oscilador interno.
  • Visualización clara del estado actual del semáforo a través del LED RGB.
  • Demostración de la estabilidad del diseño en la FPGA iCEBreaker.

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

Arquitectura/flujo: FSM implementada en Verilog, sintetizada con yosys y nextpnr-ice40, cargada en la FPGA con openFPGALoader.

Nivel: Básico

Objetivo del proyecto

Implementar una máquina de estados finitos (FSM) de un semáforo simple “traffic-light-fsm-with-leds” que controle las luces rojo, amarillo y verde utilizando el LED RGB integrado en la iCEBreaker (Lattice iCE40UP5K). El diseño usará Verilog, oscilará con el oscilador interno del FPGA (sin depender de un reloj externo) y se sintetizará con la toolchain abierta yosys + nextpnr-ice40 + icestorm + openFPGALoader.

La secuencia esperada:
– Rojo: 4 segundos
– Verde: 4 segundos
– Amarillo: 2 segundos
– Repite indefinidamente

Nota: En este caso didáctico usaremos el LED RGB integrado y el oscilador interno (SB_HFOSC), por lo que no necesitaremos un archivo de constraints (.pcf) ni cableado externo.

Prerrequisitos

Sistema operativo y dependencias

  • Linux (recomendado): Ubuntu 22.04.4 LTS o Ubuntu 24.04 LTS
  • Alternativas factibles: Debian 12, Arch Linux, Fedora 39/40
  • macOS: 13 Ventura o 14 Sonoma (posible con Homebrew/OSS CAD Suite)
  • Windows 10/11: viable mediante WSL2 (Ubuntu 22.04) con acceso USB habilitado para la iCEBreaker

Toolchain exacta

Se utilizará la toolchain abierta con las siguientes versiones (probadas en OSS CAD Suite):

  • OSS CAD Suite: release 2024-10-20 (binarios precompilados para Linux/macOS)
  • yosys: 0.40
  • nextpnr-ice40: 0.7
  • icestorm (icepack/iceunpack/iceprog utilidades): 0.0.0+20240915
  • openFPGALoader: 0.12.0

Cómo verificar versiones (ejemplos):
– yosys -V → Yosys 0.40
– nextpnr-ice40 –version → nextpnr-ice40 0.7
– icepack -V → icepack 0.0.0+20240915
– openFPGALoader –version → openFPGALoader v0.12.0

Si usas OSS CAD Suite (recomendado), bastará con añadir su carpeta bin al PATH y tendrás exactamente estas versiones.

Materiales

  • 1 × placa FPGA iCEBreaker (Lattice iCE40UP5K), paquete SG48
  • 1 × cable USB‑C a USB‑A/USB‑C para alimentación y programación
  • PC con Linux/macOS/Windows (WSL2) y la toolchain indicada arriba

Opcional (no necesario para este caso):
– Cronómetro (para validar tiempos de estados)
– Multímetro/analizador lógico (solo para verificación avanzada)

No se requieren LEDs externos ni PMODs: usaremos el LED RGB integrado, manejado por la macro SB_RGBA_DRV del UP5K.

Preparación y conexión

Conexión física

  • Conecta la iCEBreaker al ordenador mediante el cable USB. La placa tomará energía desde el USB y será detectable por openFPGALoader.
  • Asegúrate de no tener nada enchufado en los PMODs para evitar confusiones en este caso práctico (no se usarán).

Recursos de la placa que usaremos

  • LED RGB integrado, cableado a los pines dedicados RGB0, RGB1, RGB2 del iCE40UP5K.
  • Oscilador interno SB_HFOSC (no usaremos el cristal externo de 12 MHz).

Esto simplifica el flujo: no necesitaremos un archivo .pcf de constraints, porque la macro SB_RGBA_DRV direcciona los pines dedicados de forma implícita.

Tabla de mapeo lógico-funcional que usaremos

Aunque no asignamos pines manualmente, conviene documentar la intención de los colores:

Señal lógica Canal LED RGB (SB_RGBA_DRV) Color percibido Notas
led_r RGB2PWM Rojo Intensidad fija vía parámetros de corriente
led_g RGB1PWM Verde Idem
led_b RGB0PWM Azul (no usado) Mantendremos apagado para este semáforo
CURREN Fijado a 1 Habilita corriente del driver RGB
RGBLEDEN Fijado a 1 Habilita el LED driver

Nota: La macro SB_RGBA_DRV mapea internamente RGB2PWM ↔ canal rojo, RGB1PWM ↔ canal verde, RGB0PWM ↔ canal azul en la mayoría de diseños base. Si observas colores permutados en tu placa, intercambia las señales PWM correspondientes (ver Troubleshooting).

Código completo (Verilog) y explicación

Estructura del diseño

  • Oscilador interno SB_HFOSC configurado a ~12 MHz (CLKHF_DIV = «0b10»).
  • Divisor de frecuencia para generar un “tick” de 1 Hz (cuenta de 12,000,000 ciclos).
  • FSM con estados: RED (4 s), GREEN (4 s), YELLOW (2 s).
  • Control del LED RGB vía SB_RGBA_DRV con corriente limitada por parámetros.

Archivo: src/traffic_light_top.v

Explicación clave:
– Parámetros de corriente del LED (RGBx_CURRENT) definen brillo (valores típicos bajos para no deslumbrar).
– El contador “sec_counter” implementa el 1 Hz.
– La FSM cambia de estado en cada tick y asigna led_r/led_g/led_b.

Código:

// src/traffic_light_top.v
// Semáforo con FSM para iCEBreaker (Lattice iCE40UP5K) usando LED RGB integrado.
// Toolchain: yosys 0.40, nextpnr-ice40 0.7, icestorm 0.0.0+20240915, openFPGALoader 0.12.0

module traffic_light_top;

    // Señales internas del reloj
    wire clk_hf;       // Reloj del oscilador interno (dividido)
    wire clk_raw;      // Salida bruta del HFOSC antes de división (no usada externamente)

    // Instancia del oscilador interno HFOSC
    // CLKHF_DIV opciones:
    //  "0b00" -> 48 MHz
    //  "0b01" -> 24 MHz
    //  "0b10" -> 12 MHz
    //  "0b11" -> 6 MHz
    SB_HFOSC #(
        .CLKHF_DIV("0b10")  // 12 MHz
    ) u_hfosc (
        .CLKHFEN(1'b1),
        .CLKHFPU(1'b1),
        .CLKHF(clk_hf)
    );

    // Generación de 1 Hz: contador hasta 12_000_000 - 1
    localparam integer F_HZ       = 12_000_000;
    localparam integer ONE_HZ_MAX = F_HZ - 1;

    reg [23:0] div_cnt = 24'd0;   // 24 bits alcanzan 16,777,215 (> 12,000,000)
    reg        tick_1hz = 1'b0;

    always @(posedge clk_hf) begin
        if (div_cnt == ONE_HZ_MAX[23:0]) begin
            div_cnt  <= 24'd0;
            tick_1hz <= 1'b1;
        end else begin
            div_cnt  <= div_cnt + 24'd1;
            tick_1hz <= 1'b0;
        end
    end

    // FSM de semáforo
    typedef enum reg [1:0] {
        ST_RED    = 2'b00,
        ST_GREEN  = 2'b01,
        ST_YELLOW = 2'b10
    } state_t;

    state_t state = ST_RED;

    // Temporizaciones por estado en segundos
    localparam integer T_RED_S    = 4;
    localparam integer T_GREEN_S  = 4;
    localparam integer T_YELLOW_S = 2;

    reg [2:0] sec_counter = 3'd0;  // suficiente para contar hasta 7

    // Señales de color (PWM “digital” on/off)
    reg led_r = 1'b0;
    reg led_g = 1'b0;
    reg led_b = 1'b0; // no se usará (azul apagado)

    // Lógica de la FSM (cambia de estado con cada tick_1hz)
    always @(posedge clk_hf) begin
        if (tick_1hz) begin
            case (state)
                ST_RED: begin
                    if (sec_counter == T_RED_S-1) begin
                        state       <= ST_GREEN;
                        sec_counter <= 0;
                    end else begin
                        sec_counter <= sec_counter + 1;
                    end
                end

                ST_GREEN: begin
                    if (sec_counter == T_GREEN_S-1) begin
                        state       <= ST_YELLOW;
                        sec_counter <= 0;
                    end else begin
                        sec_counter <= sec_counter + 1;
                    end
                end

                ST_YELLOW: begin
                    if (sec_counter == T_YELLOW_S-1) begin
                        state       <= ST_RED;
                        sec_counter <= 0;
                    end else begin
                        sec_counter <= sec_counter + 1;
                    end
                end

                default: begin
                    state       <= ST_RED;
                    sec_counter <= 0;
                end
            endcase
        end
    end

    // Salidas de color según estado
    always @(*) begin
        case (state)
            ST_RED: begin
                // Rojo ON, Verde OFF, Azul OFF
                led_r = 1'b1;
                led_g = 1'b0;
                led_b = 1'b0;
            end
            ST_GREEN: begin
                // Rojo OFF, Verde ON, Azul OFF
                led_r = 1'b0;
                led_g = 1'b1;
                led_b = 1'b0;
            end
            ST_YELLOW: begin
                // Amarillo ~ Rojo + Verde
                led_r = 1'b1;
                led_g = 1'b1;
                led_b = 1'b0;
            end
            default: begin
                led_r = 1'b0;
                led_g = 1'b0;
                led_b = 1'b0;
            end
        endcase
    end

    // Driver del LED RGB dedicado del iCE40UP5K
    // Mapea internamente a los pines dedicados, no requiere .pcf.
    wire RGB0, RGB1, RGB2; // pines físicos dedicados (no exponen al top)

    SB_RGBA_DRV #(
        .CURRENT_MODE("0b1"),      // modo de control de corriente simple
        .RGB0_CURRENT("0b000111"), // canal azul (no se usa, pero definimos corriente)
        .RGB1_CURRENT("0b000111"), // canal verde
        .RGB2_CURRENT("0b000111")  // canal rojo
    ) u_rgb_drv (
        .CURREN(1'b1),
        .RGBLEDEN(1'b1),
        .RGB0PWM(led_b), // PWM canal azul
        .RGB1PWM(led_g), // PWM canal verde
        .RGB2PWM(led_r), // PWM canal rojo
        .RGB0(RGB0),     // salida a pin dedicado azul
        .RGB1(RGB1),     // salida a pin dedicado verde
        .RGB2(RGB2)      // salida a pin dedicado rojo
    );

endmodule

Puntos a notar:
– Elegimos dividir el oscilador a 12 MHz para facilitar el divisor a 1 Hz con un contador de 24 bits.
– La macro SB_RGBA_DRV se encarga de conectar el PWM de cada color a los pines físicos dedicados, así no definimos un .pcf.
– Si el brillo es muy alto/bajo, ajusta los parámetros RGBx_CURRENT (por ejemplo «0b000011» para menos corriente o «0b001111» para más, manteniendo valores razonables para no deslumbrar).

Compilación, programación y ejecución

Estructura recomendada de directorios:
– proyecto/
– src/traffic_light_top.v
– build/ (se generará)
– scripts/ (opcional)

Puedes usar estos comandos paso a paso (suponiendo que estás en la raíz de “proyecto/”):

1) Crear carpetas y verificar toolchain:
– mkdir -p src build
– Copia el archivo Verilog en src/traffic_light_top.v
– Verifica versiones (opcional, para asegurarte de reproducibilidad):

yosys -V
nextpnr-ice40 --version
icepack -V
openFPGALoader --version

2) Síntesis con yosys (genera JSON):

yosys -p "read_verilog -sv src/traffic_light_top.v; synth_ice40 -top traffic_light_top -json build/traffic_light.json" -q
  • -sv permite SystemVerilog básico (por el typedef enum). Si prefieres Verilog puro, reemplaza el typedef por parámetros/defines.

3) Place & Route con nextpnr-ice40 (UP5K, paquete sg48):

nextpnr-ice40 --up5k --package sg48 --json build/traffic_light.json --asc build/traffic_light.asc
  • No hay .pcf, porque usamos pines dedicados (RGB) y recursos internos (HFOSC).

4) Empaquetado con icepack (BIN para flashear):

icepack build/traffic_light.asc build/traffic_light.bin

5) Programación con openFPGALoader (iCEBreaker):

openFPGALoader -b icebreaker build/traffic_light.bin
  • Debe detectar la placa y programar el bitstream en SRAM (por defecto). Para flashear en memoria SPI (si la placa lo permite y deseas arranque automático), puedes usar:
openFPGALoader -b icebreaker -f build/traffic_light.bin
  • -f escribe en la flash SPI externa; tras ello, al reset, el FPGA cargará automáticamente el diseño.

Si prefieres un script reproducible, puedes crear un Makefile o script bash con exactamente estos pasos.

Ejemplo de script build.sh:

#!/usr/bin/env bash
set -e

mkdir -p build

echo "[1/4] Synth (yosys 0.40)"
yosys -p "read_verilog -sv src/traffic_light_top.v; synth_ice40 -top traffic_light_top -json build/traffic_light.json" -q

echo "[2/4] PnR (nextpnr-ice40 0.7, up5k sg48)"
nextpnr-ice40 --up5k --package sg48 --json build/traffic_light.json --asc build/traffic_light.asc

echo "[3/4] Pack (icestorm icepack 0.0.0+20240915)"
icepack build/traffic_light.asc build/traffic_light.bin

echo "[4/4] Program (openFPGALoader 0.12.0, board icebreaker)"
openFPGALoader -b icebreaker build/traffic_light.bin

echo "Done."

Dale permisos de ejecución y ejecútalo:
– chmod +x build.sh
– ./build.sh

Validación paso a paso

1) Alimentación y enumeración:
– Conecta la placa iCEBreaker por USB. Deberías ver el LED de alimentación encendido.
– openFPGALoader -l debería listar el programador/placa cuando está accesible.

2) Cargar el bitstream temporalmente:
– Ejecuta la secuencia de build y programación (pasos anteriores).
– Si todo es correcto, al finalizar, el LED RGB comenzará a mostrar la secuencia del semáforo.

3) Verificación visual de la FSM:
– Observa el LED RGB:
– Estado rojo durante ~4 s.
– Luego verde durante ~4 s.
– Luego amarillo (rojo+verde) durante ~2 s.
– Repite continuamente.

4) Medición de tiempos:
– Usa un cronómetro (o el reloj del móvil) para medir cada color. Se tolera un pequeño error debido a la tolerancia del oscilador interno (HFOSC).
– Los tiempos deberían estar muy próximos a 4 s, 4 s y 2 s respectivamente.

5) Comprobación de reinicio:
– Si programas en SRAM (sin -f), al desconectar y reconectar la placa el diseño desaparecerá (el LED no seguirá la secuencia).
– Si programas la flash SPI (-f), tras reset o reconexión el semáforo debería arrancar automáticamente.

6) Consumo y brillo:
– Si el color parece muy brillante o tenue, ajusta RGBx_CURRENT en el código y recompila.
– Comprueba que el amarillo se perciba como mezcla de rojo + verde (sin componente azul).

Troubleshooting (5–8 casos frecuentes)

1) El color no corresponde (por ejemplo, ves azul cuando esperas rojo)
– Causa: En algunas variantes, el cableado interno puede permutar canales.
– Solución:
– Intercambia las asignaciones de PWM en la instancia SB_RGBA_DRV:
– Prueba a permutar RGB0PWM, RGB1PWM, RGB2PWM hasta que rojo, verde y amarillo se vean correctamente.
– Ejemplo: si el rojo aparece en el canal RGB0, conecta led_r a RGB0PWM y ajusta los demás en consecuencia.

2) Los tiempos no coinciden exactamente (p. ej., 3.8 s en lugar de 4 s)
– Causa: El oscilador interno HFOSC tiene tolerancia (no es un TCXO).
– Soluciones:
– Cambia CLKHF_DIV a “0b01” (24 MHz) y recalcula el divisor exacto para 1 Hz (24,000,000) si buscas minimizar error de redondeo.
– Implementa un divisor parametrizable o un “calibration factor” para ajustar ciclos.

3) nextpnr-ice40 falla por dispositivo/paquete incorrecto
– Síntoma: Error tipo “Device not found” o no compila el diseño.
– Solución:
– Verifica la llamada exacta: –up5k –package sg48 (coincide con iCE40UP5K-SG48 de iCEBreaker).
– Asegúrate de no pasar un .pcf que fuerce pines inexistentes (en este caso no usamos .pcf).

4) openFPGALoader no detecta la placa
– Síntomas: “No cable found” o “Failed to open”.
– Soluciones:
– Revisa el cable USB y puerto.
– En Linux, agrega reglas udev para acceso al programador (consulta la documentación de openFPGALoader) y reconecta la placa.
– Prueba como root (sudo) solo para validar si es un tema de permisos, luego configura udev correctamente.

5) El LED no enciende nada
– Causas:
– No se ejecutó la programación (falló un paso).
– CURREN o RGBLEDEN no están habilitados en el driver.
– Corriente configurada demasiado baja.
– Soluciones:
– Repite la secuencia de build (observa mensajes de éxito).
– Verifica que en SB_RGBA_DRV: CURREN=1’b1 y RGBLEDEN=1’b1.
– Sube RGBx_CURRENT (ej., “0b001011” o “0b001111”).
– Comprueba que no tienes otro diseño residual en la flash que sobrescriba al reiniciar (si programaste con -f).

6) Los colores parpadean de forma extraña o “flickering”
– Causas:
– Señales PWM no sincronizadas o asignaciones inestables (aunque en este diseño son on/off).
– Soluciones:
– Asegúrate de que led_r/led_g/led_b provienen de lógica síncrona o combinacional estable (como en el ejemplo).
– Evita generar PWM de alta frecuencia sin un reloj bien definido.

7) El brillo es demasiado alto o molesta a la vista
– Soluciones:
– Reduce RGBx_CURRENT.
– Implementa una modulación por ancho de pulso (PWM) a baja duty para cada canal (no imprescindible en este caso, pero puede hacerse con otro divisor).

8) Programación en flash SPI falla (-f)
– Síntomas: “Failed to write flash” o errores de protección.
– Soluciones:
– Asegúrate de que la placa tiene la flash accesible y no está “write-protected”.
– Prueba primero SRAM (sin -f) para validar el diseño y luego vuelve a intentar flash.
– Actualiza openFPGALoader si la versión es antigua.

Mejoras y variantes

  • Añadir “modo noche”: tras cierta hora (o tras pulsar un botón, si lo incorporas), pasar a parpadeo amarillo intermitente. Requiere:
  • Un botón de usuario (si la iCEBreaker dispone de BTN conectado a un pin de E/S que puedas leer con SB_IO).
  • Debounce por lógica (contador y filtro).
  • Un estado adicional “BLINK_YELLOW” con duty 50% a 1 Hz.

  • Atenuación con PWM real:

  • Implementa un generador PWM de, por ejemplo, 500 Hz y controla duty cycles de cada color.
  • Así puedes crear un amarillo más “cálido” (ajustando proporción rojo/verde) o efectos de transición suave.

  • Temporización configurable:

  • Parametriza T_RED_S, T_GREEN_S y T_YELLOW_S con parámetros del módulo o registros modificables por UART (si añades una interfaz, p. ej., vía un PMOD UART externo).

  • Testbench en simulación:

  • Simula la FSM con un “tick” acelerado para verificar secuencias y tiempos relativos sin esperar segundos reales.

  • Indicadores de estado adicionales:

  • Usa parpadeos cortos al entrar en cada estado (por ejemplo, un destello azul muy breve al cambiar de fase) para depurar visualmente.

  • Portar el diseño a usar el reloj externo de 12 MHz:

  • Si quieres practicar constraints, puedes reemplazar SB_HFOSC por un pin de entrada “clk_12mhz” y asignarlo en un .pcf con el pin correspondiente al oscilador de la iCEBreaker.
  • Luego recalcula el divisor para 1 Hz con 12 MHz exactos.

Checklist de verificación

  • [ ] Cuentas con la placa exacta: iCEBreaker (Lattice iCE40UP5K, paquete SG48).
  • [ ] Toolchain instalada con versiones: yosys 0.40, nextpnr-ice40 0.7, icestorm 0.0.0+20240915, openFPGALoader 0.12.0.
  • [ ] Estructura de proyecto creada (src/, build/).
  • [ ] El archivo src/traffic_light_top.v está copiado exactamente como en el ejemplo (o con tus ajustes).
  • [ ] Síntesis completó sin errores (se generó build/traffic_light.json).
  • [ ] Place & Route completó sin errores (se generó build/traffic_light.asc).
  • [ ] Empaquetado con icepack OK (se generó build/traffic_light.bin).
  • [ ] Programación con openFPGALoader OK (sin errores).
  • [ ] El LED RGB muestra rojo ~4 s, verde ~4 s y amarillo ~2 s, en bucle.
  • [ ] Si los colores no coinciden, probaste a permutar las señales PWM en SB_RGBA_DRV.
  • [ ] Si programaste a flash (-f), el diseño arranca tras reset automáticamente.

Cierre

Este caso práctico te ha guiado, de forma coherente con el modelo iCEBreaker (Lattice iCE40UP5K), por todas las fases para construir un semáforo por FSM: análisis, diseño en Verilog, uso del oscilador interno, control del LED RGB con SB_RGBA_DRV, y un flujo de síntesis y programación 100% abierto (yosys + nextpnr‑ice40 + icestorm + openFPGALoader) con versiones concretas. Es una base excelente para explorar mejoras (PWM, entradas de usuario, temporizaciones dinámicas) y adquirir soltura con la toolchain y recursos del UP5K.

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: ¿Cuál es el objetivo del proyecto mencionado en el artículo?




Pregunta 2: ¿Qué tipo de luz se controla en el proyecto?




Pregunta 3: ¿Cuánto tiempo debe estar la luz roja encendida según la secuencia esperada?




Pregunta 4: ¿Qué sistema operativo se recomienda para el proyecto?




Pregunta 5: ¿Cuál es la herramienta utilizada para la síntesis del diseño?




Pregunta 6: ¿Qué componente se utiliza para la oscilación en el diseño?




Pregunta 7: ¿Cuánto tiempo debe estar la luz amarilla encendida?




Pregunta 8: ¿Qué versión de openFPGALoader se menciona en el artículo?




Pregunta 9: ¿Qué se necesita para utilizar la toolchain recomendada?




Pregunta 10: ¿Cuánto tiempo debe estar la luz verde encendida según la secuencia esperada?




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:
error: Contenido Protegido / Content is protected !!
Scroll to Top