1. Поради та підказки¶
Нижче наведено кілька прикладів використання вбудованого асемблера та інформацію про способи обходу його обмежень. У цьому документі термін «функція асемблера» стосується функції, оголошеної в Python з декоратором @micropython.asm_thumb, тоді як «підпрограма» — це код асемблера, що викликається зсередини функції асемблера.
1.1. Гілки коду та підпрограми¶
Важливо розуміти, що мітки є локальними для функції асемблера. На даний момент немає можливості викликати підпрограму, оголошену в одній функції, з іншої.
Для виклику підпрограми використовується інструкція bl(LABEL). Вона передає керування на інструкцію після директиви label(LABEL) і зберігає адресу повернення в регістрі зв’язку (lr або r14). Для повернення виконується інструкція bx(lr), яка продовжує виконання з інструкції після виклику підпрограми. Цей механізм означає, що якщо підпрограма повинна викликати іншу, вона має зберегти регістр зв’язку перед викликом і відновити його перед завершенням.
Наведений нижче штучний приклад ілюструє виклик функції. Зверніть увагу, що на початку потрібно виконати перехід через усі виклики підпрограм: підпрограми завершують виконання за допомогою bx(lr), тоді як зовнішня функція просто «досягає кінця» у стилі 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))
Наступний приклад коду демонструє вкладений (рекурсивний) виклик: класична послідовність Фібоначчі. Тут перед рекурсивним викликом регістр зв’язку зберігається разом з іншими регістрами, збереження яких вимагає логіка програми.
@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. Передача аргументів і повернення значень¶
Функції асемблера можуть підтримувати від нуля до трьох аргументів, які (якщо використовуються) повинні мати імена r0, r1 і r2. Під час виконання коду регістри будуть ініціалізовані цими значеннями.
Типи даних, що можуть передаватися таким чином, — це цілі числа та адреси пам’яті. З поточною мікропрограмою можуть передаватися та повертатися всі можливі 32-бітові значення. Якщо значення, що повертається, може мати встановлений найстарший біт, слід використовувати підказку типу Python, щоб MicroPython міг визначити, чи слід інтерпретувати значення як знакове або беззнакове ціле: типи int або uint.
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
hex(uadd(0x40000000,0x40000000)) поверне 0x80000000, демонструючи передачу та повернення цілих чисел, у яких біти 30 та 31 відрізняються.
Обмеження на кількість аргументів і значень, що повертаються, можна подолати за допомогою модуля array, який дозволяє отримувати доступ до будь-якої кількості значень будь-якого типу.
1.2.1. Кілька аргументів¶
Якщо масив цілих чисел Python передається як аргумент функції асемблера, функція отримає адресу суцільного набору цілих чисел. Таким чином, кілька аргументів можуть передаватися як елементи одного масиву. Аналогічно функція може повертати кілька значень, присвоюючи їх елементам масиву. Функції асемблера не мають засобів визначення довжини масиву: це потрібно передавати функції окремо.
Використання масивів можна розширити для роботи більш ніж з трьома масивами. Це робиться за допомогою непрямої адресації: модуль uctypes підтримує addressof(), який повертає адресу масиву, переданого як аргумент. Таким чином можна заповнити масив цілих чисел адресами інших масивів:
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. Нецілочисельні типи даних¶
Їх можна обробляти за допомогою масивів відповідного типу даних. Наприклад, дані з плаваючою комою одинарної точності можуть оброблятися наступним чином. Цей приклад коду бере масив чисел з плаваючою комою та замінює їх вміст квадратами.
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)
Модуль uctypes підтримує використання структур даних, що виходять за межі простих масивів. Він дозволяє відобразити структуру даних Python на екземпляр bytearray, який потім може бути переданий до функції асемблера.
1.3. Іменовані константи¶
Код асемблера можна зробити більш читабельним і зручним для обслуговування, використовуючи іменовані константи замість числових літералів. Це досягається наступним чином:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
Конструкція const() змушує MicroPython замінити ім’я змінної її значенням під час компіляції. Якщо константи оголошені у зовнішній області видимості Python, вони можуть бути спільними для кількох функцій асемблера та коду Python.
1.4. Код асемблера як методи класу¶
MicroPython передає адресу екземпляра об’єкта як перший аргумент до методів класу. Зазвичай це малокорисно для функції асемблера. Цього можна уникнути, оголосивши функцію як статичний метод наступним чином:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. Використання непідтримуваних інструкцій¶
Їх можна закодувати за допомогою оператора data, як показано нижче. Хоча push() і pop() підтримуються, наведений нижче приклад ілюструє принцип. Необхідний машинний код можна знайти в ARM v7-M Architecture Reference Manual. Зверніть увагу, що перший аргумент викликів data, таких як
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
вказує, що кожен наступний аргумент є двобайтовою величиною.
1.6. Подолання обмеження цілих чисел MicroPython¶
Малі цілі числа MicroPython на 32-бітових портах не можуть зберігати значення, у яких відрізняються біти 30 і 31 (наприклад, 0x80000000), тому підпрограма асемблера, що виробляє повний 32-бітовий результат, не може просто повернути його безпосередньо. Це обмеження долається за допомогою наступного коду, який використовує асемблер для збереження результату в масив та код Python для перетворення результату в беззнакове ціле число довільної точності.
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