UART on an FPGA - Part 1

uart

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 required
  • clkFreq - The input clock frequency was also given to be able to do timing tasks
  • clk - The input clock signal. Everything was done on the rising clock edge
  • signal - The UART data line
  • dataReceived - A vector that contains the byte the controller received
  • dataReceivedFlag - 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