Objetivo y caso de uso
Lo que construirás: Un monitor UART práctico en la Radiona ULX3S (Lattice ECP5-85F) que se conecta pasivamente a una línea TX real de 3.3 V, 115200 baud, 8N1, decodifica cada byte en lógica FPGA y reenvía líneas legibles como RX 48 OK a un PC a través de una segunda UART. El diseño también hace parpadear un LED integrado cuando hay tráfico y es lo bastante limpio como para pasar lint con Verilator y sintetizar con Yosys.
Por qué importa / Casos de uso
- Depura dispositivos embebidos sin cambiar su firmware observando de forma no invasiva un flujo UART en vivo de 3.3 V.
- Convierte tráfico serie sin procesar en salida de monitor legible por humanos para puesta en marcha, prueba de fábrica y diagnóstico en campo.
- Practica un diseño serie FPGA fiable con temporización concreta: 115200 baud significa aproximadamente 86.8 µs por trama de byte en 8N1, por lo que el monitor debe muestrear y formatear los datos correctamente a la velocidad de línea.
- Útil al validar registros de arranque, controladores de sensores, módulos GPS o mensajes de depuración de MCU que ya transmiten por UART.
Resultado esperado
- La ULX3S recibe bytes de una fuente UART externa de 3.3 V y los decodifica correctamente a 115200 baud, 8N1.
- Por cada byte recibido, la FPGA emite una línea legible como
RX 48 OKa un adaptador USB-UART conectado a un terminal de PC. - Un LED integrado parpadea brevemente con cada carácter, proporcionando confirmación visual inmediata del tráfico.
- El RTL pasa el lint de Verilator y sintetiza con Yosys para la ECP5-85F, con una carga de FPGA muy baja en relación con la lógica disponible y sin uso significativo de GPU (0% GPU).
Público: Estudiantes de FPGA, ingenieros embebidos y depuradores de hardware que trabajan con sistemas basados en UART; Nivel: principiante a intermedio
Arquitectura/flujo: TX de dispositivo de 3.3 V -> decodificador RX UART de ULX3S -> formateador -> TX UART de ULX3S -> adaptador USB-UART -> terminal de PC
Diagrama conceptual de bloques
Vista de alto nivel: qué entra en el sistema, qué procesa cada bloque y qué sale.
Arquitectura funcional
Flujo conceptual de señales y responsabilidades entre bloques del dispositivo.
Ruta de validación
Resumen conceptual de las herramientas usadas para verificar el material publicado.
Prerrequisitos
Nota educativa de validación
Antes de la publicación, este caso pasó la compuerta de validación automatizada de Prometeo con estado PASS. Para este perfil FPGA/ULX3S, los bloques Verilog sintetizables se verificaron con Yosys (read_verilog) y el conjunto de diseño/prueba Verilog se analizó con Verilator. El validador también comprobó la estructura de los bloques de código, las opciones de comandos ASCII seguras para copiar/pegar, las pilas no compatibles y la disponibilidad de la cadena de herramientas ULX3S/ECP5 (yosys, nextpnr-ecp5, ecppack, openFPGALoader).
Esta validación confirma la sintaxis y la compatibilidad de herramientas para el código publicado, pero no sustituye las pruebas físicas en la revisión exacta de tu placa ULX3S, el archivo de restricciones de pines y el cableado real.
Necesitas:
- Una placa ULX3S de la familia Lattice ECP5-85F
- Un dispositivo fuente UART de 3.3 V
- Un adaptador USB-UART
- Cables puente
- Un entorno de shell con estas herramientas instaladas:
verilatoryosysnextpnr-ecp5ecppackopenFPGALoader- Un programa de terminal serie como
picocomoscreen
Comprobación rápida de herramientas:
verilator --version
yosys -V
nextpnr-ecp5 --version
ecppack --help | head -n 1
openFPGALoader --version
Materiales
| Elemento | Modelo/familia exactos | Propósito |
|---|---|---|
| Placa FPGA | Radiona ULX3S, Lattice ECP5-85F | Ejecuta el monitor UART |
| Fuente serie | Dispositivo UART de 3.3 V | Señal que se está observando |
| Adaptador USB-UART | Adaptador compatible con 3.3 V | Envía la salida del monitor al PC |
| Cable USB | Para ULX3S | Alimentación y programación |
| Cable USB | Para adaptador | Conexión serie del PC |
| Cables puente | Según sea necesario | Cableado TX y GND |
Nota educativa de seguridad
Solo electrónica digital de baja tensión.
- No conecte RS-232 niveles de tensión directamente a los pines de la FPGA.
- No conecte UART de 5 V directamente a las E/S de ULX3S.
- Comparta GND entre el dispositivo externo, ULX3S y el adaptador USB-UART.
- Este proyecto asume únicamente señalización UART de 3.3 V UART.
Cableado
Señales utilizadas por el diseño FPGA:
mon_rx: entrada UART monitorizada desde el TX del dispositivo externohost_tx: salida UART desde la FPGA hacia el RX del adaptador USB-UARTled0: LED de actividad
Conecte:
- Dispositivo externo TX -> pin de ULX3S asignado a
mon_rx - Dispositivo externo GND -> ULX3S GND
- Pin de ULX3S asignado a
host_tx-> adaptador USB-UART RX - Adaptador USB-UART GND -> ULX3S GND
- USB de ULX3S -> PC
- USB del adaptador USB-UART -> PC
Archivos del proyecto
Cree estos archivos:
uart_monitor_top.vtb_uart_monitor_top.vulx3s_uart_monitor.lpf
Verilog: uart_monitor_top.v
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
module uart_rx #(
parameter integer CLK_HZ = 25000000,
parameter integer BAUD = 115200
)(
input wire clk,
input wire rst,
input wire rx,
output reg [7:0] data,
output reg valid,
output reg framing_error
);
localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
localparam integer HALF_BIT_CLKS = CLKS_PER_BIT / 2;
reg rx_sync_0;
reg rx_sync_1;
reg [15:0] clk_count;
reg [3:0] bit_index;
reg [7:0] rx_shift;
reg [1:0] state;
localparam [1:0] S_IDLE = 2'd0;
localparam [1:0] S_START = 2'd1;
localparam [1:0] S_DATA = 2'd2;
localparam [1:0] S_STOP = 2'd3;
always @(posedge clk) begin
if (rst) begin
rx_sync_0 <= 1'b1;
rx_sync_1 <= 1'b1;
end else begin
rx_sync_0 <= rx;
rx_sync_1 <= rx_sync_0;
end
end
always @(posedge clk) begin
if (rst) begin
data <= 8'h00;
valid <= 1'b0;
framing_error <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
rx_shift <= 8'h00;
state <= S_IDLE;
end else begin
valid <= 1'b0;
case (state)
S_IDLE: begin
framing_error <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
if (rx_sync_1 == 1'b0) begin
state <= S_START;
end
end
S_START: begin
if (clk_count == HALF_BIT_CLKS - 1) begin
clk_count <= 16'd0;
if (rx_sync_1 == 1'b0) begin
state <= S_DATA;
end else begin
state <= S_IDLE;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
S_DATA: begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
rx_shift[bit_index] <= rx_sync_1;
if (bit_index == 4'd7) begin
bit_index <= 4'd0;
state <= S_STOP;
end else begin
bit_index <= bit_index + 4'd1;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
S_STOP: begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
data <= rx_shift;
valid <= 1'b1;
framing_error <= (rx_sync_1 != 1'b1);
state <= S_IDLE;
end else begin
clk_count <= clk_count + 16'd1;
end
end
default: begin
state <= S_IDLE;
end
endcase
end
end
endmodule
module uart_tx #(
parameter integer CLK_HZ = 25000000,
parameter integer BAUD = 115200
)(
input wire clk,
input wire rst,
input wire [7:0] data,
input wire start,
output reg tx,
output reg busy
);
localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
reg [15:0] clk_count;
reg [3:0] bit_index;
reg [9:0] shifter;
always @(posedge clk) begin
if (rst) begin
tx <= 1'b1;
busy <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
shifter <= 10'b1111111111;
end else begin
if (!busy) begin
tx <= 1'b1;
clk_count <= 16'd0;
bit_index <= 4'd0;
if (start) begin
shifter <= {1'b1, data, 1'b0};
busy <= 1'b1;
tx <= 1'b0;
end
end else begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
bit_index <= bit_index + 4'd1;
shifter <= {1'b1, shifter[9:1]};
tx <= shifter[1];
if (bit_index == 4'd9) begin
busy <= 1'b0;
tx <= 1'b1;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
end
end
endmodule
// ... continúa para miembros en el código completo validado ...module uart_rx #(
parameter integer CLK_HZ = 25000000,
parameter integer BAUD = 115200
)(
input wire clk,
input wire rst,
input wire rx,
output reg [7:0] data,
output reg valid,
output reg framing_error
);
localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
localparam integer HALF_BIT_CLKS = CLKS_PER_BIT / 2;
reg rx_sync_0;
reg rx_sync_1;
reg [15:0] clk_count;
reg [3:0] bit_index;
reg [7:0] rx_shift;
reg [1:0] state;
localparam [1:0] S_IDLE = 2'd0;
localparam [1:0] S_START = 2'd1;
localparam [1:0] S_DATA = 2'd2;
localparam [1:0] S_STOP = 2'd3;
always @(posedge clk) begin
if (rst) begin
rx_sync_0 <= 1'b1;
rx_sync_1 <= 1'b1;
end else begin
rx_sync_0 <= rx;
rx_sync_1 <= rx_sync_0;
end
end
always @(posedge clk) begin
if (rst) begin
data <= 8'h00;
valid <= 1'b0;
framing_error <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
rx_shift <= 8'h00;
state <= S_IDLE;
end else begin
valid <= 1'b0;
case (state)
S_IDLE: begin
framing_error <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
if (rx_sync_1 == 1'b0) begin
state <= S_START;
end
end
S_START: begin
if (clk_count == HALF_BIT_CLKS - 1) begin
clk_count <= 16'd0;
if (rx_sync_1 == 1'b0) begin
state <= S_DATA;
end else begin
state <= S_IDLE;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
S_DATA: begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
rx_shift[bit_index] <= rx_sync_1;
if (bit_index == 4'd7) begin
bit_index <= 4'd0;
state <= S_STOP;
end else begin
bit_index <= bit_index + 4'd1;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
S_STOP: begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
data <= rx_shift;
valid <= 1'b1;
framing_error <= (rx_sync_1 != 1'b1);
state <= S_IDLE;
end else begin
clk_count <= clk_count + 16'd1;
end
end
default: begin
state <= S_IDLE;
end
endcase
end
end
endmodule
module uart_tx #(
parameter integer CLK_HZ = 25000000,
parameter integer BAUD = 115200
)(
input wire clk,
input wire rst,
input wire [7:0] data,
input wire start,
output reg tx,
output reg busy
);
localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
reg [15:0] clk_count;
reg [3:0] bit_index;
reg [9:0] shifter;
always @(posedge clk) begin
if (rst) begin
tx <= 1'b1;
busy <= 1'b0;
clk_count <= 16'd0;
bit_index <= 4'd0;
shifter <= 10'b1111111111;
end else begin
if (!busy) begin
tx <= 1'b1;
clk_count <= 16'd0;
bit_index <= 4'd0;
if (start) begin
shifter <= {1'b1, data, 1'b0};
busy <= 1'b1;
tx <= 1'b0;
end
end else begin
if (clk_count == CLKS_PER_BIT - 1) begin
clk_count <= 16'd0;
bit_index <= bit_index + 4'd1;
shifter <= {1'b1, shifter[9:1]};
tx <= shifter[1];
if (bit_index == 4'd9) begin
busy <= 1'b0;
tx <= 1'b1;
end
end else begin
clk_count <= clk_count + 16'd1;
end
end
end
end
endmodule
module uart_monitor_top(
input wire clk_25mhz,
input wire btn_rst,
input wire mon_rx,
output wire host_tx,
output reg led0
);
wire rst;
wire [7:0] rx_data;
wire rx_valid;
wire rx_ferr;
reg [7:0] tx_data;
reg tx_start;
wire tx_busy;
reg [7:0] msg_mem [0:17];
reg [4:0] msg_len;
reg [4:0] msg_idx;
reg sending;
reg [23:0] led_count;
integer i;
assign rst = btn_rst;
uart_rx #(
.CLK_HZ(25000000),
.BAUD(115200)
) u_rx (
.clk(clk_25mhz),
.rst(rst),
.rx(mon_rx),
.data(rx_data),
.valid(rx_valid),
.framing_error(rx_ferr)
);
uart_tx #(
.CLK_HZ(25000000),
.BAUD(115200)
) u_tx (
.clk(clk_25mhz),
.rst(rst),
.data(tx_data),
.start(tx_start),
.tx(host_tx),
.busy(tx_busy)
);
function [7:0] hexchar;
input [3:0] nib;
begin
if (nib < 4'd10) begin
hexchar = 8'h30 + {4'b0000, nib};
end else begin
hexchar = 8'h41 + ({4'b0000, nib} - 8'd10);
end
end
endfunction
always @(posedge clk_25mhz) begin
if (rst) begin
tx_data <= 8'h00;
tx_start <= 1'b0;
msg_len <= 5'd0;
msg_idx <= 5'd0;
sending <= 1'b0;
led0 <= 1'b0;
led_count <= 24'd0;
for (i = 0; i < 18; i = i + 1) begin
msg_mem[i] <= 8'h20;
end
end else begin
tx_start <= 1'b0;
if (led_count != 24'd0) begin
led_count <= led_count - 24'd1;
led0 <= 1'b1;
end else begin
led0 <= 1'b0;
end
if (rx_valid && !sending) begin
led_count <= 24'd5000000;
msg_mem[0] <= "R";
msg_mem[1] <= "X";
msg_mem[2] <= " ";
msg_mem[3] <= hexchar(rx_data[7:4]);
msg_mem[4] <= hexchar(rx_data[3:0]);
msg_mem[5] <= " ";
if (!rx_ferr) begin
msg_mem[6] <= "O";
msg_mem[7] <= "K";
msg_mem[8] <= 8'h0A;
msg_len <= 5'd9;
end else begin
msg_mem[6] <= "F";
msg_mem[7] <= "R";
msg_mem[8] <= "A";
msg_mem[9] <= "M";
msg_mem[10] <= "I";
msg_mem[11] <= "N";
msg_mem[12] <= "G";
msg_mem[13] <= "_";
msg_mem[14] <= "E";
msg_mem[15] <= "R";
msg_mem[16] <= "R";
msg_mem[17] <= 8'h0A;
msg_len <= 5'd18;
end
msg_idx <= 5'd0;
sending <= 1'b1;
end
if (sending && !tx_busy) begin
if (msg_idx < msg_len) begin
tx_data <= msg_mem[msg_idx];
tx_start <= 1'b1;
msg_idx <= msg_idx + 5'd1;
end else begin
sending <= 1'b0;
end
end
end
end
endmodule
Banco de pruebas: tb_uart_monitor_top.v
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
`timescale 1ns/1ps
module tb_uart_monitor_top;
reg clk;
reg btn_rst;
reg mon_rx;
wire host_tx;
wire led0;
localparam integer CLK_HALF_NS = 20;
localparam integer BIT_NS = 8680;
integer fd;
integer i;
reg [9:0] frame;
uart_monitor_top dut (
.clk_25mhz(clk),
.btn_rst(btn_rst),
.mon_rx(mon_rx),
.host_tx(host_tx),
.led0(led0)
);
always #CLK_HALF_NS clk = ~clk;
task uart_send_byte;
input [7:0] b;
integer j;
begin
mon_rx = 1'b0;
#(BIT_NS);
for (j = 0; j < 8; j = j + 1) begin
mon_rx = b[j];
#(BIT_NS);
end
mon_rx = 1'b1;
#(BIT_NS);
// ... continúa para miembros en el código completo validado ...`timescale 1ns/1ps
module tb_uart_monitor_top;
reg clk;
reg btn_rst;
reg mon_rx;
wire host_tx;
wire led0;
localparam integer CLK_HALF_NS = 20;
localparam integer BIT_NS = 8680;
integer fd;
integer i;
reg [9:0] frame;
uart_monitor_top dut (
.clk_25mhz(clk),
.btn_rst(btn_rst),
.mon_rx(mon_rx),
.host_tx(host_tx),
.led0(led0)
);
always #CLK_HALF_NS clk = ~clk;
task uart_send_byte;
input [7:0] b;
integer j;
begin
mon_rx = 1'b0;
#(BIT_NS);
for (j = 0; j < 8; j = j + 1) begin
mon_rx = b[j];
#(BIT_NS);
end
mon_rx = 1'b1;
#(BIT_NS);
end
endtask
initial begin
clk = 1'b0;
btn_rst = 1'b1;
mon_rx = 1'b1;
fd = $fopen("sim_host_tx_bits.txt", "w");
#500;
btn_rst = 1'b0;
#(BIT_NS * 3);
uart_send_byte(8'h48);
#(BIT_NS * 2);
uart_send_byte(8'h45);
#(BIT_NS * 2);
uart_send_byte(8'h4C);
#(BIT_NS * 250);
$fclose(fd);
$finish;
end
initial begin
forever begin
@(negedge host_tx);
#(BIT_NS/2);
frame[0] = host_tx;
for (i = 1; i < 10; i = i + 1) begin
#(BIT_NS);
frame[i] = host_tx;
end
$fwrite(fd, "frame bits: %b\n", frame);
end
end
endmodule
Restricciones: ulx3s_uart_monitor.lpf
Edite los valores de SITE para que coincidan con el pinout exacto de su ULX3S.
BLOCK RESETPATHS;
BLOCK ASYNCPATHS;
FREQUENCY PORT "clk_25mhz" 25 MHz;
LOCATE COMP "clk_25mhz" SITE "ULX3S_PIN_CLK25";
IOBUF PORT "clk_25mhz" IO_TYPE=LVCMOS33;
LOCATE COMP "btn_rst" SITE "ULX3S_PIN_BTN";
IOBUF PORT "btn_rst" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "mon_rx" SITE "ULX3S_PIN_MON_RX";
IOBUF PORT "mon_rx" IO_TYPE=LVCMOS33;
LOCATE COMP "host_tx" SITE "ULX3S_PIN_HOST_TX";
IOBUF PORT "host_tx" IO_TYPE=LVCMOS33;
LOCATE COMP "led0" SITE "ULX3S_PIN_LED0";
IOBUF PORT "led0" IO_TYPE=LVCMOS33;
Compilar y ejecutar
1) Lint de Verilator
verilator -Wall -Wno-DECLFILENAME --lint-only uart_monitor_top.v tb_uart_monitor_top.v
2) Ejecutar simulación
verilator -Wall -Wno-DECLFILENAME --binary uart_monitor_top.v tb_uart_monitor_top.v
./obj_dir/Vtb_uart_monitor_top
Evidencia esperada:
- La simulación finaliza normalmente.
- Se crea un archivo llamado
sim_host_tx_bits.txt. - Ese archivo contiene muestras de tramas UART generadas por el transmisor FPGA.
Este es el método de validación para la afirmación RTL de que los bytes recibidos activan una salida UART formateada.
3) Sintetizar
yosys -p "read_verilog uart_monitor_top.v; synth_ecp5 -top uart_monitor_top -json uart_monitor_top.json"
4) Colocar y rutear
nextpnr-ecp5 --85k --json uart_monitor_top.json --lpf ulx3s_uart_monitor.lpf --textcfg uart_monitor_top.config
5) Empaquetar bitstream
ecppack uart_monitor_top.config uart_monitor_top.bit
6) Programar la placa
openFPGALoader -b ulx3s uart_monitor_top.bit
7) Abrir un terminal serie en el adaptador USB-UART
picocom ejemplo:
picocom -b 115200 /dev/ttyUSB0
screen ejemplo:
screen /dev/ttyUSB0 115200
Validación de hardware
Validar el comportamiento en reposo
Con el dispositivo serie externo desconectado:
- El terminal debe permanecer en silencio.
- El LED debe permanecer apagado después del reset.
mon_rxno debe ser excitado por ninguna tensión fuera de rango.
Validar con una fuente UART conocida
Configura el dispositivo externo de 3.3 V para enviar repetidamente HELLO a 115200 8N1.
Evidencia esperada en el terminal:
RX 48 OK
RX 45 OK
RX 4C OK
RX 4C OK
RX 4F OK
RX 0D OK
RX 0A OK
Este es el método de validación para la afirmación de precisión de que el monitor decodifica bytes correctamente: compara la cadena conocida transmitida con los valores hexadecimales de bytes impresos por la FPGA.
Validar el manejo de errores de trama
Mantén el monitor FPGA en 115200 8N1, pero configura el dispositivo fuente a una velocidad en baudios diferente, como 9600.
Evidencia esperada:
- La salida se vuelve escasa, incorrecta o inexistente.
- Algunas líneas recibidas pueden mostrar
FRAMING_ERR.
Solución de problemas
Sin salida en el terminal
Comprobar:
host_txva al adaptador RX- Las masas están compartidas
- Se abre el dispositivo serie correcto en el PC
- El dispositivo fuente realmente está transmitiendo
- La asignación de pines del LPF coincide con la placa real
El LED parpadea pero no hay texto en el PC
Causas probables:
- Asignación incorrecta del pin
host_tx - Cableado incorrecto del adaptador USB-UART
- Dispositivo de terminal incorrecto en el PC
Lint o la síntesis fallan
Compruebe que:
- Los nombres de archivo coinciden exactamente con los comandos
- Solo
uart_monitor_top.vse pasa a la síntesis de Yosys - El LPF usa los mismos nombres de señal de nivel superior que el Verilog
Errores de trama en cada byte
Normalmente causado por:
- Desajuste de baudios
- Nivel de voltaje incorrecto
- Cableado ruidoso
- Asignación incorrecta del pin de reloj
Capturar registros del terminal
Para guardar una sesión del monitor:
script -c "picocom -b 115200 /dev/ttyUSB0" uart_monitor_session.txt
Lista de comprobación final
- [ ] Usé una Radiona ULX3S (Lattice ECP5-85F).
- [ ] Mi señal serie observada es UART de 3.3 V, no RS-232 ni UART de 5 V.
- [ ] Todas las tierras están conectadas entre sí.
- [ ] Actualicé
ulx3s_uart_monitor.lpfcon pines ULX3S válidos. - [ ] La verificación lint de Verilator se completa correctamente.
- [ ] La síntesis de Yosys se completa.
- [ ] nextpnr se completa.
- [ ] El bitstream se programa correctamente.
- [ ] El terminal del PC está configurado a 115200 baudios.
- [ ] El terminal muestra las líneas esperadas del monitor para un flujo de bytes conocido.
Esto te proporciona un monitor de banco UART reutilizable basado en FPGA para una línea de transmisión en la plataforma ULX3S.
<div class="amazon-affiliate">
<p><strong>Find this product and/or books on this topic on Amazon</strong></p>
<p><a class="amazon-affiliate-btn" href="https://amzn.to/4mt8r4C" target="_blank" rel="nofollow sponsored noopener">Go to Amazon</a></p>
<p class="amazon-affiliate-disclaimer">As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.</p>
</div>




