Sharing variables between interrupt and normal code

C programming, build, interpreter/VM.
Target audience: MicroPython Developers.
User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Sharing variables between interrupt and normal code

Post by pythoncoder » Tue Nov 24, 2015 8:10 am

@PappaPeppar I agree that something more fine-grained than disabling all interrupts is worth investigation. If a global disable is required it should surely be very brief.

I thought I understood ldrex/strex thanks to your description, but now I'm not so sure. The following code shows that the interrupt handler is acquiring the lock when I'd have thought it should always fail.

Code: Select all

import pyb, micropython, array, uctypes
micropython.alloc_emergency_exception_buf(100)

class Foo:
    def __init__(self):
        self.lockword = array.array('i', (0,))

    @micropython.asm_thumb
    def _test(r0, r1):           # Called from ISR context
        ldrex(r0, [r1, 0])
        strex(r0, r0, [r1, 0])  # Should always fail returning 1 (?)

    def test(self):
        return self._test(uctypes.addressof(self.lockword))

    @micropython.asm_thumb
    def _lock(r0, r1):
        ldrex(r0, [r1, 0])

    def lock(self):
        self._lock(uctypes.addressof(self.lockword))

    @micropython.asm_thumb
    def _unlock(r0, r1):
        mov(r0, 0)
        strex(r0, r0, [r1, 0])

    def unlock(self):
        self._unlock(uctypes.addressof(self.lockword))

foo = Foo()

def update(t):  # Interrupt handler records success or failure of strex
    global can_lock, cannot_lock
    if foo.test():
        cannot_lock += 1
    else:
        can_lock += 1

def main():
    global can_lock, cannot_lock
    can_lock, cannot_lock = 0, 0
    foo.lock()         # does an ldrex which should cause the ISR test to fail
    timer = pyb.Timer(4, freq = 1000, callback = update)
    for x in range(10000):
        pyb.delay(1)
        if x % 1000 == 0:
            print('Lock count {} fail count {}'.format(can_lock, cannot_lock))
    timer.deinit()
    foo.unlock()     # strex to clear the lock

main()
Outcome

Code: Select all

>>> 
PYB: sync filesystems
PYB: soft reboot
MicroPython v1.5-60-g1657345-dirty on 2015-10-31; PYBv1.0 with STM32F405RG
Type "help()" for more information.
>>> import rats11
Lock count 0 fail count 0
Lock count 1000 fail count 0
Lock count 2000 fail count 0
Lock count 3000 fail count 0
Lock count 4000 fail count 0
Lock count 5000 fail count 0
Lock count 6000 fail count 0
Lock count 7000 fail count 0
Lock count 8000 fail count 0
Lock count 9000 fail count 0
>>> 
Can you shed any light on this?
Peter Hinch
Index to my micropython libraries.

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

Re: Sharing variables between interrupt and normal code

Post by dhylands » Tue Nov 24, 2015 8:53 am

I'm planning on enhancing disable/enable_irq to add support for levels. I put forward a proposal near the end of this thread:
https://github.com/micropython/micropython/issues/1555

I'm just waiting for some other code to land so I don't have to go through rebase hell.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Sharing variables between interrupt and normal code

Post by pythoncoder » Wed Nov 25, 2015 6:31 am

@dhylands Sounds good. On the more general issue
I'd rather just use a lockless queue
My example was oversimplified and your observation is correct: the parsing of the data should be performed by the main loop rather than the ISR.

But there are real cases where critical sections are needed in interrupt handlers. Consider a case where the incoming data must of necessity be interpreted in real time at the symbol rate, with results being passed to the main loop at a lower rate. For example an ISR might perform correlation using code akin to my FIR solution - incoming data feeds a fixed length ring buffer and is correlated with one or more targets. Information is fed back to the main loop every N incoming symbols and comprises multiple values such as, for each target, the maximum and minimum correlation coefficients in the time interval. The update comprises a critical section which is protected. In the case where the main loop has the lock the update is merely delayed by a number of symbol periods.

The mechanism I described is not a spinlock - at least by my quarter century recollection of realtime systems. A true spinlock is the limiting case of a semaphore whose value is binary. It is symmetrical: every process accessing it does so via a blocking wait. As such, I'd agree that its only application is in multiprocessor systems. I was attempting to create a test-and-set mutex which can be used asymmetrically. The main loop uses it as a spinlock, but the ISR never waits on the lock. It attempts to lock it: on success it executes the critical section, on failure it defers execution until a subsequent interrupt. As I understand it this is an orthodox technique.

As you're doubtless aware some processors support explicit atomic test-and-set instructions. In their absence - as in our case - it is usually necessary to disable interrupts for a brief period to guarantee atomicity. I had thought that ldrex/strex might remove this requirement but my testing suggests that they don't seem to detect the ISR as being a different context from the main loop. I do have a vague recollection that these instructions are intended for multi-core chip variants. Do you have any knowledge of this issue?
Peter Hinch
Index to my micropython libraries.

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

Re: Sharing variables between interrupt and normal code

Post by dhylands » Wed Nov 25, 2015 9:22 am

I was using this definition of a spinlock: https://en.wikipedia.org/wiki/Spinlock which I believe is the same definition used in the linux kernel.

I normally think of semaphores as blocking. If the thread in question can't obtain it, then it blocks, giving up the CPU to other threads. I've seen loops in some implementations of semaphores to deal with certains type of race conditions, but those would only loop a couple times, not busy-wait for the semaphore to be released.

Binary semaphores are sometimes referred to as a mutex. I've only ever seen splnlocks used on SMP or other multi-processor systems (my years of RTOS were all non-SMP, and the only SMP work I've done has been in the linux kernel).

PappaPeppar
Posts: 30
Joined: Thu Dec 18, 2014 10:38 pm

Re: Sharing variables between interrupt and normal code

Post by PappaPeppar » Wed Nov 25, 2015 10:55 pm

@pythoncoder, your observation is correct: The ldrex/strex mechanism does not depend on if the cpu is in IRQ mode or thread mode, it only tracks memory accesses. It is documented in this http://infocenter.arm.com/help/index.js ... index.html (Registration required) document. These instructions exists instead of test and set instructions available in other CPU architectures. ARM is a load store architecture and a atomic test and set would break that model, and test and set instructions can be costly performance wise on machines with pipelines. They are necessary in multi core systems, but I can definitely see their benefits in small MCU systems as well.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Sharing variables between interrupt and normal code

Post by pythoncoder » Fri Nov 27, 2015 7:26 am

@PappaPeppar Thanks for that. I've coded an alternative way to protect critical sections by disabling interrupts for a few assembler instructions to implement an atomic read-modify-write. I'll offer it for review as part of my attempt to better document interrupt handling http://forum.micropython.org/viewtopic.php?f=6&t=1156.
Peter Hinch
Index to my micropython libraries.

Post Reply