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

Материал из Онлайн справочника
Версия от 18:17, 14 мая 2023; EducationBot (обсуждение | вклад)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Перейти к навигацииПерейти к поиску

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


Ассемблерная вставка для архитектур Thumb2[1]

Это руководство рассчитано на тех, кто уже более-менее знаком с ассемблерным программированием и ознакомился с этим руководством. Подробное описание команд читайте в справочнике по архитектуре ARM v7-M (см. ниже). В ассемблерной вставке MicroPython поддерживается использование некоторых команд из набора ARM Thumb-2 – именно о них и пойдет речь ниже. Эти команды переделаны в Python-функции, но их синтаксис максимально приближен к синтаксису из вышеупомянутого справочника по ARM v7-M.

Команды оперируют 32-битными знаковыми целочисленными данными, за исключением случаев, когда указано обратное. Большинство поддерживаемых команд работают только на регистрах R0-R7, но есть и команды, работающие на R8-R15 (в этом случае об этом будет сказано в ее описании). Перед тем, как функция вернет результат, регистрам R8-R12 должно быть возвращено их первоначальное значение. Регистры R13-R15 – это, соответственно, регистр связи, указатель стека и счетчик программ.

Условные обозначения и правила оформления кода

Где это возможно, поведение каждой команды описано на Python. Например:

add(Rd, Rn, Rm): Rd = Rn + Rm

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

Типы команд

В разделах ниже содержится описание команд ARM Thumb-2, поддерживаемых в MicroPython:

  1. Команды перемещения данных
  2. Команды чтения данных из регистра
  3. Команды записи данных в регистр
  4. Логические команды и команды сдвига
  5. Арифметические команды
  6. Команды сравнения
  7. Команды условных переходов
  8. Команды записи и чтения из стека
  9. Прочие команды
  10. Команды для чисел с плавающей точкой
  11. Ассемблерные директивы

Примеры использования

В этом разделе можно найти примеры и советы по использованию ассемблерного кода.

Справочные материалы

Команды перемещения данных

Условные обозначения

Под Rd и Rn подразумеваются ARM-регистры R0-R15. Под immN подразумевается непосредственное значение с разрешением в N бит. Эти инструкции влияют на флаги состояния.

Перемещение данных в регистр

Если используется непосредственное значение, оно будет дополнено нулями, чтобы «дотянуть» его до 32 бит. Таким образом, mov(R0, 0xff) запишет в R0 значение «255».

  • mov(Rd, imm8): Rd = imm8
  • mov(Rd, Rn): Rd = Rn
  • movw(Rd, imm16): Rd = imm16
  • movt(Rd, imm16): Rd = (Rd & 0xffff) | (imm16 << 16)
    • Функция movt() записывает непосредственное значение в верхнее полуслово целевого регистра. Содержимое нижнего полуслова она не затрагивает.
  • movwt(Rd, imm32): Rd = imm32
    • Функция movwt() – это псевдо-команда: чтобы поместить 32-битное значение в Rd, ассемблер MicroPython генерирует movwt(), а затем вслед за movt().

Чтение данных из регистра

Условные обозначения

Под Rt и Rn подразумеваются ARM-регистры R0-R7 (если не указано что-то другое). Под immN подразумевается непосредственное значение с разрешением в N бит – следовательно, imm5 будет ограничено числами в диапазоне 0-31. Запись [Rn + immN] – это данные, находящиеся по адресу, который был получен сложением Rn и смещения immN. Смещение измеряется в байтах. Эти инструкции влияют на флаги состояния.

Чтение данных

  • ldr(Rt, [Rn, imm7]): Rt = [Rn + imm7] – чтение 32-битного слова
  • ldrb(Rt, [Rn, imm5]): Rt = [Rn + imm5] – чтение байта
  • ldrh(Rt, [Rn, imm6]): Rt = [Rn + imm6] – чтение 16-битного полуслова

При чтении байта или полуслова значение дополняется нулями, чтобы «дотянуть» его до 32 бит.

Указанные выше смещения измеряются в байтах. Таким образом, в случае ldr() и 7-битного смещения вы можете считывать 32-битные значения, выровненные по одному слову, с максимальным смещением в 31 слово. А в случае ldrh() и 6-битного смещения вы можете считывать 16-битные значения, выровненные по полуслову, с максимальным смещением в 31 полуслово.

Запись данных в регистр

Условные обозначения

Под Rt и Rn подразумеваются ARM-регистры R0-R7 (если не указано что-то другое). Под immN подразумевается непосредственное значение с разрешением в N бит – следовательно, imm5 будет ограничено числами в диапазоне 0-31. Под [Rn + imm5] подразумеваются данные, находящиеся по адресу, который был получен сложением Rn и смещения imm5. Смещения измеряются в байтах. Эти инструкции не влияют на флаги состояния.

Запись данных

  • str(Rt, [Rn, imm7]): [Rn + imm7] = Rt – запись 32-битного слова
  • strb(Rt, [Rn, imm5]): [Rn + imm5] = Rt – запись байта (b0-b7)
  • strh(Rt, [Rn, imm6]): [Rn + imm6] = Rt – запись 16-битного полуслова (b0-b15)

Указанные выше смещения измеряются в байтах. Таким образом, в случае str() и 7-битного смещения вы можете записывать 32-битные значения, выровненные по целому слову, с максимальным смещением в 31 слово. А в случае strh() и 6-битного смещения вы можете записывать 16-битные значения, выровненные по полуслову, с максимальным смещением в 31 полуслово.

Логические команды и команды сдвига

Условные обозначения

Под Rd и Rn подразумеваются ARM-регистры R0-R7, за исключением специальных команд, где могут быть использованы регистры R0-R15. Под Rn<a-b> подразумевается ARM-регистр, чье содержимое должно находиться в диапазоне a <= число <= b. В командах с двумя регистровыми аргументами их разрешается делать одинаковыми. Например, результатом функции ниже (в Python: R0 ^= R0), выполняющей операцию «исключающее или», всегда будет «0» независимо от того, какое число хранилось в этом регистре до этой операции:

eor(r0, r0)

Эти команды влияют на флаги состояния (если не указано обратное).

Логические команды

  • and_(Rd, Rn): Rd &= Rn
  • orr(Rd, Rn): Rd |= Rn
  • eor(Rd, Rn): Rd ^= Rn
  • mvn(Rd, Rn): Rd = Rn ^ 0xffffffff – то есть Rd является обратным двоичным кодом от Rn
  • bic(Rd, Rn): Rd &= ~Rn – сброс битов, т.е. побитовое «И» между Rd и инвертированным (при помощи побитового «НЕ») содержимым Rn

Обратите внимание, что здесь вместо and используется _and, потому что and уже зарезервировано для ключевого слова в Python.

Команды сдвига

  • lsl(Rd, Rn<0-31>): Rd <<= Rn
  • lsr(Rd, Rn<1-32>): Rd = (Rd & 0xffffffff) >> Rn – логический сдвиг вправо
  • asr(Rd, Rn<1-32>): Rd >>= Rn – арифметический сдвиг вправо
  • ror(Rd, Rn<1-31>): Rd = rotate_right(Rd, Rn) – циклический сдвиг Rd вправо на количество бит в Rn

Циклический сдвиг, к примеру, на 3 бита работает следующим образом: если в Rd изначально хранятся биты b31 b30..b0, то после циклического сдвига в нем будет содержаться b2 b1 b0 b31 b30..b3.

Специальные команды

Эти инструкции не влияют на флаги состояния.

  • clz(Rd, Rn): Rd = count_leading_zeros(Rn)
    • Функция count_leading_zeros(Rn) возвращает количество нулей, стоящих до первой единицы в Rn.
  • rbit(Rd, Rn): Rd = bit_reverse(Rn)
    • Функция bit_reverse(Rn) возвращает инвертированное содержимое Rn. То есть, если в Rn содержится b31 b30..b0, в Rd в конечном счете окажется b0 b1 b2..b3.

Количество нулей в конце регистра можно определить, если сначала выполнить rbit(), чтобы реверсировать порядок битов, а уже потом выполнить clz().

Арифметические команды

Условные обозначения

Под Rd, Rm и Rn подразумеваются ARM-регистры R0-R7. Под immN подразумевается непосредственное значение с разрешением в N бит – например, imm8, imm3 и т.д. Под перенос подразумевается флаг переноса, а под не(перенос) – заимствование.

Если у команды больше одного регистрового аргумента, их разрешается делать одинаковыми. Например, команда ниже добавит к R0 его же содержимое и сохранит результат опять же в R0:

add(r0, r0, r0)

Арифметические команды влияют на флаги состояния (если не указано другое).

Сложение

  • add(Rdn, imm8): Rdn = Rdn + imm8
  • add(Rd, Rn, imm3): Rd = Rn + imm3
  • add(Rd, Rn, Rm): Rd = Rn +Rm
  • adc(Rd, Rn): Rd = Rd + Rn + перенос

Вычитание

  • sub(Rdn, imm8): Rdn = Rdn - imm8
  • sub(Rd, Rn, imm3): Rd = Rn - imm3
  • sub(Rd, Rn, Rm): Rd = Rn - Rm
  • sbc(Rd, Rn): Rd = Rd - Rn - не(перенос)

Отрицание

  • neg(Rd, Rn): Rd = -Rn

Умножение и деление

  • mul(Rd, Rn): Rd = Rd * Rn

Генерирует 32-битный результат с потерей переполнения. Результатом может быть и знаковое, и беззнаковое значение – в зависимости от того, какие значения были в операндах.

  • sdiv(Rd, Rn, Rm): Rd = Rn / Rm
  • udiv(Rd, Rn, Rm): Rd = Rn / Rm

Эти функции выполняют, соответственно, знаковое и беззнаковое деление. На флаги состояния эти команды не влияют.

Команды сравнения

Эти команды выполняют арифметические и логические операции с двумя аргументами – они не выдают никакого результата, но влияют на флаги состояния. Обычно они используются для тестирования данных без их изменения до выполнения условного перехода.

Условные обозначения

Под Rd, Rm и Rn подразумеваются ARM-регистры R0-R7. Под imm8 подразумевается непосредственное значение с разрешением в 8 бит.

Регистр состояния приложения (APSR)

В этом регистре содержится 4 бита, которые используются командами условного перехода. Как правило, команда условного перехода – например, bge(МЕТКА) – проверяет несколько битов. То, за что отвечают флаги состояния, может зависеть от того, как арифметические команды видят операнды – как беззнаковые целые числа или целые числа со знаком. К примеру, команда bhi(МЕТКА) считает, что обрабатывает беззнаковые числа, а bgt(МЕТКА) – числа со знаком.

Биты APSR-регистра

  • Z – ноль
    • В этом флаге будет задана единица, если результатом операции является «0» или сравниваемые операнды равны.
  • N – отрицательное значение
    • В этом флаге будет задана единица, если результат получился отрицательным.
  • C – перенос
    • Операция сложения задает в этом флаге единицу, если результат получится больше, чем может уместиться в 32-битном значении – например, при сложении «0x80000000» и «0x80000000». При вычитании этот механизм инвертируется по принципу вычислений с дополнительным кодом, и в результате при заимствовании в этом флаге ставится ноль. Соответственно, операция «0x10 - 0x01» выполняется как «0x10 + 0xffffffff», а в флаге переноса ставится единица.
  • V – переполнение
    • В этом флаге будет задана единица, если у результата (который будет считаться числом дополнительного кода) получился «неправильный» знак относительно операндов. К примеру, сложение «1» и «0x7fffffff» задаст в флаге V единицу, потому что результат («0x8000000»), рассматриваемый как целое число дополнительного кода, получится отрицательным. Обратите внимание, что флаг переноса С (см. выше) в этом случае задан не будет.

Команды сравнения

Эти команды задают значения APSR-регистра, то есть флаги N (отрицательное значение), Z (ноль), C (перенос) и V (переполнение).

  • cmp(Rn, imm8): Rn - imm8
  • cmp(Rn, Rm): Rn - Rm
  • cmn(Rn, Rm): Rn + Rm
  • tst(Rn, Rm): Rn & Rm

Условные операторы

Команды it и ite позволяют использовать условные операторы для 1-4 следующих дальше в коде команд без необходимости использовать метку.

  • it(<условие>) If then
    • Эта команда запустит выполнение команды ниже, если <условие> в аргументе верно. Например:
cmp(r0, r1)
it(eq)
mov(r0, 100) # runs if r0 == r1
# здесь будет совершаться выполнение
  • ite(<условие>) If then else

Эта команда запустит выполнение первой команды, если <условие> в аргументе верно, а если неверно, будет выполнена вторая команда. Например:

cmp(r0, r1)
ite(eq)
mov(r0, 100) # runs if r0 == r1
mov(r0, 200) # runs if r0 != r1
# здесь будет совершаться выполнение

Функционал этих команд можно расширить вплоть до оперирования четырьмя командами. Это расширение делается по принципу it[x[y[z]]], где x, y и z – это t (then) или e (else). В результате могут получиться команды itt, itee, itete, ittte, itttt, iteee и т.д.

Команды условных переходов

Эти команды выполняют переход в заданное место, обычно задаваемое с помощью метки (более подробно о метках – label() – читайте в разделе «Ассемблерные директивы»). Чтобы определить, нужно ли им делать переход, команды условных переходов, а также it и ite проверяют флаги N (отрицательное значение), Z (ноль), C (перенос) и V (переполнение) в регистре состояния приложения (APSR-регистре).

На эти флаги влияют большинство ассемблерных команд, о которых рассказывается в этом руководстве (включая команды перемещения данных из раздела 1), но проверка этих флагов осуществляется при помощи команд сравнения, о которых рассказывалось в разделе 1.10.

Подробное описание того, за что отвечает каждый из этих флагов, читайте в разделе «6. Команды сравнения».

Условные обозначения

Под Rm подразумеваются ARM-регистры R0-R15. Под МЕТКА подразумевается метка, задаваемая с помощью ассемблерной директивы label(). В <условие> можно задать одно из следующих условий:

  • eq – равно
  • ne – не равно
  • cs – в флаге переноса задана единица
  • cc – в флаге переноса задан ноль
  • mi – знак «минус» (отрицательное значение)
  • pl – знак «плюс» (положительное значение)
  • vs – в флаге переполнения задана единица
  • vc – в флаге переполнения задан ноль
  • hi – больше (беззнаковое сравнение)
  • ls – меньше или равно (беззнаковое сравнение)
  • ge – больше или равно (знаковое сравнение)
  • lt – меньше (знаковое сравнение)
  • gt – больше (знаковое сравнение)
  • le – меньше или равно (знаковое сравнение)

Переходы по метке

  • b(МЕТКА) – безусловный переход
  • beq(МЕТКА) – переход, если равно
  • bne(МЕТКА) – переход, если не равно
  • bge(МЕТКА) – переход, если больше или равно
  • bgt(МЕТКА) – переход, если больше
  • blt(МЕТКА) – переход, если меньше (знаковое сравнение)
  • ble(МЕТКА) – переход, если меньше или равно (знаковое сравнение)
  • bcs(МЕТКА) – переход, если в флаге переноса стоит единица
  • bcc(МЕТКА) – переход, если в флаге переноса стоит ноль
  • bmi(МЕТКА) – переход, если у значения знак «минус»
  • bpl(МЕТКА) – переход, если у значения знак «плюс»
  • bvs(МЕТКА) – переход, если в флаге переполнения стоит единица
  • bvc(МЕТКА) – переход, если в флаге переполнения стоит ноль
  • bhi(МЕТКА) – переход, если выше (беззнаковое сравнение)
  • bls(МЕТКА) – переход, если меньше или равно (беззнаковое сравнение)

Длинные переходы

Чтобы задать место перехода, код, генерируемый командами условных переходов (см. выше), использует фиксированное битовое разрешение, зависящее от используемого ПК. Соответственно, в больших программах, где место назначения может быть далеко от исходной точки перехода, ассемблер сгенерирует ошибку типа «переход вне зоны досягаемости». Эту проблему можно решить с помощью «длинных» переходов вроде:

  • beq_w(МЕТКА) – длинный переход, если равно

При использовании длинного перехода команда занимает 4 байта для кодировки команды (а у стандартных команд условных переходов используется 2 байта).

Подпрограммы (функции)

При входе в подпрограмму процессор сохраняет возвратный адрес в регистр R14 (регистр связи или LR). Возврат к команде после подпрограммы выполняется обновлением счетчика команд (R15 или PC) из регистра связи. Этот процесс выполняется с помощью следующих инструкций:

  • bl(МЕТКА) – выполняет переход в МЕТКА и сохраняет возвратный адрес в регистр связи (R14).
  • bx(Rm) – выполняет переход на адрес, заданный в Rm.

Как правило, чтобы вернуться из подпрограммы, bx() используется в виде bx(lr). Что касается вложенных подпрограмм, то перед выполнением внутренних подпрограмм сначала нужно сохранить (обычно в стек) возвратные адреса, находящиеся в наружных областях видимости.

Команды записи и чтения из стека

Условные обозначения

В аргументах команд push() и pop() можно задать некоторые или даже все регистры общего назначения R0-R12 и вдобавок регистр связи (LR или R14). Как и всегда в Python, порядок расстановки элементов не важен. Таким образом, в примере ниже pop() вернет регистрам R1, R7 и R8 их значения даже несмотря на то, что порядок регистров в push() указан по-другому.

push({r1, r8, r7}) # сохраняем данные регистров в стек
pop({r7, r1, r8})  # восстанавливаем данные регистров из стека

Команды для работы со стеком

  • push({набор_регистров}) – сохраняем набор регистров в стек
  • pop({набор_регистров}) – восстанавливаем набор регистров из стека

Прочие команды

  • nop() – не делает ничего.
  • wfi() – приостанавливает выполнение кода до возникновения прерывания, переводя устройство в режим пониженного энергопотребления.
  • cpsid(flags) – задает единицу в регистре маски приоритетов (отключает прерывания).
  • cpsie(flags) – задает ноль в регистре маски приоритетов (включает прерывания).
  • mrs(Rd, special_reg): Rd = special_reg – копирует данные из специального регистра в регистр общего назначения. Специальным регистром может быть IPSR (регистр состояния прерывания) или BASEPRI (регистр базового приоритета). Регистр IPSR позволяет определить номер обрабатываемого прерывания. Если в этом регистре содержится «0», то никакого прерывания в данный момент не обрабатывается.

В настоящий момент функции cpsie() и cpsid() реализованы лишь частично – в них нужно указать аргумент flags, но они на самом деле будут его игнорировать. Эти функции служат для включения и отключения прерываний.

Команды для чисел с плавающей точкой

Эти команды поддерживают использование ARM-сопроцессора для чисел с плавающей точкой (таким оснащена, к примеру, Pyboard) – его также называют FPU (от англ. «floating point unit», что можно перевести как «модуль для операций с плавающей точкой»). FPU-сопроцессор оснащен 32 регистрами в диапазоне S0-S31, каждый из которых может хранить одно число с плавающей точкой одинарной точности. Данные между FPU-регистрами и основными ARM-регистрами можно передавать при помощи команды vmov().

Помните, что в MicroPython не поддерживается передача чисел с плавающей точкой ассемблерным функциям, и также вряд ли какой-то адекватный результат получится, если поместить число с плавающей точкой в R0. Эту проблему можно решить двумя способами. Во-первых, можно воспользоваться массивом, а во-вторых, можно передать и/или вернуть целое число, а потом в коде преобразовать его в число с плавающей точкой (или наоборот).

Условные обозначения

Под Sd, Sm и Sn подразумеваются FPU-регистры, а под Rd, Rm и Rn – главные ARM-регистры. В Rd, Rm и Rn может быть любой основной ARM-регистр, но регистры R13-R15 плохо подходят для использования в этом контексте.

Арифметические операции

  • vadd(Sd, Sn, Sm): Sd = Sn + Sm
  • vsub(Sd, Sn, Sm): Sd = Sn - Sm
  • vneg(Sd, Sm): Sd = -Sm
  • vmul(Sd, Sn, Sm): Sd = Sn * Sm
  • vdiv(Sd, Sn, Sm): Sd = Sn / Sm
  • vsqrt(Sd, Sm): Sd = sqrt(Sm)

В аргументах можно задавать одинаковые регистры. Например, vmul(S0, S0, S0) выполнит S0 = S0*S0.

Перемещение данных между ARM-регистрами и FPU-регистрами

  • vmov(Sd, Rm): Sd = Rm
  • vmov(Rd, Sm): Rd = Sm
    • FPU-сопроцессор оснащен регистром FPSCR, который похож на основной ARM-регистр APSR – в нем хранятся флаги состояния и другие данные. Доступ к нему осуществляется с помощью команды ниже:
  • vmrs(APSR_nzcv, FPSCR)
    • Эта команда перемещает флаги N, Z, C и V для чисел с плавающей точкой в APSR-флаги N, Z, C и V.

Ее выполняют, например, после сравнения чисел с плавающей точкой, чтобы ассемблерный код смог проинспектировать флаги состояния. Вот более распространенная форма этой команды:

  • vmrs(Rd, FPSCR): Rd = FPSCR

Перемещение данных между FPU-регистром и памятью

  • vldr(Sd, [Rn, смещение]): Sd = [Rn + смещение]
  • vstr(Sd, [Rn, смещение]): [Rn + смещение] = Sd

Здесь под [Rn + смещение] подразумевается адрес памяти, полученный с помощью добавления Rn к смещению. Он указывается в байтах. Поскольку одно число с плавающей точкой занимает 32-битное слово, при доступе к массивам чисел с плавающей точкой смещение должно быть всегда кратно четырем байтам.

Сравнение данных

  • vcmp(Sd, Sm)
    • Эта команда сравнивает значения в Sd и Sm, а также задает FPU-флаги N, Z, C и V. Обычно за ней также следует команда vmrs(APSR_nzcv, FPSCR) – чтобы проверить результат.

Преобразование между целым числом или числом с плавающей точкой

  • vcvt_f32_s32(Sd, Sm): Sd = float(Sm)
  • vcvt_s32_f32(Sd, Sm): Sd = int(Sm)

Ассемблерные директивы

Метки

  • label(INNER1)

Эта команда задает метку для последующего использования в команде условного перехода. Соответственно, если где-то дальше в коде будет стоять b(INNER1), то выполнение программы переместится в место, заданное директивой label().

Команды для встраивания данных

Ассемблерные директивы ниже помогают встроить данные в блок ассемблерного кода:

  • data(размер, d0, d1 .. dn)

Директива data() создает в памяти массив n с данными. В первом аргументе размер задается, соответственно, размер (в байтах) последующих аргументов. Следовательно, во фрагменте кода ниже первый вызов data() поместит в смежные участки памяти три байта (со значениями «2», «3» и «4»), а второй – два 4-байтных слова:

data(1, 2, 3, 4)
data(4, 2, 100000)

Данные, чей размер превышает один байт, сохранятся в памяти в прямом порядке байтов.

  • align(nBytes)

Эта команда выравнивает следующую инструкцию по размеру, заданному в аргументе nBytes. Команды ARM Thumb-2 должны быть выровнены под 2 байта. Следовательно, после директивы data() рекомендуется всегда писать команду align(2). Благодаря этому код, который будет следовать дальше, будет запускаться безотносительно размера данных в массиве, созданном с помощью data().

См.также

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