MicroPython:Основы/Язык MicroPython и его реализация/Ассемблерная вставка для архитектур Thumb2
Ассемблерная вставка для архитектур 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:
- Команды перемещения данных
- Команды чтения данных из регистра
- Команды записи данных в регистр
- Логические команды и команды сдвига
- Арифметические команды
- Команды сравнения
- Команды условных переходов
- Команды записи и чтения из стека
- Прочие команды
- Команды для чисел с плавающей точкой
- Ассемблерные директивы
Примеры использования
В этом разделе можно найти примеры и советы по использованию ассемблерного кода.
Справочные материалы
- Руководство по использованию ассемблера
- Wiki с полезными советами
- Исходный код emitinlinethumb.c – для ассемблерных вставок в uPy
- Памятка по командам для ARM Thumb2
- Справочное руководство по RM0090
- Справочник по архитектуре ARM v7-M (можно найти на сайте ARM после простой регистрации; также есть на научных сайтах, но следите за тем, чтобы вам не попалась устаревшая версия).
Команды перемещения данных
Условные обозначения
Под 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().