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.
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
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
['] 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
idle, as used inside
“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
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
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
wake, we can avoid the