MicroPython:Основы/Язык MicroPython и его реализация/Ассемблерная вставка для архитектур Thumb2/Советы по использованию ассемблерных вставок

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Советы по использованию ассемблерных вставок[1]

В этой статье мы рассмотрим несколько примеров использования ассемблерных вставок и расскажем о том, как обойти некоторые ограничения, связанные с их использованием. Под термином «ассемблерная функция» будет подразумеваться функция, объявленная в Python при помощи декоратора @micropython.asm_thumb, а под термином «подпрограмма» – ассемблерный код, вызванный из ассемблерной функции.

Условные переходы и подпрограммы

Важно понимать, что метки – это локальные элементы ассемблерной функции. В данный момент в MicroPython нет возможности задать подпрограмму в одной функции, чтобы потом вызвать ее в другой.

Вызов подпрограммы осуществляется с помощью команды 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))

Передача и возврат аргументов

В ассемблерной функции может быть не более трех аргументов (или не быть вообще), и они должны быть (если используются) названы 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. Он позволяет получить доступ к любому количеству значений любого типа.

Использование нескольких аргументов

Если в ассемблерной функции в качестве аргумента задан Python’овский массив целых чисел, эта функция также получит адрес группы смежных целых чисел. Следовательно, несколько аргументов можно передать в качестве элементов одного массива. Аналогично функция может вернуть несколько значений, присвоив их к массиву элементов. У ассемблерных функций нет инструментария для определения размера массива, поэтому вам придется передать функции и эту информацию тоже.

Если необходимо, вы можете воспользоваться более чем тремя массивами. Вам в этом поможет косвенная адресация: в модуле uctypes есть функция addressof(), которая возвращает адрес массива, переданного ей в качестве аргумента.

Благодаря ей вы можете сохранить в массив целых чисел адреса других массивов.

from uctypes import addressof
@micropython.asm_thumb
def getindirect(r0):
    ldr(r0, [r0, 0]) # адрес массива,
                     # загруженный из переданного массива
    ldr(r0, [r0, 4]) # возвращаем 1-ый элемент массива (24)

def testindirect():
    a = array.array('i',[23, 24])
    b = array.array('i',[0,0])
    b[0] = addressof(a)
    print(getindirect(b))

Нецелочисленные типы данных

Нецелочисленные типы данных можно обрабатывать при помощи массивов соответствующего типа. Пример обработки чисел с плавающей точкой одинарной точности показан ниже. В нем содержимое массива чисел с плавающей точкой заменяется на квадраты этих чисел.

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 поддерживается использование не только обычных массивов, но и других структур данных. Это позволяет реализовать в экземпляре bytearray структуру данных из Python, а затем передать ее ассемблерной функции.

Именованные константы

Ассемблерный код не обязан быть просто нагромождением чисел – его можно сделать более читаемым и удобным в сопровождении при помощи именованных констант. Это делается, к примеру, следующим образом:

MYDATA = const(33)

@micropython.asm_thumb
def foo():
    mov(r0, MYDATA)

Здесь конструкция const() в фазе компиляции заставляет MicroPython заменить название переменной на ее числовое значение. Если константа объявлена во внешней области видимости Python, ее можно одновременно использовать к Python-коде и разных ассемблерных функциях.

Ассемблерный код как метод класса

MicroPython передает адрес экземпляра объекта в первом аргументе метода класса. В ассемблерной функции он обычно не используется. Обойти это можно, объявив эту функцию как статический метод – вот так:

class foo:
  @staticmethod
  @micropython.asm_thumb
  def bar(r0):
    add(r0, r0, r0)

Использование неподдерживаемых команд

Их можно создать при помощи ассемблерной директивы data() как в примере ниже – в нем создается команда, работающая по принципу команды push()MicroPython поддерживается команда push(), а цель примера ниже – чисто иллюстративная).

Необходимый машинный код можно найти в справочнике по архитектуре ARM v7-M. Помните, что первый аргумент в команде data() отвечает за размер всех последующих аргументов:

data(2, 0xe92d, 0x0f00) # сохраняем r8,r9,r10,r11 в стек

В примере выше это «2», а это значит, что размер всех последующих аргументов будет 2 байта.

Обход ограничения MicroPython, связанного с целыми числами

Чип Pyboard оснащен CRC-генератором, но его использование проблемно для MicroPython. В MicroPython поддерживается возврат всего диапазона 32-битных чисел, но у малых целых чисел в битах 30 и 31 не может быть разных значений. Обойти это ограничение можно при помощи кода ниже – в нем используется ассемблер, чтобы поместить результат в массив, и Python-код, чтобы сделать принудительную трансформацию в беззнаковые числа произвольной точности.

from array import array
import stm

def enable_crc():
    stm.mem32[stm.RCC + stm.RCC_AHB1ENR] |= 0x1000

def reset_crc():
    stm.mem32[stm.CRC+stm.CRC_CR] = 1

@micropython.asm_thumb
def getval(r0, r1):
    movwt(r3, stm.CRC + stm.CRC_DR)
    str(r1, [r3, 0])
    ldr(r2, [r3, 0])
    str(r2, [r0, 0])

def getcrc(value):
    a = array('i', [0])
    getval(a, value)
    return a[0] & 0xffffffff # принудительная трансформация
                             # в произвольную точность

enable_crc()
reset_crc()
for x in range(20):
    print(hex(getcrc(0)))

См.также

Внешние ссылки