2110dc5a6d
Thanks to Peter Hinch for contributing this.
233 lines
7.3 KiB
ReStructuredText
233 lines
7.3 KiB
ReStructuredText
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. Further, integers are restricted in that the top two bits
|
|
must be identical, limiting the range to -2**30 to 2**30 -1. Return
|
|
values are similarly limited. These limitations can be overcome by means
|
|
of the ``array`` module to allow 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 precison 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 mutiple 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)))
|