1. Astuces et conseils¶
Voici quelques exemples d’utilisation de l’assembleur en ligne ainsi que des informations sur la manière de contourner ses limitations. Dans ce document, le terme « fonction assembleur » désigne une fonction déclarée en Python avec le décorateur @micropython.asm_thumb, tandis que « sous-routine » désigne du code assembleur appelé depuis une fonction assembleur.
1.1. Branchements de code et sous-routines¶
Il est important de comprendre que les étiquettes sont locales à une fonction assembleur. Il n’existe actuellement aucun moyen pour qu’une sous-routine définie dans une fonction soit appelée depuis une autre.
Pour appeler une sous-routine, on émet l’instruction bl(LABEL). Cela transfère le contrôle à l’instruction qui suit la directive label(LABEL) et stocke l’adresse de retour dans le registre de lien (lr ou r14). Pour revenir, on émet l’instruction bx(lr) qui fait reprendre l’exécution à l’instruction qui suit l’appel de la sous-routine. Ce mécanisme implique que, si une sous-routine doit en appeler une autre, elle doit sauvegarder le registre de lien avant l’appel et le restaurer avant de se terminer.
L’exemple suivant, quelque peu artificiel, illustre un appel de fonction. Notez qu’il est nécessaire au début d’effectuer un branchement contournant tous les appels de sous-routines : les sous-routines terminent leur exécution avec bx(lr) tandis que la fonction externe « tombe simplement en fin de code », à la manière des fonctions 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))
L’exemple de code suivant illustre un appel imbriqué (récursif) : la classique suite de Fibonacci. Ici, avant un appel récursif, le registre de lien est sauvegardé en même temps que d’autres registres que la logique du programme exige de préserver.
@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. Passage des arguments et retour¶
Les fonctions assembleur peuvent accepter de zéro à trois arguments qui, s’ils sont utilisés, doivent être nommés r0, r1 et r2. Lorsque le code s’exécute, les registres sont initialisés avec ces valeurs.
Les types de données pouvant être passés de cette manière sont les entiers et les adresses mémoire. Avec le micrologiciel actuel, toutes les valeurs 32 bits possibles peuvent être passées et retournées. Si la valeur de retour peut avoir son bit de poids fort positionné, il convient d’employer une indication de type Python pour permettre à MicroPython de déterminer si la valeur doit être interprétée comme un entier signé ou non signé : les types sont int ou uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) renverra 0x80000000, démontrant le passage et le retour d’entiers dont les bits 30 et 31 diffèrent.
Les limitations sur le nombre d’arguments et de valeurs de retour peuvent être contournées au moyen du module array, qui permet d’accéder à un nombre quelconque de valeurs de n’importe quel type.
1.2.1. Arguments multiples¶
Si un tableau Python d’entiers est passé comme argument à une fonction assembleur, la fonction recevra l’adresse d’un ensemble contigu d’entiers. Ainsi, plusieurs arguments peuvent être passés sous forme d’éléments d’un même tableau. De même, une fonction peut renvoyer plusieurs valeurs en les affectant à des éléments d’un tableau. Les fonctions assembleur n’ont aucun moyen de déterminer la longueur d’un tableau : celle-ci devra être passée à la fonction.
Cet usage des tableaux peut être étendu pour permettre d’utiliser plus de trois tableaux. Cela se fait au moyen de l’indirection : le module uctypes prend en charge addressof() qui renvoie l’adresse d’un tableau passé en argument. Vous pouvez ainsi remplir un tableau d’entiers avec les adresses d’autres tableaux :
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. Types de données non entiers¶
Ceux-ci peuvent être traités au moyen de tableaux du type de données approprié. Par exemple, des données en virgule flottante à simple précision peuvent être traitées comme suit. Cet exemple de code prend un tableau de flottants et remplace son contenu par leurs carrés.
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)
Le module uctypes prend en charge l’utilisation de structures de données allant au-delà des simples tableaux. Il permet de mapper une structure de données Python sur une instance de bytearray qui peut ensuite être passée à la fonction assembleur.
1.3. Constantes nommées¶
Le code assembleur peut être rendu plus lisible et plus facile à maintenir en utilisant des constantes nommées plutôt qu’en parsemant le code de nombres. Cela peut être réalisé ainsi :
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
La construction const() amène MicroPython à remplacer le nom de la variable par sa valeur au moment de la compilation. Si les constantes sont déclarées dans une portée Python externe, elles peuvent être partagées entre plusieurs fonctions assembleur et avec du code Python.
1.4. Le code assembleur comme méthodes de classe¶
MicroPython passe l’adresse de l’instance de l’objet comme premier argument aux méthodes de classe. Cela n’est normalement guère utile à une fonction assembleur. On peut l’éviter en déclarant la fonction comme méthode statique, ainsi :
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Utilisation d’instructions non prises en charge¶
Celles-ci peuvent être codées au moyen de l’instruction data, comme illustré ci-dessous. Bien que push() et pop() soient pris en charge, l’exemple ci-dessous en illustre le principe. Le code machine nécessaire peut être trouvé dans le manuel de référence de l’architecture ARM v7-M. Notez que le premier argument des appels à data tels que
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
indique que chaque argument suivant est une quantité de deux octets.
1.6. Contourner la restriction des entiers de MicroPython¶
Les petits entiers de MicroPython sur les portages 32 bits ne peuvent pas contenir une valeur dont les bits 30 et 31 diffèrent (par exemple 0x80000000), de sorte qu’une routine assembleur produisant un résultat complet sur 32 bits ne peut pas simplement le renvoyer directement. Cette limitation est contournée par le code suivant, qui utilise l’assembleur pour placer le résultat dans un tableau et du code Python pour convertir le résultat en un entier non signé de précision arbitraire.
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