A simple 8 bit UART implementation in Verilog, with tests and timing diagrams
Loading README and project media…
8 bit UART with tests, documentation, timing diagrams
For simulation and Electronic Design Automation
Consists of TX module and RX module. The two modules are also downloadable in an Icestudio .ice block, pre-packaged.
Full validation of the UART is described below in 30 tests.
Select the rates:
CLOCK_RATE = 12000000
BAUD_RATE = 115200
Select mode 8-N-1 or 8-N-2 (8 bits data, no parity, and 1 or 2 stop bits):
TURBO_FRAMES = 0 // The default 2 stop bits - more robust communication
TURBO_FRAMES = 1 // 1 stop bit - higher max bandwidth
The code: See UART8.v and supporting .v files (Uart8Transmitter.v, Uart8Receiver.v, ...).
UART.ice block: You can explore a hierarchical design workflow and plug this UART in your larger design:
Download "UART01-V" device. Then use Icestudio for virtual breadboarding (aka programming an FPGA). Screenshots below show this: It mixes graphical editing with Verilog. The editing/design environment allows you to load a hardware design onto an FPGA and be testing how the circuit functions in minutes.
Tests section: The tests are meant to relate the visuals (zooming in on transmission waveform in a specific context) to the Verilog (line numbers in the code). Each different behaviour is described. Understand more about UART serial transmission, learn about the UART itself & how the code works, brush up on Verilog HDL. There are sidebars about interesting or educational details, that walk you into the Verilog.
The representation of the UART in Icestudio as it's placed and wired - the details inside are always expandable:
For reference
CodeCoverageIndex.md · Lists all the non-trivial if-else branches in the code; lists tests that cover each
The reverse index of the code coverage is below: From each test the code lines are linked, and highlighted.
Other notes
Red marker in each test points to where the essential action is
The tests without .png screenshots you can easily view with GTKWave on your own copy of the repo
Group 1 tests use two different UART chips, with one's transmitter talking to the other's receiver. A UART chip has the two fully independent submodules so transmission can go either direction between two systems.
Test results would be identical, however, if a loopback configuration were used: testing on one chip only, connecting its tx pin to its rx pin. The equivalence is demonstrated by test variant #1a: It's the same as test #1 but with the one-chip configuration. The delta of the test bench setup can be seen here: 1.v ←→ 1a.v.
Group 1 traces show the communication as an integrated whole:
Relative timing of the bits sent, synched, received
How the Uart8Transmitter (top half) and Uart8Receiver (bottom half) each indicates when it's busy, done, or the transmission is in error: txBusy, txDone, rxBusy, rxDone, rxErr signals are for purpose of external control
Result at a glance: When successful, the out value at bottom matches the in value at upper left
Code Coverage Refs
Uart8Transmitter: 84, 127
Uart8Receiver: 133, 134, 148, 238, 269
Observations
txStart must be asserted over the time when data byte "45" is accepted for transmit
txEn and rxEn must hold for the transmit and receive durations, otherwise the transmission halts in the middle
rxDone is the output that can be monitored for the purpose of grabbing the out data "45", because out is only available for a limited time
in_data is the byte of data to transmit; received_data is the byte being reassembled on the other side
First, note signal in is shown at the top of tests #4 and following - not shown in this test. in is a wire by which the data, 45, is presented to the transmitter. in_data, however, is a register accepting that data.
The bits are shifted through the register (Uart8Transmitter ref above, line 102). When the lowest-order bit of the 5 is taken away and shifted out, what's left is 2; the higher-order bits that form the 4 shift to follow, so what's left in that position is 2.
The 45 -> 22 -> ... is just an implementation detail, but worth mentioning because the design choice does not help with understandability and transparency. Not many people will ever look at that value in in_data, but you are looking at it. So the reason it is shifted 8 times is that each bit is only needed once by the next stage in processing, the bits are needed in order, and that's it: They can be thrown away as the progression happens. The shift register mechanism is very practical, very no-frills for the purpose required (see comment at line 102).
received_data shows it has the same implementation. Given that the lowest bit comes first in the transmission sequence, the shift implementation dictates how it needs to work: the bit shows up in the highest bit position, following which it progressively moves into place!
Observations
Tests #2 and #3 tweak the parameter RX_OVERSAMPLE_RATE to distort the relation between txClk and rxClk
By design, the frequency ratio is 1:16, but in reality the transmitting and receiving UARTs' clocks are independent, so unsynchronized. A degree of synchronization occurs through the UART protocol, though: Every 8 bits the receiver waits and listens for the idle-to-start transition. See the idle waiting interval in other tests, for example #4, #5
The idle interval between each 8-bit packet gives a "reset" for sampling drift (from the precise middle of each bit) that may build up
Observations
RX_OVERSAMPLE_RATE is outside the range where the sampling of 8 bits by the receiver actually aligns with the 8 bits, so this demonstrates how communication will go wrong when two systems don't have the same UART protocol configured, or don't have the same clock rateCode Coverage Refs
Uart8Transmitter: 127
Observations
Demonstrates the indefinitely long idle time for the transmitter (while enabled): After txBusy and txDone, transmitter state is 001
IDLE state 001 is ended by the txStart signal being clocked in
Receiver is very much the same, except it relies on the transmitter to wake it from IDLE state 001
Note transmitter out and receiver in/in_sample: The 1 value of these is known as a "mark" and it signals waiting, in a state between transmits (terminology that I use here will be "stop bit"). The drop to 0 is the signal to start receiving. Because it is not the data yet, but a fixed length pause before the data, this 0 is known as a "space" (terminology here: "start bit")
Second transmission frame is shortened, but the cutting-off is not enough to affect the result since it's during the output. Note when rxEn drops to 0: It makes the rxDone pulse shorter than rxDone in the first frame, it makes the state 101 shorter, and makes the availability of the "7F" data shorter
Code Coverage Refs
Uart8Receiver: 244 (*second frame)
Code Coverage Refs
Uart8Transmitter: 84
Code Coverage Refs
Uart8Transmitter: 84
Code Coverage Refs
Uart8Transmitter: 84, 115, 116, 120, 123
Uart8Receiver: 238, 269
Observations
For this mode, txStart does not go low; so for each frame the in data just needs to be set up in time to be captured in in_data and transmitted
Limit time for set-up of in: Before the high-going clock at the high-going txDone
This trace shows a third transmit starting, because txStart goes low too late at the end of second transmit
Since this trace is longer, it reveals there is a lot of clock mismatch
By the end of 8 bits, timing appears about 3.5 RX clock periods off compared to the TX clock (for the baud rate chosen for this testing, anyway).
The mismatch comes from round-off error: It's the fault of BaudRateGenerator's simplistic code; so it's implementation, not testing-related. (As such, it is a factor of the chosen baud rate.)
Observations
in is set up just before the data capture; #9 shows earlier in the same clock cycleObservations
Unlike in #9 and #10, the second in data byte "B1" lags; at the moment of the high-going txDone, the previous in value is re-captured in in_data
Shows a third transmit starting, because txStart goes low too late at the end of second transmit
This test bench uses a feedback method of control to shut off txStart; so it's suggestive of the idea of external control of the UART; but these tests do not go into how you can use outputs for external control, nor how to decide the timing of inputs
In general, the test benches rely on tuned timings to present the inputs according to the intent of the test - in other words, empirical or ad hoc timings. Examples to illustrate:
Code Coverage Refs
Uart8Transmitter: 115, 116, 118
Uart8Receiver: 79, 282
Observations
Here the UART is instantiated with parameter TURBO_FRAMES = 1, and it means the transmitter sends a "stop bit" of the duration of 1 bit rather than duration 2 bits
Documentation for the 8-N-1, 8-N-2 modes: See Uart8Transmitter header
This mode 8-N-1 provides the maximum bandwidth: It's an effective data rate of 80% over the serial line, because 10 bits are transmitted for each 8-bit packet
But I gave the Verilog code a default of 8-N-2, TURBO_FRAMES = 0, because it fits the project's purpose, namely: simulation & testing, either for the UART's own sake or to support other projects in development; and also: education, visualization. So by default, the UART might as well be more bullet-proof in use; if you are getting specific about your use case, then you'll set the parameters
The Verilog that implements the TURBO_FRAMES feature (see Uart8Transmitter refs above), deserves a note for the reader
The code for STOP_BIT state waits in that state for either 1 tick or 2 - but how, and why, is it using that done variable?
You need to know the meaning of "<=", in context, in procedural block code.
Specifically, done <= 1'b1; appears to do something, but remember, its change to the value is not applied till the end of the time slice; consequently, the code after it if (done == 1'b0) is referring to the value at the current time before done is changed at all; so it is not a mistake!
The code done <= 1'b0; in the same block is simply contradicting (overriding) the prior done <= 1'b1; which is (was) pending. ...So you see that that makes perfect sense as well!
Those are hints to reveal how the if-else code works to introduce a single-clock-tick delay (that is, an extra one). The logic could present itself more clearly if there was a separate new variable, or another state, but for convenience and economy it uses variable done that is boolean and is already at hand.
Code Coverage Refs
Observations
The differences between #13 and #14 are seen in:
out of the transmitter: The narrowing of the high (stop) signal which is the pulse directly below each txDone pulserxBusy of the receiver: The disappearance of the one-tx-clock-period IDLE stateObservations
Input byte "99" misses its deadline; however, it is still present at the input for next data capture, so it is transmitted
The bytes after it are all accounted for, synchronously, until byte "7" misses its deadline
This shows the virtue of limiting the length of bursts of data sent with this simple protocol; if each burst in this test were 8 bytes (frames), followed by driving the txStart signal low to go on to the next burst, then there would have been no data errors (*note this is an extreme example though - 9 bytes for the sync to go off)
There is no rxErr signal for this scenario because there is no breach of the protocol
Group 2 tests are for the receiver RX part of the UART.
The TX module is fairly deterministic, and it's been tested by all the transmits of the Group 1 scenarios.
The RX module has a tougher job, because it receives an arbitrary signal pattern as input and must make sense of it. It must lock on and accept good serial data (forming a frame), or otherwise must reject a data stream if the data doesn't start cleanly from a baseline signal, or if it doesn't end in the accepted way, to certify that it's well-formed.
These test signals don't have to come from a well-behaved or realistic TX module. You could consider them from a potentially "malicious" transmitter.
To note: If the protocol requirements are not met, and the output isn't the 8-bit byte expected, then the output can include an error signal or can just be garbled data.
So, these tests are fine-grained in order to nail down the behaviour of the RX device by exploring the range of signal waveforms possible (mainly the variety of timings; and the signal held low when it should revert to high or vice versa; in addition, high-low or low-high glitches). Variants of tests are necessary to do this. I named files with an "a" suffix, like #18a.v, when they were used to explore changed waveforms - over a range related to that test - to keep them separate from the canonical test.
Code Coverage Refs
Uart8Receiver: 134
Observations
Look at in_sample rather than rx. rx is the external signal to the Device Under Test; in_sample tracks/follows it, but is in a register. So the latter is the reference: All subsequent or coincident signal changes are tied to it
clk in these traces is rxClk (16x higher frequency than the txClk of this communication)
Code Coverage Refs
Uart8Receiver: 141
Observations
Here is an example of the err signal turning on (err is rxErr)
The device is in state 001 the whole time; after err clears, it's ready to go on and start receiving data
Look at the Uart8Receiver code ref to see where and why err is signaled; at around the same place in the code, you'll see the condition under which it's cleared
The "&" operator of &in_prior_hold_reg collects the bits, and the expression is true if all the bits are 1. Secondly, in_prior_hold_reg is a vector of size 4, and is a shift register. This provides a connection to time passing: 4 ticks of the clock for it to fill up (say with 1s).
Ticks of the clock are implicitly being examined, and waited for, by this section of code: 4 ticks, 8 ticks, 12 ticks; 16 ticks is the nominal duration of an incoming bit being sampled. If you understand line 152: sample_count <= 4'b0100; and how sample_count is being used cycling from 0 to F, then you've understood a lot of the code and the protocol, and how a Finite State Machine is useful.
When in_sample drops to 0, that's the trigger for recovering from the error: in_prior_hold_reg is losing its 1 bits and goes away from the F or &in_prior_hold_reg condition; sample_count, if it continues to increase, will allow moving from the IDLE state to START_BIT state.
Code Coverage Refs
Observations
Compared to #17, it's a different condition, a different code branch that turns on the err signal
Note below and in some other tests a "variant" test bench is included:
1 to 0 etc.; for example, when a 0 is lined up as first bit after the "start" bit or a 1 is lined up as last bit before the "stop" bit, these are edge cases needing to be tested)Variant #18a
Focuses on the high in IDLE state after a false "start" bit (the signal has gone high too early)
18a.v line 63: #230 is too short | #250 meets the minimum | #300 long
Code Coverage Refs
Uart8Receiver: 158
Observations
Start is recognized at the time a high signal eventually holds for a full 4 ticks; then err is cleared
As in #17 and #18, the test ends in IDLE state, looking to proceed after the low signal holds for 12 ticks
Code Coverage Refs
Observations
Stop bit recognition and the associated transition to next frame is the most complicated logic, so numerous tests are devoted to it
Some of the issues:
Allowed detection time of the 1 has double requirement: must be half way into the nominal sample period or >= 8 clock ticks (see sample_count at red marker); and has had continuous hold time of >= 4 ticks (see in_sample at red marker).
The stop bit signal doesn't have a defined length. That's because the start bit 0 following the stop bit 1 - "space" following "mark" - defines the start of a next frame. The code has to be able to respond to the drop to 0 (!in_sample) from multiple locations, and this may lie within any of the 3 states: STOP_BIT, READY or IDLE.
Raising the "done" signal plus "out" signal - or alternatively an "err" signal - then sustaining the signal is the purpose of the READY state. But this timed functionality is actually decoupled from the state somewhat; that's because of the overlap of handling the start bit while simultaneously signaling (see line 282 - observe the use of a second counter).
The reader can explore the meaning of splitting in_current_hold_reg from in_prior_hold_reg
These "current"/"prior" variables are views into the register that stores the most recent in signal values/changes. Picture a shift register that keeps the 4 most recent values: This information is the look-back that allows for signal hold time checks, of length 4.
Line 238 is the only place in_current_hold_reg is used.
At the red marker on the trace: The logic decision for state transition can and should be made at the fourth tick, and the value seen by in_current_hold_reg is F; compare in_prior_hold_reg.
For the other logic (2 locations in the code), in_prior_hold_reg does the correct job checking the hold time, when it reaches F.
Variant #20a
Code Coverage Refs
Variant #21a
Code Coverage Refs
Variant #22a
Code Coverage Refs
Uart8Receiver: 228
Code Coverage Refs
Observations
err tests #17, #18 and #19Code Coverage Refs
Observations
Shows done sustained for 16-tick cycle which overlaps with the next frame start
Passes the condition at line 134, immediately on entry to IDLE state
Code Coverage Refs
Observations
The done signal counts out to 16 as required
err, caused during the next transmit start, overlaps and actually ends before done ends
Code Coverage Refs
Observations
Shows going to the READY state, but only remaining in that state for a few clock ticks; whereupon the next frame starts
Despite the transition from READY to IDLE state, done is sustained for a 16-tick cycle; this is implemented by moving the value in sample_count over to out_hold_count (line 287)
At line 287, the value assigned to out_hold_count tracks whatever sample_count has gone up to by that time - It does not use sample_count <= 4'b1; from the previous line, for the reason explained above about a value not changing till the end of a time slice in procedural block code
Variant #27a
Code Coverage Refs
Uart8Receiver: 274
Observations
Shows a complete READY state of exactly 16 ticks; whereupon the next frame starts
At the red marker when sample_count is E, nothing happens
In this particular case in_sample drops to 0 between E and F
When sample_count is F, the assignments after line 274 are what start the next frame, the IDLE state, and the "start" bit hold count of 12 ticks
If you follow further: In the IDLE state, the condition at line 132 holds, the condition at line 133 does not hold, and so the counting continues in the branch at line 146
The logic fix for getting #28 working properly impacted some of the traces - the change was non-functional, only to an internal signal: a transit through RESET state was eliminated. Which I liked. This delta for test #1 shows the change, at state at the red marker: 1_cee44e1.png ←→ 1.png.
Code Coverage Refs
Observations
Shows a READY state of exactly 1 tick, because in_sample drops to 0 at the same tick that READY state is entered
The done signal counts out to 16 as required
err, caused during the next transmit start, overlaps; duration of err is unconstrained and it continues past the done signal
Variant #29a
Code Coverage Refs
Uart8Receiver: 290, 293
Observations
Shows err sustained high; we don't want a glitch low-high (RESET state), since the busy state signal is continuing high
Test #30 is the unique test for this glitch low-high behaviour, meaning it wouldn't have been seen and caught in other transitions from READY to IDLE
With that thought, I leave an exercise for the reader: Determine if there should have been, and will be, a test #31
The test benches can be run using the open source simulator Icarus Verilog: Installation, Getting Started.
With it installed, you can run a command like the following that specifies the required input files and one output file (.vvp):
> iverilog -g2012 -I.. -osimout.vvp -D"DUMP_FILE_NAME=\"1.vcd\"" 1.v
(This is run in the "tests" directory, and ".." thus references the device .v files or .vh files at root level.)
It then requires a second step: Run the Icarus Verilog simulator/runtime to store all signal and timing data to a .vcd file (viewable signal trace):
> vvp simout.vvp
I combine these:
> iverilog -g2012 -I.. -osimout.vvp -D"DUMP_FILE_NAME=\"1.vcd\"" 1.v && timeout 1 >NUL && vvp simout.vvp
Also, here's the complete batch that runs all tests: RunAllTests.txt.
GTKWave viewer is used to view the trace (waveforms): Installation, Getting Started.
IceChips devices of the 7400 TTL family
Icestudio and Apio built on top of IceStorm, Yosys, nextpnr
Yosys synthesis by Claire Wolf
Icarus Verilog simulator by Stephen Williams
GTKWave for viewing waveforms
© 2022-2023 Tim Rudy