UART on an FPGA - Part 1
Motivation
This series of blog posts will go through my process for developing, simulating, and running a custom UART driver for FPGAs written in SystemVerilog. I’ve been wanting to do this project for a long time, but because of a combination of university work and exams I never got around to starting the project. My main motivation behind the project is to learn Verilog/SystemVerilog as they are both widely used in the industry.
The UART protocol was chosen as it is one of the simplest and easiest to understand and implement protocols out there as can be seen in the diagram below. For simplicity the parity bit was not taken into concideration and 8bit data was used. The image was taken from here.
This first post will go through the development and simulation process for the receiving end of the UART controller. A SystemVerilog module was written that implements the logic and a testbench that tested the module by providing it with every possible input.
Developing the Controller
Initially, I considered all the inputs that were needed for the UART receive controller to function properly. I came up with 6 pieces of the data that the controller needed to work properly.
baudRate
- As UART is an asynchronous protocol a baudrate is requiredclkFreq
- The input clock frequency was also given to be able to do timing tasksclk
- The input clock signal. Everything was done on the rising clock edgesignal
- The UART data linedataReceived
- A vector that contains the byte the controller receiveddataReceivedFlag
- A signal that is pulled high for one clock cycle if data has been successfully received.
With this information, I started building the SystemVerilog module. When I started this project I didn’t know a lot about SystemVerilog. After trying to find a way to pass constants (parameters) to a module, I stumbled upon this website which pointed me to the #(...)(...)
syntax for creating a module.
module uart_recv #(
parameter baudRate,
parameter clkFreq
)(
input logic clk,
input logic signal,
output logic[7:0] data,
output logic dataReceivedFlag
);
Then, I started thinking about a smart way of implementing the controller. After some consideration, I decided to go with a Finite State Machine (FMT) which keeps track of which bit is received every time. The FMT had 4 states. A case statement was used to manage the FSM and read the data from the line.
IDLE
- Waiting for the data line to drop low and start the transaction.START_BIT
- Confirm that it wasn’t a glitch in the signal by sampling the middle of the pulse according to the baudrate.DATA_BITS
- Receive the 8 data bits.STOP_BIT
- Confirm that this was a valid transaction by checking that the line is pulled high after the data bits.
typedef enum { IDLE, START_BIT, DATA_BITS, STOP_BIT } State;
State uart_recv_state = IDLE;
int clkPulsesPerBit = clkFreq / baudRate;
int count = 0;
int bitReceived = 0;
always @(posedge clk) begin
count <= count + 1;
case (uart_recv_state)
IDLE: begin
end
START_BIT: begin
end
DATA_BITS: begin
end
STOP_BIT: begin
end
default: begin
end
endcase
end
IDLE State
In this state, it just waits until the signal is pulled low and resets the clock counter and jumps to the next state (START_BIT).
dataReceivedFlag <= 0;
if (signal == 0) begin
count <= 0;
uart_recv_state = START_BIT;
end
START_BIT State
Here the hardware waits until the clock counter reaches a value corresponding to the middle of the supposed pulse (according to the baudrate) and samples the signal. If the signal is still low, the FSM jumps to the DATA_BITS state, if not it is assumed that the detected pulse was a glitch on the line and resets back to the IDLE state.
if (count == clkPulsesPerBit / 2) begin
if (signal == 0) begin
count <= 0;
bitReceived <= 0;
uart_recv_state = DATA_BITS;
end else begin
uart_recv_state = IDLE;
end
end
DATA_BITS State
In this state, it waits 1 bit cycle, to sample the middle of the next bit, and according to the index of the received bit (bitReceived
) it stores the sampled point into an index in the data output vector. When 8 bits have been read the FMT changes to the STOP_BIT state. Note that the if statement checks if the bit is 7 instead of 8 as the updating of the signal is done at the end of the always block.
if (count == clkPulsesPerBit) begin
data[bitReceived] <= signal;
bitReceived <= bitReceived + 1;
count <= 0;
if (bitReceived == 7) begin
uart_recv_state = STOP_BIT;
count <= 0;
end
end
STOP_BIT State
For the final state, it again sampled the middle of the bit and if it is equal to one the dataReceivedFlag
is set to indicate a valid data byte. Otherwise, it is assumed that the transaction was invalid and it just resets back to IDLE state.
if (count == clkPulsesPerBit) begin
if (signal == 1) begin
dataReceivedFlag <= 1;
end
uart_recv_state <= IDLE;
end
Developing the Testbench
An important part of creating code for FPGAs is rigorous simulation and testing of the code to confirm that everything works as expected. This is usually done by writing more HDL code in your favorite HDL language that takes the developed modules and passes to it different data and tests if the output is the expected one. I created this testbench in SystemVerilog and simulated it in ModelSim.
At the beginning of the file, the timescale was set to 1ns. Then after some logic and parameter decelerating the device under test was instantiated and the clock signal was generated.
`timescale 1ns/1ns
module uart_recv_tb;
parameter baudRate = 115200;
parameter timePerBit = 1e9 / baudRate;
parameter clkHalfPeriod = 5ns;
parameter clkFreq = 1e9 / (2 * clkHalfPeriod); // ns
logic clk;
logic tx_line;
logic[7:0] dataReceived;
logic dataReceivedFlag;
logic[7:0] toSend;
uart_recv #(
.baudRate(baudRate),
.clkFreq(clkFreq)
) dut(
.clk(clk),
.signal(tx_line),
.data(dataReceived),
.dataReceivedFlag(dataReceivedFlag)
);
always begin
clk <= 1; #5;
clk <= 0; #5;
end
To make testing easier a Verilog task was created that sends a byte to DUT over UART.
task uart_send(logic[7:0] data);
tx_line <= 0; #timePerBit; // Start bit
for (int i = 0; i < 8; i++) begin // Data Bits
tx_line <= data[i]; #timePerBit;
end
tx_line <= 1; #timePerBit; // Stop bit
endtask
The main part of a SystemVerilog testbench is the “initial” code block. This block starts executing at the beginning of the simulation and only executes once. The code for testing this is very simple. It just prints some messages on the screen that indicate the start and end of the simulation. In between, a for loop is used to send all possible byte values (0x00 - 0xff) and checks if the value received is the same as the one sent. If not an error is shown on screen to notify me of the error in the code.
initial begin
$display("--- Starting Simulation ---");
tx_line <= 1;
for (logic[7:0] i = 0; i <= 8'hff; i++) begin
uart_send(i);
if (dataReceived != i) begin
$error("Received %b, expected %b", dataReceived, i);
end
end
$display("--- Ending Simulation ---");
end
Another feature that was added to the testbench but not really used was a positive edge always block for the dataReceivedFlag
signal.
Full Module and Test Code
UART Receive Module
module uart_recv #(
parameter baudRate,
parameter clkFreq
)(
input logic clk,
input logic signal,
output logic[7:0] data,
output logic dataReceivedFlag
);
typedef enum { IDLE, START_BIT, DATA_BITS, STOP_BIT } State;
State uart_recv_state = IDLE;
int clkPulsesPerBit = clkFreq / baudRate;
int count = 0;
int bitReceived = 0;
always @(posedge clk) begin
count <= count + 1;
case (uart_recv_state)
IDLE: begin
dataReceivedFlag <= 0;
if (signal == 0) begin
count <= 0;
uart_recv_state = START_BIT;
end
end
START_BIT: begin
// Sample for start bit to confirm
if (count == clkPulsesPerBit / 2) begin
if (signal == 0) begin
count <= 0;
bitReceived <= 0;
uart_recv_state = DATA_BITS;
end else begin
uart_recv_state = IDLE;
end
end
end
DATA_BITS: begin
if (count == clkPulsesPerBit) begin
data[bitReceived] <= signal;
bitReceived <= bitReceived + 1;
count <= 0;
if (bitReceived == 7) begin
uart_recv_state = STOP_BIT;
count <= 0;
end
end
end
STOP_BIT: begin
if (count == clkPulsesPerBit) begin
if (signal == 1) begin
dataReceivedFlag <= 1;
end
uart_recv_state <= IDLE;
end
end
default: begin
uart_recv_state <= IDLE;
end
endcase
end
endmodule
Testbench Code
`timescale 1ns/1ns
module uart_recv_tb;
parameter baudRate = 115200;
parameter timePerBit = 1e9 / baudRate;
parameter clkHalfPeriod = 5ns;
parameter clkFreq = 1e9 / (2 * clkHalfPeriod); // ns
logic clk;
logic tx_line;
logic[7:0] dataReceived;
logic dataReceivedFlag;
logic[7:0] toSend;
uart_recv #(
.baudRate(baudRate),
.clkFreq(clkFreq)
) dut(
.clk(clk),
.signal(tx_line),
.data(dataReceived),
.dataReceivedFlag(dataReceivedFlag)
);
initial begin
$display("--- Starting Simulation ---");
tx_line <= 1;
for (logic[7:0] i = 0; i <= 8'hff; i++) begin
uart_send(i);
if (dataReceived != i) begin
$error("Received %b, expected %b", dataReceived, i);
end
end
$display("--- Ending Simulation ---");
end
always @(posedge dataReceivedFlag) begin
// $display("[i] Data Received: %b", dataReceived);
end
always begin
clk <= 1; #5;
clk <= 0; #5;
end
task uart_send(logic[7:0] data);
tx_line <= 0; #timePerBit; // Start bit
for (int i = 0; i < 8; i++) begin // Data Bits
tx_line <= data[i]; #timePerBit;
end
tx_line <= 1; #timePerBit; // Stop bit
endtask
endmodule