Baby's First UART on FPGA
So I got this Sipeed Tang Nano 9K FPGA development board recently (last year), and I wanted to write a bit about what I did as one of my first little projects, just to get this blog started.
As the title suggests, I implemented a UART controller. I started by making a tx module that would print an “S” over the serial console when I pressed a button. Then I implemented an rx that received a byte and displayed it using the onboard LEDs.
I will write about the tx (Sender) part because I think people would find that more interesting. Before I explain the Verilog logic, let me tell you about a UART Frame:
Here’s what a UART frame looks like:
- start bit: UART wires (
rxandtx) are pulled high (in state 1) by default. UART works in a master-slave configuration so there is always a sender and receiver. To begin a transmission, the sender pulls itstxlow. This is called the “start bit”. - d0..d7: This is the byte being transmitted, LSB first.
- pb: Parity Bit. UART devices agree on Even, Odd or No parity. I went with Even parity.
- stop bit: similar to the start bit, the sender sends the line high again to end the frame. UART doesn’t specify a frame length, but the typical UART frame has 1 start bit, 8 data bits, an optional parity bit and 1 stop bit.
UART operates on a “baudrate” concept, in contrast with other serial protocols like I2C and SPI that need separate clock lines to synchronize the data between sender and receiver. UART sender and receiver agree on a baudrate, basically listening for and sending data after the same time intervals, eliminating the need for a separate clock wire. Baudrate here is 115200 and it is specified using the CLK_PER_BIT parameter, calculated based on the FPGA’s clock frequency. (27 MHz)
Here’s the module (Verilog):
module uart_tx #(
parameter CLK_PER_BIT = 234
) (
input clk,
input start,
input [7:0] data,
output reg tx
);
assign start_tx = start;
localparam BTN_PRESS_TIMEOUT = 27000000;
localparam S_IDLE = 4'b0000;
localparam S_START_BIT = 4'b0001;
localparam S_SEND_BYTE = 4'b0010;
localparam S_PARITY_BIT = 4'b0011;
localparam S_STOP_BIT = 4'b0100;
localparam S_DELAY = 4'b0101;
reg [3:0] state = S_IDLE;
reg [7:0] data_reg = 0;
reg data_parity_reg = 1; // even parity
reg [31:0] counter = 0;
reg [4:0] bit_index = 0;
initial tx = 1;
always @(posedge clk) begin
case (state)
S_IDLE: begin
if (start_tx) begin
data_reg <= data;
data_parity_reg <= 1;
state <= S_START_BIT;
end
else begin
state <= S_IDLE;
end
end
S_START_BIT: begin
tx <= 0;
if (counter < CLK_PER_BIT-1) begin
counter <= counter + 1;
state <= S_START_BIT;
end
else begin
counter <= 0;
state <= S_SEND_BYTE;
end
end
S_SEND_BYTE: begin
tx <= data_reg[bit_index];
if (counter < CLK_PER_BIT-1) begin
counter <= counter + 1;
state <= S_SEND_BYTE;
end
else begin
counter <= 0;
if (bit_index < 8) begin
bit_index <= bit_index + 1;
state <= S_SEND_BYTE;
end
else begin
bit_index <= 0;
state <= S_PARITY_BIT;
end
end
end
S_PARITY_BIT: begin
tx <= data_parity_reg;
if (counter < CLK_PER_BIT-1) begin
counter <= counter + 1;
state <= S_PARITY_BIT;
end
else begin
counter <= 0;
state <= S_STOP_BIT;
end
end
S_STOP_BIT: begin
tx <= 1'b1;
if (counter < CLK_PER_BIT-1) begin
counter <= counter + 1;
state <= S_STOP_BIT;
end
else begin
counter <= 0;
state <= S_IDLE;
end
end
endcase
end
endmodule
I know this is shit code and kind of naive because state transitions should be handled separately in a clocked always block, and the corresponding logic for each state should be handled in a combinational logic block. Just trying to record how I did it as a noob.
Let me explain how this works, piece by piece:
module uart_tx #(
parameter CLK_PER_BIT = 234
) (
input clk,
input start,
input [7:0] data,
output reg tx
);
This is the module declaration. It is the “function header” or the “interface” which usually follows this kind of structure - the module name followed by parameters, followed by port specification (clk here is a 1-bit input, data is a 7-bit input, etc.). Parameters are constant values that can be passed into the module when it’s instantiated. We can use parameters for many things, from controlling bus widths to conditional compilation using ifdefs.
Scrolling down, we see a bunch of localparams:
localparam S_IDLE = 4'b0000;
localparam S_START_BIT = 4'b0001;
localparam S_SEND_BYTE = 4'b0010;
localparam S_PARITY_BIT = 4'b0011;
localparam S_STOP_BIT = 4'b0100;
localparam S_DELAY = 4'b0101;
These are the states for the finite state machine that generates the actual UART output.
Numbers are specified in Verilog in the format: <width>'<format><value>. Widths of all assignments to the localparams here are 4, the format we’re using is binary (b) and then the values have been specified using that format. We could also do 4d0, 4d1, 4d2…
reg [3:0] state = S_IDLE;
reg [7:0] data_reg = 0;
reg data_parity_reg = 1; // even parity
reg [31:0] counter = 0;
reg [4:0] bit_index = 0;
initial tx = 1;
reg stands for “register”. These are basically mutable variables from a language PoV. state stores the current state, and I have set it up to have the initial value of S_IDLE (0). data_reg stores the actual byte to be transmitted. data_parity_reg stores the parity bit. The byte being transmitted here is the ASCII value of ‘S’, i.e. 83 (1010011), which has an even number of 1s. Since we are going with an even parity setup, we hardcode the parity as 1.
counter is used to maintain the baudrate using this bit of redundant code in every state (by counting up to CLK_PER_BIT):
if (counter < CLK_PER_BIT-1) begin
counter <= counter + 1;
state <= S_SEND_BYTE;
end
else
// state logic, transitions
end
bit_index is the bit position in the byte being sent, starting from LSB.
tx has already been declared as reg in the module declaration, so we just set it to have the initial value of 1.
When control moves into the else block, the actual operation(s) related to that state can be performed, like sending the start bit, data bits, parity bit and stop bit.
With this, the UART tx was done. I went on to make an rx (receiver) core that, well, does the opposite of this and samples at the baudrate instead of transmitting, and I was able to display the data I sent using Python pyserial on my laptop using the 6 LEDs on my FPGA board. I further connected the two cores to make an “echo”, where I would send some character using Python and receive that character back. I will write about the rx and echo bits in the future after some refining. Instead of supporting only single characters, I would like to use a small buffer with it so I can echo back any number of characters.
That’s it for the first blog post! Thank you for your time.