UARTint

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:

  1. PutString fills the transmit FIFO.
  2. If the string to be printed does not fit into the FIFO, PutString prepares and enables the transmit interrupt, and its handler TxFifoLvl, to complete the job, while PutString returns to its caller.
  3. 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.
  4. 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 and LF 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 (pin PutPinNo);
  • 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 (pin TxFifoLvlPinNo).
  • blue: TxFifoLvl disables its interrupt (pin IntDisablePinNo).
  • 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 and LF.
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 and LF.

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 and LF;
    • immediately print another string of 42 via the same buffer.

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 and LF;
    • print another string of 42 via the same buffer, after a one scheduler tick delay.

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


  1. The first character gets moved into the transmit shift registers immediately while the copying to a full 32 slot FIFO still takes place. ↩︎