Playing with Interrupts

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Playing with Interrupts

Post by dhylands » Sun Jun 29, 2014 7:03 pm

I decided to play with interrupts today.

In all of my embedded projects, I like to have what I call a heartbeat LED. So I typically use that as a hello world program.

Here's the basic heartbeat program (not interrupt driven):

Code: Select all

import pyb

tick = 0
led = pyb.LED(4)    # 4 = Blue
while True:
    if tick <= 3:
        led.toggle()
    tick = (tick + 1) % 10
    pyb.delay(100)
I copy heartbeat.py to my pyboard file system, and then press Control-D at the REPL, and finally do:

Code: Select all

import heartbeat
You should now see the blue LED blinking, twice per second. You should also notice that you don't get your REPL back. You can press Control-C to break the program and get back to the REPL.

This is the interrupt driven version, which I called heartbeat_irq.py:

Code: Select all

import pyb

class Heartbeat(object):

    def __init__(self):
        self.tick = 0
        self.led = pyb.LED(4) # 4 = Blue
        tim = pyb.Timer(4)
        tim.init(freq=10)
        tim.callback(self.heartbeat_cb)

    def heartbeat_cb(self, tim):
        if self.tick <= 3:
            self.led.toggle()
        self.tick = (self.tick + 1) % 10

Heartbeat()
I chose to use a class here so that tick and led wouldn't be in the global namespace.

When you import heartbeat_irq you'll notice that you get your REPL back, and the blue LED is continuing to blink. You can execute other commands.

While I was coding this up, I noticed that errors in the exception handler aren't being reported properly. I had forgotten the self on the call to led.toggle. So it looked like this:

Code: Select all

    def heartbeat_cb(self, tim):
        if self.tick <= 3:
            led.toggle()
        self.tick = (self.tick + 1) % 10
and when I tried to import this, I got the following very unhelpful error:

Code: Select all

>>> import heartbeat_irq
Uncaught exception in Timer(4) interrupt handler
MemoryError: 
This led me to believe that there was an error in the callback, but it wasn't obvious how a MemoryError could occur (maybe its just the process of trying to allocate an exception while the interrupt handler is running).

So I commented out the call to tim.callback, and then invoked the callback manually:

Code: Select all

>>> import heartbeat_irq
>>> hb = heartbeat_irq.Heartbeat()
>>> hb.heartbeat_cb(None)
Traceback (most recent call last):
  File "<stdin>", line 0, in <module>
  File "0://heartbeat_irq.py", line 14, in heartbeat_cb
NameError: name 'led' is not defined
Now that is a much more useful error.

I also noticed that while at the REPL with heartbeat_irq running, if I press Control-D, the REPL tends to lockup, and I have to press RESET to get control back. I think that this is because the IRQ is continuing to run, however, the callback is no longer present. I'll file a bug report to get that fixed.

I'd also like to see uncaught IRQ exceeptions reported like the ones in REPL. I tried the following:

Code: Select all

    def heartbeat_cb(self, tim):
        try:
            if self.tick <= 3:
                led.toggle()
            self.tick = (self.tick + 1) % 10
        except BaseException as err:
            print(err.__class__.__name__, ":", str(err))
            #raise err
but it still reports just a MemoryError (and the print doesn't execute). If I run heartbeat_cb from REPL, I get the expected results (i.e. NameError reported).

fma
Posts: 164
Joined: Wed Jan 01, 2014 5:38 pm
Location: France

Re: Playing with Interrupts

Post by fma » Mon Jun 30, 2014 6:37 am

I also noticed that MemoryError happening when playing with interrupts... I though that my code was wrong. It explain things.
Frédéric

User avatar
thoralt
Posts: 11
Joined: Wed Jul 02, 2014 11:22 am
Location: Germany

Re: Playing with Interrupts

Post by thoralt » Wed Jul 02, 2014 11:40 am

from http://micropython.org/doc/tut-switch:
Note that your callback functions must not allocate any memory (for example they cannot create a tuple or list). Callback functions should be relatively simple. If you need to make a list, make it beforehand and store it in a global variable (or make it local and close over it). If you need to do a long, complicated calculation, then use the callback to set a flag which some other code then responds to.
The question is: Does the above limitation apply to your exception handling? Possibly. Interesting side effect: Does this limitation also apply to the default exception handler? If yes, then it would explain the "MemoryError" as the inability to allocate memory for any exception message from within the interrupt handler and that's why you don't see any meaningful output.

Thoralt

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Playing with Interrupts

Post by dhylands » Wed Jul 02, 2014 4:12 pm

So, the MemoryError was originating from inside the exception handling mechanism in the interpreter.

To deal with cases where the memory allocator can't be used, it has a statically allocated fallback exception. However, the VM tries to add traceback information to the exception, and this was what was trying to allocate memory, and caused the MemoryError.

I fixed that part of the problem here: https://github.com/micropython/micropython/pull/737 and I've got a solution to get traceback from exceptions raised during an irq as well (it just needs some polishing). See https://github.com/micropython/micropython/pull/738

stijn
Posts: 735
Joined: Thu Apr 24, 2014 9:13 am

Re: Playing with Interrupts

Post by stijn » Thu Jul 03, 2014 8:04 pm

Interesting, it's been a while I worked on embedded systems, interrupts and the likes so probably you can refresh my memory here.
Suppose you do something like this:

Code: Select all

x = HeartBeat()
for i in range( 1 : 1000 ) :
  x.heartbeat_cb( None )
Normally this opens the gates for race conditions, right? And if so, is there a way around it?

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Playing with Interrupts

Post by dhylands » Thu Jul 03, 2014 9:05 pm

stijn wrote:Interesting, it's been a while I worked on embedded systems, interrupts and the likes so probably you can refresh my memory here.
Suppose you do something like this:

Code: Select all

x = HeartBeat()
for i in range( 1 : 1000 ) :
  x.heartbeat_cb( None )
Normally this opens the gates for race conditions, right? And if so, is there a way around it?
I'm not sure what it is exactly that you're asking.

The script that you posted will wind up calling heartbeat_cb 10 times per second from interrupt context and a whole bunch of times from the main thread.

So yes, while heartbeat_cb is being called from your for loop, it could be interrupted and also be called from the timer interrupt.
If you wanted to prevent that, then you do something like this instead:

Code: Select all

x = HeartBeat()
for i in range( 1 : 1000 ) :
  pyb.disable_irq()
  x.heartbeat_cb( None )
  pyb.enable_irq()
That would prevent heartbeat_cb being called on the main thread from being interrupted by the timer interrupt.

A more interesting example would be something like this:

Code: Select all

import pyb

class AtomicTest:

    def start(self):
        self.counter = 0
        self.irq_count = 0
        self.main_count = 0
        self.done = False
        tim = pyb.Timer(4)
        tim.init(freq=1000)
        tim.callback(self.callback)

    def report(self):
        print("main_count = %d irq_count = %d counter = %d sum = %d" % (self.main_count, self.irq_count, self.counter, self.main_count + self.irq_count))

    def callback(self, tim):
        self.counter += 1
        self.irq_count += 1
        if self.irq_count >= 10000:
            tim.callback(None)
            self.done = True

t = AtomicTest()

t.start()
while not t.done:
    t.main_count += 1
    t.counter += 1
t.report()

t.start()
while not t.done:
    t.main_count += 1
    pyb.disable_irq()
    t.counter += 1
    pyb.enable_irq()
t.report()
When I ran it, I got the following results:

Code: Select all

>>> import atomic
main_count = 338848 irq_count = 10000 counter = 346406 sum = 348848
main_count = 229284 irq_count = 10000 counter = 239284 sum = 239284
Note that the test takes 10 seconds to output each line.

So in the first print, notice that counter is not equal to sum. This is because while the main thread executing self.counter += 1 it was interrupted 2446 times (348848 - 346406 = 2446)

Here's a more detailed explanation of what happened:
  1. main: loads self.counter (let's say it has a value of 150)
  2. interrupt comes along, and loads self.counter, gets 150
  3. interrupt adds 1
  4. interrupt saves 151 back into self.counter
  5. main thread increments loaded value to 151 (remember it loaded 150 before it was interrupted)
  6. main thread write 151 back into self.counter
What the main thread is doing is called a read-modify-write, and because these are not single CPU instructions, they're interruptible. By putting a disable_irq/enable_irq call around the increment of t.counter we've made the read-modify-write atomic.

Where you need to be concerned about things being atomic is when both the main thread and the interrupt are trying to modify the same memory location (self.counter). I don't need to worry about self.done since only the interrupt writes to the location and the write is atomic (because its writing a single 32-bit piece of data).

stijn
Posts: 735
Joined: Thu Apr 24, 2014 9:13 am

Re: Playing with Interrupts

Post by stijn » Fri Jul 04, 2014 6:48 am

Thanks, that is *exactly* the information I was after!
I'll checkif I can port enable/disable_irq and timer for INTime to see what the output is there
I'm not sure what it is exactly that you're asking.
The script that you posted will wind up calling heartbeat_cb 10 times per second from interrupt context and a whole bunch of times from the main thread.
Yeah sorry, the confusion is my mistake: I didn't grasp the timer was configured with a 10 second interval - I really meant to ask what happens for much higher interrupt rates like in your second example

User avatar
Leo
Posts: 2
Joined: Sun Jul 06, 2014 9:51 am
Location: Sofia, Bulgaria

Re: Playing with Interrupts

Post by Leo » Sun Jul 13, 2014 6:14 am

Hi All!
I have a question (mainly to Dave) about possibility to implement UART interrupt after receiving char. Is it possible to set UART RX pin to generate interrupt as GPIO pin on falling edge and then to use UART reading in callback or something else?
I'm using Python (limited version 1.5.2+) in Telit GSM modules and there are no interrupt at all, but RX/TX buffers are 4096 bytes long, so frequently polling prevents from fuffer overflow.
Nikolay

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Playing with Interrupts

Post by dhylands » Sun Jul 13, 2014 7:26 am

Leo wrote:Hi All!
I have a question (mainly to Dave) about possibility to implement UART interrupt after receiving char. Is it possible to set UART RX pin to generate interrupt as GPIO pin on falling edge and then to use UART reading in callback or something else?
I'm using Python (limited version 1.5.2+) in Telit GSM modules and there are no interrupt at all, but RX/TX buffers are 4096 bytes long, so frequently polling prevents from fuffer overflow.
Currently there isn't anything setup to support that. But it is on my todo list to add support for uart irqs and to add DMA fifo support.

User avatar
Leo
Posts: 2
Joined: Sun Jul 06, 2014 9:51 am
Location: Sofia, Bulgaria

Re: Playing with Interrupts

Post by Leo » Sun Jul 13, 2014 8:51 am

Great!!! :D Thanks, Dave!
Nikolay

Post Reply