Text Output and Input

Overview

Module Main installs an infrastructure to print out to two serial terminals, so that module Out can be used as usual. Analogously, there are mechanics to read from the terminals with module In.

Two Terminal Windows

The two UARTs of the RP2040 are used for two serial text terminals. See module Main for the pins used. Main sets up the two terminals using module Terminals, assigns the two TextIO.Writer to module Out, and the two TextIO.Reader to module In.

I use the terminal programs Tera Term or PuTTY, together with two 3.3V USB-to-serial cables, to have two terminal windows open to the RP2040. PuTTY has worked better for me for terminal input, due to its local line editing capabilities.

Out

Module Out provides the usual output procedures so that all code running on core 0 writes to one terminal window, and on core 1 to the other. See the example programs.

Each terminal can be written to by any piece of code using module Texts (its output procedures take a TextIO.Writer as parameter), provided they don’t interfere with each other regarding the use of the hardware peripherals.

Library modules that print messages, such as RuntimeErrorsOut, should use Texts, not Out, to retain maximum flexibility. See how module Main sets this up for RuntimeErrorsOut.

In

Module In is symmetrical in its usage concept to Out: it reads from terminal 0 used by code on core 0, and from terminal 1 on core 1. It currently offers reading strings and integers in decimal format. I’ll might add a hexadecimal reader.

However, input is basically different from output. With output, the MCU is in control, with input it’s the user. For each read item, we must check for errors. Traditionally in Oberon, this is done by reading a result value, exported by In. But since we really need to check the validity for each input, the procedures in In use a result parameter as part of the API, which makes this clear(er). Also, a set-up with an exported global, and the need to check it in a subsequent extra step, may lead to issues further down the road (think: pre-emptive scheduling, for example).

Writers and Readers

Module TextIO defines TextIO.Writer and TextIO.Reader, respectively, which are basically data structures that hold output and input procedures to and from any device. This abstraction allows to use module Texts to write to any output device, if a corresponding TextIO.Writer is provided. Output could be to an LCD-module via SPI. Or to read from any device with a suitable TextIO.Reader.

Modules Out and In makes use of these features, and use the Writer and Reader to/from the UART as set up by module Terminals.

The Serial Terminals for Input

The input infrastructure outlined above assumes that we use a serial terminal, or terminal program, that

  • locally echoes, captures and buffers the input line, allowing line edits;
  • sends the whole input line to the UART upon hitting carriage return.

That is, our program does not “see” the user type their single “raw” characters – including control characters, such as for backspace or ^H for editing –, and cannot, for example, intercept wrong non-numerical letters in an integer value. While it can be done, of course, I am not sure it’s a good use of a microcontroller’s CPU cycles to implement terminal line editing, also considering the many different terminal types and makes. Operating systems have whole subsystems just dedicated to terminal control.

In the same vein, it is assumed that we want to capture a whole user input line in one input buffer. We could read the input line in smaller chunks, but since we don’t know what the user typed, we would need to assemble the chunks into a larger buffer before parsing it. Hence, we can use the larger buffer in the first place.

This whole set-up is intended to take away complexity and processing from the control program.

PuTTY can be set up thusly, and that’s what I have used.

User Input Handling

Reading user terminal input can be quite challenging, since we simply don’t know what we will get, regarding contents and length of the received data. On a technical level, there’s usually no handshake between UART and terminal – the latter simply sends at the set baud rate (see below).

Possible user input issues include:

  • buffer overflow: the user provides more input than our buffer can capture;
  • syntax errors: the user types wrong characters, such as non-numerical for an integer;
  • out of limit values: the user inputs a number that is outside the 32 bit 2-complement limits, both positive and negative.

Causing buffer overflow, the user can type too many characters, even when providing valid input. For example, they could type - 002147483648 , which is a valid number. If we tune our input buffer to a size of twelve characters (ten for the integer, one for the sign, and one for the terminating OX), the above string will not fit.

Unfortunately, only increasing the buffer size is no panacea, since we can always increase the number of leading zeros. Or add trailing blanks. Simply instructing the user not to do it is maybe not safe enough for a control system. A solution is to handle the overflow gracefully and return a suitable result code. The input procedure can then decide what to do. For a string, it may be OK to continue with a truncated value, but for an integer probably not.

In the case of syntax errors or out of limit values, the current implementation does not even try so salvage part of the input for the integer, and make sense of it. The result code indicates the error, and the returned value is not valid.

Design Considerations

Since the user is in control, not our program, we need to detect the input. Unless we use interrupts, we need to poll the UART for the start of the input. As we have a control program that needs to continue running, we cannot simply busy-wait while polling. OK, with the luxury of two processor cores, we could dedicate one core to user interface handling, and do blocking polling, while the control program runs on the other core. For certain situations this is an appropriate design, especially if we have lots of user interactions, and maybe a whole graphical display with menus and all that to handle.

Otherwise, we need to “interweave” the polling into our control program, either by designing the “big main loop” accordingly, or using the kernel. In both cases, there will be a certain elapsed time between the user sending the input, and our detection and handling of it. We must make sure the UART’s receive FIFO does not get overrun in this elapsed time, which puts a limit on the maximum baud-rate for the terminal.

As an example, if we use the time-driven kernel with a 5 ms cycle time, the 32 character FIFO would be overrun with a baud-rate of 115,000, since the terminal can send close to 60 characters in the maximum 5 ms it takes to detect the first character sent. As soon as we have FIFO overrun, the input read is unreliable, since we don’t know which characters sent were rejected by the UART with its full FIFO, and which “made it” due to our program reading the FIFO and thus freeing up slots. We can only say that the FIFO contents is correct at the point we detect the overrun, but not thereafter – see UARTkstr.mod for example.

Relevant Modules