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
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.



