Lecture 10 – Programming with interrupts (Digital Systems)
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.
- It's really inefficient: the constant checking of the UART slows down the most computationally-intensive parts of the program.
- 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]
.
[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:
is one reason, but there may be others.UART.TXDRDY
= 1 - 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 markedvolatile
. 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 ofbufcnt
that is incremented. The simple solution is to disable interrupts throughout this function by puttingintr_disable()
at the start andintr_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 instructionwfe
(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.
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.
[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.
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.