not logged in | [Login]

Inline assembler is supported on STM32-based boards like the pyboard and STMicroelectronics Discovery or Nucleo boards.


The information here was largely compiled before the inline assembler was officially documented. Much of it has therefore been superseded. It is recommended that you read the first two references below for the latest information.


uPy assembler functions

Are prefixed by the decorator @micropython.asm_thumb which causes uPy to interpret the content of the function as assembler, compile and wrap it accordingly, so that it can be called from by Python code.

Assembler functions always return the content of register r0 on exit, if uPy code needs to receive a value from your assembler function - make sure it's in r0 when the function completes, the content of r0 will be returned - whether you want it or not.

Inline assembler functions can have a minimum of zero, and maximum of three arguments, which are passed into registers r0, r1 and r2 in order and must be named r0, r1 and r2 in the function defenition;


def test()
def test(r0)
def test(r0, r1)
def test(r0, r1, r2)


def test(self)
def test(r1,r3)
def test(r0, r1, r2, r3)


def test(r0,r1):
  nop() #don't do anything
>>> test(1,2)
#test returns 1 because that's what we passed into r0 when the function was called, and its value wasn't changed

The stm32f405 in the pyboard has a total of 16 32bit CPU registers that can be used with assembler functions to store and manipulate data (r0-r15) ... albeit r15 is interchangeably referred to as pc ('programme counter used internally to hold address instruction to exec' reference) and is probably therefore best avoided unless you understand what it actually does ...

The MCU also has three special purpose registers: stack pointer sp (r13), link register lr (r14), program counter pc (r15). Avoid unless you understand their purpose.

Note: it is the callee's responsibility to save registers. r0-r7 are saved automatically by the MicroPython assembler so they can be used at will. Registers r8-r11 are not. If you need to use them they should be saved on the stack:


and to restore them issue


before return. Note that some instructions specify registers using a 3-bit field, consequently will only accept r0-r7 as arguments. The "ARM v7-M Architecture Reference Manual" is your friend here.

Exposed Assembler Instructions

Not all of the stm32f405 chips instructions are exposed for use in uPy assembler functions - those that are may be gleaned from uPy's Inline Assembler source-code, emitinlinethumb.c and header Basic notes and a few examples of the currently implemented instructions can be found at the links hereunder;


Under certain circumstances Python, therefore uPy, automagically passes objects into functions, but this functionality is likely undesirable in assembler functions. For example a method of a class, defined as a function, is automatically passed a reference to the object it as an instance of as its first argument (Python Classes Tutorial. In Python this object instance is often referred to as self or new, depending on context ... however this is just a convention and any variable name can be used.

This behaviour is by default applied to methods defined as assembler functions ... with the result that a pointer to the class object is passed into r0, which is highly unlikely to be of any use in assembler, this has the additional side-effect of reducing the number of 'useable' assembler arguments from 3 to 2 (r1 and r2) for an assembler method of a class.

This behaviour can be excepted by using one of Python's existing decorators - @staticmethod - which has made it's way across into uPy;


class test:
  def t1(r0):
  def t2(r0):
>>> t=test()
>>> t.t1()
536884576 #pointer to the test class, not very useful in assembler!
>>> t.t2(42)

Argument passing and the three register limit

The fact that an assembler function can receive a maximum of three register arguments might appear to be a significant limitation. In practice this isn't the case. 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.

This use of arrays can be extended. Functions which are intended to process data arrays might be thought to be limited to handling three arrays. However this can be worked round 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
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)

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 with the latter being passed to the assembler function.

Hints and tips

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)

def foo():
    mov(r0, mydata)

The const() construct causes MicroPython to replace the variable name with its value at compile time.

Workround for instructions not yet exposed

These can be coded using the data statement as shown below. Note that push and pop are now supported but the example below illustrates the principle. There are also some simple examples in the file here The source for the necessary machine code is the following document, widely available on the web "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.