Interrupts, tamed at last Mar 2017

Before going into interrupts, why they’re needed, and why they are tricky, let’s first look into an example which does not use interrupts: writing a pass-through USB-to-serial application.

Note that many of the observations and issues that follow apply to any procedural language!

Here’s a simple loop to pass characters from the UART to USB, and from USB to the UART:

: run
  uart-init  19200 uart-baud
  begin
    uart-key? if uart-key emit      then
    key?      if key      uart-emit then
    \ do other things here...
  again ;

From this layout, you can see that it’s a symmetrical process: whatever comes in on one port, gets emitted out the other. Easy stuff, right? And indeed: the above code does work.

Sort of…

The problem is that there will be speed differences, with characters coming in a lot faster, potentially, than the serial port can handle. This will cause the uart-emit call to block, preventing the other side of the transfer from proceeeding while the UART is busy sending.

As a result, data coming into the UART will get dropped, since it won’t be read out in time before more characters come in. With a fancy term: the incoming UART feed does not support back-pressure (since neither hardware- nor software- handshaking have been implemented).

We can fix that with the help of the multi-tasker:

task: uart-task

: uart-reader&
  uart-task activate
  begin
    uart-key? if uart-key emit then
  again ;

: run
  uart-init  19200 uart-baud
  multitask uart-reader&
  begin
    key? if key uart-emit then
    \ do other things here...
  again ;

Now, a separate background task is started before the main loop, copying data from UART to USB. That task handles the reverse flow, and sure enough: we’ve solved the blocking issue!

Well, sort of…

This simple example will work just fine, because the processor is not doing anything else. But at say 115,200 baud, we have to read out every byte coming into the UART within some 10 µs.

That’s where interrupts come in: instead of polling the UART all the time, to check whether a byte has been received, we can configure it to generate an interrupt each time this happens. The code for this has already been written (for F103 and for L052). Being layered on top of the polled version, each of these interrupt-based variants is in fact under two dozen lines of code.

Note that we still can’t prevent the data from arriving at the UART receive port at full speed. The only benefit of interrupts here, is that we can immediately store it in a (ring) buffer, and collect more bytes until our application is willing and able to process them.

The code with interrupt handling and a 128-byte buffer is delightfully similar to the original:

task: uart-task

: uart-reader&
  uart-task activate
  begin
    uart-irq-key? if uart-irq-key emit then
  again ;

: run
  uart-irq-init  19200 uart-baud
  multitask uart-reader&
  begin
    key? if key uart-emit then
    \ do other things here...
  again ;

Now our application can take over 100 times as long between calls to pause as before, and we won’t lose any data. Interrupts will quickly take stash away each incoming byte, and that’s it!

Yeah, more or less…

But this code is still based on a couple of constantly-polling loops, consuming lots of idle CPU cycles - so yes, although it does work fine, it’s not such a great approach for low-power nodes.

There’s one more refinement we can easily add to this: instead of having the background task poll for new data in the buffer, we can wake it up when filling the input buffer. What we need to do is replace the interrupt handler with a slightly more advanced one.

The trick is to replace this one line of code in uart-irq-init:

['] uart-irq-handler irq-usart2 !

But instead of changing the library, let’s simply replace the handler with our own version:

[: uart-irq-handler uart-task wake ;] irq-usart2 !

It does everything the original interrupt handler does, but it also wakes up uart-task every time an interrupt triggers. Here is the final code, with uart-task sleeping most of the time:

task: uart-task

: uart-reader&
  uart-task background
  begin
    begin uart-irq-key? while uart-irq-key emit repeat
    stop
  again ;

: run
  uart-irq-init  19200 uart-baud
  [: uart-irq-handler uart-task wake ;] irq-usart2 !
  multitask uart-reader&
  begin
    key? if key uart-emit then
    \ do other things here...
  again ;

The key enabler here, is that wake (and idle, as used inside stop) are “interrupt-safe”: they can be used from inside interrupt handlers. That’s what makes the above architecture possible.

There is one detail which needs to be mentioned: note that every UART receive interrupt will wake up uart-task, but that it won’t run right away, since the multitasker is collaborative.

It’s up to the app to decide when and where to pass control to the multi-tasker (using pause). At times, several interrupts might be triggered before uart-task actually gets a chance to run. Because of that, we must process all pending data before going back to sleep by calling stop.

Is that all there is to it, then?

Yes, it really is. Interrupt handlers still need to be written with great care to avoid affecting variables which the application also uses (in this case the ring buffer), but the beauty of this approach is the clear-cut separation of responsibilities: interrupt handlers should only do what’s time critical (“get that byte out of the UART!“), everything else happens when & where the application is ready for it. And by using stop and wake, we can avoid the frantic polling.

Weblog © Jean-Claude Wippler. Generated by Hugo.