Сборка AVR - номер бита для маскирования

В моей программе сборки AVR ATtiny84a я получаю число битов от 0 до 7 в регистре, скажем, r16. Теперь мне нужно создать маску с этим набором битов. Чтобы сделать это более сложным, синхронизация операции должна быть одинаковой, независимо от того, какой бит установлен.

Например, если r16 = 5, результирующая маска будет 0x20 (установлен бит 5).

До сих пор я немного смещал бит в положение с помощью LSL и использовал r16 (номер бита) в качестве счетчика цикла, а затем, чтобы сохранить точное время независимо от номера бита, выполните фиктивный цикл NOP< /strong> 8-r16 раз.

Ассемблерная инструкция SBR устанавливает бит(ы) в регистре из маски, чтобы его нельзя было использовать. Ассемблерная инструкция SBI устанавливает бит в регистре ввода-вывода из номера бита, но это константа, а не регистр (я мог бы использовать регистр ввода-вывода в качестве временного регистра).

Затем маска используется для очистки бита в ячейке памяти, поэтому, если есть другое решение, позволяющее сделать это из номера бита в регистре, это тоже нормально.

У меня есть другое решение, которое можно попробовать (сдвиг на основе переноса), но я надеялся, что у кого-то есть более элегантное решение, чем циклы и сдвиги.


person Max Kielland    schedule 25.06.2020    source источник
comment
Как насчет таблицы поиска с 8 записями?   -  person Michael    schedule 25.06.2020
comment
Приведенный ниже подход @Michael sub/ror также корректно терпит неудачу при выходе за пределы входного индекса, тогда как поиск, вероятно, не будет выполняться без дополнительных проверок или дополнительной вспышки.   -  person bigjosh    schedule 25.06.2020
comment
@bigjosh 19 слов, а не байтов   -  person ReAl    schedule 26.06.2020
comment
@ReAl Так верно. Комментарий удален. Спасибо.   -  person bigjosh    schedule 27.06.2020


Ответы (5)


Я думаю, что твоя догадка со сменами и переносками — элегантное решение. Вы в основном уменьшаете индексный регистр, устанавливаете перенос, когда декремент равен нулю, а затем сдвигаете перенос в выходной регистр.

Вы можете использовать subtract для выполнения декремента, который автоматически установит бит переноса, когда индекс достигнет 0.

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

Тогда вы можете пойти на хитрость и использовать бит-дозорный в выводе в качестве счетчика цикла psuedu для завершения после 8 итераций цикла.

Так что-то вроде...

; Assume r16 is the index 0-7 of the bit to set in the output byte
; Assume r17 is the output byte
; r17 output will be 0 if r16 input is out of bounds
; r16 is clobbered in the process (ends up as r16-8)

ldi r17, 0b10000000 ; Sort of a psuedo-counter. When we see this 
                    ; marker bit fall off the right end
                    ; then we know we did 8 bits of rotations

loop:
subi r16,1  ; decrement index by 1, carry will be set if 0
ror r17     ; rotate output right, carry into the high bit
brcc loop   ; continue until we see our marker bit come output

Я насчитал 4 слова (8 байт) памяти и 24 цикла этой операции на всех AVR, так что я считаю победителем по размеру, неожиданно (даже для меня!) опережая сильное поле записей на основе таблицы поиска.

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

Большое спасибо @ReAI и @PeterCordes, чьи советы и вдохновение сделали этот код возможным! :)

person bigjosh    schedule 25.06.2020
comment
Стоит ли рассматривать вычисляемый ijmp в последовательности просто ror или rol инструкций? Если вы начнете с набора переносов, первый сместится на 1, а остальные просто сдвинут его. Время в худшем случае, вероятно, аналогично, и это, вероятно, имеет значение для большинства приложений. - person Peter Cordes; 25.06.2020
comment
@PeterCordes Интересная идея! К сожалению, я думаю, что этот подход будет связан с таблицей поиска по пространству, но всегда будет проигрывать ей по времени, поскольку для настройки и восстановления регистра Z требуются все те же шаги, а IJMP заменяет LPM. Есть ли способ запрыгнуть в стол на AVR без регистрации Z? - person bigjosh; 26.06.2020
comment
Я не очень хорошо знаю AVR, только то, что я вижу, пролистывая таблицу инструкций, например это. Хорошо, что если вы настроены на ijmp, вы можете вместо этого загружать данные из таблицы. Однако вам не обязательно сохранять/восстанавливать Z (или X или Y); просто уничтожьте его и позвольте следующему коду установить его на что-то, если он этого хочет. - person Peter Cordes; 26.06.2020
comment
Первые две команды должны быть clr r17 $ inc r17 - person ReAl; 26.06.2020
comment
@ReAI исправил clr на r17, спасибо! Я думаю, что inc должно быть r16, так как это предназначено для увеличения ввода, чтобы мы могли затем уменьшить его, чтобы установить флаг переноса. Есть смысл? - person bigjosh; 27.06.2020
comment
Можете ли вы использовать обычный subi для первого вместо clc / sbci r16,1? Или, возможно, cmp r16, 1 установить Carry, если ввод равен 0, не изменяя его? - person Peter Cordes; 27.06.2020
comment
Очень креативное решение для оформления бита, мне нравится! - person Max Kielland; 28.06.2020
comment
Ах да, @PeterCordes, это намного лучше! Я предполагал, что subi не обновлял бит переноса, но таблицы данных говорят об обратном. Это также позволяет мне изменить все остальные sbci и, таким образом, избавиться от clr r17 вверху, поскольку теперь не имеет значения, что ротируется в перенос ror. Спасибо! - person bigjosh; 28.06.2020
comment
На большинстве ISA, которые имеют отдельные инструкции add/adc, операция BigInt выглядит как add / adc / adc / ... или sub / sbb / sbb / ..., где add/sub обычно записывает флаги, но не читает флаги в качестве входных данных. Точно так же sub / cmp устанавливают флаги для проверки условных ветвей; например brlo просто проверяет, установлен ли флаг переноса. Надеюсь, это поможет вам понять, почему он разработан таким образом. - person Peter Cordes; 28.06.2020
comment
@PeterCordes Полностью понял - когда я увидел, что инструкция называется вычитание без переноса, я по глупости прочитал, что это означает, что бит переноса вообще не используется! Всегда проверяйте спецификации! В любом случае, этот обмен в конечном итоге привел к гораздо лучшему алгоритму, так что еще раз спасибо! :) - person bigjosh; 29.06.2020
comment
Хорошая идея для оптимизированного по размеру цикла, приятное изменение для замены развернутой версии, которая, как оказалось, не имеет преимуществ перед умным sbrc/swap от ReAl. Но да, хотя вычитание без переноса звучит как забавное описание. Я экспериментировал с AVR GCC на godbolt.org, чтобы посмотреть, как GCC делает такие вещи, как увеличение int или указателя, так что меня не ввели в заблуждение. Я предполагаю, что работа с более широкими целыми числами настолько распространена на 8-битной машине, что это исключение, а не правило? например повороты выполняются через перенос (в отличие от x86, где rcrrot-through-carry, ror нет). - person Peter Cordes; 29.06.2020

9 слов, 9 циклов

ldi r17, 1

; 4
sbrc    r16, 2  ; if n >= 4
swap    r17     ; 00000001 -> 00010000, effectively shift left by 4

; 2
sbrc    r16, 1
lsl     r17
sbrc    r16, 1
lsl     r17

; 1
sbrc    r16, 0
lsl     r17
person ReAl    schedule 26.06.2020
comment
Это мило! Я никогда раньше не видел, чтобы SWAP использовалось продуктивно! - person bigjosh; 27.06.2020

Поскольку ваш вывод имеет только 8 вариантов, вы можете использовать таблицу поиска. Он будет выполнять одни и те же операции, независимо от ввода, поэтому время выполнения будет одинаковым.

  ldi r30, low(shl_lookup_table * 2) // Load the table address into register Z
  ldi r31, high(shl_lookup_table * 2)

  clr r1 // Make zero

  add r30, r16 // Add our r16 to the address
  adc r31, r1  // Add zero with carry to the upper half of Z

  lpm r17, Z // Load a byte from program memory into r17

  ret // assuming we are in a routine, i.e. call/rcall was performed

...

shl_lookup_table:
  .db 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80

person AterLux    schedule 27.06.2020
comment
Разве вы не можете выровнять таблицу поиска, чтобы избежать clr/adc? (Или просто убедитесь, что он не пересекает границу в 256 байт, но выравнивание по 8 — это простой способ сделать это.) Еще лучше, если вы выравниваете по 256, вы можете отбросить ldi r30. Или, если вы сделаете так, чтобы значение было в r30 в первую очередь, вы можете add r30, low(shl_lookup_table * 2), я думаю. - person Peter Cordes; 27.06.2020
comment
Опубликовал ответ с этой идеей: 5 циклов, общая память 7 слов, включая таблицу. - person Peter Cordes; 27.06.2020

Выровненная по 8 байтам справочная таблица упрощает индексацию и подходит для чипов AVR, поддерживающих lpm - Загрузка из памяти программ. (Оптимизировано из ответа @AterLux). Выравнивание таблицы по 8 означает, что все 8 записей имеют одинаковый старший байт своего адреса. И никакого переноса младших 3 битов, поэтому мы можем использовать ori вместо того, чтобы инвертировать адрес для subi. (adiw работает только для 0..63, поэтому может не представлять адрес.)

Я показываю наилучший сценарий, когда вы можете удобно сгенерировать входные данные в r30 (нижняя половина Z), в противном случае вам понадобится mov. Кроме того, это становится слишком коротким, чтобы вызывать функцию, поэтому я не показываю ret, а просто фрагмент кода.

Предполагается, что ввод действителен (в диапазоне 0..7); рассмотрите @ReAl, если вам нужно игнорировать старшие биты, или просто andi r30, 0x7

Если вы можете легко перезагрузить Z после этого или вам все равно не нужно было его сохранять, это здорово. Если затирание Z отстой, вы можете подумать о создании таблицы в ОЗУ во время первоначального запуска (с циклом), чтобы вы могли использовать X или Y для указателя с загрузкой данных вместо lpm. Или, если ваш AVR не поддерживает lpm.

## gas / clang syntax
### Input:    r30 = 0..7 bit position
### Clobbers: r31.  (addr of a 256-byte chunk of program memory where you might have other tables)
### Result:   r17 = 1 << r30

  ldi   r31, hi8(shl_lookup_table)    // Same high byte for all table elements.  Could be hoisted out of a loop
  ori   r30, lo8(shl_lookup_table)    // Z = table | bitpos  = &table[bitpos] because alignment

  lpm   r17, Z

.section .rodata
.p2align 3        // 8-byte alignment so low 3 bits of addresses match the input.
           // ideally place it where it will be aligned by 256, and drop the ORI
           // but .p2align 8 could waste up to 255 bytes of space!  Use carefully
shl_lookup_table:
  .byte 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80

Если вы можете найти таблицу на границе выравнивания в 256 байт, вы можете отбросить lo8(table) = 0, чтобы вы могли отбросить ori и просто использовать r30 непосредственно как младший байт адреса.

Стоимость для версии с ori, не считая перезагрузки Z с чем-то после или, что еще хуже, сохранения/восстановления Z. (Если Z ценен в тот момент, когда вам это нужно, рассмотрите другую стратегию).

  • размер = 3 слова кода + 8 байт (4 слова) данные = 7 слов. (Плюс до 7 байт заполнения для выравнивания, если вы не заботитесь о расположении памяти программы)
  • циклы = 1(ldi) + 1(ori) + 3(lpm) = 5 циклов

В цикле, если вам нужны другие данные в том же фрагменте программной памяти объемом 256 байт, ldi r31, hi8 можно поднять/выполнить только один раз.

Если вы можете выровнять таблицу по 256, это сэкономит слово кода и цикл времени. Если вы также поднимете ldi из цикла, останется только 3-цикл lpm.

(Не проверено, у меня нет набора инструментов AVR, кроме clang -target avr. Я думаю, что GAS / clang нужны только обычные ссылки на символы и обрабатывают symbol * 2 внутри. Это успешно ассемблируется с clang -c -target avr -mmcu=atmega128 shl.s, но дизассемблирование .o приводит к сбою llvm-objdump -d 10.0.0. )

person Peter Cordes    schedule 27.06.2020
comment
Это отлично. Я МНОГО использую таблицы поиска и всегда был немного разочарован необходимостью использовать add, а затем adc для вычисления смещения, но никогда не рассматривал это решение, которое действительно очевидно задним числом. - person Andy Preston; 23.05.2021

Спасибо всем за ваши творческие ответы, но я использовал таблицу поиска в качестве макроса. Я считаю, что это наиболее гибкое решение, потому что я могу легко иметь разные таблицы поиска для различных целей с фиксированными 7 циклами.

; @0 mask table
; @1 bit register
; @2 result register
.MACRO GetMask
    ldi     ZL,low(@0)
    ldi     ZH,high(@0)
    add     ZL,@1
    adc     ZH,ZERO
    lpm     @2,Z
.ENDM

bitmask_lookup:
    .DB 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80
inverse_lookup:
    .DB ~0x01,~0x02,~0x04,~0x08,~0x10,~0x20,~0x40,~0x80
lrl2_lookup:
    .DB 0x04,0x08,0x10,0x20,0x40,0x80,0x01,0x02

ldi r16,2
GetMask bitmask_lookup, r16, r1 ; gives r1 = 0b00000100
GetMask inverse_lookup, r16, r2 ; gives r2 = 0b11111011
GetMask lrl2_lookup,    r16, r3 ; gives r3 = 0b00010000 (left rotate by 2)

Пространство не столько важно, сколько скорость. Однако я думаю, что это хороший компромисс, и я не обязан выравнивать данные по четверным словам. 7 против 5 циклов - это цена, которую нужно заплатить.

У меня уже есть один нулевой регистр, зарезервированный для всей программы, поэтому мне ничего не стоит сделать 16-битное добавление.

person Max Kielland    schedule 28.06.2020
comment
Выравнивание вашей таблицы сохранит инструкцию adc, как показано в моем ответе. Вы также можете сэкономить место для версии lrl2, сделав ее перекрывающей обычную таблицу. (т.е. просто добавьте еще 2 байта). Если вы используете режим адресации lpm r1, Z+2, вы все равно можете просто выполнить add ZL,@1 для младшей части, при этом обработка вычисления адреса может быть перенесена в старший байт Z. Обратите внимание, что требуемое выравнивание данных не является словом, это четверное слово (8 байтов). для моего ответа, а не только выравнивание слов данных. - person Peter Cordes; 28.06.2020
comment
Да, хороший момент, я отредактировал выравнивание слов по четверным словам. Другие таблицы были просто примерами различного использования, но если я использую поворот, перекрывающиеся таблицы действительно сэкономят несколько байтов. - person Max Kielland; 28.06.2020
comment
Терминология: inverted_lookup было бы более понятным названием. обратный поиск звучит так, как будто вы сопоставляете 1<<n с n, обратное сопоставление n с 1<<n. Если вы беспокоитесь о том, чтобы иметь целую дополнительную таблицу для побитовой НЕТ-версии результата (вместо использования com r1 после загрузки), мне кажется странным, что вы не готовы пройти лишнюю милю и избежать adc путем выравнивания данные; это заняло бы меньше места, чем целая дополнительная таблица, и дало бы инвертированный результат за то же время, что и сейчас, нормальный результат за меньшее время. (Или, может быть, это должны быть альтернативы, не все используются) - person Peter Cordes; 28.06.2020
comment
Кроме того, если вы используете ldi/add вместо ori, вы можете отказаться от adc, если таблица просто не охватывает 256-байтовую границу. IDK с практической точки зрения, как легко сделать какую-то сборку assert, чтобы проверить это. - person Peter Cordes; 28.06.2020
comment
Не могли бы вы избежать необходимости регистра ZERO здесь, используя sub/sbci с конца массива, с таблицами в обратном порядке? Нет adci, но есть sbci ZH, 0. Я предполагаю, что нулевая регистрация полезна для других случаев, но, возможно, другие будущие читатели могли бы извлечь из этого пользу. (Хотя, как я уже сказал, выравнивание таблиц кажется еще лучшим методом.) - person Peter Cordes; 28.06.2020