1. Sugerencias y consejos

A continuación se presentan algunos ejemplos del uso del ensamblador en línea y cierta información sobre cómo sortear sus limitaciones. En este documento, el término «función ensambladora» se refiere a una función declarada en Python con el decorador @micropython.asm_thumb, mientras que «subrutina» se refiere a código ensamblador invocado desde dentro de una función ensambladora.

1.1. Bifurcaciones de código y subrutinas

Es importante tener en cuenta que las etiquetas son locales a una función ensambladora. Actualmente no hay forma de que una subrutina definida en una función sea invocada desde otra.

Para llamar a una subrutina se emite la instrucción bl(LABEL). Esto transfiere el control a la instrucción que sigue a la directiva label(LABEL) y almacena la dirección de retorno en el registro de enlace (lr o r14). Para regresar se emite la instrucción bx(lr), que provoca que la ejecución continúe con la instrucción que sigue a la llamada de la subrutina. Este mecanismo implica que, si una subrutina va a llamar a otra, debe guardar el registro de enlace antes de la llamada y restaurarlo antes de terminar.

El siguiente ejemplo, algo artificial, ilustra una llamada de función. Observe que es necesario al inicio bifurcar alrededor de todas las llamadas a subrutinas: las subrutinas terminan su ejecución con bx(lr), mientras que la función externa simplemente «se cae por el final» al estilo de las funciones de Python.

@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))

El siguiente ejemplo de código demuestra una llamada anidada (recursiva): la clásica sucesión de Fibonacci. Aquí, antes de una llamada recursiva, el registro de enlace se guarda junto con otros registros que la lógica del programa requiere preservar.

@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))

1.2. Paso y retorno de argumentos

Las funciones ensambladoras pueden admitir de cero a tres argumentos, que deben (si se usan) llamarse r0, r1 y r2. Cuando el código se ejecute, los registros se inicializarán con esos valores.

Los tipos de datos que pueden pasarse de esta manera son enteros y direcciones de memoria. Con el firmware actual pueden pasarse y retornarse todos los valores posibles de 32 bits. Si el valor de retorno puede tener el bit más significativo activado, se debería emplear una sugerencia de tipo de Python para permitir que MicroPython determine si el valor debe interpretarse como un entero con signo o sin signo: los tipos son int o uint.

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

hex(uadd(0x40000000,0x40000000)) retornará 0x80000000, demostrando el paso y retorno de enteros donde los bits 30 y 31 difieren.

Las limitaciones en el número de argumentos y valores de retorno pueden superarse mediante el módulo array, que permite acceder a cualquier número de valores de cualquier tipo.

1.2.1. Argumentos múltiples

Si se pasa un array de enteros de Python como argumento a una función ensambladora, la función recibirá la dirección de un conjunto contiguo de enteros. De este modo, múltiples argumentos pueden pasarse como elementos de un único array. De forma similar, una función puede retornar múltiples valores asignándolos a elementos del array. Las funciones ensambladoras no tienen medios para determinar la longitud de un array: esto deberá pasarse a la función.

Este uso de arrays puede ampliarse para permitir el uso de más de tres arrays. Esto se hace mediante indirección: el módulo uctypes admite addressof(), que retornará la dirección de un array pasado como su argumento. Así, puede rellenar un array de enteros con las direcciones de otros 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))

1.2.2. Tipos de datos no enteros

Estos pueden manejarse mediante arrays del tipo de dato apropiado. Por ejemplo, los datos de punto flotante de precisión simple pueden procesarse de la siguiente manera. Este ejemplo de código toma un array de números de punto flotante y reemplaza su contenido con sus cuadrados.

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)

El módulo uctypes admite el uso de estructuras de datos más allá de los arrays simples. Permite que una estructura de datos de Python se mapee sobre una instancia de bytearray que luego puede pasarse a la función ensambladora.

1.3. Constantes con nombre

El código ensamblador puede hacerse más legible y mantenible usando constantes con nombre en lugar de llenar el código de números. Esto puede lograrse así:

MYDATA = const(33)

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

El constructo const() hace que MicroPython reemplace el nombre de la variable con su valor en tiempo de compilación. Si las constantes se declaran en un ámbito externo de Python, pueden compartirse entre múltiples funciones ensambladoras y con código de Python.

1.4. Código ensamblador como métodos de clase

MicroPython pasa la dirección de la instancia del objeto como primer argumento a los métodos de clase. Normalmente esto es de poca utilidad para una función ensambladora. Puede evitarse declarando la función como método estático, así:

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

1.5. Uso de instrucciones no admitidas

Estas pueden codificarse usando la sentencia data como se muestra a continuación. Aunque push() y pop() sí están admitidas, el ejemplo siguiente ilustra el principio. El código máquina necesario puede encontrarse en el ARM v7-M Architecture Reference Manual. Observe que el primer argumento de las llamadas a data tales como

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

indica que cada argumento subsiguiente es una cantidad de dos bytes.

1.6. Cómo superar la restricción de enteros de MicroPython

Los enteros pequeños de MicroPython en los ports de 32 bits no pueden contener un valor cuyos bits 30 y 31 difieran (por ejemplo 0x80000000), por lo que una rutina ensambladora que produce un resultado completo de 32 bits no puede simplemente retornarlo de forma directa. Esta limitación se supera con el siguiente código, que usa ensamblador para colocar el resultado en un array y código de Python para forzar el resultado a un entero sin signo de precisión arbitraria.

from array import array

@micropython.asm_thumb
def getval(r0):
    movwt(r1, 0x80000000)  # a 32-bit value whose bits 30 and 31 differ
    str(r1, [r0, 0])

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

print(hex(get()))  # 0x80000000