1. Indicații și sfaturi

În continuare sunt prezentate câteva exemple de utilizare a asamblorului inline, precum și informații despre cum se pot ocoli limitările acestuia. În acest document termenul „funcție asamblor” se referă la o funcție declarată în Python cu decoratorul @micropython.asm_thumb, în timp ce „subrutină” se referă la cod asamblor apelat din interiorul unei funcții asamblor.

1.1. Ramificații de cod și subrutine

Este important de înțeles că etichetele sunt locale unei funcții asamblor. În prezent nu există nicio modalitate prin care o subrutină definită într-o funcție să fie apelată din alta.

Pentru a apela o subrutină se folosește instrucțiunea bl(LABEL). Aceasta transferă controlul către instrucțiunea care urmează după directiva label(LABEL) și stochează adresa de retur în registrul de legătură (lr sau r14). Pentru a reveni se folosește instrucțiunea bx(lr), care face ca execuția să continue cu instrucțiunea de după apelul subrutinei. Acest mecanism implică faptul că, dacă o subrutină urmează să apeleze alta, ea trebuie să salveze registrul de legătură înainte de apel și să îl restaureze înainte de a se încheia.

Următorul exemplu, oarecum artificial, ilustrează un apel de funcție. Rețineți că la început este necesară o ramificare în jurul tuturor apelurilor de subrutine: subrutinele își încheie execuția cu bx(lr), în timp ce funcția exterioară pur și simplu „se termină la capăt”, în stilul funcțiilor 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))

Următorul exemplu de cod demonstrează un apel imbricat (recursiv): clasica secvență Fibonacci. Aici, înainte de un apel recursiv, registrul de legătură este salvat împreună cu alte registre pe care logica programului impune să fie păstrate.

@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. Transmiterea argumentelor și returnarea

Funcțiile asamblor pot accepta de la zero până la trei argumente, care trebuie (dacă sunt folosite) să fie denumite r0, r1 și r2. Când codul se execută, registrele vor fi inițializate cu acele valori.

Tipurile de date care pot fi transmise în acest mod sunt numerele întregi și adresele de memorie. Cu firmware-ul actual, pot fi transmise și returnate toate valorile pe 32 de biți posibile. Dacă valoarea returnată poate avea cel mai semnificativ bit setat, ar trebui folosit un indiciu de tip Python pentru a permite MicroPython să determine dacă valoarea trebuie interpretată ca întreg cu semn sau fără semn: tipurile sunt int sau uint.

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

hex(uadd(0x40000000,0x40000000)) va returna 0x80000000, demonstrând transmiterea și returnarea de numere întregi unde biții 30 și 31 diferă.

Limitările privind numărul de argumente și de valori returnate pot fi depășite prin intermediul modulului array, care permite accesarea oricărui număr de valori de orice tip.

1.2.1. Argumente multiple

Dacă un array Python de numere întregi este transmis ca argument unei funcții asamblor, funcția va primi adresa unui set contiguu de numere întregi. Astfel, mai multe argumente pot fi transmise ca elemente ale unui singur array. În mod similar, o funcție poate returna mai multe valori atribuindu-le unor elemente de array. Funcțiile asamblor nu au niciun mijloc de a determina lungimea unui array: aceasta va trebui transmisă funcției.

Această utilizare a array-urilor poate fi extinsă pentru a permite folosirea a mai mult de trei array-uri. Acest lucru se realizează prin indirectare: modulul uctypes oferă addressof(), care va returna adresa unui array transmis ca argument. Astfel puteți popula un array de numere întregi cu adresele altor array-uri:

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. Tipuri de date neîntregi

Acestea pot fi gestionate prin intermediul array-urilor de tipul de date corespunzător. De exemplu, datele în virgulă mobilă de precizie simplă pot fi procesate după cum urmează. Acest exemplu de cod preia un array de numere în virgulă mobilă și îi înlocuiește conținutul cu pătratele acestora.

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)

Modulul uctypes acceptă utilizarea de structuri de date dincolo de simplele array-uri. El permite maparea unei structuri de date Python pe o instanță bytearray care poate fi apoi transmisă funcției asamblor.

1.3. Constante denumite

Codul asamblor poate fi făcut mai lizibil și mai ușor de întreținut folosind constante denumite în loc de a umple codul cu numere. Acest lucru se poate realiza astfel:

MYDATA = const(33)

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

Construcția const() face ca MicroPython să înlocuiască numele variabilei cu valoarea sa la momentul compilării. Dacă constantele sunt declarate într-un domeniu Python exterior, ele pot fi partajate între mai multe funcții asamblor și cu codul Python.

1.4. Cod asamblor ca metode de clasă

MicroPython transmite adresa instanței obiectului ca prim argument către metodele de clasă. Acest lucru este de obicei de puțin folos pentru o funcție asamblor. Poate fi evitat declarând funcția ca metodă statică, astfel:

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

1.5. Utilizarea instrucțiunilor neacceptate

Acestea pot fi codate folosind instrucțiunea data, după cum se arată mai jos. Deși push() și pop() sunt acceptate, exemplul de mai jos ilustrează principiul. Codul mașină necesar poate fi găsit în ARM v7-M Architecture Reference Manual. Rețineți că primul argument al apelurilor data, precum

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

indică faptul că fiecare argument următor este o cantitate pe doi octeți.

1.6. Depășirea restricției MicroPython privind numerele întregi

Numerele întregi mici din MicroPython pe porturile pe 32 de biți nu pot reține o valoare ai cărei biți 30 și 31 diferă (de exemplu 0x80000000), așa că o rutină asamblor care produce un rezultat complet pe 32 de biți nu îl poate returna pur și simplu direct. Această limitare este depășită cu următorul cod, care folosește asamblor pentru a pune rezultatul într-un array și cod Python pentru a converti rezultatul într-un întreg fără semn de precizie arbitrară.

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