Una UART es un periférico que transmite y recibe tramas serie sin compartir una señal de reloj con el otro extremo.
Pero nuestra FPGA no tiene una UART (algunas si) así que no podemos comunicarnos con una placa, como hacemos con un microcontrolador normal.
Para solucionar esto, vamos a construir el puente de comunicación más universal, robusto y querido por los ingenieros: La UART (Universal Asynchronous Receiver-Transmitter).
Hoy vamos a diseñar, desde cero y bit a bit, un circuito capaz de enviar y recibir texto hacia tu ordenador.
¿Cómo hablar sin un reloj compartido?
La clave de la UART es que es Asíncrona. No hay un cable de reloj que viaje del PC a la FPGA. Entonces, ¿cómo saben cuándo leer? Se ponen de acuerdo de antemano en una velocidad (Baud Rate).
Si acordamos hablar a 9600 baudios y cada símbolo representa un bit, cada bit dura 1 / 9600 = 104,17 µs.
La trama UART
La línea en reposo (Idle) está siempre a 1 (3.3V). Para enviar un byte (8 bits), seguimos esta coreografía estricta:
- Start Bit (0): Bajamos la línea a 0 para despertar al receptor.
- Datos (D0..D7): Enviamos los 8 bits, empezando por el menos significativo (LSB).
- Stop Bit (1): Subimos la línea a 1 para descansar y separar del siguiente byte.
El transmisor (Tx)
El módulo Tx es básicamente una máquina de estados que gestiona un registro de desplazamiento.
El generador de baudios
Primero, necesitamos saber cuántos ciclos de nuestro reloj (12 MHz) dura un bit de la UART.
CLKS_POR_BIT = 12.000.000 / 9600 = 1.250 ciclos.
Cuando la división no es exacta aparece un error de baud rate. Debes comprobar que el error combinado del transmisor, el receptor y sus relojes quede dentro de la tolerancia de ambos equipos.
Código Verilog para un Tx sencillo
module uart_tx #(
parameter CLK_HZ = 12000000,
parameter BAUD_RATE = 9600
)(
input wire clk,
input wire start, // Pulso para iniciar envío
input wire [7:0] data, // Byte a enviar
output reg tx = 1'b1, // Pin de salida, en reposo a nivel alto
output wire busy // Indica si está ocupado
);
localparam CLKS_PER_BIT = CLK_HZ / BAUD_RATE;
// Estados de la FSM
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;
reg [1:0] state = IDLE;
localparam COUNT_WIDTH = (CLKS_PER_BIT <= 1) ? 1 : $clog2(CLKS_PER_BIT);
reg [COUNT_WIDTH-1:0] clk_count = 0;
reg [2:0] bit_index = 0; // Para saber qué bit (0-7) enviamos
reg [7:0] data_temp = 0; // Copia del dato para no perderlo
assign busy = (state != IDLE);
always @(posedge clk) begin
case (state)
IDLE: begin
tx <= 1'b1; // Línea en reposo (High)
clk_count <= 0;
bit_index <= 0;
if (start) begin
data_temp <= data; // Capturamos el dato
state <= START;
end
end
START: begin
tx <= 1'b0; // Start Bit (Low)
// Esperamos 1 tiempo de bit completo
if (clk_count < CLKS_PER_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
clk_count <= 0;
state <= DATA;
end
end
DATA: begin
tx <= data_temp[bit_index]; // Enviamos bit actual
if (clk_count < CLKS_PER_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
clk_count <= 0;
// ¿Hemos enviado los 8 bits?
if (bit_index < 7) begin
bit_index <= bit_index + 1;
end else begin
bit_index <= 0;
state <= STOP;
end
end
end
STOP: begin
tx <= 1'b1; // Stop Bit (High)
if (clk_count < CLKS_PER_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
state <= IDLE; // Fin de transmisión
end
end
default: state <= IDLE;
endcase
end
endmoduleEl receptor (Rx)
Recibir es más difícil que transmitir. ¿Por qué? Porque no sabemos cuándo llega el mensaje. El PC puede enviarlo en cualquier momento.
- Tenemos que esperar a que la línea baje a 0 (Start Bit).
- Una vez detectado el start bit, esperamos medio tiempo de bit (
CLKS_PER_BIT / 2).
- ¿Por qué? Para colocarnos justo en el centro temporal del bit. Así, si el reloj está un poco desfasado, seguimos leyendo el dato correctamente y no el ruido de los bordes.
Código Verilog para un Rx sencillo
module uart_rx #(
parameter CLK_HZ = 12000000,
parameter BAUD_RATE = 9600
)(
input wire clk,
input wire rx, // Pin de entrada
output reg [7:0] data, // Byte recibido
output reg valid // Pulso cuando data es válido
);
localparam CLKS_PER_BIT = CLK_HZ / BAUD_RATE;
localparam CLKS_HALF_BIT = CLKS_PER_BIT / 2;
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;
reg [1:0] state = IDLE;
localparam COUNT_WIDTH = (CLKS_PER_BIT <= 1) ? 1 : $clog2(CLKS_PER_BIT);
reg [COUNT_WIDTH-1:0] clk_count = 0;
reg [2:0] bit_index = 0;
reg [7:0] data_scratch = 0; // Registro temporal
// Sincronización de entrada (Evitar metastabilidad)
reg rx_meta, rx_sync, rx_prev;
always @(posedge clk) begin
rx_meta <= rx;
rx_sync <= rx_meta;
rx_prev <= rx_sync;
end
always @(posedge clk) begin
valid <= 0; // Pulso por defecto a 0
case (state)
IDLE: begin
clk_count <= 0;
bit_index <= 0;
// Detectamos flanco de bajada (Start Bit)
if (rx_prev == 1'b1 && rx_sync == 1'b0) begin
state <= START;
end
end
START: begin
// Esperamos MEDIO bit para centrarnos
if (clk_count < CLKS_HALF_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
clk_count <= 0;
// Comprobamos que sigue siendo 0 (evitar ruido)
if (rx_sync == 1'b0)
state <= DATA;
else
state <= IDLE; // Falsa alarma
end
end
DATA: begin
// Ahora esperamos UN tiempo de bit completo
if (clk_count < CLKS_PER_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
clk_count <= 0;
data_scratch[bit_index] <= rx_sync; // Muestreamos
if (bit_index < 7) begin
bit_index <= bit_index + 1;
end else begin
bit_index <= 0;
state <= STOP;
end
end
end
STOP: begin
if (clk_count < CLKS_PER_BIT - 1) begin
clk_count <= clk_count + 1;
end else begin
// Solo aceptamos la trama si el bit de parada es alto
if (rx_sync == 1'b1) begin
valid <= 1;
data <= data_scratch;
end
state <= IDLE;
end
end
default: state <= IDLE;
endcase
end
endmoduleUn echo como primera prueba
Ahora que tienes Tx y Rx, puedes hacer el proyecto “Echo”:
- Conectas Rx y Tx en tu
top_module. - Si
Rxrecibe un dato válido, activas la señalstartdelTxcon ese mismo dato. - Abres un terminal serial en tu PC (Putty / Arduino Monitor).
- Escribes una ‘A’, y la FPGA te devuelve una ‘A’.
¡Enhorabuena! Has creado tu primer periférico de comunicación.