fpga-uart-tx-rx-puerto-serie-verilog

UART on FPGA: transmitter and receiver in Verilog

  • 6 min

A UART is a peripheral that transmits and receives serial frames without sharing a clock signal with the other end.

But our FPGA doesn’t have a UART (some do) so we can’t communicate with a board like we do with a normal microcontroller.

To solve this, we are going to build the most universal, robust, and engineer-loved communication bridge: The UART (Universal Asynchronous Receiver-Transmitter).

Today we are going to design, from scratch and bit by bit, a circuit capable of sending and receiving text to your computer.

How to talk without a shared clock?

The key to UART is that it is Asynchronous. There is no clock wire traveling from the PC to the FPGA. So, how do they know when to read? They agree in advance on a speed (Baud Rate).

If we agree to talk at 9600 baud and each symbol represents one bit, each bit lasts 1 / 9600 = 104.17 µs.

The UART frame

The idle line is always at 1 (3.3V). To send a byte (8 bits), we follow this strict choreography:

  1. Start Bit (0): We pull the line low to 0 to wake up the receiver.
  2. Data (D0..D7): We send the 8 bits, starting with the least significant bit (LSB).
  3. Stop Bit (1): We pull the line high to 1 to rest and separate from the next byte.

The Transmitter (Tx)

The Tx module is basically a state machine that manages a shift register.

The Baud Rate Generator

First, we need to know how many cycles of our clock (12 MHz) a UART bit lasts.

CLKS_PER_BIT = 12,000,000 / 9600 = 1,250 cycles.

When the division is not exact, a baud rate error appears. You must check that the combined error of the transmitter, receiver, and their clocks stays within the tolerance of both devices.

Simple Tx Verilog Code

module uart_tx #(
    parameter CLK_HZ = 12000000,
    parameter BAUD_RATE = 9600
)(
    input wire clk,
    input wire start,       // Pulse to start transmission
    input wire [7:0] data,  // Byte to send
    output reg tx = 1'b1,   // Output pin, idle high
    output wire busy        // Indicates if busy
);

    localparam CLKS_PER_BIT = CLK_HZ / BAUD_RATE;

    // FSM States
    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;  // To know which bit (0-7) we are sending
    reg [7:0] data_temp = 0;  // Copy of data to avoid loss

    assign busy = (state != IDLE);

    always @(posedge clk) begin
        case (state)
            IDLE: begin
                tx <= 1'b1; // Line idle (High)
                clk_count <= 0;
                bit_index <= 0;
                
                if (start) begin
                    data_temp <= data; // Capture the data
                    state <= START;
                end
            end

            START: begin
                tx <= 1'b0; // Start Bit (Low)
                
                // Wait for 1 full bit time
                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]; // Send current bit
                
                if (clk_count < CLKS_PER_BIT - 1) begin
                    clk_count <= clk_count + 1;
                end else begin
                    clk_count <= 0;
                    // Have we sent all 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; // End of transmission
                end
            end
            
            default: state <= IDLE;
        endcase
    end
endmodule
Copied!

The Receiver (Rx)

Receiving is harder than transmitting. Why? Because we don’t know when the message arrives. The PC can send it at any time.

  1. We have to wait for the line to go low to 0 (Start Bit).
  2. Once the start bit is detected, we wait half a bit time (CLKS_PER_BIT / 2).
    • Why? To position ourselves right in the temporal center of the bit. This way, if the clock is slightly out of phase, we still read the data correctly and not the edge noise.

Simple Rx Verilog Code

module uart_rx #(
    parameter CLK_HZ = 12000000,
    parameter BAUD_RATE = 9600
)(
    input wire clk,
    input wire rx,           // Input pin
    output reg [7:0] data,   // Received byte
    output reg valid         // Pulse when data is valid
);

    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; // Temporary register

    // Input synchronization (Avoid metastability)
    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; // Default pulse to 0

        case (state)
            IDLE: begin
                clk_count <= 0;
                bit_index <= 0;
                
                // Detect falling edge (Start Bit)
                if (rx_prev == 1'b1 && rx_sync == 1'b0) begin
                    state <= START;
                end
            end

            START: begin
                // Wait HALF bit to center ourselves
                if (clk_count < CLKS_HALF_BIT - 1) begin
                    clk_count <= clk_count + 1;
                end else begin
                    clk_count <= 0;
                    // Check if it's still 0 (avoid noise)
                    if (rx_sync == 1'b0)
                        state <= DATA;
                    else
                        state <= IDLE; // False alarm
                end
            end

            DATA: begin
                // Now wait ONE full bit time
                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; // Sample
                    
                    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
                    // Only accept the frame if the stop bit is high
                    if (rx_sync == 1'b1) begin
                        valid <= 1;
                        data  <= data_scratch;
                    end
                    state <= IDLE;
                end
            end
            
            default: state <= IDLE;
        endcase
    end

endmodule
Copied!

An Echo as a First Test

Now that you have Tx and Rx, you can make a “Echo” project:

  1. Connect Rx and Tx in your top_module.
  2. If Rx receives a valid data, activate the start signal of Tx with that same data.
  3. Open a serial terminal on your PC (Putty / Arduino Monitor).
  4. Type an ‘A’, and the FPGA sends back an ‘A’.

Congratulations! You have created your first communication peripheral.