La salida HDMI en plataformas FPGA es una forma poderosa de generar gráficos en tiempo real directamente desde hardware personalizado. En este artículo exploraremos cómo implementar salida HDMI utilizando el framework LiteX en la Tang Nano 9K, una FPGA de bajo costo y sorprendente versatilidad. Ya he escrito una entrada sobre cómo inicializar LiteX y crear un SoC básico en la entrada anterior.
🧱 Arquitectura HDMI en LiteX
Por defecto, el archivo tangnano9k.py
de LiteX incluye un módulo de salida HDMI que simplemente muestra una señal de prueba con barras de color verticales. Esta se activa agregando la opción --with-video-terminal
al comando de construcción del SoC.
También existe una función (comentada por defecto) que permite enviar la salida de UART a video, mostrando un terminal directamente en HDMI. Sin embargo, estas funciones consumen una cantidad considerable de recursos lógicos, por lo que en este artículo nos enfocaremos en construir nuestros propios módulos gráficos más prácticos y personalizados.
Componentes clave
Si queremos generar gráficos personalizados, debemos entender cómo funciona la arquitectura de salida HDMI en LiteX. El flujo de datos sigue esta estructura:
VideoTimingGenerator
: genera señales de sincronización horizontal (hsync
), vertical (vsync
) y de habilitación (de
).VideoPattern
(módulo personalizado): genera los valores RGB de cada píxel en base a su posición. 1VideoGowinHDMIPHY
: módulo físico que convierte la señal digital a HDMI compatible.
🖼️ Dibujando gráficos
LiteX nos permite definir módulos personalizados que generan gráficos en hardware. A continuación, presento algunos de los que he implementado:
🧱 TilemapRenderer
: escenarios estilo videojuego retro
Este módulo permite renderizar una cuadrícula de tiles (por ejemplo, de 16×16 píxeles) sobre toda la pantalla:
- Usa una ROM con los datos de píxel de cada tile.
- Utiliza un mapa de tiles (
tilemap
) para saber qué tile mostrar en cada celda. - Calcula la dirección del píxel correspondiente dentro del tile.
- Lee los valores RGB desde memorias independientes para cada canal.
Ideal para construir escenarios tipo Zelda, Pokémon o Metroidvania.
🔵 BarsRenderer
: render de tiles como franjas verticales
Este módulo es perfecto para testear visualmente todos los tiles de la ROM sin necesidad de un tilemap.
- Divide la pantalla horizontalmente en franjas.
- Cada franja representa un tile diferente del tileset.
- El patrón de cada tile se repite verticalmente a lo largo de su franja.
Fragmento clave del código:
bar_idx = Signal(max=stripes_count)
expr = 0
for i in range(1, stripes_count):
expr = Mux(h >= i * stripe_width, i, expr)
self.comb += bar_idx.eq(expr)
Este bloque implementa un Mux
encadenado para determinar en qué franja (y por tanto, qué tile) se encuentra el píxel actual.
📌 Muy útil para:
- Verificar visualmente todos los tiles cargados.
- Crear efectos tipo demo scene.
- Renderizado eficiente sin FSMs ni sincronización extra.
⚙️ BarsC
: franjas controladas por software
Una versión avanzada de BarsRenderer
que permite controlar la posición de cada franja desde la CPU usando CSRs (CSRStorage
):
- Cada franja tiene su propio registro que define su inicio horizontal.
- Se puede modificar en tiempo real desde software (por UART, consola o lógica C).
- Ideal para crear interfaces dinámicas o animaciones programables.

self.comb += sprite_visible.eq(
(hcount >= sprite_x) & (hcount < sprite_x + SPRITE_W) &
(vcount >= sprite_y) & (vcount < sprite_y + SPRITE_H)
)
Perfecto para testear movimiento, colisiones básicas y visibilidad por hardware.
🛠️ Recomendaciones
- Cargar
tilemaps
en BRAM local y no accederlos en tiempo real desde Wishbone. - Para cargas grandes, usar
LiteDRAMDMAReader
en modo burst. - Separar UART de consola vs. UART de bridge:
Usa
uart_name="crossover"
junto conadd_uartbone()
para evitar interferencias.
🧪 Ejemplo de conexión
self.videophy = VideoGowinHDMIPHY(platform.request("hdmi"), clock_domain="hdmi")
self.submodules.vtg = VideoTimingGenerator(default_video_timings="640x480@75Hz")
self.submodules.video_pattern = MovingSpritePattern(hres=640, vres=480)
self.comb += [
self.vtg.source.connect(self.video_pattern.vtg_sink),
self.video_pattern.source.connect(self.videophy.sink)
]
Este fragmento muestra cómo conectar cualquier patrón de video al pipeline HDMI. Solo hace falta que el patrón implemente las interfaces vtg_sink
y source
.
📆 Archivos auxiliares
logo.mem
: archivo con los datos RGB del sprite o tiles.patterns.py
: contiene la definición de los módulos mencionados arriba.
📊 Rendimiento
La Tang Nano 9K puede manejar resoluciones de 640×480 a 75 Hz sin problemas. Para resoluciones superiores:
- Optimiza la lógica combinacional.
- Reduce la profundidad de FSMs o fragmenta procesos.
- Usa memoria externa si es necesario (HyperRAM + DMA).
🧹 Recursos y enlaces
📌 Conclusión
La Tang Nano 9K demuestra que incluso una FPGA económica puede renderizar gráficos HDMI en tiempo real mediante lógica personalizada. Gracias a LiteX, podemos diseñar SoCs que incluyan video, memoria, CPU y periféricos, todo en un mismo chip, y adaptarlos completamente a nuestras necesidades.
El nombre
VideoPattern
es un término genérico. En este artículo veremos varios módulos personalizados comoTilemapRenderer
,BarsRenderer
, entre otros. ↩︎