Caso práctico: panel VGA vía UART con ULX3S

Caso práctico: panel VGA vía UART con ULX3S — hero

Objetivo y caso de uso

Qué construirás: Construirás un pequeño panel de estado VGA gobernado por UART en la Radiona ULX3S. Un dispositivo serie envía R, Y o G, y la FPGA cambia el fondo del monitor a rojo, amarillo o verde mientras replica el estado en tres LEDs.

Para qué sirve

  • Mostrar de forma visible el estado de una producción, laboratorio o banco de pruebas sin dejar un PC conectado a la FPGA.
  • Convertir una salida UART simple de 3,3 V de otro controlador en un indicador local grande.
  • Enseñar una ruta de datos FPGA completa: recepción UART, decodificación de estado, temporización VGA y salida visible para humanos.
  • Dar al alumnado un display práctico de depuración que podrá reutilizar después en otros proyectos con ULX3S.

Resultado esperado

  • El receptor UART captura un byte a 115200 baudios con el reloj de 25 MHz de la ULX3S.
  • El byte R selecciona rojo, Y selecciona amarillo y G selecciona verde.
  • La sincronía VGA funciona continuamente y el área visible cambia de color según el último comando válido.
  • El testbench usa un divisor UART reducido y verifica que recibir G activa el estado verde.

Público y nivel: Estudiantes de FPGA de nivel medio que ya conocen registros y quieren combinar entrada serie con salida de vídeo.

Arquitectura/flujo en una línea: USB-UART -> receptor UART ULX3S -> registro de estado -> generador de temporización/color VGA -> monitor y LEDs.

Nota educativa de validación

Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil FPGA/ULX3S, los bloques de Verilog sintetizable se comprobaron con Yosys (read_verilog) y el conjunto Verilog de diseño/test se revisó con Verilator. El validador también comprobó la estructura de los bloques de código, que los comandos usen opciones copiables con guiones ASCII, que no aparezcan stacks no soportados y que esté disponible la toolchain ULX3S/ECP5 (yosys, nextpnr-ecp5, ecppack, openFPGALoader).

Evidencia de validación publicada

  • Resultado automático: PASS.
  • Estructura parseada: 17 apartados, 4 tablas y 4 bloques de código detectados antes de publicar.
  • Código comprobado: 2 Verilog/Yosys-Verilator, 1 Bash/copy-paste checks.
  • Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
  • Hallazgos del informe: sin hallazgos bloqueantes.

Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu revisión exacta de ULX3S, tu archivo de restricciones de pines y tu cableado real.

Nota educativa de seguridad

Este es un indicador educativo de baja tensión. Usa solo señales UART de 3,3 V en los pines de E/S de la ULX3S. No conectes niveles RS-232, lógica de 5 V, cableado industrial ni equipos alimentados por red directamente a la placa. Usa un adaptador USB-UART, desplazador de nivel o aislador adecuado cuando la fuente no sea ya compatible con 3,3 V.

Prerrequisitos

  • Radiona ULX3S con Lattice ECP5-85F.
  • OSS CAD Suite con verilator, yosys, nextpnr-ecp5, ecppack y openFPGALoader.
  • Adaptador USB-UART de 3,3 V conectado al pin uart_rx seleccionado y masa común.
  • PMOD VGA o adaptador VGA probado con ULX3S.
  • Programa de terminal capaz de enviar caracteres sueltos a 115200 baudios.

Materiales

ElementoModelo / señal exactaPropósito
Placa FPGARadiona ULX3S, Lattice ECP5-85FDecodificación UART y generación VGA
Adaptador serieUSB-UART 3,3 V, TX conectado al RX de FPGAEnvía comandos de estado
Salida de vídeoPMOD VGA o cableado VGA ULX3SPanel visible
LEDsTres LEDs de ULX3S o LEDs en PMODRéplica local del estado

Setup/Conexión

Conecta la salida TX del adaptador a la entrada uart_rx de la FPGA, une las masas y deja el RX del adaptador sin conectar salvo que amplíes el diseño más adelante. Cablea el PMOD VGA según los pines LPF usados en tu laboratorio.

Señal ULX3SConecta conNotas
clkReloj de placa de 25 MHzBase de temporización de píxel y UART
uart_rxTX USB-UART, 3,3 VEntrada de comandos serie
vga_hsync, vga_vsyncPines de sincronía VGATemporización 640×480
vga_r[3:0], vga_g[3:0], vga_b[3:0]Pines de color VGAPantalla de estado de color sólido
status_led[2:0]LEDsRéplica rojo/amarillo/verde

Código validado

uart_vga_status_ulx3s.v

Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.

module uart_vga_status_ulx3s #(
    parameter CLK_HZ = 25000000,
    parameter BAUD = 115200,
    parameter CLKS_PER_BIT = 217
) (
    input wire clk,
    input wire rst_n,
    input wire uart_rx,
    output reg vga_hsync,
    output reg vga_vsync,
    output reg [3:0] vga_r,
    output reg [3:0] vga_g,
    output reg [3:0] vga_b,
    output reg [2:0] status_led
);
    reg [9:0] h_count;
    reg [9:0] v_count;
    reg [1:0] status;
    reg [1:0] rx_state;
    reg [15:0] rx_clk_count;
    reg [2:0] rx_bit_index;
    reg [7:0] rx_shift;
    reg [7:0] rx_byte;
    reg rx_ready;
    reg rx_sync_0;
    reg rx_sync_1;

    localparam RX_IDLE = 2'd0;
    localparam RX_START = 2'd1;
    localparam RX_DATA = 2'd2;
    localparam RX_STOP = 2'd3;

    wire visible = h_count < 10'd640 && v_count < 10'd480;

    always @(posedge clk) begin
        if (!rst_n) begin
            h_count <= 10'd0;
            v_count <= 10'd0;
        end else if (h_count == 10'd799) begin
            h_count <= 10'd0;
            if (v_count == 10'd524) begin
                v_count <= 10'd0;
            end else begin
                v_count <= v_count + 10'd1;
            end
        end else begin
            h_count <= h_count + 10'd1;
        end
    end

    always @(posedge clk) begin
        if (!rst_n) begin
            rx_state <= RX_IDLE;
            rx_clk_count <= 16'd0;
            rx_bit_index <= 3'd0;
            rx_shift <= 8'd0;
            rx_byte <= 8'd0;
            rx_ready <= 1'b0;
            rx_sync_0 <= 1'b1;
            rx_sync_1 <= 1'b1;
        end else begin
            rx_sync_0 <= uart_rx;
            rx_sync_1 <= rx_sync_0;
            rx_ready <= 1'b0;
            case (rx_state)
                RX_IDLE: begin
                    rx_clk_count <= 16'd0;
                    rx_bit_index <= 3'd0;
                    if (!rx_sync_1) begin
                        rx_state <= RX_START;
                    end
                end
// ...

🔒 Este contenido es premium. Con la suscripción mensual (7 días gratis) podrás desbloquear el material didáctico completo y el pack PDF listo para imprimir.🔓 Desbloquéalo — 7 días gratis
module uart_vga_status_ulx3s #(
    parameter CLK_HZ = 25000000,
    parameter BAUD = 115200,
    parameter CLKS_PER_BIT = 217
) (
    input wire clk,
    input wire rst_n,
    input wire uart_rx,
    output reg vga_hsync,
    output reg vga_vsync,
    output reg [3:0] vga_r,
    output reg [3:0] vga_g,
    output reg [3:0] vga_b,
    output reg [2:0] status_led
);
    reg [9:0] h_count;
    reg [9:0] v_count;
    reg [1:0] status;
    reg [1:0] rx_state;
    reg [15:0] rx_clk_count;
    reg [2:0] rx_bit_index;
    reg [7:0] rx_shift;
    reg [7:0] rx_byte;
    reg rx_ready;
    reg rx_sync_0;
    reg rx_sync_1;

    localparam RX_IDLE = 2'd0;
    localparam RX_START = 2'd1;
    localparam RX_DATA = 2'd2;
    localparam RX_STOP = 2'd3;

    wire visible = h_count < 10'd640 && v_count < 10'd480;

    always @(posedge clk) begin
        if (!rst_n) begin
            h_count <= 10'd0;
            v_count <= 10'd0;
        end else if (h_count == 10'd799) begin
            h_count <= 10'd0;
            if (v_count == 10'd524) begin
                v_count <= 10'd0;
            end else begin
                v_count <= v_count + 10'd1;
            end
        end else begin
            h_count <= h_count + 10'd1;
        end
    end

    always @(posedge clk) begin
        if (!rst_n) begin
            rx_state <= RX_IDLE;
            rx_clk_count <= 16'd0;
            rx_bit_index <= 3'd0;
            rx_shift <= 8'd0;
            rx_byte <= 8'd0;
            rx_ready <= 1'b0;
            rx_sync_0 <= 1'b1;
            rx_sync_1 <= 1'b1;
        end else begin
            rx_sync_0 <= uart_rx;
            rx_sync_1 <= rx_sync_0;
            rx_ready <= 1'b0;
            case (rx_state)
                RX_IDLE: begin
                    rx_clk_count <= 16'd0;
                    rx_bit_index <= 3'd0;
                    if (!rx_sync_1) begin
                        rx_state <= RX_START;
                    end
                end
                RX_START: begin
                    if (rx_clk_count == (CLKS_PER_BIT / 2)) begin
                        rx_clk_count <= 16'd0;
                        rx_state <= rx_sync_1 ? RX_IDLE : RX_DATA;
                    end else begin
                        rx_clk_count <= rx_clk_count + 16'd1;
                    end
                end
                RX_DATA: begin
                    if (rx_clk_count == CLKS_PER_BIT - 1) begin
                        rx_clk_count <= 16'd0;
                        rx_shift[rx_bit_index] <= rx_sync_1;
                        if (rx_bit_index == 3'd7) begin
                            rx_state <= RX_STOP;
                        end else begin
                            rx_bit_index <= rx_bit_index + 3'd1;
                        end
                    end else begin
                        rx_clk_count <= rx_clk_count + 16'd1;
                    end
                end
                default: begin
                    if (rx_clk_count == CLKS_PER_BIT - 1) begin
                        rx_byte <= rx_shift;
                        rx_ready <= 1'b1;
                        rx_clk_count <= 16'd0;
                        rx_state <= RX_IDLE;
                    end else begin
                        rx_clk_count <= rx_clk_count + 16'd1;
                    end
                end
            endcase
        end
    end

    always @(posedge clk) begin
        if (!rst_n) begin
            status <= 2'd0;
        end else if (rx_ready) begin
            if (rx_byte == 8'h52) begin
                status <= 2'd0;
            end else if (rx_byte == 8'h59) begin
                status <= 2'd1;
            end else if (rx_byte == 8'h47) begin
                status <= 2'd2;
            end
        end
    end

    always @* begin
        vga_hsync = ~((h_count >= 10'd656) && (h_count < 10'd752));
        vga_vsync = ~((v_count >= 10'd490) && (v_count < 10'd492));
        status_led = 3'b000;
        vga_r = 4'h0;
        vga_g = 4'h0;
        vga_b = 4'h0;
        if (visible) begin
            if (status == 2'd0) begin
                vga_r = 4'hf;
                status_led = 3'b100;
            end else if (status == 2'd1) begin
                vga_r = 4'hf;
                vga_g = 4'hc;
                status_led = 3'b110;
            end else begin
                vga_g = 4'hf;
                status_led = 3'b001;
            end
        end
    end
endmodule

tb_uart_vga_status_ulx3s.v

`timescale 1ns/1ps
module tb_uart_vga_status_ulx3s;
    reg clk = 1'b0;
    reg rst_n = 1'b0;
    reg uart_rx = 1'b1;
    wire vga_hsync;
    wire vga_vsync;
    wire [3:0] vga_r;
    wire [3:0] vga_g;
    wire [3:0] vga_b;
    wire [2:0] status_led;

    always #5 clk = ~clk;

    uart_vga_status_ulx3s #(
        .CLKS_PER_BIT(4)
    ) dut (
        .clk(clk),
        .rst_n(rst_n),
        .uart_rx(uart_rx),
        .vga_hsync(vga_hsync),
        .vga_vsync(vga_vsync),
        .vga_r(vga_r),
        .vga_g(vga_g),
        .vga_b(vga_b),
        .status_led(status_led)
    );

    task uart_send;
        input [7:0] value;
        integer i;
    begin
        uart_rx = 1'b0;
        repeat (4) @(posedge clk);
        for (i = 0; i < 8; i = i + 1) begin
            uart_rx = value[i];
            repeat (4) @(posedge clk);
        end
        uart_rx = 1'b1;
        repeat (8) @(posedge clk);
    end
    endtask

    initial begin
        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        uart_send(8'h47);
        repeat (12) @(posedge clk);
        if (status_led != 3'b001) begin
            $fatal(1, "Green status was not latched");
        end
        if (vga_g == 4'h0) begin
            $fatal(1, "VGA green channel is not active");
        end
        $finish;
    end
endmodule

ulx3s_uart_vga_status.lpf

LOCATE COMP "clk" SITE "G2";
IOBUF PORT "clk" IO_TYPE=LVCMOS33;
LOCATE COMP "rst_n" SITE "R1";
IOBUF PORT "rst_n" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "uart_rx" SITE "P1";
IOBUF PORT "uart_rx" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "vga_hsync" SITE "A1";
LOCATE COMP "vga_vsync" SITE "B1";
IOBUF PORT "vga_hsync" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_vsync" IO_TYPE=LVCMOS33;
LOCATE COMP "status_led[0]" SITE "C1";
LOCATE COMP "status_led[1]" SITE "D1";
LOCATE COMP "status_led[2]" SITE "E1";
IOBUF PORT "status_led[0]" IO_TYPE=LVCMOS33;
IOBUF PORT "status_led[1]" IO_TYPE=LVCMOS33;
IOBUF PORT "status_led[2]" IO_TYPE=LVCMOS33;

Comandos de build/flash/run

PasoComandoResultado esperado
Lintverilator --lint-only -Wall -Wno-fatal -Wno-DECLFILENAME --timing uart_vga_status_ulx3s.v tb_uart_vga_status_ulx3s.vVerilog sin errores fatales
Síntesisyosys -q -p "read_verilog uart_vga_status_ulx3s.v; synth_ecp5 -top uart_vga_status_ulx3s -json build/uart_vga_status.json"Netlist JSON ECP5
Ruteonextpnr-ecp5 --85k --package CABGA381 --speed 6 --json build/uart_vga_status.json --lpf ulx3s_uart_vga_status.lpf --textcfg build/uart_vga_status.configConfiguración rutada
Empaquetadoecppack build/uart_vga_status.config build/uart_vga_status.bitArchivo bitstream
ProgramaciónopenFPGALoader -b ulx3s build/uart_vga_status.bitPlaca configurada
mkdir -p build
verilator --lint-only -Wall -Wno-fatal -Wno-DECLFILENAME --timing uart_vga_status_ulx3s.v tb_uart_vga_status_ulx3s.v
yosys -q -p "read_verilog uart_vga_status_ulx3s.v; synth_ecp5 -top uart_vga_status_ulx3s -json build/uart_vga_status.json"
nextpnr-ecp5 --85k --package CABGA381 --speed 6 --json build/uart_vga_status.json --lpf ulx3s_uart_vga_status.lpf --textcfg build/uart_vga_status.config
ecppack build/uart_vga_status.config build/uart_vga_status.bit
openFPGALoader -b ulx3s build/uart_vga_status.bit

Validación paso a paso

  1. Ejecuta el lint con Verilator y confirma que no aparecen errores fatales de sintaxis UART/VGA.
  2. Ejecuta Yosys y confirma que build/uart_vga_status.json se crea solo desde el archivo sintetizable.
  3. Rutea y empaqueta el bitstream con el LPF usado en tu cableado.
  4. Programa la ULX3S, abre un terminal serie a 115200 baudios y envía R, Y y G.
  5. Confirma que el fondo del monitor y los LEDs siguen el último byte válido.

Troubleshooting

SíntomaCausa probableSolución
Hay sincronía pero color incorrectoEl byte UART no llegaRevisa baud rate, masa y cable TX-adaptador a RX-FPGA
No hay imagen VGAEl mapa de pines PMOD no coincideActualiza el LPF para tu conector
Cambios de estado aleatoriosRX UART está flotanteMantén el pull-up y conecta un adaptador real de 3,3 V
Falla la cargaUSB/JTAG no disponibleRevisa openFPGALoader -b ulx3s y permisos de cable

Mejoras

  • Añade un pequeño generador de texto para mostrar RUN, WAIT o STOP.
  • Añade un transmisor UART de eco para depurar desde el terminal.
  • Añade un timeout que ponga la pantalla azul si no llega ningún comando durante varios segundos.

Checklist

  • El adaptador UART es compatible con 3,3 V.
  • Los pines LPF coinciden con el cableado ULX3S.
  • Verilator y Yosys pasan antes de programar.
  • La sincronía VGA aparece en el monitor.
  • Enviar R, Y y G cambia el estado mostrado.

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 gobierna el panel de estado VGA descrito?




Pregunta 2: ¿Sobre qué placa FPGA se construye el panel de estado?




Pregunta 3: ¿Qué ocurre cuando el dispositivo serie envía la letra 'R'?




Pregunta 4: ¿Qué señal visual adicional replica el estado además del monitor VGA?




Pregunta 5: ¿Cuál es una utilidad mencionada para este proyecto?




Pregunta 6: ¿Qué velocidad de transmisión debe capturar el receptor UART?




Pregunta 7: ¿Con qué reloj trabaja la ULX3S en este caso para la recepción UART?




Pregunta 8: ¿Qué byte selecciona el color amarillo?




Pregunta 9: ¿Qué verifica el testbench según el artículo?




Pregunta 10: ¿Qué color selecciona el byte 'G'?




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:
Scroll al inicio