1. Dicas e sugestões

A seguir, alguns exemplos do uso do assembler inline e algumas informações sobre como contornar suas limitações. Neste documento o termo “função assembler” refere-se a uma função declarada em Python com o decorador @micropython.asm_thumb, enquanto “sub-rotina” refere-se a código assembler chamado de dentro de uma função assembler.

1.1. Desvios de código e sub-rotinas

É importante compreender que os rótulos são locais a uma função assembler. Atualmente não há como uma sub-rotina definida em uma função ser chamada a partir de outra.

Para chamar uma sub-rotina, a instrução bl(LABEL) é emitida. Isso transfere o controle para a instrução seguinte à diretiva label(LABEL) e armazena o endereço de retorno no registrador de vínculo (lr ou r14). Para retornar, a instrução bx(lr) é emitida, fazendo com que a execução continue na instrução seguinte à chamada da sub-rotina. Esse mecanismo implica que, se uma sub-rotina precisar chamar outra, ela deve salvar o registrador de vínculo antes da chamada e restaurá-lo antes de terminar.

O exemplo a seguir, um tanto artificial, ilustra uma chamada de função. Observe que é necessário, no início, desviar ao redor de todas as chamadas de sub-rotina: as sub-rotinas encerram a execução com bx(lr), enquanto a função externa simplesmente “chega ao fim”, no estilo das funções 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))

O exemplo de código a seguir demonstra uma chamada aninhada (recursiva): a clássica sequência de Fibonacci. Aqui, antes de uma chamada recursiva, o registrador de vínculo é salvo junto com outros registradores que a lógica do programa exige que sejam preservados.

@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. Passagem de argumentos e retorno

Funções assembler podem aceitar de zero a três argumentos, os quais devem (se usados) ser nomeados r0, r1 e r2. Quando o código executar, os registradores serão inicializados com esses valores.

Os tipos de dados que podem ser passados dessa forma são inteiros e endereços de memória. Com o firmware atual, todos os valores possíveis de 32 bits podem ser passados e retornados. Se o valor de retorno puder ter o bit mais significativo definido, deve-se empregar uma dica de tipo Python para permitir que o MicroPython determine se o valor deve ser interpretado como um inteiro com sinal ou sem sinal: os tipos são int ou uint.

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

hex(uadd(0x40000000,0x40000000)) retornará 0x80000000, demonstrando a passagem e o retorno de inteiros em que os bits 30 e 31 diferem.

As limitações quanto ao número de argumentos e valores de retorno podem ser superadas por meio do módulo array, que permite acessar qualquer quantidade de valores de qualquer tipo.

1.2.1. Múltiplos argumentos

Se um array Python de inteiros for passado como argumento para uma função assembler, a função receberá o endereço de um conjunto contíguo de inteiros. Assim, múltiplos argumentos podem ser passados como elementos de um único array. De forma semelhante, uma função pode retornar múltiplos valores atribuindo-os a elementos de um array. Funções assembler não têm como determinar o comprimento de um array: isso precisará ser passado para a função.

Esse uso de arrays pode ser estendido para permitir que mais de três arrays sejam utilizados. Isso é feito por meio de indireção: o módulo uctypes oferece suporte a addressof(), que retorna o endereço de um array passado como seu argumento. Assim, você pode preencher um array de inteiros com os endereços de outros 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 dados não inteiros

Esses podem ser tratados por meio de arrays do tipo de dado apropriado. Por exemplo, dados de ponto flutuante de precisão simples podem ser processados da seguinte forma. Este exemplo de código recebe um array de floats e substitui seu conteúdo pelos respectivos quadrados.

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)

O módulo uctypes oferece suporte ao uso de estruturas de dados além de arrays simples. Ele permite que uma estrutura de dados Python seja mapeada em uma instância de bytearray, que pode então ser passada para a função assembler.

1.3. Constantes nomeadas

O código assembler pode ser tornado mais legível e fácil de manter usando constantes nomeadas em vez de preencher o código com números. Isso pode ser obtido da seguinte maneira:

MYDATA = const(33)

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

A construção const() faz com que o MicroPython substitua o nome da variável por seu valor em tempo de compilação. Se as constantes forem declaradas em um escopo Python externo, elas podem ser compartilhadas entre múltiplas funções assembler e com código Python.

1.4. Código assembler como métodos de classe

O MicroPython passa o endereço da instância do objeto como o primeiro argumento para os métodos de classe. Isso normalmente é de pouca utilidade para uma função assembler. Pode-se evitar isso declarando a função como um método estático, assim:

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

1.5. Uso de instruções não suportadas

Essas podem ser codificadas usando a instrução data, como mostrado a seguir. Embora push() e pop() sejam suportadas, o exemplo a seguir ilustra o princípio. O código de máquina necessário pode ser encontrado no ARM v7-M Architecture Reference Manual. Observe que o primeiro argumento de chamadas data, como

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

indica que cada argumento subsequente é uma quantidade de dois bytes.

1.6. Superando a restrição de inteiros do MicroPython

Os pequenos inteiros do MicroPython em ports de 32 bits não podem armazenar um valor cujos bits 30 e 31 difiram (por exemplo, 0x80000000), portanto uma rotina assembler que produz um resultado completo de 32 bits não pode simplesmente retorná-lo diretamente. Essa limitação é superada com o código a seguir, que usa assembler para colocar o resultado em um array e código Python para converter o resultado em um inteiro sem sinal de precisão arbitrária.

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