Extracting single float from array in timer callback function

The official pyboard running MicroPython.
This is the reference design and main target board for MicroPython.
You can buy one at the store.
Target audience: Users with a pyboard.
Post Reply
jim
Posts: 5
Joined: Tue Feb 23, 2021 10:22 pm

Extracting single float from array in timer callback function

Post by jim » Wed Feb 24, 2021 12:03 am

I'm trying to write a class that allows for function calls to perform basic floating point operations using assembler. As shown below for multiplication, I can pass it a couple floats, and it will return the correct product, packaged in a single element array. I'd like to assign the value from that array to a variable, but haven't been able to. Any help is appreciated.

Code: Select all

#
# MAIN.PY
#

import micropython
import array

class FloatingPointMath:

    def __init__(self):
        self.x =                array.array('f', [0.0])
        self.y =                array.array('f', [0.0])
        self.res =              array.array('f', [0.0])

    # multiplication in assembler
    @staticmethod
    @micropython.asm_thumb
    def f_mult(r0, r1, r2):
        vldr(s0, [r1, 0])
        vldr(s1, [r2, 0])
        vmul(s1, s1, s0)
        vstr(s1, [r0, 0])

    # function call for external use
    def mult(self, a, b):
        self.x[0] = a
        self.y[0] = b
        self.f_mult(self.res, self.x, self.y)
        return self.res

def isr(timer):
    global f
    global output                           # used as global to expose to REPL

    input1 = 1.2                            # causes no problem
    input2 = 2.0

    output = f.mult(input1, input2)         # array('f', [2.4])
    result = output[0]                      # causes MemoryError exception

f = FloatingPointMath()
output = array.array('f', [0.0])

timer_test = pyb.Timer(3)
timer_test.init(freq = 1)
timer_test.callback(isr)

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

Re: Extracting single float from array in timer callback function

Post by dhylands » Wed Feb 24, 2021 12:33 am

The issue is that you're no allowed to do memory allocations from within a hard ISR.

In MicroPython the statement:

Code: Select all

result = output[0]
will attempt to allocate memory to store the result.

See: http://docs.micropython.org/en/latest/r ... rules.html for more details.

jim
Posts: 5
Joined: Tue Feb 23, 2021 10:22 pm

Re: Extracting single float from array in timer callback function

Post by jim » Wed Feb 24, 2021 12:48 am

Yes, this part I understand. However, I'm able to assign floats to variables. For instance

Code: Select all

input1 = 1.2
input2 = input1
input2 = 2.0
all cause no problems.

I'd like to be able to assign a floating point variable, even an existing floating point variable, to the value returned by the assembler.

I've tried returning a bytearray, and confirmed that the four bytes form a properly-constructed float32. How might I get a variable to recognize them as such?

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

Re: Extracting single float from array in timer callback function

Post by dhylands » Wed Feb 24, 2021 1:03 am

I think that for the examples you've provided, they're effectively constants, so the pointers wind up coming from flash, so no memory allocation is required.

If you were to try:

Code: Select all

x = foo() + 3.4
from within the ISR then it needs to allocate memory to store the result.

Note that ISR's can't return anything (or more precisely anything that you return will be ignored).

So in order for the ISR to return anything it would need to do so in global variables.

Having the assembler store the result in a pre-allocated array is probably the simplest way to achieve this (and seems to be what your code is doing).

What are you trying to do with "result"?

jim
Posts: 5
Joined: Tue Feb 23, 2021 10:22 pm

Re: Extracting single float from array in timer callback function

Post by jim » Wed Feb 24, 2021 4:53 am

I don’t need the ISR to return anything. My application is a servo controller in a distributed robotics system. For esoteric reasons, the ISR needs to perform a number of floating point operations, which are currently in assembler, but I’d like the code base to be accessible to more team members, which is the reason for building a class to interface with the assembler functions.

jim
Posts: 5
Joined: Tue Feb 23, 2021 10:22 pm

Re: Extracting single float from array in timer callback function

Post by jim » Wed Feb 24, 2021 6:36 pm

Curiously, this works fine:

Code: Select all

import pyb
import micropython

class Foo:
    def bar(self):
        return float(2.41978)

def isr(timer):
    global f

    variable = 1.2      # float instance created
    print(variable)

    result = f.bar()
    print(result)

    variable = result   # float value updated
    print(variable)

f = Foo()

timer_test = pyb.Timer(3)
timer_test.init(freq = 4)
timer_test.callback(isr)

# 1.2
# 2.14978
# 2.14978
# (repeats)

User avatar
jimmo
Posts: 2244
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Extracting single float from array in timer callback function

Post by jimmo » Wed Feb 24, 2021 11:57 pm

jim wrote:
Wed Feb 24, 2021 6:36 pm
Curiously, this works fine:
dhylands is right, the issue here is the allocation of a float, and allocations are not allowed in hard IRQ context.

When you write

Code: Select all

a = 1.2
That doesn't actually allocate a float because the compiler will generate a const float object during compilation. These are the "const objects" that are generated during compilation, the same happens for strings (that are longer than the QSTR limit) and tuples, etc.

The implementation detail is that in MicroPython a float is an instance of the "float" type and they are immutable. Every float calculation at runtime needs to allocate a new one.

The second example works because the ISR doesn't have to do any allocation, it's still using the const object. (py/objfloat.c line 135 -- construction of a float from a float just returns the existing one).

There are a couple of workarounds here:
- You can make your ISR a "soft" handler instead (which runs in the micropython scheduler), which is allowed to allocate.
- You can build your firmware with a different object representation. The default is MICROPY_OBJ_REPR_A, but MICROPY_OBJ_REPR_C allows for packing floating point values into the object pointer itself. See py/mpconfig.h for more info.
- Use the array-of-floats to pass data back from the ISR, rather that putting it into a standalone float object.

(Or of course, don't use floats inside the ISR and save the computation until outside ISR, but this might not be practical)

jim
Posts: 5
Joined: Tue Feb 23, 2021 10:22 pm

Re: Extracting single float from array in timer callback function

Post by jim » Mon Mar 01, 2021 4:03 pm

Setting MICROPY_OBJ_REPR_C works beautifully. For PYBD_SF6, I also needed to set

Code: Select all

MICROPY_FLOAT_IMPL = single
in /ports/stm32/boards/PYBD_SF6/mpconfigboard.mk. By default, this parameter is `double` for PYBD_SF6, but `single` for earlier boards.

Floats work great in ISR now, and things seem to run fine for our purposes (<1000 Hz).

Thanks much, jimmo!

Post Reply