Hints and tips
==============

The following are some examples of the use of the inline assembler and some
information on how to work around its limitations. In this document the term
"assembler function" refers to a function declared in Python with the
``@micropython.asm_thumb`` decorator, whereas "subroutine" refers to assembler
code called from within an assembler function.

Code branches and subroutines
-----------------------------

It is important to appreciate that labels are local to an assembler function.
There is currently no way for a subroutine defined in one function to be called
from another.

To call a subroutine the instruction ``bl(LABEL)`` is issued. This transfers
control to the instruction following the ``label(LABEL)`` directive and stores
the return address in the link register (``lr`` or ``r14``). To return the
instruction ``bx(lr)`` is issued which causes execution to continue with
the instruction following the subroutine call. This mechanism implies that, if
a subroutine is to call another, it must save the link register prior to
the call and restore it before terminating.

The following rather contrived example illustrates a function call. Note that
it's necessary at the start to branch around all subroutine calls: subroutines
end execution with ``bx(lr)`` while the outer function simply "drops off the end"
in the style of Python functions.

::

    @micropython.asm_thumb
    def quad(r0):
        b(START)
        label(DOUBLE)
        add(r0, r0, r0)
        bx(lr)
        label(START)
        bl(DOUBLE)
        bl(DOUBLE)

    print(quad(10))

The following code example demonstrates a nested (recursive) call: the classic
Fibonacci sequence. Here, prior to a recursive call, the link register is saved
along with other registers which the program logic requires to be preserved.

::

    @micropython.asm_thumb
    def fib(r0):
        b(START)
        label(DOFIB)
        push({r1, r2, lr})
        cmp(r0, 1)
        ble(FIBDONE)
        sub(r0, 1)
        mov(r2, r0) # r2 = n -1
        bl(DOFIB)
        mov(r1, r0) # r1 = fib(n -1)
        sub(r0, r2, 1)
        bl(DOFIB)   # r0 = fib(n -2)
        add(r0, r0, r1)
        label(FIBDONE)
        pop({r1, r2, lr})
        bx(lr)
        label(START)
        bl(DOFIB)

    for n in range(10):
        print(fib(n))

Argument passing and return
---------------------------

The tutorial details the fact that assembler functions can support from zero to
three arguments, which must (if used) be named ``r0``, ``r1`` and ``r2``. When
the code executes the registers will be initialised to those values.

The data types which can be passed in this way are integers and memory
addresses. With current firmware all possible 32 bit values may be passed and
returned. If the return value may have the most significant bit set a Python
type hint should be employed to enable MicroPython to determine whether the
value should be interpreted as a signed or unsigned integer: types are
``int`` or ``uint``.

::

    @micropython.asm_thumb
    def uadd(r0, r1) -> uint:
        add(r0, r0, r1)

``hex(uadd(0x40000000,0x40000000))`` will return 0x80000000, demonstrating the
passing and return of integers where bits 30 and 31 differ.

The limitations on the number of arguments and return values can be overcome by means
of the ``array`` module which enables any number of values of any type to be accessed.

Multiple arguments
~~~~~~~~~~~~~~~~~~

If a Python array of integers is passed as an argument to an assembler
function, the function will receive the address of a contiguous set of integers.
Thus multiple arguments can be passed as elements of a single array. Similarly a
function can return multiple values by assigning them to array elements.
Assembler functions have no means of determining the length of an array:
this will need to be passed to the function.

This use of arrays can be extended to enable more than three arrays to be used.
This is done using indirection: the ``uctypes`` module supports ``addressof()``
which will return the address of an array passed as its argument. Thus you can
populate an integer array with the addresses of other arrays:

::

    from uctypes import addressof
    @micropython.asm_thumb
    def getindirect(r0):
        ldr(r0, [r0, 0]) # Address of array loaded from passed array
        ldr(r0, [r0, 4]) # Return element 1 of indirect array (24)

    def testindirect():
        a = array.array('i',[23, 24])
        b = array.array('i',[0,0])
        b[0] = addressof(a)
        print(getindirect(b))

Non-integer data types
~~~~~~~~~~~~~~~~~~~~~~

These may be handled by means of arrays of the appropriate data type. For
example, single precision floating point data may be processed as follows.
This code example takes an array of floats and replaces its contents with
their squares.

::

    from array import array

    @micropython.asm_thumb
    def square(r0, r1):
        label(LOOP)
        vldr(s0, [r0, 0])
        vmul(s0, s0, s0)
        vstr(s0, [r0, 0])
        add(r0, 4)
        sub(r1, 1)
        bgt(LOOP)

    a = array('f', (x for x in range(10)))
    square(a, len(a))
    print(a)

The uctypes module supports the use of data structures beyond simple
arrays. It enables a Python data structure to be mapped onto a bytearray
instance which may then be passed to the assembler function.

Named constants
---------------

Assembler code may be made more readable and maintainable by using named
constants rather than littering code with numbers. This may be achieved
thus:

::

    MYDATA = const(33)

    @micropython.asm_thumb
    def foo():
        mov(r0, MYDATA)

The const() construct causes MicroPython to replace the variable name
with its value at compile time. If constants are declared in an outer
Python scope they can be shared between multiple assembler functions and
with Python code.

Assembler code as class methods
-------------------------------

MicroPython passes the address of the object instance as the first argument
to class methods. This is normally of little use to an assembler function.
It can be avoided by declaring the function as a static method thus:

::

    class foo:
      @staticmethod
      @micropython.asm_thumb
      def bar(r0):
        add(r0, r0, r0)

Use of unsupported instructions
-------------------------------

These can be coded using the data statement as shown below. While
``push()`` and ``pop()`` are supported the example below illustrates the
principle. The necessary machine code may be found in the ARM v7-M
Architecture Reference Manual. Note that the first argument of data
calls such as

::

    data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11

indicates that each subsequent argument is a two byte quantity.

Overcoming MicroPython's integer restriction
--------------------------------------------

The Pyboard chip includes a CRC generator. Its use presents a problem in
MicroPython because the returned values cover the full gamut of 32 bit
quantities whereas small integers in MicroPython cannot have differing values
in bits 30 and 31. This limitation is overcome with the following code, which
uses assembler to put the result into an array and Python code to
coerce the result into an arbitrary precision unsigned integer.

::

    from array import array
    import stm

    def enable_crc():
        stm.mem32[stm.RCC + stm.RCC_AHB1ENR] |= 0x1000

    def reset_crc():
        stm.mem32[stm.CRC+stm.CRC_CR] = 1

    @micropython.asm_thumb
    def getval(r0, r1):
        movwt(r3, stm.CRC + stm.CRC_DR)
        str(r1, [r3, 0])
        ldr(r2, [r3, 0])
        str(r2, [r0, 0])

    def getcrc(value):
        a = array('i', [0])
        getval(a, value)
        return a[0] & 0xffffffff # coerce to arbitrary precision

    enable_crc()
    reset_crc()
    for x in range(20):
        print(hex(getcrc(0)))
