Как стать автором
Обновить

Комментарии 35

Для AVR fasm не портировали. Но это совсем не обязательно, например на форуме fasma местные извращенцы пишут библиотеки макросов для программирования под множество различных архитектур. )))
Как вам такое? :-)

.def ZERO = r0
.def ONE = r1

;
; Инициализация модуля
;

;
; Константа 0
;
clr ZERO

;
; Константа 1
;
clr ONE
inc ONE
А в чем смысл? Использовать регистры для хранения констант?
Ну да.

Вы же не можете записать:

sts ... , 0
sts ... , 1
adc ... , 0
ldi R2, 1


но сможете:

sts ... , ZERO
sts ... , ONE
adc ... , ZERO
mov R2, ONE


Да и «clr Rx» и «mov Rx,ZERO» — это не эквивалентные по результату команды.
MOV rx,ZERO заменяется на LDI rx,0, если правильно пользоваться регистрами и использовать старшие 16 по назначению. А для хранения всех констант все равно регистров не напастись. Регистров всего 32 а возможных значений 256. Использовать константы в регистрах конечно можно, но только временно и для оптимизации множественных присвоений. Если больше нравится ZERO а не 0 есть директива .EQU ZERO = 0
| А для хранения всех констант все равно регистров не напастись.

В системе команд msp430 ввели фишку — генератор констант. (для использования наиболее востребованных констант)
MOV rx,ZERO заменяется на LDI rx,0, если правильно пользоваться регистрами

Что значит, «правильно пользоваться»? Что это за правила?

А для хранения всех констант все равно регистров не напастись.

Никто и не говорит, что нужно хранить все константы. Как упомянул FForth, в MSP430 есть два константо-генерирующих регистра: R2 и R3.

Они дают возможность использовать шесть констант: -1, 0, 1, 2, 4, 8.



Пояснение, как это «работает»:



www.ti.com/lit/ug/slau144j/slau144j.pdf

COKPOWEHEU: А единицу зачем хранить предлагаете?

Разработчики MSP430 упомянули, что единица входит в набор «six most used immediate values».

Я согласен с ними :)
Если интересно, в avr-gcc действительно ноль хранится в регистре, но не r0, а r1. А единицу зачем хранить предлагаете?

Приведите, пожалуйста, какой-нибудь осмысленный код для AVR в пользу своих рассуждений. Тот, где долгосрочное хранение констант 0 и 1 в регистрах уменьшает размер кода или увеличивает скорость работы. Что касается небольших блоков, то я уже писал, что ВРЕМЕННОЕ хранение часто используемой константы для оптимизации не только возможно, но и приветствуется. Только не глобально и в фиксированных регистрах 0 и 1, а в целях оптимизации конкретные значения там, и где это оправданно. Как пример — подпрограмма расчета CRC8. В ней в цикле используется константа 0x31 в регистре tmp1. При этом после выхода из подпрограммы регистр tmp1 используется для других целей.


calc_crc8:
   eor tmp,tmp1 
   ldi crc_tmp,8 
   ldi tmp1,0x31 
lab_crc:
   lsl tmp 
   brcc only_lsl
   eor tmp,tmp1 
only_lsl: 
   dec crc_tmp 
   brne lab_crc
ret_crc:
ret

Упоминание MSP430 в рассуждениях, касающихся ассемблера AVR выглядит несколько странно, с учетом архитектурных различий и, соответственно, набора инструкций. Что касается места хранения констант (r0 и r1), то это вообще не самое удачное место. Почитайте спецификацию команд MUL.

Вы, очевидно, веткой ошиблись. Я не утверждал что хранение этих констант дает какое-то преимущество, я лишь упомянул что соглашение avr-gcc предполагает хранение нуля в r1. И это имеет смысл, хотя регистр они выбрали неудачный. Запись нуля в порт, сложение с переносом, сравнения и т.п. используются довольно часто.
На счет mul и работы с флешом — возможно, посчитали что это достаточно редкие операции и дешевле потратить лишний такт на обнуление, чем лишний регистр. Других вариантов, почему они не взяли хотя бы r2 у меня нет.
А вот зачем автор предлагает хранить где-то единицу — неизвестно.
А вот зачем автор предлагает хранить где-то единицу — неизвестно.

А я выше пояснил: потому что единица входит в число наиболее часто используемых констант при написании программ. И ссылку привел на авторитетный источник — документацию по TI MSP430.

На счет mul и работы с флешом — возможно, посчитали что это достаточно редкие операции

Мне тоже ни разу не понадобился mul. По-моему, там такая реализация, полезность которой стремится к нулю, насколько я помню.

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

Ну это смотря что кодить. А реализация там вполне нормальная умножалка 8 на 8 с выводом 16 битного результата в регистры R1:R0.

И ссылку привел на авторитетный источник — документацию по TI MSP430.

Ключевая фраза. Нельзя переносить решения от одного МК на другой. Вы берете рецепт от неймановского PDP11, где нет ни инкрементов ни декрементов и переносите на гарвардовский процессор с абсолютно другой системой команд. В TI константы нужны для операций, которые в AVR без всяких констант итак есть. Если я не прав — приведите часто встречающиеся операции для AVR, где без 0 и 1 не обойтись.
Что касается полезности MUL — подумайте, как можно поделить байт на нибблы в разные регистры без mul. А потом с использованием mul.

подумайте, как можно поделить байт на нибблы в разные регистры без mul. А потом с использованием mul.
Вероятно, я чего-то не понимаю, но разве mov + andi + swap + andi не решает задачу? Как это сделать через mul с ходу не придумывается.

ldi r16,16
mul r*,r16
swap r0

Что касается полезности MUL — подумайте, как можно поделить байт на нибблы в разные регистры без mul. А потом с использованием mul.

Прикалываетесь, что ли?

Если на входе данные в R16 и результат нужно закинуть в R16 и R17, то без MUL будет код на 4 такта и без использования лишних регистров:

mov  R17, R16
andi R16, 0x0F
swap R17
andi R17, 0x0F


а с MUL будет на 5 тактов и с использованием двух лишних регистров R0 и R1:

ldi  R17,0x10
mul  R16,R17
swap R0
movw R17:R16,R1:R0


… переносите на гарвардовский процессор...

Гарвардская архитектура — это не о системе команд, и даже не о способе адресации, причем здесь это?

Если я не прав — приведите часто встречающиеся операции для AVR, где без 0 и 1 не обойтись.

Без констант можно обойтись где угодно. Типовые операции для использования 0/1 — это использование логических переменных в памяти FALSE/TRUE.

Я не совсем понимаю темы дискуссии. Вы хотите доказать, что подход к оптимальному программированию на ассемблере не зависит от системы команд процессора и количества и возможностей регистров или все это не имеет отношения к архитектуре процессора?
Что касается Вашего примера. Из 4 команд и 5 тактов по факту нужны 2 команды и 3 такта против 4 команд и 4 тактов. Все остальное можно убрать. В описании задачи нет условия обязательного размещения результата в конкретном регистре (кто мешает производить последующие операции с результатом не таская данные с места на место? ), а константа может быть назначена один раз и использоваться для группы операций.
Что касается использования 0 и 1 в качестве FALSE/TRUE — использовать целый регистр (байт памяти) для хранения битового значения с учетом особенностей реализации условных переходов это как-то не комильфо. Есть более простой и эффективный способ хранения и работы с булевыми переменными.
В целом создается впечатление, что вы изучали подробно TI, а теперь пытаетесь проецировать полученный опыт применительно к ассемблеру AVR.

Я не совсем понимаю темы дискуссии.

Ну дискуссия началась с вашего мнения, что константы не нужны, а закончилась: "… а константа может быть назначена один раз и использоваться для группы операций...".

:-)

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

Совсем нет, оптимизация — это хорошо, если она к месту и от нее есть реальная польза. Если, как я показал в примере ранее, это получается не оптимизация, а самообман, то от нее только вред.

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

Хаки, костыли и экономия тактов — это удел или компилятора, или когда выхода другого нет.

Так же замечу, что когда я пишу «sts MY_VAR, ONE», или что-то подобное, то я не такты экономлю, а свое время.

… Что касается Вашего примера. Из 4 команд и 5 тактов по факту нужны 2 команды и 3 такта против 4 команд и 4 тактов...

Да неужели? А давайте еще такой пример рассмотрим. Вход в R0, нужно раскидать значения по R0 и R1.

Что получим?

mov  R16, R0
ldi  R17,0x10
mul  R16,R17
swap R0


получили те же 5 тактов и 2 лишних регистра.

В описании задачи нет условия обязательного размещения результата в конкретном регистре (кто мешает производить последующие операции с результатом не таская данные с места на место?

В исходном предложении нет четкой формулировки, поэтому я ее сделал на свое усмотрение. Ничего в ней экстраординарного нет. А регистры с R16 и выше более функциональные для последующей обработки, чем R0..R15.

Так же замечу, что когда я пишу «sts MY_VAR, ONE», или что-то подобное, то я не такты экономлю, а свое время.
Использовать константы в регистрах конечно можно, но только временно и для оптимизации множественных присвоений. Если больше нравится ZERO а не 0 есть директива .EQU ZERO = 0

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


типовой понятный или переносимый код

Вы это про что? Про то, что вы собираетесь переносить ассемблерный код под другой тип процессоров? Под TI или ARM или C51? Или даже в пределах AVR c Mega на Tiny. Вы серьезно?
Если нужно разжевать, вот пример подпрограммы для преобразования 4-х байтного BCD числа в 8-и разрядный 7-и сегментный код.


.include "macro.inc"
.DEF    TempL = r16
.DEF    TempH = r17
.DEF    cnt = r18
.DEF    const = r19
.DEF    ZERO = r13
.DEF    PtrL = r14
.DEF    PtrH = r15
...
CnvBCDto7seg:
    clr     ZERO
    ldiw    Z, C7S<<1
    movw    PtrH:PtrL, ZH:ZL    
    ldiw    Y, SRC
    ldiw    X, DST
    ldi     cnt, 4
    ldi     const,16
loop:
    ld      TempL, Y+
    mul     TempL, const; тот самый код разбиения на нибблы
    swap    R0          ; тот самый код разбиения на нибблы
    add     ZL, R1      
    adc     ZH, ZERO
    lpm     TempL, Z
    st      X+,TempL
    movw    ZH:ZL, PtrH:PtrL
    add     ZL, R0
    adc     ZH, ZERO
    lpm     TempL, Z
    st      X+,TempL
    dec     cnt
    brne    loop
    ret

C7S: .DB 0x3F,0x6,0x5B,0x4F,0x66,0x6D,0x7D,0x7,0x7F,0x6F
.DSEG
SRC:    .BYTE 4
DST:    .BYTE 8

Напишите свой вариант и докажите, что Ваш подход лучше.

Напишите свой вариант и докажите, что Ваш подход лучше.

Вот вы поместили сюда свой код, но не сказали самого главного: с какой целью он написан в таком виде?

Вы пытались сделать наиболее компактный код, наиболее быстрый, совместимый, экономный по регистрам, из-за каких-то других ограничений?

В таком виде, в котором вы его представили, код явно не самый быстрый или компактный.

Если нужно написать наиболее компактный, то нужно выравнить таблицу преобразования.

Если нужно написать наиболее быстрый код, то нужно развернуть цикл.

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

ldi  ZH, HIGH(C7S<<1)

ldi  YL, LOW(SRC)
ldi  YH, HIGH(SRC)
ldi  XL, LOW(DST)
ldi  XH, HIGH(DST)


Тело развернутого/размотанного цикла, оптимизированного по времени исполнения содержало бы код:

ld    R16, Y+
mov   ZL, R16
andi  ZL, 0x0f
lpm   ZL, Z
st    X+, ZL

swap R16
mov  ZL, R16
andi ZL, 0x0f
lpm  ZL, Z
st   X+, ZL


Тело цикла, оптимизированного по размеру содержало бы код и не требовало бы ни одного лишнего регистра, кроме X, Y, Z и счетчика:

ld   ZL, Y
andi ZL, 0x0f
lpm  ZL, Z
st   X+, ZL

ld   ZL, Y+
swap ZL
andi ZL, 0x0f
lpm  ZL, Z
st   X+, ZL


Оптимизированный вариант тела цикла с MUL ну ничем не лучше других вариантов, использует лишние регистры и константу:

mov  R17, 0x10

...

ld   R16, Y+
mul  R16, R17
swap R0

mov  ZL, R0
lpm  ZL, Z
st   X+, ZL

mov  ZL, R1
lpm  ZL, Z
st   X+, ZL
lpm ZL, Z

Извините, Вы действительно программируете на ассемблере AVR и поняли, что делает подпрограмма? Вы пробовали свой бред запускать?

У Вас в какое место указатель Z показывает перед LPM? Приведите работающий код, который можно запустить и померять размер и производительность.

Приведите работающий код, который можно запустить...

Привести пример чего, кода выравнивания данных? Вот:

_C7S:
.org (_C7S+127)/128*128
C7S:
    ...


На флешке 8 Кб так можно разместить до 23 таблиц.

Вот скриншот из отладчика:



Если сделать выравнивание только по 8 словам, это тоже упростит, сократит и ускорит программу.

Ниже пояснение с учетом вашей ошибки в оригинальном коде (который из-за ошибки не работает):

На флешке 8 Кб так можно разместить до 23 таблиц

* на флешке 8 Кб так можно разместить до 31 таблицы (31
= 8*4-1).

Предлагаю завершить дискуссию и подвести итоги. Здесь я высказываю свое мнение и оставляю за Вами право согласиться с ним или остаться при своем мнении.


  1. Начальный вопрос был об оптимизации кода, путем использования регистров r0 и r1 для хранения констант 0 и 1 на постоянной основе. Основание — аналогия с процессором TI. Не согласен. Свои доводы я изложил. Каких-либо доказательств, свидетельствующих о том, что подобная практика позволяет усовершенствовать код не увидел.
  2. Бесполезность MUL, и как следствие, возможность использования регистров r0 и r1 для хранения констант. В процессе дискуссии представленный финальный код содержит MUL. Могу предположить, что доводы о том, что использование всего спектра возможных команд позволяет писать более оптимальный код были услышаны.
  3. Оптимизация циклов за счет выравнивания блоков данных в коде. Здесь я соглашусь с тем, что подобное может быть использовано, но только с определенными оговорками. Во-первых можно вести речь только об оптимизации скорости, но не размера кода. Посчитаем: (31*10)/8196 = 0,037 — коэффициент использования памяти для выровненных блоков данных, размером 10 байт. Плохая реклама с точки зрения оптимизации размера кода. Если рассматривать что-то более приемлемое, например одну таблицу, выровненную по минимальному сегменту (16 байт), то потеря на выравнивание составит всего 6 байт. Экономия по командам для версии "в цикле" — 4 команды. Оптимизация по размеру довольно сомнительна.
    Практика оптимизации, путем выравнивания хорошо применима к сегменту .DSEG, если используется изменение порядка размещения массивов и переменных в памяти для их выравнивания по границам минимального сегмента. В этом случае, потерь как правило, удается избежать.
    Итого: выравнивание по границе блоков данных в .CSEG следует применять только с точки зрения повышения быстродействия, если суммарное увеличение размера кода допустимо.
  4. Замена циклов линейным кодом. Здесь так же вопрос оптимальности. Потери на организацию цикла — 2 команды и 3 тика на каждый цикл, (с учетом инициализации еще 1 команда и 1 тик на весь код). Выигрыш от "разматывания" на один цикл таким образом составит [количество тиков цикла + 3]/[количество тиков цикла]. Увеличение же размера кода при этом получается кратным количеству циклов. В приведенном примере выигрыш по скорости составит ок 17%, а размер кода при этом увеличится примерно в 3,7 раза.
    Я предполагаю, что в результате каждый остался при своем мнении по большинству рассматриваемых вопросов, но в любом случае спасибо за интересную дискуссию.
1. и 2. Да даже в вашем коде есть константа ZERO, о чем тогда может быть речь? Константы не обязаны располагаться в r0 и r1. TI — это не шарашкина контора, у них есть высококлассные специалисты. Их исследованиям можно верить. Это авторитетный источник в отличие от меня, анонимуса в интернете.

3. Выравнивание начала блока данных совсем не означает, что это приводит к потере количества памяти. Все что у вас входит, пожалуйста, размещайте в памяти перед выровненным блоком, если есть в этом потребность. Не путайте это с выравниванием ВНУТРИ структур.

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

4. Это классический пример оптимизации, тут даже комментировать нечего.

эм, а .MACRO OUTIW точно работает без глюков с регистрами таймера 16 бит?


Accessing 16-bit Registers
.....
To do a 16-bit write, the High byte must be written before the Low byte. For a 16-bit read, the
Low byte must be read before the High byte.
The following code examples show how to access the 16-bit Timer Registers assuming that no interrupts updates the temporary register. The same principle can be used directly for accessing the OCR1A/B and ICR1 Registers.


; Set TCNT1 to 0x01FF
ldi r17,0x01
ldi r16,0xFF
out TCNT1H,r17
out TCNT1L,r16
; Read TCNT1 into r17:r16
in r16,TCNT1L
in r17,TCNT1H

Вы правы. Спасибо, что заметили. Исправил.
В этом примере мы использовали описанные выше макросы, что позволило существенно упростить код программы и сделать его более понимаемым
Гораздо более понимаемым оба фрагмента сделало бы нормальное форматирование и подсветка синтаксиса, жаль на хабре с этим для ассемблеров все печально.
А вообще, я бы, как любитель, выбрал первый вариант — скажу банальность конечно, но нравится в ассемблере именно полный контроль над происходящим и дополнительные прослойки только мешают. Хотя если профессионально заниматься, когда много однотипных задач и код будет кто-то читать кроме тебя, макросы, конечно, необходимы.
В отличие от прочих способов использование макросов позволяет видеть и сжатую форму (только макросы) и полный ассемблерный текст. Визуализацией можно управлять при помощи директивы .listmac. Смотрим листинг и имеем полный контроль. Кроме этого я сознательно включил в статью только такие примеры макросов, реализация которых практически однозначна.
Для ввода и вывода первые 64 порта используются команды in/out, а для остальных lds/sts. Для того, чтобы каждый раз не смотреть в документацию в поисках нужной команды для конкретного порта, создадим набор универсальных команд, которые самостоятельно будет подставлять нужные значения.

FTGJ — то же самое есть в фирменном «AVR001: Conditional Assembly and portability
macros
»:
SETB [Address, Bit Mask, Register]
«Set Bit» — Set a bit in any location in the I/0 space. Registers that can be used are R16-R31.
CLRB [Address, Bit Mask, Register]
«Clear Bit» — Clear a bit in any location in the I/0 space. Registers that can be used are R16-R31.
SKBS [Address, Bit Mask, Register]
«Skip if Bit Set» — skip the instruction following the macro if the bit specified by Bit Mask in any location in the I/0 space is set.
SKBC [Address, Bit Mask, Register]
«Skip if Bit Cleared» — skip the instruction following the macro if the bit specified by Bit Mask in any location in the I/0 space is cleared.
STORE [Address, Register]
«Store register» — Stores the contents of a register in a location in any location in the I/0 space.
LOAD [Register, Address]
«Load register» — Load a register with the contents from any location in the I/0 space.
Нет сомнений, что большинство приведенных макросов так или иначе уже многократно были реализованы. В статье я постарался донести мысль о том, что удобство использования макросов не только в их наличии, но и в интуитивно понятном именовании. LOAD и STORE у меня например никак не ассоциируется с командами, которые они заменяют, в отличии от именования основных инструкций, где есть определенная логика префиксов и суффиксов. Это мое мнение и оно может не совпадать с Вашим.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации