TimRudy / uart-verilog

A simple 8 bit UART implementation in Verilog, with tests and timing diagrams

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool


uart-verilog

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.

Parameters

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

Getting Started

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:
.ice icon 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.

Multi-module design of the UART

The representation of the UART in Icestudio as it's placed and wired - the details inside are always expandable:

Inside view

Inside view of one module


Tests

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 - TX module to RX module transmission

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


#1 One successful transmission frame

Test case 1

1.v

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

    But what's the reason "in_data" changes during the progression?

    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!


#2 – #3

Tolerance for mismatch in transmit, receive clocks

#2 Leading RX clock, correct data is read out

2.v

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


#3 Lagging RX clock, incorrect data is read out

3.v

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 rate

#4 – #8

Two transmission frames: Enabling, disabling and the use of "txStart" signal

#4 Two successful frames, second is shortened due to external reset

Test case 4

4.v

Code 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


#5 Correct data is read out for first frame, incorrect for second due to mismatch

5.v

Code Coverage Refs

Uart8Receiver: 244 (*for second frame)


#6 Interrupted transmit followed by complete transmit

6.v

Code Coverage Refs

Uart8Transmitter: 84


#7 Interrupted transmit followed by complete transmit ("txStart" overlaps)

7.v

Code Coverage Refs

Uart8Transmitter: 84


#8 Interrupted transmit followed by failed transmit ("txStart" too short)

Test case 8

8.v


#9 Continuous mode: Two successful transmission frames

Test case 9

9.v

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

    How much clock mismatch is there?

    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.)


#10 Continuous mode, input data is set up just in time for transmit

10.v

Observations

  • The second transmit data in is set up just before the data capture; #9 shows earlier in the same clock cycle

#11 Continuous mode, input data is not set up in time

11.v

Observations

  • 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:

    • Interval #11500 used in test #13: Because each cycle is about 11.5ms, this gives prep or set-up times that land mid-way through each transmit for pushing each next byte (observe the in transitions relative to the txDone pulses)
    • Compare #10750 used in test #14, because its cycles are shorter

#12 Continuous mode: 8-N-1 (TURBO_FRAMES = 1)

Test case 12

12.v

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

    Not 100% transparent Verilog implementation

    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.


#13 – #15

Twenty transmission frames continuous mode: 8-N-1, 8-N-2

#13 TURBO_FRAMES = 0

Test case 13

13.v

Code Coverage Refs

Uart8Transmitter: 120, 123


#14 TURBO_FRAMES = 1

Test case 14

14.v

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 pulse
    • rxBusy of the receiver: The disappearance of the one-tx-clock-period IDLE state
    • Completion, at the red marker, about 1.5ms earlier

#15 TURBO_FRAMES = 1, two transmissions are lost due to clock mismatch

Test case 15

15.v

Observations

  • 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 - RX module

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 to accept good serial data (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 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.


#16 Transmit start is recognized because rx signal was high for minimum 4 ticks

Test case 16

16.v

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 it's 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)


#17 Transmit start is not recognized because rx signal was not high for 4 ticks

Test case 17

17.v

Code Coverage Refs

Uart8Receiver: 141

Observations

  • Here's 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, to 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

    Some Verilog hints to understand the code

    The "&" operator of "&in_prior_hold_reg" collects all the bits, and the expression is true if they're all 1. Secondly, in_prior_hold_reg is a vector of size 4, and is a shift register. So it 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; and 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.


#18 Transmit start fails because rx signal goes high too early

Test case 18

18.v

Code Coverage Refs

Uart8Receiver: 134, 158

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:

    • This was used to plug in different wait-time numbers, basically a range of timings for this specific signal and transition that's being tested; the process can find and go beyond the threshold where the response changes
    • The variation can be down to individual clock ticks, because exactitude is needed if there's any doubt or there could be "off-by-one" errors (*there are examples later of single-clock-tick issues & fixes; I'm particularly thinking of #20 and #28, #29, #30)
    • (I also made variations to test benches to switch input values, 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


#19 Transmit start fails because rx signal goes low-high-low

19.v

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


#20 Transmit stop is recognized after rx signal goes high-low-high then is stable

Test case 20

20.v

Code Coverage Refs

Uart8Receiver: 215, 238

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:

    1. 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).

    2. 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.

    3. 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 for this - observe the use of a second counter).

  • The reader can explore the meaning of splitting in_current_hold_reg from in_prior_hold_reg

    Interesting Verilog code down to the clock tick

    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, up to 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


#21 Transmit stop is recognized after rx signal goes high-low-high past tick 8

Test case 21

21.v

Code Coverage Refs

Uart8Receiver: 215, 216, 238

Variant #21a


#22 Transmit fails because rx signal goes high too late, after tick 8

Test case 22

22.v

Code Coverage Refs

Uart8Receiver: 215, 216, 244

Variant #22a


#23 Transmit fails because rx signal does not go high; error state begins

23.v

Code Coverage Refs

Uart8Receiver: 228


#24 Error state holds as long as rx signal has never gone high

24.v

Code Coverage Refs

Uart8Receiver: 134, 141

Observations

  • This test covers an edge case of the err tests #17, #18 and #19

#25 – #30

Transition between two frames: Overlap of done and error signals

#25 Next transmit frame starts after rx signal was high for minimum 8 ticks

Test case 25

25.v

Code Coverage Refs

Uart8Receiver: 79, 220

Observations

  • Shows done sustained for 16-tick cycle, and this overlaps with the next frame start

  • Passes the condition at line 134, immediately on entry to IDLE state


#26 Successful transmit done signal overlaps with next transmit start error

Test case 26

26.v

Code Coverage Refs

Uart8Receiver: 79, 158, 220

Observations

  • The done signal counts out to 16 as required

  • err, caused during the next transmit start, overlaps and actually ends before done ends


#27 Next transmit frame start overlaps with previous frame done signal

27.v

Code Coverage Refs

Uart8Receiver: 238, 261, 282

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


#28 Next transmit frame start overlaps with previous frame done signal 2

28.v

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, and they start the IDLE state, and the "start" bit hold check of counting 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 this change, at state at the red marker: 1_cee44e1.png ←→ 1.png. (Nice, right?!)


#29 Successful transmit done signal overlaps with next transmit start error 2

29.v

Code Coverage Refs

Uart8Receiver: 238, 261, 282

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


#30 Transmit fails, error state is continuous with next transmit start error

30.v

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


Running the tests on your machine

Run Icarus Verilog and GTKWave

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.



Top

To top of Tests

Topics: Device and circuit simulation

  • HDLs · Hardware Description Languages
  • EDA · Electronic Design Automation
  • FPGAs · Field-Programmable Gate Arrays

Related open source technology

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

About

A simple 8 bit UART implementation in Verilog, with tests and timing diagrams

License:GNU General Public License v3.0


Languages

Language:Verilog 99.8%Language:SystemVerilog 0.2%