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 (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