1. Aanwijzingen en tips¶
Hieronder volgen enkele voorbeelden van het gebruik van de inline assembler en wat informatie over hoe je de beperkingen ervan kunt omzeilen. In dit document verwijst de term “assembler-functie” naar een functie die in Python is gedeclareerd met de @micropython.asm_thumb decorator, terwijl “subroutine” verwijst naar assembler-code die wordt aangeroepen vanuit een assembler-functie.
1.1. Codevertakkingen en subroutines¶
Het is belangrijk te beseffen dat labels lokaal zijn binnen een assembler-functie. Er is momenteel geen manier om een subroutine die in de ene functie is gedefinieerd vanuit een andere functie aan te roepen.
Om een subroutine aan te roepen wordt de instructie bl(LABEL) uitgevoerd. Hiermee wordt de controle overgedragen aan de instructie die volgt op de label(LABEL) directive en wordt het retouradres opgeslagen in het link-register (lr of r14). Om terug te keren wordt de instructie bx(lr) uitgevoerd, waardoor de uitvoering wordt voortgezet met de instructie die volgt op de subroutine-aanroep. Dit mechanisme houdt in dat, als een subroutine een andere moet aanroepen, deze het link-register vóór de aanroep moet opslaan en vóór het beëindigen moet herstellen.
Het volgende, enigszins gekunstelde, voorbeeld illustreert een functie-aanroep. Merk op dat het aan het begin nodig is om om alle subroutine-aanroepen heen te vertakken: subroutines beëindigen de uitvoering met bx(lr) terwijl de buitenste functie eenvoudigweg “van het einde afvalt” in de stijl van Python-functies.
@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))
Het volgende codevoorbeeld demonstreert een geneste (recursieve) aanroep: de klassieke Fibonacci-reeks. Hier wordt, voorafgaand aan een recursieve aanroep, het link-register opgeslagen samen met andere registers die volgens de programmalogica behouden moeten blijven.
@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. Argumenten doorgeven en retourneren¶
Assembler-functies kunnen nul tot drie argumenten ondersteunen, die (indien gebruikt) r0, r1 en r2 moeten heten. Wanneer de code wordt uitgevoerd worden de registers met die waarden geïnitialiseerd.
De datatypes die op deze manier kunnen worden doorgegeven zijn integers en geheugenadressen. Met de huidige firmware kunnen alle mogelijke 32-bits waarden worden doorgegeven en geretourneerd. Als de retourwaarde mogelijk de meest significante bit gezet heeft, moet er een Python-typehint worden gebruikt zodat MicroPython kan bepalen of de waarde moet worden geïnterpreteerd als een signed of unsigned integer: de types zijn int of uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) retourneert 0x80000000, wat het doorgeven en retourneren van integers demonstreert waarbij de bits 30 en 31 verschillen.
De beperkingen op het aantal argumenten en retourwaarden kunnen worden omzeild met behulp van de array module, waarmee een willekeurig aantal waarden van elk type toegankelijk wordt.
1.2.1. Meerdere argumenten¶
Als een Python-array van integers als argument aan een assembler-functie wordt doorgegeven, ontvangt de functie het adres van een aaneengesloten reeks integers. Zo kunnen meerdere argumenten worden doorgegeven als elementen van één enkele array. Op dezelfde manier kan een functie meerdere waarden retourneren door ze aan array-elementen toe te wijzen. Assembler-functies hebben geen manier om de lengte van een array te bepalen: deze zal aan de functie moeten worden doorgegeven.
Dit gebruik van arrays kan worden uitgebreid om meer dan drie arrays te kunnen gebruiken. Dit gebeurt met behulp van indirectie: de uctypes module ondersteunt addressof(), die het adres retourneert van een array die als argument wordt doorgegeven. Zo kun je een integer-array vullen met de adressen van andere 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. Niet-integer datatypes¶
Deze kunnen worden afgehandeld met behulp van arrays van het juiste datatype. Bijvoorbeeld, single-precision floating-point data kan als volgt worden verwerkt. Dit codevoorbeeld neemt een array van floats en vervangt de inhoud ervan door de kwadraten daarvan.
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)
De uctypes-module ondersteunt het gebruik van datastructuren die verder gaan dan eenvoudige arrays. Het maakt het mogelijk om een Python-datastructuur af te beelden op een bytearray-instantie, die vervolgens aan de assembler-functie kan worden doorgegeven.
1.3. Benoemde constanten¶
Assembler-code kan leesbaarder en beter onderhoudbaar worden gemaakt door benoemde constanten te gebruiken in plaats van de code te bezaaien met getallen. Dit kan als volgt worden bereikt:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
De const()-constructie zorgt ervoor dat MicroPython de variabelenaam tijdens het compileren vervangt door de waarde ervan. Als constanten in een buitenste Python-scope worden gedeclareerd, kunnen ze worden gedeeld tussen meerdere assembler-functies en met Python-code.
1.4. Assembler-code als klassemethoden¶
MicroPython geeft het adres van de object-instantie door als eerste argument aan klassemethoden. Dit is doorgaans van weinig nut voor een assembler-functie. Het kan worden vermeden door de functie als statische methode te declareren, en wel als volgt:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Gebruik van niet-ondersteunde instructies¶
Deze kunnen worden gecodeerd met behulp van de data-instructie zoals hieronder weergegeven. Hoewel push() en pop() worden ondersteund, illustreert het onderstaande voorbeeld het principe. De benodigde machinecode is te vinden in de ARM v7-M Architecture Reference Manual. Merk op dat het eerste argument van data-aanroepen zoals
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
aangeeft dat elk volgend argument een twee-byte-grootheid is.
1.6. De integer-beperking van MicroPython omzeilen¶
Kleine integers in MicroPython op 32-bits ports kunnen geen waarde bevatten waarvan de bits 30 en 31 verschillen (bijvoorbeeld 0x80000000), dus een assembler-routine die een volledig 32-bits resultaat produceert kan dit niet zomaar rechtstreeks retourneren. Deze beperking wordt omzeild met de volgende code, die assembler gebruikt om het resultaat in een array te plaatsen en Python-code om het resultaat te dwingen tot een unsigned integer met willekeurige precisie.
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