Overview
This example and test program explores the use of the transmit interrupt of the UARTs. When working on using the UART via DMA I realised that I first needed to better understand the interrupts, not least as both use the same configuration register for the watermark levels in the transmit and receive FIFO. Hence, this simple program implements a DMA-like functionality using the transmit interrupt.
The terminal output – see below – is rather boring. The real insights, if we dare to call them as such, stem from signal traces using an oscilloscope, which measure GPIO pin outputs, set and cleared via SIO in the test code. Printing diagnostic messages as the code runs would be too slow. We could collect timing data in specific test records, as we did for TrapHandlers and other test programs, and print them after each test run, but real-time signal traces seemed easier to implement and to read in this case.
As a general remark, using GPIO pins for measurements via the Oscilloscope, as well as the eight LEDs via module LEDext
, have proven to be a big help for visualising and debugging the functionality in real-time.
Description
Program Module
The program is implemented in one module UARTint.mod
:
- Thread 0: heartbeat LED blinker.
- Thread 1: print different strings from a RAM buffer, not flash-based strings, which is relevant for one of the test cases.
The program uses kernel-v1.
Module Main
The module Main
used is specific for this example program. It is also the place to select a specific test case via Main.TestCase
. Note the higher baudrate of 230,400 for TERM0
, in order to get more compact signal traces.
Module UARTintStr
This module implements a DMA-like behaviour for printing strings to the standard terminal. The TextIO-compatible procedure PutString
can be used to set up to use Out.String
in the usual way, see module Main
. The concept implemented is:
PutString
fills the transmit FIFO.- If the string to be printed does not fit into the FIFO,
PutString
prepares and enables the transmit interrupt, and its handlerTxFifoLvl
, to complete the job, whilePutString
returns to its caller. - Whenever the FIFO level crosses the set threshold, the interrupt handler will copy the subsequent character from the string memory to the FIFO, until full again.
- Repeat until all characters are copied over, then the interrupt handler disabled its interrupt.
The UART’s transmit interrupt reacts to level changes in the corresponding FIFO. Several different thresholds can be set, from 1/8 to 7/8 of the full FIFO range of 32 items, ie. from four to 28 items, in five steps. UARTdev
defines the corresponding settings as CONSTs. The interrupt will only fire by a transition through this threshold. So if we set a threshold of, say, 16 entries (4/8), and write 15 entries into the FIFO, the interrupt does not fire, even though the condition 16 entries or fewer
is true.
As with DMA, direct memory to peripheral copying is used.
Note that there’s only an implementation for UART0
, and it is far from optimised. It does the job for this test program, though.
The UART Datasheet
As other parts of the RP2040’s datasheet that are “excerpted” from ARM’s documentation, in this case for the PrimeCell UART (PL001), I find the UART description sometimes lacking in clarity and detail, and with info that’s not applicable for the RP2040 implementation. For example, it describes an RTS/CTS hardware flow control which I would not know how to use, since the corresponding signals do not appear on the RP2040’s pins. It goes at length describing different interrupts, but the RP2040 only uses an OR-ed combined signal at its NVIC. Not a word about interrupt behaviour in the context of DMA. Sure, we can always combine different pieces of information, and infer what’s what for this specific implementation, but why not describing what’s actually readily useable and useful.
So one of the motivations for this example program was to improve my understanding, and verify that the UART implementation and behaviour corresponds to the specs. In this sense, this test program is also a documentation for my own use and later reference (a “note to self”).
Oscilloscope Connections
The four GPIO pins used for the oscilloscope channel probes are defined in module UARTintStr
as CONSTs. See the test cases for what the corresponding signals signify.
Test Cases
Test Case 0
- UART: FIFO threshold at 4/8 (16 entries, see
Main
). - UARTint: print a string of 42 characters, of which the last two are
CR
andLF
for clearer output.
0123456789012345678901234567890123456789
0123456789012345678901234567890123456789
...
See module UARTintStr
for when the corresponding GPIO pins are set and cleared via SIO from software.
- yellow:
PutString
fills the FIFO with the first 331 characters, sets up and enables the interrupt (pinPutPinNo
); - red: interrupt handler
TxFifoLvl
copies the rest of the characters into the FIFO as soon as the corresponding threshold of 4/8 (16) is reached (pinTxFifoLvlPinNo
). - blue:
TxFifoLvl
disables its interrupt (pinIntDisablePinNo
). - green: unused here (pin
AwaitPinNo
).
Remark: since the CPU and the software are so much faster than the transmission over the serial line, there are simple delay counters to make the signals a bit better visible on the oscilloscope, and still get the whole result in one screenshot. Else the signals would only be narrow spikes. Clicking the full screen icon (top right) may make the pictures clearer since we get a dark background.
The interrupt handler (red) kicks in after about 692 us (see the cursors), which corresponds to 16 characters transmitted at 230,400 Baud. To copy the rest of the string of 42 characters to the FIFO after PutString
, one interrupt is sufficient, and the handler disables its interrupt (blue).
Test Case 1
- UART: FIFO threshold at 4/8 (16 entries).
- UARTint: print a string of 60 characters, of which the last two are
CR
andLF
.
0123456789012345678901234567890123456789012345678901234567
0123456789012345678901234567890123456789012345678901234567
...
Now two interrupts are needed (red) to copy the rest of the 60 characters string after PutString
(yellow).
Test Case 2
- UART: FIFO threshold at 7/8 (28 entries).
- UARTint: print a string of 60 characters, of which the last two are
CR
andLF
.
Now seven interrupts are needed (red) to copy the rest of the 60 characters string after PutString
(yellow), since each interrupt only fills from FIFO entry 29 to 32. With an empty transmit FIFO, PutString
can copy 33 characters, leaving 27 for the interrupt handler, hence seven interrupts for four additional characters each.
Test Case 3
Up to now we have just printed one string buffer, then waited for the next run of the thread to repeat. If we immediately start to fill that same buffer again in the same thread run, it will overwrite what the interrupt handler is still in process of putting out to the UART.
- UART: FIFO threshold at 4/8 (16 entries).
- UARTint:
- print a string of 42 characters, of which the last two are
CR
andLF
; - immediately print another string of 42 via the same buffer.
- print a string of 42 characters, of which the last two are
We get, as expected:
012345678901234567890123456789012defghij
abcdefghijabcdefghijabcdefghijabcdefghij
...
The green signal shows how the thread is waiting for the interrupt for the first string to finish, to then again start to copy the second string to the FIFO (yellow). No interference there. However, before even entering PutString
, and being “put on hold” to await the first string to finish (green), the thread had already overwritten the string buffer for the second string (UARTint.t1c
), so the first string gets garbled.
To resolve, we could use a local buffer in UARTintString
, which would entail to copy the whole string over before starting to feed the UART transmit FIFO. Or use a circular buffer, or set of circularly used line string buffers. To be kept in mind for a DMA-based solution as well, as DMA does exactly what our interrupt handler here does: directly copy from buffer memory to peripheral.
As an aside, this behaviour would not show if we only use flash-based string constants.
Test Case 4
Of course, we can also make thread 1 wait a short time before overwriting the string buffer with the second string, in order to allow the interrupt handler to finish its job of putting out the first string. Test case 4 inserts a Kernel.DelayMe(1)
for one scheduler tick of 2 ms.
- UART: FIFO threshold at 4/8 (16 entries).
- UARTint:
- print a string of 42 characters, of which the last two are
CR
andLF
; - print another string of 42 via the same buffer, after a one scheduler tick delay.
- print a string of 42 characters, of which the last two are
We now get the correct output:
0123456789012345678901234567890123456789
abcdefghijabcdefghijabcdefghijabcdefghij
...
The signal traces confirm the behaviour.
Note that Kernel.DelayMe
is non-blocking, ie. we have context switches to and from the scheduler, which can allow other threads to run.
Test Case 5
Alas, as with many of these kinds of delay-based “synchronisations”, we depend on factors that are not actually part of the mechanism, in this example such as the baudrate, or the length of the string. In a control program, a series of higher prio interrupts could impact “our” interrupt here. It’s a brittle design.
If we increase the length of the two strings to 80 characters, the one scheduler tick delay does not suffice anymore.
01234567890123456789012345678901234567890123456789012345678901234fghijabcdefgh
abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefgh
...
Output Terminal
- See Set-up, one-terminal set-up.
- Note the baudrate of 230,400 for terminal
TERM0
. The terminal program needs to be adjusted accordingly.
Build and Run
Select a test case in module Main
, then build module UARTint
with Astrobe, and create and upload the UF2 file using abin2uf2
.
Set Astrobe’s memory options as listed, and the library search path as explained. The program has been developed and tested with kernel-v1, which should be reflected in the library search path.
The program has been designed and implemented using Astrobe v9.1.
Repository
-
The first character gets moved into the transmit shift registers immediately while the copying to a full 32 slot FIFO still takes place. ↩︎