Lecture 10 – Programming with interrupts (Digital Systems)

Copyright © 2024 J. M. Spivey
Jump to navigation Jump to search

Without interrupts

We could improve the performance of the primes program by storing in a buffer (deteailed implementation later) the characters that had been generated by printf but not yet sent over the UART. Then we could rewrite the inner loop of the prime-testing subroutine so that it checked whether the UART was ready to transmit another character:

int prime(int n) {
    int k = 2;

    while (k * k <= n) {
        if (n % k == 0) return 0;
        poll_uart();
        k++;
    }

    return 1;
}


void poll_uart(void) {
    if (UART_TXDRDY) {
       // send another char
    }
}

There are (at least) two big problems with this approach.

  1. It's really inefficient: the constant checking of the UART slows down the most computationally-intensive parts of the program.
  2. It makes maintaining the program really hard, because different parts of the task must be mingled together. In particular, the program can't use any library routines if those routines take an appreciable time, because any loops in them must be rewritten to include a call to poll_uart().

So we need to find a better way.


Interrupts

[10.1] A better approach in the primes program is to control the UART using an interrupt. We can configure the hardware of the UART specially so that the normal flow of execution of the program is interrupted when UART_TXDRDY becomes true, and at that point (between two successive instructions of the program), a special subroutine

uart_handler()

is called. We can arrange that serial_putc(ch) does not usually wait until it can output the character ch, but instead stores it in an array txbuf, and the interrupt handler retrieves characters from the array and sends them to the UART.

[10.2] We'll set up txbuf as a circular buffer, with two indices bufin and bufout, and (redundantly) a counter bufcnt that tells how many characters are stored. We can declare the variables we need like this:

#define NBUF 64

static volatile int bufcnt = 0;
static int bufin = 0;
static int bufout = 0;
static volatile char txbuf[NBUF];

static volatile int txidle;

Note the use of the volatile keyword for variables that are shared between the main program and the interrupt handler.

[10.3] If bufin < bufout, then the part of the array that is occupied wraps around from txbuf[NBUF-1] to txbuf[0].

A circular buffer

[10.4] Let's write the interrupt handler first:

void uart_handler(void) {
    if (UART_TXDRDY) {
        UART_TXDRDY = 0;
        if (bufcnt == 0)
            txidle = 1;
        else {
            UART_TXD = txbuf[bufout];
            bufcnt--;
            bufout = (bufout+1)%NBUF;
        }
    }
}
  • The interrupt handler begins by checking why it has been called: UART_TXDRDY = 1 is one reason, but there may be others.
  • If there is nothing to transmit, the handler sets the flag txidle.
  • Otherwise, it fetches a character from the buffer and starts the UART transmitting it.
  • Making NBUF a power of 2 will make the modulo operation %NBUF cheaper on a machine with no divide instruction.

[10.5] Here is the corresponding code for serial_putc:

void serial_putc(char ch) {
    while (bufcnt == NBUF) pause(); 
    intr_disable();
    if (txidle) {
        UART_TXD = ch;
        txidle = 0;
    } else {
        txbuf[bufin] = ch;
        bufcnt++;
        bufin = (bufin+1)%NBUF;
    }
    intr_enable();

}

Because an interrupt may come at any time, we must be quite careful.

  • Variables such as bufcnt that are mentioned in both the main program and the interrupt handler are marked volatile. This prevents the compiler from looking at a loop such as
while (bufcnt == NBUF) pause();
and assuming that bufcnt doesn't change in the loop, and so does not need loading from memory in each iteration. On the contrary, bufcnt will eventually be reduced by the interrupt handler, and then the loop should terminate. This won't happen if the loop continues to look at a stale copy of the value in a register.
  • [10.6] We know bufcnt++ will be implemented by code like this:
ldr r0, =bufcnt
ldr r1, [r0]
                <-- Here!
add r1, r1, #1
str r1, [r0]
If an interrupt happens "here", then the effect of the statement bufcnt-- in the interrupt handler will be lost, because when the interrupt handler returns, it is an old value of bufcnt that is incremented. The simple solution is to disable interrupts throughout this function by putting intr_disable() at the start and intr_enable() at the end.
  • If the buffer is full when we want to add a character to it, then we must wait until at least one character has been removed from the buffer and sent to the UART. The idiom for doing this is the call pause(), implemented by a special instruction wfe (wait-for-event): this halts the processor until an interrupt occurs, saving power. It's possible that, even if this task is held up because the buffer is full, there are other tasks that could continue to run, and we might need them to run in order (for example) to continue updating the display. For that, we will need to introduce an operating system that allows multiple processes that run independently.

[10.7] For interrupts to work, we must enable them when the program starts. Here's some code to add to serial_init that does this.

void serial_init(void) {
    ...
    // Interrupt for transmit only
    UART_INTENSET = BIT(UART_INT_TXDRDY);
    enable_irq(UART_IRQ);
    txidle = 1;
}

There are three bits of hardware that play a part: the UART itself must be told to interrupt when TXDRDY is set (and not in this case when, e.g., RXDRDY is set because an input character has arrived). Then the chip's "Nested Vectored Interrupt Controller" must be set up, assigning an arbitrary priority to the UART interrupt, and setting a bit that allows the UART to interrupt the processor. Finally, the processor itself must be in a state where it accepts interrupts; this is the default, but it can be modified with the operations intr_disable() and intr_enable() used above.

Updated results

[10.8] We can run the modified program again to see whether the delays in the output have disappeared. The program starts in much the same way.

Primes with interrupts

The LED is on for a period about two seconds shorter than before. When we look at prime 458, the gaps has disappeared, as have all other gaps in the output.

No more gaps

[10.9] If we look at the end of the run, there is a surprise: the output goes on after the main program has finished, as the interrupt handler continues to send characters until the buffer is empty.

Cheating at the end

Even though it cheats in this way, the program still runs a bit more quickly, because the time to find the primes is now entirely overlapped with the printing process.

Lecture 11