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:
- Start Bit (0): We pull the line low to 0 to wake up the receiver.
- Data (D0..D7): We send the 8 bits, starting with the least significant bit (LSB).
- 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
endmoduleThe 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.
- We have to wait for the line to go low to 0 (Start Bit).
- 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
endmoduleAn Echo as a First Test
Now that you have Tx and Rx, you can make a “Echo” project:
- Connect Rx and Tx in your
top_module. - If
Rxreceives a valid data, activate thestartsignal ofTxwith that same data. - Open a serial terminal on your PC (Putty / Arduino Monitor).
- Type an ‘A’, and the FPGA sends back an ‘A’.
Congratulations! You have created your first communication peripheral.