Lecture 9 – Serial I/O (Digital Systems)

From Spivey's Corner
Jump to: navigation, search

Serial communication[edit]

GPIO pins let us communicate from a program to the outside world, but they are little more (as far as we've seen) than a direct connection between the bits of a register in the microcontroller and signal wires in the external circuit. A serial communications interface provides an example of a more elaborate I/O device, capable of actions independent of the CPU; the complexity this adds is necessary, because serial transmission must be done with timing that is more precise than we can achieve with direct CPU control – or at least, more precise unless the CPU is doing nothing else.

Here is an oscilloscope trace of a character being sent over a serial interface at a speed of 9600 baud: that is, 9600 bit-clocks per second.

RS-232 waveform

The character begins with a start bit – just after the oscilloscope trigger mark – during which the signal is 0V; the start bit lasts for precisely 104μs = 1/9600s. There then follow 8 bits of data, sent LSB first, with 0 encoded by 0V and 1 encoded by +3.3V. The bits (in order of transmission) are 11000010, making (reading backwards) the character 0x43, or upper-case 'C'. The transmission ends with a stop bit at the high level, 3.3V. The signal could then stay at a high level for a while, but just disappearing off the right-hand side of the screen, you can see that the start bit for the next character in this case follows immediately. This pattern, a start bit followed by 8 data bits, (no hardware parity bit), and a stop bit, is the most common variant of serial transmission, and is described by the abbreviation 8N1. There are ten bit-periods per character transmitted, so 9600 8N1 is capable of a peak transmission rate of 960 characters/sec. These conventions are standardised and often go by the term RS-232: but strictly speaking that standard requires voltage levels of ±12–15V suitable for use with old-fashioned teleprinters, and recent computers more commonly use voltages of 5V and 0V or (as here) 3.3V and 0v. Although we shall not use it, it's possible to add a parity bit to each character so that the receiving end can check whether a bit of the character has been corrupted in transmission.

Note that there is no clock signal shared between receiver and transmitter, so precise timing is essential. In order to achieve this, both transmission and reception are commonly implemented in hardware. The transmitter makes sure each bit-period lasts precisely 104μs, and the receiver, after seeing the falling edge of the start bit, will wait for 1.5 bit-periods in order to sample the first bit in the middle of its period; then it samples at 2.5, 3.5, ... bit-periods after the start so as to capture successive bits. (Interfaces commonly sample multiple times per bit-period and use majority voting to improve their immunity to noise.) The receiver can look for the stop bit to verify that the timing is reasonably accurate before announcing that it has received a character.

The serial interface is called a UART (for Universal Asynchronous Receiver/Transmitter) – Universal because it can deal with different speeds from 9600 baud and different encodings from 8N1; and Asynchronous because of the lack of a shared clock. The UART derives its timing by dividing the 16MHz system clock, and part of the configuration process is to set the division ratio to give an accurate bit-rate. The UART has two halves – a receiver and a transmitter – and a serial connection usually consists of two wires, on linking TX on one device to RX on the other, and the second linking RX to TX, for "full duplex" operation. It's possible to add two additional wires labelled RTS and CTS for 'hardware flow control', so that either end can signal to the other that it is not ready to receive any more characters and transmission should be delayed. As is common, we will not bother with these signals, assuming that both ends are able to consume characters at the highest rate they can be transmitted. At 9600 baud, this should not cause any problems.

A basic driver[edit]

The nRF51822 has a single UART that can be connected to the USB interface chip on the micro:bit board, and then appears as a USB serial interface on the host computer. We have already used this interface to experiment with assembly language programming. The UART is presented to a program running on the nRF51822 as a collection of device registers at fixed addresses, and predefined contants like UART_TXD let us access these registers. Some of the registers are used for configuration, and we need not list them in detail, but two are used in the process of transmitting characters. By putting a character in the register UART_TXD, the program can start the UART transmitting the character over the serial line. To find out when the transmission is finished, the program can check another register, UART_TXDRDY, which is set to 1 when the UART is ready for another character. If another character is immediately put in UART_TXD, then it will follow the first character without a gap, as shown in the scope trace above. If there is some delay before the program provides another character, then the TX line will just remain high, and the device on the other end of the wire will wait.

The main difficulty is that we mustn't ask the UART to transmit another character until it has finished with the previous one, or it will get confused, either cutting off the transmission of the first character in the middle, or maybe ignoring the second character. So before sending a character, we must ensure that UART_TXDRDY has been set to 1 by the device. The simplest way of doing this is to wait in a busy loop.

void serial_putc(char ch) {
    while (! UART_TXDRDY) { /* do nothing */ }
    UART_TXDRDY = 0;
    UART_TXD = ch;
}

(This code omits a detail connected with transmitting the very first character.) This works, but it suffers from the same problem as the LED program – while the serial_putc function is waiting, no useful work is being done. We can illustrate the problem with a program for printing the first 500 primes.

void init(void) {
     int n = 2, count = 0;

     serial_init();
     start_timer();

     while (count < 500) {
          if (prime(n)) {
               count++;
               serial_printf("prime(%d) = %d\r\n", count, n);
          }
          n++;
     }

     stop_timer();
}

The program uses serial_printf to print its results, a wrapper around serial_putc that looks like the standard C function printf for formatted output. I've included calls to functions start_timer and stop_timer that turn on and off an LED, so that we can time how long the program takes. We can run the program and connect to the micro:bit with minicom as before and see the primes scrolling past:

prime(1) = 2
prime(2) = 3
prime(3) = 5
...
prime(500) = 3571

Lab 3 has a version of this program: there, prime(n) is implemented by a simple algorithm that is quite slow, particularly because it is using a naïve algorithm for division. The time to find the next prime can get quite long, particularly when large gaps begin to appear, such as the one between 3229 and 3251. (And yes, we are testing the even numbers as well as the odd ones!)

Initialising the UART is a bit involved, but the code can be copied from manufacturer's example programs, and amounts to making proper settings of several device registers. The two pins (defined by constants USB_TX and USB_RX) must be configured one as an output and the other as an input, both with pullup resistors enabled. Then there is a device register that is set according to the baud rate, and another to set the format to 8-N-1. At the end come some assignments to start the sending and receiving processes and clear the flags that indicate the interface is ready for a character.

/* serial_init -- set up UART connection to host */
void serial_init(void) {
    UART_ENABLE = 0;

    GPIO_DIRSET = BIT(USB_TX);
    GPIO_DIRCLR = BIT(USB_RX);
    SET_FIELD(GPIO_PINCNF[USB_TX], GPIO_PINCNF_PULL, GPIO_Pullup);
    SET_FIELD(GPIO_PINCNF[USB_RX], GPIO_PINCNF_PULL, GPIO_Pullup);

    UART_BAUDRATE = UART_BAUD_9600;     // 9600 baud
    UART_CONFIG = 0;                    // format 8N1
    UART_PSELTXD = USB_TX;              // choose pins
    UART_PSELRXD = USB_RX;
    UART_ENABLE = UART_Enabled;
    UART_STARTTX = 1;
    UART_STARTRX = 1;
    UART_RXDRDY = 0;
    UART_TXDRDY = 0;
    txinit = 1;
}

Actually, this program allows a small amount of overlap between printing one prime and looking for the next one. (How?) But once the primes start to thin out, pauses start to appear between lines of output. We can show this by connecting the micro:bit up to a logic analyser and capturing a trace. I am using an inexpensive PC-based logical analyser called Saleae Logic 8.

Connecting a logic analyser

In the picture, the micro:bit has been augmented with pin headers soldered to the edge connector, and the logic analyser probes are connected to these.

  • Channel 0 (black wire) is connected to a GPIO pin that is used to drive one of the LEDs. This shows when the LED signal is illuminated. (Detail: the column I've used is not one of the ones that is lit, so it is high when the LED pattern is placed on the GPIO pins, and reverts to low when the output register is reset to zero.)
  • Channel 1 (brown wire) is connected to the transmit line of the UART. The transmit and receive lines between the UART and the USB interface chip are not brought out to the edge connector, but are avaiable at certain test points on the board. In the picture, a bodge wire connects the relevant test point to a header glued to the edge of the board.
  • The third connection (black wire with black sleeve) is ground.

The logic analyser lets us see traces of multiple signals over time: unlike the scope, it can record data from 8 channels for an almost unlimited time, but it records only a digital logic level for each channel, and not an analogue voltage. The logic analyser's software is equipped with decoders for a variety of protocols inluding serial data, so we can ask it to annotate the trace with the decoded characters. Here is a trace of the beginning of a run of the program.

Start of execution
  • In the picture, you can see two traces: one (Channel 0) is the long pulse that powers the LED, showing that it takes 11.44 seconds to find all 500 primes; the other (Channel 1) is the signal on the serial line, and above it a decoding of the characters that are being sent. You can see the string "prime(1) = 2", followed by a carriage return (\r) and line feed (\n). The p that begins the next line of output "prime(2) = 3" follows immediately after the line feed without any delay.
  • The scale at the top is in milliseconds, and you can see that transmitting each character takes just a bit more than 1ms.
  • Unlike an oscilloscope, the logic analyser does not show the actual voltage on each channel, but just an indication of whether the logic level is high or low. (Actually, the Saleae logic analyser can capture analog signals too, but that's not how we are using it here.)

Further along the trace, we can see delays start to appear between one line of output and the next. Two successive lines of output read

prime(457) = 3229
prime(458) = 3251

Here is the corresponding part of the logic analyser trace.

Gap in the output

From the times at the top, you can see that this portion comes from late in the run, about 10.2s in. There is a period of 25.11ms when the serial line is idle, between the line feed that ends one line of output and the p that begins the next one. During this period, the machine is testing successive numbers to see whether they are prime, and it is spending most of its time in a loop inside the division subroutine. Once the next prime has been found, the activity changes, and the machine spends most of its time in the loop shown above, waiting for UART_TXDRDY to become true. This behaviour is silly, because instead of idly waiting for transmission of a character to finish, the micro:bit could be doing useful work towards finding the next prime. As we shall see later, we can arrange for this to happen (without having to restructure our whole program) by introducing a buffer to store pending output characters, and using interrupts to make the processor respond whenever the UART is ready for the next character, without having it sit in a loop waiting.

Update[edit]

The newer scopes don't really make it possible to decipher the characters being printed, but with zoom view we can detect the gaps in the output towards the end of the run.

Zooming in on the Primes program
Zooming in on the Primes program

In this display, the upper traces have a timebase of 1s/div, showing the most of the run of 11s or so. The lower traces zoom in one marked segment of the run and as 10ms/div make individual bursts of output visible, together with the gaps between them.

The 24MHz logic analyser pod with Pulseview does better.

Gaps in the output

Lecture 10

(General-Purpose Input/Output). A peripheral interface that provides direct access to pins of the microcontroller chip. Pins may be configured as inputs or outputs, and interrupts may be associated with state changes on certain input pins. On the micro:bit, the LEDs and pushbuttons are connected to GPIO pins.

A single integrated circuit that contains a microprocessor together with some memory (usually both RAM for dynamic state and ROM for storing a persistent program) and peripheral interfaces.

(Universal Asynchronous Receiver/Transmitter). A peripheral interface that is able to send and receive characters serially, commonly used in the past for communication between a computer and an attached terminal. It is commonly used in duplex mode, with the transmitter of one device connected to the receiver of the other with one wire, and the receiver of the one connected to transmitter of the other with a different wire. The asynchronous part of the name refers to the fact that the transmitter and receiver on each wire do not share a common clock, but rely instead on the signalling protocol and precise timing to achieve synchronisation.

A symbolic representation of the machine code for a program.