1. Tips och råd¶
Nedan följer några exempel på användning av den inbyggda assemblern samt information om hur man kringgår dess begränsningar. I detta dokument syftar termen ”assemblerfunktion” på en funktion som deklarerats i Python med dekoratorn @micropython.asm_thumb, medan ”subrutin” syftar på assemblerkod som anropas inifrån en assemblerfunktion.
1.1. Kodgrenar och subrutiner¶
Det är viktigt att förstå att etiketter är lokala för en assemblerfunktion. Det finns för närvarande inget sätt att anropa en subrutin som definierats i en funktion från en annan funktion.
För att anropa en subrutin används instruktionen bl(LABEL). Den överför kontrollen till instruktionen efter direktivet label(LABEL) och lagrar returadressen i länkregistret (lr eller r14). För att återvända används instruktionen bx(lr), vilket gör att exekveringen fortsätter med instruktionen efter subrutinanropet. Denna mekanism innebär att om en subrutin ska anropa en annan måste den spara länkregistret före anropet och återställa det innan den avslutas.
Följande något konstruerade exempel illustrerar ett funktionsanrop. Observera att det i början är nödvändigt att grena runt alla subrutinanrop: subrutiner avslutar exekveringen med bx(lr) medan den yttre funktionen helt enkelt ”faller av slutet” på samma sätt som Python-funktioner.
@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))
Följande kodexempel demonstrerar ett nästlat (rekursivt) anrop: den klassiska Fibonacci-sekvensen. Här sparas länkregistret tillsammans med andra register som programlogiken kräver ska bevaras, före ett rekursivt anrop.
@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. Argumentöverföring och retur¶
Assemblerfunktioner kan stödja från noll till tre argument, vilka (om de används) måste heta r0, r1 och r2. När koden exekveras initieras registren till dessa värden.
De datatyper som kan överföras på detta sätt är heltal och minnesadresser. Med aktuell fast programvara kan alla möjliga 32-bitarsvärden överföras och returneras. Om returvärdet kan ha den mest signifikanta biten satt bör en Python-typhint användas för att MicroPython ska kunna avgöra om värdet ska tolkas som ett signerat eller osignerat heltal: typerna är int eller uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) returnerar 0x80000000, vilket demonstrerar överföring och retur av heltal där bitarna 30 och 31 skiljer sig åt.
Begränsningarna på antalet argument och returvärden kan kringgås med hjälp av modulen array, som möjliggör åtkomst till valfritt antal värden av valfri typ.
1.2.1. Flera argument¶
Om en Python-array av heltal överförs som ett argument till en assemblerfunktion, tar funktionen emot adressen till en sammanhängande uppsättning heltal. Således kan flera argument överföras som element i en enda array. På liknande sätt kan en funktion returnera flera värden genom att tilldela dem till array-element. Assemblerfunktioner har inget sätt att avgöra längden på en array: detta behöver överföras till funktionen.
Denna användning av arrayer kan utökas för att möjliggöra användning av fler än tre arrayer. Detta görs med hjälp av indirektion: modulen uctypes stöder addressof() som returnerar adressen till en array som överförs som dess argument. Således kan du fylla en heltalsarray med adresserna till andra arrayer:
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. Datatyper som inte är heltal¶
Dessa kan hanteras med hjälp av arrayer av lämplig datatyp. Till exempel kan flyttalsdata med enkel precision behandlas på följande sätt. Detta kodexempel tar en array av flyttal och ersätter dess innehåll med deras kvadrater.
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)
Modulen uctypes stöder användning av datastrukturer utöver enkla arrayer. Den möjliggör att en Python-datastruktur mappas till en bytearray-instans som sedan kan överföras till assemblerfunktionen.
1.3. Namngivna konstanter¶
Assemblerkod kan göras mer läsbar och underhållbar genom att använda namngivna konstanter i stället för att fylla koden med siffror. Detta kan uppnås på följande sätt:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
Konstruktionen const() får MicroPython att ersätta variabelnamnet med dess värde vid kompileringstidpunkten. Om konstanter deklareras i ett yttre Python-omfång kan de delas mellan flera assemblerfunktioner och med Python-kod.
1.4. Assemblerkod som klassmetoder¶
MicroPython överför adressen till objektinstansen som det första argumentet till klassmetoder. Detta är normalt till liten nytta för en assemblerfunktion. Det kan undvikas genom att deklarera funktionen som en statisk metod på följande sätt:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Användning av instruktioner som inte stöds¶
Dessa kan kodas med data-satsen som visas nedan. Även om push() och pop() stöds illustrerar exemplet nedan principen. Den nödvändiga maskinkoden kan hittas i ARM v7-M Architecture Reference Manual. Observera att det första argumentet i data-anrop som
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
anger att varje efterföljande argument är en kvantitet på två byte.
1.6. Att kringgå MicroPythons heltalsbegränsning¶
Små heltal i MicroPython på 32-bitarsportar kan inte hålla ett värde vars bitar 30 och 31 skiljer sig åt (till exempel 0x80000000), så en assemblerrutin som producerar ett fullständigt 32-bitarsresultat kan inte helt enkelt returnera det direkt. Denna begränsning kringgås med följande kod, som använder assembler för att placera resultatet i en array och Python-kod för att omvandla resultatet till ett osignerat heltal med godtycklig precision.
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