1. Hinweise und Tipps¶
Im Folgenden finden Sie einige Beispiele für die Verwendung des Inline-Assemblers sowie Informationen dazu, wie sich seine Einschränkungen umgehen lassen. In diesem Dokument bezeichnet der Begriff „Assembler-Funktion“ eine in Python mit dem Dekorator @micropython.asm_thumb deklarierte Funktion, während „Subroutine“ sich auf Assembler-Code bezieht, der innerhalb einer Assembler-Funktion aufgerufen wird.
1.1. Code-Verzweigungen und Subroutinen¶
Es ist wichtig zu verstehen, dass Labels lokal zu einer Assembler-Funktion sind. Derzeit gibt es keine Möglichkeit, eine in einer Funktion definierte Subroutine aus einer anderen aufzurufen.
Um eine Subroutine aufzurufen, wird die Anweisung bl(LABEL) verwendet. Dies übergibt die Steuerung an die Anweisung, die auf die Direktive label(LABEL) folgt, und speichert die Rücksprungadresse im Link-Register (lr bzw. r14). Zur Rückkehr wird die Anweisung bx(lr) verwendet, wodurch die Ausführung mit der Anweisung fortgesetzt wird, die auf den Subroutinen-Aufruf folgt. Dieser Mechanismus bedeutet, dass eine Subroutine, die eine weitere aufruft, das Link-Register vor dem Aufruf sichern und vor dem Beenden wiederherstellen muss.
Das folgende, etwas konstruierte Beispiel veranschaulicht einen Funktionsaufruf. Beachten Sie, dass zu Beginn um alle Subroutinen-Aufrufe herum verzweigt werden muss: Subroutinen beenden die Ausführung mit bx(lr), während die äußere Funktion einfach im Stil von Python-Funktionen „am Ende ausläuft“.
@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))
Das folgende Code-Beispiel demonstriert einen verschachtelten (rekursiven) Aufruf: die klassische Fibonacci-Folge. Hier wird vor einem rekursiven Aufruf das Link-Register zusammen mit anderen Registern gesichert, die die Programmlogik erhalten muss.
@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übergabe und Rückgabe¶
Assembler-Funktionen können null bis drei Argumente unterstützen, die (falls verwendet) r0, r1 und r2 benannt sein müssen. Bei der Ausführung des Codes werden die Register mit diesen Werten initialisiert.
Die Datentypen, die auf diese Weise übergeben werden können, sind Ganzzahlen und Speicheradressen. Mit der aktuellen Firmware können alle möglichen 32-Bit-Werte übergeben und zurückgegeben werden. Wenn beim Rückgabewert das höchstwertige Bit gesetzt sein kann, sollte ein Python-Typ-Hint verwendet werden, damit MicroPython bestimmen kann, ob der Wert als vorzeichenbehaftete oder vorzeichenlose Ganzzahl interpretiert werden soll: die Typen sind int oder uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) gibt 0x80000000 zurück und demonstriert die Übergabe und Rückgabe von Ganzzahlen, bei denen sich die Bits 30 und 31 unterscheiden.
Die Einschränkungen bei der Anzahl von Argumenten und Rückgabewerten können mithilfe des Moduls array umgangen werden, das den Zugriff auf eine beliebige Anzahl von Werten beliebigen Typs ermöglicht.
1.2.1. Mehrere Argumente¶
Wenn ein Python-Array aus Ganzzahlen als Argument an eine Assembler-Funktion übergeben wird, erhält die Funktion die Adresse einer zusammenhängenden Menge von Ganzzahlen. So können mehrere Argumente als Elemente eines einzelnen Arrays übergeben werden. Ebenso kann eine Funktion mehrere Werte zurückgeben, indem sie sie Array-Elementen zuweist. Assembler-Funktionen haben keine Möglichkeit, die Länge eines Arrays zu bestimmen: diese muss an die Funktion übergeben werden.
Diese Verwendung von Arrays lässt sich erweitern, um mehr als drei Arrays zu nutzen. Dies geschieht über Indirektion: das Modul uctypes unterstützt addressof(), das die Adresse eines als Argument übergebenen Arrays zurückgibt. So können Sie ein Ganzzahl-Array mit den Adressen anderer Arrays füllen:
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. Nicht-ganzzahlige Datentypen¶
Diese können mithilfe von Arrays des entsprechenden Datentyps verarbeitet werden. Zum Beispiel können Gleitkommadaten mit einfacher Genauigkeit wie folgt verarbeitet werden. Dieses Code-Beispiel nimmt ein Array aus Floats und ersetzt dessen Inhalt durch deren Quadrate.
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)
Das Modul uctypes unterstützt die Verwendung von Datenstrukturen, die über einfache Arrays hinausgehen. Es ermöglicht, eine Python-Datenstruktur auf eine bytearray-Instanz abzubilden, die dann an die Assembler-Funktion übergeben werden kann.
1.3. Benannte Konstanten¶
Assembler-Code kann lesbarer und wartbarer gestaltet werden, indem benannte Konstanten verwendet werden, anstatt den Code mit Zahlen zu durchsetzen. Dies kann folgendermaßen erreicht werden:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
Das const()-Konstrukt veranlasst MicroPython, den Variablennamen zur Kompilierzeit durch seinen Wert zu ersetzen. Wenn Konstanten in einem äußeren Python-Geltungsbereich deklariert werden, können sie zwischen mehreren Assembler-Funktionen und mit Python-Code geteilt werden.
1.4. Assembler-Code als Klassenmethoden¶
MicroPython übergibt die Adresse der Objektinstanz als erstes Argument an Klassenmethoden. Dies ist für eine Assembler-Funktion normalerweise von geringem Nutzen. Es kann vermieden werden, indem die Funktion folgendermaßen als statische Methode deklariert wird:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Verwendung nicht unterstützter Anweisungen¶
Diese können mit der data-Anweisung wie unten gezeigt codiert werden. Obwohl push() und pop() unterstützt werden, veranschaulicht das folgende Beispiel das Prinzip. Den erforderlichen Maschinencode finden Sie im ARM v7-M Architecture Reference Manual. Beachten Sie, dass das erste Argument von data-Aufrufen wie
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
angibt, dass jedes nachfolgende Argument eine Zwei-Byte-Größe ist.
1.6. Umgehung der Ganzzahl-Einschränkung von MicroPython¶
Kleine Ganzzahlen von MicroPython können auf 32-Bit-Ports keinen Wert aufnehmen, dessen Bits 30 und 31 sich unterscheiden (zum Beispiel 0x80000000), sodass eine Assembler-Routine, die ein vollständiges 32-Bit-Ergebnis erzeugt, dieses nicht einfach direkt zurückgeben kann. Diese Einschränkung wird mit dem folgenden Code umgangen, der Assembler verwendet, um das Ergebnis in ein Array zu legen, und Python-Code, um das Ergebnis in eine vorzeichenlose Ganzzahl beliebiger Genauigkeit umzuwandeln.
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