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.