1. Suggerimenti e consigli¶
Di seguito sono riportati alcuni esempi sull’uso dell’assembler inline e alcune informazioni su come aggirarne le limitazioni. In questo documento il termine «funzione assembler» si riferisce a una funzione dichiarata in Python con il decoratore @micropython.asm_thumb, mentre «subroutine» si riferisce a codice assembler chiamato dall’interno di una funzione assembler.
1.1. Diramazioni di codice e subroutine¶
È importante tenere presente che le etichette sono locali a una funzione assembler. Attualmente non esiste alcun modo perché una subroutine definita in una funzione possa essere chiamata da un’altra.
Per chiamare una subroutine si emette l’istruzione bl(LABEL). Questo trasferisce il controllo all’istruzione successiva alla direttiva label(LABEL) e memorizza l’indirizzo di ritorno nel link register (lr o r14). Per tornare si emette l’istruzione bx(lr), che fa proseguire l’esecuzione con l’istruzione successiva alla chiamata della subroutine. Questo meccanismo implica che, se una subroutine deve chiamarne un’altra, deve salvare il link register prima della chiamata e ripristinarlo prima di terminare.
Il seguente esempio, piuttosto artificioso, illustra una chiamata di funzione. Si noti che all’inizio è necessario diramare attorno a tutte le chiamate di subroutine: le subroutine terminano l’esecuzione con bx(lr), mentre la funzione esterna semplicemente «cade oltre la fine» nello stile delle funzioni 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))
Il seguente esempio di codice dimostra una chiamata annidata (ricorsiva): la classica sequenza di Fibonacci. Qui, prima di una chiamata ricorsiva, il link register viene salvato insieme agli altri registri che la logica del programma richiede di preservare.
@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. Passaggio degli argomenti e valore di ritorno¶
Le funzioni assembler possono supportare da zero a tre argomenti, che (se usati) devono essere denominati r0, r1 e r2. Quando il codice viene eseguito, i registri saranno inizializzati a tali valori.
I tipi di dati che possono essere passati in questo modo sono interi e indirizzi di memoria. Con il firmware attuale possono essere passati e restituiti tutti i possibili valori a 32 bit. Se il valore di ritorno potrebbe avere il bit più significativo impostato, occorre utilizzare un suggerimento di tipo (type hint) Python per consentire a MicroPython di determinare se il valore debba essere interpretato come intero con segno o senza segno: i tipi sono int o uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) restituirà 0x80000000, dimostrando il passaggio e il ritorno di interi in cui i bit 30 e 31 differiscono.
Le limitazioni sul numero di argomenti e di valori di ritorno possono essere superate per mezzo del modulo array, che consente di accedere a un numero qualsiasi di valori di qualsiasi tipo.
1.2.1. Argomenti multipli¶
Se un array Python di interi viene passato come argomento a una funzione assembler, la funzione riceverà l’indirizzo di un insieme contiguo di interi. In questo modo è possibile passare argomenti multipli come elementi di un singolo array. Analogamente, una funzione può restituire valori multipli assegnandoli a elementi di un array. Le funzioni assembler non hanno alcun mezzo per determinare la lunghezza di un array: questa dovrà essere passata alla funzione.
Questo uso degli array può essere esteso per consentire l’utilizzo di più di tre array. Ciò avviene tramite l’indirezione: il modulo uctypes supporta addressof(), che restituisce l’indirizzo di un array passato come suo argomento. In questo modo è possibile popolare un array di interi con gli indirizzi di altri array:
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. Tipi di dati non interi¶
Questi possono essere gestiti per mezzo di array del tipo di dati appropriato. Ad esempio, dati in virgola mobile a precisione singola possono essere elaborati come segue. Questo esempio di codice prende un array di float e ne sostituisce il contenuto con i rispettivi quadrati.
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)
Il modulo uctypes supporta l’uso di strutture dati che vanno oltre i semplici array. Consente di mappare una struttura dati Python su un’istanza di bytearray, che può poi essere passata alla funzione assembler.
1.3. Costanti con nome¶
Il codice assembler può essere reso più leggibile e manutenibile utilizzando costanti con nome anziché disseminare il codice di numeri. Ciò può essere ottenuto così:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
Il costrutto const() fa sì che MicroPython sostituisca il nome della variabile con il suo valore in fase di compilazione. Se le costanti sono dichiarate in uno scope Python esterno, possono essere condivise tra più funzioni assembler e con il codice Python.
1.4. Codice assembler come metodi di classe¶
MicroPython passa l’indirizzo dell’istanza dell’oggetto come primo argomento ai metodi di classe. Questo è normalmente di scarsa utilità per una funzione assembler. Può essere evitato dichiarando la funzione come metodo statico, così:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Uso di istruzioni non supportate¶
Queste possono essere codificate usando l’istruzione data, come mostrato di seguito. Sebbene push() e pop() siano supportate, l’esempio seguente ne illustra il principio. Il codice macchina necessario può essere trovato nell’ARM v7-M Architecture Reference Manual. Si noti che il primo argomento delle chiamate data, come ad esempio
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
indica che ciascun argomento successivo è una quantità di due byte.
1.6. Superare la restrizione di MicroPython sugli interi¶
Gli interi piccoli di MicroPython sui port a 32 bit non possono contenere un valore i cui bit 30 e 31 differiscono (ad esempio 0x80000000), quindi una routine assembler che produce un risultato completo a 32 bit non può semplicemente restituirlo direttamente. Questa limitazione si supera con il seguente codice, che usa l’assembler per inserire il risultato in un array e codice Python per forzare il risultato in un intero senza segno a precisione 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