Day 4 of making a UART đ˛ I made a transmitter
This is a continuation of the PISO shift register. Or more accurately, a design refactor. After learning a few things in passing here and there, I came to the conclusion that implementing a shift register should really only require a few lines at most. If youâve been following this learning journey, youâd know that I previously made modules for a D flip flop, and PISO/SIPO shift registers. Those âexperimentsâ letâs call them, were good for learning and understanding what they are and how they work. But it gets redundant real fast and only adds to your projectâs complexity unnecessarily.
As per my last article in the series Iâd like to first define what it is weâre trying to achieve in the context of implementing a UART transmitter. So without further ado letâs get to it.
A high level overview of a transmitter module in UART
The most important part of the transmission module is the tx
output wire. Its purpose is to transmit serial data as per the UART protocol. So what is the UART protocol you might ask? Like any protocol, itâs just a simple ruleset that two (or more) devices agree upon for communication. Just like we humans have language, culture, social norms to dictate how we communicate. Machines have protocols!
Our UART has multiple components, notably a PISO shift register, baud rate generator and a state machine that all work together to make the transmission of a UART packet possible. We can easily break down the packet into four distinct parts, of which one is optional.
Figure 1: UART Packet StructureStart bit
Itâs purpose is fairly obvious, we use it to signal the start of a packet to the receiving device. The way we do this, is by sending a 0
. Because our UART runs in the IDLE state by default, which is constantly sending 1âs. Sending a 0
is an effective method to show a change, which signals the start of a transmission! In this state our transmitter has no other purpose than to transmit 1âs continuously, no matter what its input bus or shift register holds. Which leads us to the data frame.
Data frame
This holds the actual information weâre transmitting and is what the receiving device will end up processing. Anything outside of this, is just a protocol formality you could say. This is where you can hold an ASCII characterâs value for example. Like 87, the decimal representation of W
which translates to 01010111 in your data frame! Now we can move on to how to stop this transmission.
Stop bit(s)
When weâre done with the packet, we send a final 1
bit to indicate the transmission is finished! This is just convention and might make less sense that the start bit, but thatâs a detection job for our receiver ;) You might not think that it makes less sense but I find that the abrupt switch from a constant stream of 1âs to a 0 is a clear indicator. Whereas our last bit in the data frame might very well be a 1, which makes it less intuitive to me.
You get to choose between one or two stop bits. This choice is mostly made based on your area of focus. Do you care more about speed or reliability? One stop bit is enough for most cases (and itâs faster!), but if you really need to make sure the data gets sent properly you can include a second one which will improve synchronization and error detection. It might seem like overkill but it makes sense, especially in long distance communications or noisy environments which are noisier by default. Keep in mind that UART isnât typically used in long distance communications though.
Parity bit (optional)
I kept this one last even though thatâs not its actual position in a packet, as evidenced in the above figure. But since itâs not a vital part of the packet, I felt it shouldnât get prioritized over the other fundamental parts.
This exists mostly as a way to improve error detection. The way it works is by making the total number of 1s in the data frame (including the parity bit) even or odd. Being that the receiver will know this as theyâd be using an agreed-upon parity scheme. Itâd request retransmission of the data if it detects a mismatch!
My implementation
As Iâve mentioned above, there are a few things which made me reconsider using full modules to represent the flip flops and shift registers, and âabstract awayâ those concepts in a few lines inside the transmitter module.
The first one being the discovery of enums! Donât get me wrong although Iâm quite new to programming I was vaguely aware of what enums were, I had just never considered using them in verilog (or even thought of a way to do it). This discovery made it very simple to incorporate a state machine in my design.
Transmitter states
I defined all my states in a file called states.vh
(vh for verilog header):
typedef enum reg[2:0] {IDLE, WRITE, SHIFT, CLEAR, START, STOP} state;
Which allowed me to create variables with the state
data type! I declared states to use in my receiver as well, so donât mind those. The ones that matter are shown in the figure below (excluding IDLE).
I then found a code snippet (I canât remember where) that directly glued to my memory, as I knew it was the missing piece in actually handling the change of those newly acquired states!
It looked something like this:
state cur_state, nxt_state;
always @(posedge clk or posedge rst) begin
if (rst) begin
cur_state <= IDLE;
end else begin
cur_state <= nxt_state;
end
end
Which I think is a very clever way to handle state transitions! Our nxt_state
variable controls the next state as you couldâve guessed and we stay in IDLE as long as reset is active.
Now that weâve covered all our bases, I feel comfortable breaking down my full transmitter.v
file.
Port declarations
I was struggling a little with this because we always hear that UART communicates only using two wires. See, if they said two signals I wouldnât have gotten stuck on this point, but unfortunately itâs something you see in every article talking about UART basics. I ended up declaring my tx signal to be of the reg
data type because thatâs the only way I could come up with to handle hardcoding the transmission of a stream of 1âs.
module transmitter (
input [7:0] bus,
input clk,
input rst,
output reg tx
);
As you can see itâs quite a minimal list, I did this on purpose to not confuse myself further. Iâd experienced some troubles debugging in the past so I wanted to keep this list very clean to know exactly what was going on.
clk
& rst
are conventional but whatâs really unique to the UART transmitter here is the input bus vector and the output tx
which Iâve already covered. The input bus is what our main device will send us when itâs trying to communicate.
Weâll have to deal with that data bus accordingly by sending it through a shift register.
Variables
We need a baud signal to handle our sequential logic and send the bits at each posedge as per the UART protocol. You might be asking yourself why the shift register is 10-bits wide when weâre dealing with an 8-bit data bus. Well if you remember correctly we use start and stop bits in our packet and they have to be included in th transmission somehow, since we canât force them into the data bus as thatâs an input port, we just extend our shift register by 2 bits to give them space.
The shift counter is what weâll use to get out of the SHIFT state but letâs not get ahead of ourselves, and I believe youâre already familiar with our enum by now!
reg baud;
reg [9:0] shift_register;
reg [3:0] shift_counter;
state cur_state, nxt_state;
Sequential logic
We start our always block with a switch statement which will match cur_stateâs value against its elements.
IDLE
The default state of our transmitter is one that weâve covered already so I wonât get into it too much other than explain my code:
IDLE: begin
shift_counter <= 4'b0000;
tx <= 1'b1;
if (bus >= 8'b00000001) begin
nxt_state <= WRITE;
end else begin
nxt_state <= IDLE;
end
end
Here I simply initialize the counter to zero and force an active high signal into tx
. I also check for the input bus being greater than 0 to initiate the state change so that as long as we have nothing, we just stay IDLE!
WRITE
Here we load the full packet into our shift register, keep in mind that UART is little-endian so we go from LSB to MSB which makes us put the start bit at index 0.
We then check to see if the packet has been correctly loaded and shift if thatâs the case, if not we stay in WRITE!
WRITE: begin
shift_register <= {1'b1, bus, 1'b0};
if (shift_register == {1'b1, bus, 1'b0}) begin
nxt_state <= SHIFT;
end else begin
nxt_state <= WRITE;
end
end
SHIFT
Here you can see what I meant with the shift register only requiring a few lines instead of being a full module! We output index 0 of the shift register and concatenate 1 bit to the left of the shift registerâs 9 most significant bits (first bits). Effectively pushing out the last value at index 0 on each baud cycle as per figure 2.
We increment the counter for every shift and check once weâve reached 9 (1001 in bits) to change states if the condition is satisfied!
SHIFT: begin
tx <= shift_register[0];
shift_register <= {1'b1, shift_register[9:1]};
shift_counter <= shift_counter + 1'b1;
if (shift_counter == 4'b1001) begin
nxt_state <= CLEAR;
end else begin
nxt_state <= SHIFT;
end
end
CLEAR
Now that the transmission was successful we can clear the counter and shift register so that we donât deal with any leftover data or unexpected behaviors and prepare for the next bus!
Once itâs clean we can go back to IDLE, otherwise we keep retrying to clear it all.
CLEAR: begin
shift_counter <= 4'b0000;
shift_register <= 10'b0000000000;
if (shift_register == 10'b0000000000) begin
nxt_state <= IDLE;
end else begin
nxt_state <= CLEAR;
end
end
A few considerations
Voila! Thatâs my implementation of a UART transmitter feel free to go check out the repo. Iâd also like to acknowledge the fact that some people will probably see the flaws in my design straight away, from what I know, I still have to implement things like handling the data bus changing in between states with something like a tx_busy
signal. I know there are many ways to make this design more robust, but for now Iâd like to focus on a basic implementation to understand the basics. I plan on coming back to this as a more knowledgeable person and rewriting it from top to bottom with better practices.
If you have any advice on how to improve this design or ideas on how to better understand computers, feel free to reach out on X (formerly twitter).