1. Dicas e sugestões¶
A seguir apresentam-se alguns exemplos de utilização do assemblador inline e algumas informações sobre como contornar as suas limitações. Neste documento, o termo «função assembladora» refere-se a uma função declarada em Python com o decorador @micropython.asm_thumb, enquanto «subrotina» se refere a código assemblador chamado a partir de uma função assembladora.
1.1. Ramificações de código e subrotinas¶
É importante compreender que as etiquetas são locais a uma função assembladora. Atualmente não existe forma de chamar, a partir de outra função, uma subrotina definida numa determinada função.
Para chamar uma subrotina, é emitida a instrução bl(LABEL). Isto transfere o controlo para a instrução a seguir à diretiva label(LABEL) e armazena o endereço de retorno no registo de ligação (lr ou r14). Para retornar, é emitida a instrução bx(lr), que faz com que a execução continue com a instrução a seguir à chamada da subrotina. Este mecanismo implica que, se uma subrotina chamar outra, deve guardar o registo de ligação antes da chamada e restaurá-lo antes de terminar.
O exemplo seguinte, um tanto artificial, ilustra uma chamada de função. Note que é necessário, no início, desviar a execução em torno de todas as chamadas de subrotina: as subrotinas terminam a execução com bx(lr), enquanto a função exterior simplesmente «cai pelo fim» à semelhança 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 seguinte demonstra uma chamada encadeada (recursiva): a clássica sequência de Fibonacci. Aqui, antes de uma chamada recursiva, o registo de ligação é guardado juntamente com outros registos que a lógica do programa necessita de 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. Passagem de argumentos e retorno¶
As funções assembladoras podem suportar de zero a três argumentos, que devem (se utilizados) ser designados r0, r1 e r2. Quando o código é executado, os registos são inicializados com esses valores.
Os tipos de dados que podem ser passados desta forma são inteiros e endereços de memória. Com o firmware atual, todos os possíveis valores de 32 bits podem ser passados e retornados. Se o valor de retorno puder ter o bit mais significativo definido, deve ser utilizada uma anotação de tipo Python para permitir que o MicroPython determine se o valor deve ser interpretado como um inteiro com 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 retorno de inteiros onde os bits 30 e 31 diferem.
As limitações no número de argumentos e valores de retorno podem ser ultrapassadas através do módulo array, que permite aceder a qualquer número de valores de qualquer tipo.
1.2.1. Múltiplos argumentos¶
Se um array Python de inteiros for passado como argumento a uma função assembladora, 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. Da mesma forma, uma função pode retornar múltiplos valores atribuindo-os a elementos do array. As funções assembladoras não têm forma de determinar o comprimento de um array: este terá de ser passado à função.
Este uso de arrays pode ser alargado para permitir a utilização de mais de três arrays. Isso é feito através de indireção: o módulo uctypes suporta addressof(), que retorna o endereço de um array passado como argumento. Assim, é possível 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¶
Estes podem ser tratados através de arrays do tipo de dados apropriado. Por exemplo, dados de vírgula flutuante de precisão simples podem ser processados da seguinte forma. Este exemplo de código recebe um array de floats e substitui o seu conteúdo pelos respetivos 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 suporta a utilização de estruturas de dados para além de simples arrays. Permite mapear uma estrutura de dados Python sobre uma instância bytearray que pode depois ser passada à função assembladora.
1.3. Constantes com nome¶
O código assemblador pode ser tornado mais legível e fácil de manter utilizando constantes com nome em vez de dispersar o código com números. Tal pode ser conseguido da seguinte forma:
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 pelo seu valor em tempo de compilação. Se as constantes forem declaradas num âmbito Python exterior, podem ser partilhadas entre múltiplas funções assembladoras e com código Python.
1.4. Código assemblador como métodos de classe¶
O MicroPython passa o endereço da instância do objeto como primeiro argumento para os métodos de classe. Isto normalmente tem pouca utilidade para uma função assembladora. Pode ser evitado declarando a função como método estático da seguinte forma:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Utilização de instruções não suportadas¶
Estas podem ser codificadas utilizando a instrução data como mostrado abaixo. Embora push() e pop() sejam suportados, o exemplo abaixo ilustra o princípio. O código de máquina necessário pode ser encontrado no Manual de Referência da Arquitetura ARM v7-M. Note 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. Ultrapassar a restrição de inteiros do MicroPython¶
Os inteiros pequenos do MicroPython em portes de 32 bits não podem conter um valor cujos bits 30 e 31 difiram (por exemplo 0x80000000), pelo que uma rotina assembladora que produza um resultado completo de 32 bits não pode simplesmente retorná-lo diretamente. Esta limitação é ultrapassada com o seguinte código, que utiliza assemblador para colocar o resultado num array e código Python para converter o resultado num 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