Программирование AVR AVR201: использование аппаратного перемножителя AVR Sat, October 21 2017  

Поделиться

нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


AVR201: использование аппаратного перемножителя AVR Печать
Добавил(а) microsin   

Это перевод апноута Atmel "AVR201: Using the AVR® Hardware Multiplier" [1], посвященного использованию команд умножения ассемблера микроконтроллеров AVR.

Общие возможности аппаратного перемножителя AVR, описанные в этом апноуте, включают в себя:

• 8- и 16-bit реализации.
• Подпрограммы для знаковых (Signed) и беззнаковых (Unsigned) чисел.
• Дробное (Fractional) знаковых (Signed) и беззнаковых (Unsigned) чисел.
• Пример выполняемых программ.

[Введение]

Термин megaAVR относится к сериям относительно новых устройств микроконтроллеров семейства AVR RISC, которое включает в себя, помимо других новых улучшений, также и аппаратный перемножитель. Этот перемножитель может перемножать два 8-битных числа, выдавая 16-битный результат только за 2 тактовых цикла. Перемножитель может обработать числа со знаком, и числа без знака, и дробные числа без уменьшения скорости и без лишних затрат кода. Первая секция этого документа даст некоторые примеры по использованию перемножителя в 8-битной арифметике.

Чтобы можно было использовать перемножитель, в систему команд AVR были добавлены 6 новых инструкций:

MUL, умножение целых чисел без знака (unsigned integer).
MULS, умножение целых чисел со знаком (signed integer).
MULSU, умножение signed integer на unsigned integer.
FMUL, умножение дробных чисел без знака (unsigned fractional).
FMULS, умножение дробных чисел со знаком (signed fractional).
FMULSU, умножение signed fractional на unsigned fractional.

Отдельные инструкции MULSU и FMULSU добавлены для улучшения скорости и плотности кода при умножении 16-битных операндов. Во второй секции будут показаны примеры, как эффективно использовать умножитель для 16-bit арифметики.

Компонентом, который делает выделенный цифровой сигнальный процессор специально предназначенным для обработки сигналов, является так называемый узел умножения с добавлением (Multiply-Accumulate unit, MAC). Этот узел функционально эквивалентен перемножителю, напрямую соединенному с узлом арифметики и логики (Arithmetic Logic Unit, ALU). Микроконтроллеры megaAVR разработаны так, чтобы предоставить семейству AVR возможность эффективно выполнять такую же multiply-accumulate операцию. В этом апноуте будут рассмотрены примеры реализации операции MAC.

Операция Multiply-Accumulate (которая иногда называется как multiply-add) имеет один критический недостаток. Когда добавляются несколько значений к одному значению результата, даже когда положительные и отрицательные значения могут компенсировать друг друга, имеется риск переполнения результата, что становится очевидным. То есть, добавляя к примеру единицу к signed переменной-байту, которая содержит +127, результат вдруг станет -128, вместо того чтобы стать +128. Один из способов, который часто используется для решения этой проблемы, вовлекает использование дробных чисел, например чисел, которые меньше 1 и больше или равны -1. В последней секции будут рассмотрены некоторые проблемы, касающиеся использования дробных чисел.

В дополнение к новой инструкции умножения, к ядру megaAVR добавлены и несколько других изменений и улучшений. Одно из них - удобная в некоторых случаях новая инструкция MOVW (копирование регистрового слова, Copy Register Word), которая выполняет копирование одной регистровой пары в другую регистровую пару.

Файл AVR201.asm содержит исходный код для этого апноута, где применяется 16-битное умножение. Список всех реализаций алгоритмов с указанием их быстродействия приведен в следующих таблицах.

Подпрограммы 8-bit * 8-bit Cлов кода (циклов CPU)
Беззнаковое (unsigned) умножение 8 * 8 = 16 bit 1 (2)
Знаковое (signed) умножение 8 * 8 = 16 bit 1 (2)
Дробное signed/unsigned умножение 8 * 8 = 16 bit 1 (2)
Дробная signed операция MAC 8 * 8 = 16 bit 3 (4)
Подпрограммы 16-bit * 16-bit Cлов кода (циклов CPU)
Умножение signed/unsigned 16 * 16 = 16 bit 6 (9)
Умножение unsigned 16 * 16 = 32 bit 13 (17)
Умножение signed 16 * 16 = 32 bit 15 (19)
MAC signed 16 * 16 += 32 bit 19 (23)
Дробное signed умножение 16 * 16 = 32 bit 16 (20)
Дробное signed MAC 16 * 16 += 32 bit 21 (25)
Умножение unsigned 16 * 16 = 24 bit 10 (14)
Умножение signed 16 * 16 = 24 bit 10 (14)
Умножение signed MAC 16 * 16 += 24 bit 12 (16)

[8-разрядное умножение (8-bit Multiplication)]

8-битное умножение с использованием аппаратного перемножителя выполняется просто, что хорошо показывают примеры в этой секции. Просто загрузите операнды в два регистра (или только один для возведения в квадрат), и выполните одну из нескольких инструкций умножения. Результат будет помещен в регистровую пару R1:R0. Однако имейте в виду, что только инструкция MUL не имеет ограничений на использование регистров. На рисунке 1 показаны допустимые варианты использования регистров в качестве операндов для каждой их инструкций умножения.

AVR201-multuply-valid-register-usage

Рис. 1. Допустимое использование регистров для инструкций умножения.

Пример 1 - базовое использование инструкции умножения

В этом примере приведен код ассемблера, который читает входное значение из порта B, умножает его на константу 5, и затем сохраняет результат в регистровой паре R17:R16.

1
2
3
4
5
      in r16,PINB          ; Прочитать значение из логических уровней
                           ; выводов ножек микроконтроллера порта B.
      ldi r17,5            ; Загрузить константу 5 в регистр r17.
      mul r16,r17          ; r1:r0 = r17 * r16
      movw r17:r16,r1:r0   ; Переместить результат в регистровую пару r17:r16.

Обратите внимание, что здесь используется новая инструкция MOVW. Этот пример допустим для всех инструкций умножения.

Пример 2 – специальные случаи

Этот пример показывает некоторые особые случаи допустимого применения инструкции MUL.

1
2
3
4
5
6
;Переменные A и B находятся в ячейках памяти SRAM.
      lds r0,variableA     ; Загрузка в r0 значения переменной A.
      lds r1,variableB     ; Загрузка в r1 значения переменной B.
      mul r1,r0            ; r1:r0 = variable A * variable B
      lds r0,variableA     ; Загрузка в r0 значения переменной A.
      mul r0,r0            ; r1:r0 = square(variable A) - квадрат A.

Даже если операнд помещен в регистровую пару R1:R0 (которая используется для результата умножения), операция умножения все равно даст корректный результат, поскольку значения R1 и R0 будут захвачены по первому циклу тактов, и результат операции будет сохранен обратно на втором цикле тактов.

Пример 3 - операция MAC

Последний пример в этой секции показывает реализацию операции multiply-accumulate (умножение с накоплением). Основная формула для этой операции может быть написана следующим образом:

c(n) = a(n) * b + c(n – 1)

Вот код, который выполняет операцию MAC:

1
2
3
4
5
6
      in r18,PINB       ; Прочитать значение из логических уровней
                        ; выводов ножек микроконтроллера порта B.
      ldi r19,b         ; Загрузить константу b в регистр r19.
      muls r19,r18      ; r1:r0 = variable A * variable B
      add r16,r0        ; r17:r16 += r1:r0
      adc r17,r1        ; r17:r16 = r18 * r19 + r17:r16

Обычное применение операции multiply-accumulate - вычисления для фильтров FIR (Finite Impulse Response) и IIR (Infinite Impulse Response), регуляторы PID и FFT (Fast Fourier Transform) [3]. Для этих приложений может быть удобной инструкция FMULS. Главный выигрыш использования инструкции FMULS вместо MULS в том, что 16-битный результат операции FMULS всегда может быть аппроксимирован к (четко определенному) 8-битному формату. Это будет обсуждаться далее в секции "Использование дробных чисел (Fractional Numbers)".

[16-разрядное умножение (16-bit Multiplication)]

Новые инструкции умножения специально разработаны для улучшения 16-bit умножения. В этой секции представлены решения для использования аппаратного перемножителя с 16-bit операндами.

На рис. 2 схематически проиллюстрирован основной алгоритм умножения двух 16-bit чисел с получением 32-bit результата (C = A • B). AH обозначает старший байт и AL младший байт операнда A. CMH обозначает средний старший байт и CML средний младший байт результата C. Аналогичные нотации используются и для остальных байт.

AVR201-16-bit-Multiplication-General-Algorithm

Рис. 2. Основной алгоритм 16-разрядного умножения.

Алгоритм является базовым для всех умножений. Все отдельные 16-битные результаты сдвигаются и складываются друг с другом. Знаковое расширение (sign ext) нужно только для чисел со знаком (signed), однако имейте в виду, что распространение бита переноса (carry) должно быть учтено для беззнаковых чисел (unsigned).

Операция 16-bit * 16-bit = 16-bit

Эта операция допустима и для чисел unsigned, и для чисел signed, даже если нужна только инструкция беззнакового умножения (MUL). Это иллюстрируется на рис. 3. Математическое объяснение следующее: когда A и B являются положительными числами, или как минимум одно из них равно 0, алгоритм полностью корректен, предоставляя результат C = A • B меньше 2^16, если результат пользуется как число unsigned, или меньше 2^15, если результат используется как число signed. Если оба множителя отрицательны, то используется нотация дополнения до двух (two’s complement); A = 2^16 - |A| и B = 2^16 - |B|:

C = A • B = (2^16 - |A|) • (216 - |B|) = |A • B| + 2^32 - 2^16 • (|A| + |B|)

В данном случае учитываются только 16 младших бит; последняя часть суммы отбрасывается, и мы получим корректный результат C = C = |A • B|.

AVR201-16-bit-Multiplication-16-bit-Result

Рис. 3. 16-битное умножение, 16-битный результат.

Когда один множитель отрицательный и другой положительный, например A отрицательный и B положительный:

C = A • B = (2^16 - |A|) • |B| = (2^16 • |B|) - |A • B| = (2^16 - |A • B|) + 2^16 • (|B| - 1)

Старшие биты будут отброшены и корректный результат с нотацией дополнения до двух будет C = 2^16 - |A • B|.

Полученный результат будет в диапазоне 0 .. 2^16 при использовании чисел unsigned, и в диапазоне -2^15 .. 2^15-1 при использовании чисел signed.

Этим показано, как реализовано внутри целочисленное умножение на языке C. Алгоритм может быть расширен для умножения 32-bit чисел с получением 32-bit результата.

Операция 16-bit * 16-bit = 24-bit

Функционал подпрограммы иллюстрируется на рис. 4. Для 24-bit версии подпрограмм умножения результат представлен в регистрах r18:r17:r16. Алгоритм дает корректный результат C = A • B, когда он меньше 2^24 при использовании умножения unsigned, и меньше чем ±2^23 при использовании умножения signed.

AVR201-16-bit-Multiplication-24-bit-Result

Рис. 4. 16-битное умножение, 24-битный результат.

Операция 16-bit * 16-bit = 32-bit

Пример 4 - базовое использование умножения целых чисел 16-bit * 16-bit = 32-bit. Ниже приведен пример, как вызывать подпрограмму умножения 16 x 16 = 32. Это также проиллюстрировано на рис. 5.

1
2
3
4
5
      ldi R23,HIGH(672)
      ldi R22,LOW(672)     ; Число 672 загружено в пару r23:r22.
      ldi R21,HIGH(1844)
      ldi R20,LOW(184)     ; Число 1844 загружено в пару r21:r20.
      call mul16x16_32     ; Вызов подпрограммы умножения.

AVR201-16-bit-Multiplication-32-bit-Result

Рис. 5. 16-битное умножение, 32-битный результат.

32-bit результат умножения unsigned чисел 672 и 1844 will теперь будет сохранен в регистрах R19:R18:R17:R16. Если будет вызвана muls16x16_32 вместо mul16x16_32, то будет выполнено умножение signed. Если будет вызвана mul16x16_16, то результат будет иметь длину только 16 бит, и сохранен в пару регистров R17:R16. Для данного примера значений чисел 16-bit результат будет некорректен.

Операция 16-бит MAC (16-bit multiply-accumulate)

AVR201 16-bitMultiplication 32-bit Accumulated Result

Рис. 6. 16-битное умножение, 32-битный результат с накоплением.

[Использование дробных чисел (Fractional Numbers)]

Беззнаковые (unsigned) 8-битные дробные (fractional) числа используют формат, где допустимы числа в диапазоне 0 .. < 2. Биты 6 .. 0 представляют дробную часть числа, и бит 7 представляет целую часть числа (0 или 1), т. е. получается формат 1.7. Инструкция FMUL выполняет ту же самую работу, что и инструкция MUL, за исключением того, что результат получается сдвинутым влево на 1 бит, так что старший байт двухбайтного результата будет иметь тот же 1.7 формат, что и операнды (вместо формата 2.6). Имейте в виду, что если результат равен или больше 2, то он некорректен.

Для полного понимания формата дробных чисел полезно сравнить его с форматом целого числа: таблица показывает два 8-bit формата чисел unsigned. Дробные числа со знаком (signed fractional), как и целые числа со знаком (signed integer), используют знакомый формат числа с дополнением до 2 (two’s complement format). В этом формате могут быть представлены числа в диапазоне -1 .. < 1.

Номер бита 7 6 5 4
Значение бит целого беззнакового числа (unsigned integer) 27=128 26=64 25=32 24=16
Значение бит целого беззнакового дробного числа (unsigned fractional) 20=1 2-1=0.5 2-2=0.25 2-3=0.125
Номер бита 3 2 1 0
Значение бит целого беззнакового числа (unsigned integer) 23=8 22=4 21=2 20=1
Значение бит целого беззнакового дробного числа (unsigned fractional) 2-4=0.0625 2-5=0.03125 2-6=0.015625 2-7=0.0078125

Если байт 10110010 интерпретировать как unsigned integer, то он будет равен 128 + 32 + 16 + 2 = 178. Иначе он может быть интерпретирован как unsigned fractional, тогда он равен 1 + 0.25 + 0.125 + 0.015625 = 1.390625. Если подразумевается, что это целое число signed integer, тогда он равен 178 - 256 = -122. Если же это число signed fractional, то 1.390625 - 2 = -0.609375.

Использование инструкций FMUL, FMULS и FMULSU не должно быть более сложным, чем инструкций MUL, MULS и MULSU. Однако, одна потенциальная проблема состоит в присваивании значений дробных переменных простым способом. Например, дробь 0.75 (= 0.5 + 0.25) будет 0110 0000, если использовать 8 бит.

Для конвертации положительного дробного числа в диапазоне 0 .. < 2 (например 1.8125) в формат, используемый в AVR, должен использоваться следующий алгоритм:

Есть ли "1" в числе?
Да, 1.8125 больше или равно 1.
   Байт теперь будет 1xxxxxxx
Имеется ли в остатке 0.5?
0.8125/0.5 = 1.625
Да, 1.625 больше или равно 1.
   Байт теперь будет 11xxxxxx
Имеется ли в остатке 0.25?
0.625/0.5 = 1.25
Да, 1.25 больше или равно 1.
   Байт теперь будет 111xxxxx
Имеется ли в остатке 0.125?
0.25/0.5 = 0.5
Нет, 0.5 меньше 1.
   Байт теперь будет 1110xxxx
Имеется ли в остатке 0.0625?
0.5/0.5 = 1
Да, 1 больше или равно 1.
   Байт теперь будет 11101xxx

Поскольку у нас не осталось больше остатка, остальные 3 бита будут равны 0, и конечный результат будет равен 11101000, где 1 + 0.5 + 0.25 + 0.0625 = 1.8125.

Для конвертации отрицательного дробного числа (negative fractional number) сначала добавьте 2 к числу, а потом используйте тот же самый алгоритм.

16-bit дробные числа используют тот же формат, что и 8-bit дробные числа; старшие 8 бит имеют точно такой же формат, как и у дробных чисел 8-bit. Младшие 8 бит увеличивают точность 8-bit формата; если формат 8-bit имеет точность of ±2^-8, то формат 16-bit имеет точность ±2^-16. Аналогично 32-bit дробные числа также увеличивают точность 16-bit дробных чисел. Обратите внимание на важное отличие между integer и fractional числами по использованию дополнительных байт: для дробных чисел увеличивается точность, а для целых чисел увеличивается диапазон.

Как было уже упомянуто ранее, использование чисел signed fractional в диапазоне -1 .. < 1 имеет преимущество по сравнению с использованием integer: когда умножаются два числа в диапазоне -1 .. < 1, то результат будет в диапазоне -1 .. 1, и в приближении (старшие байты) результата могут быть сохранены в том же количестве байт, что и множители, с одним лишь исключением: когда оба множителя равны -1, результат умножения должен быть равен 1, по поскольку число 1 не может быть представлено в этом дробном формате числа, то инструкция FMULS поместит вместо этого число -1 в паре регистров R1:R0. Таким образом, пользователь должен при использовании FMULS гарантировать, что по крайней мере хотя бы один из операндов не равен -1. Умножение 16-bit * 16-bit дробных чисел также имеет это ограничение.

Пример 5 – базовое использование умножения дробных чисел со знаком (Signed Fractional) 8-bit * 8-bit = 16-bit

Этот пример показывает код ассемблера, который читает входное значение из порта B, умножает его на дробную константу (-0.625), и затем сохраняет результат в регистровой паре R17:R16.

1
2
3
4
      in r16,PINB          ; Прочитать значение из ножек микроконтроллера.
      ldi r17,$B0          ; Загрузить -0.625 в регистр r17.
      fmuls r16,r17        ; r1:r0 = r17 * r16
      movw r17:r16,r1:r0   ; Переместить результат в r17:r16.

Обратите внимание, что использование инструкций FMULS (и FMUL) точно так же просто, как и использование MULS и MUL.

Пример 6 - операция MAC

Пример ниже использует данные из ADC (аналого-цифровой преобразователь) для данных операции multiply accumulate (MAC, сложение с накоплением). ADC должен быть предварительно сконфигурирован, чтобы формат результата ADC был совместим с дробным форматом с дополнением до 2 (fractional two’s complement format). Для ATmega83/163 это означает установку бита ADLAR в I/O регистре ADMUX, и использование дифференциального канала (тогда результат ADC будет нормализован к 1).

1
2
3
4
5
      ldi r23,$62       ; Загрузка старшего байта дроби 0.771484375
      ldi r22,$C0       ; Загрузка младшего байта дроби 0.771484375
      in r20,ADCL       ; Получение младшего байта преобразования ADC
      in r21,ADCH       ; Получение старшего байта преобразования ADC
      call fmac16x16_32 ; Вызов подпрограммы для дробной операции MAC.

Регистры R19:R18:R17:R16 будут инкрементированы результатом умножения 0.771484375 на значение, считанное из ADC (это может быть, к примеру, выборка звукового сигнала, над которым выполняется цифровая обработка). В этом примере результат преобразования ADC рассматривается как дробное число со знаком (signed fraction number). Мы могли бы рассматривать его как целое число со знаком (signed integer), и тогда для этого нужно вызвать mac16x16_32 вместо fmac16x16_32. В этом случае число 0.771484375 должно быть заменено на целое число (integer).

[Комментарии к реализациям]

Все 16-bit * 16-bit = 32-bit функции, реализованные здесь, начинают работу с очистки регистра R2, который просто используется как "dummy" регистр (заглушка) в операциях "сложение с переносом" (add with carry, ADC) и "вычитание с заёмом" (subtract with carry, SBC). Эти операции не изменяют значение регистра R2. Если регистр R2 не используется где-нибудь в коде, то не нужно очищать каждый раз R2 при вызове этих функций, но только один раз перед первым вызовом одной из этих функций.

[Ссылки]

1. AVR201: Using the AVR® Hardware Multiplier site:atmel.com.
2. Файл AVR201.asm, документация.
3. AVR223: Digital Filters with AVR (цифровые фильтры на AVR).

 

Добавить комментарий


Защитный код
Обновить

Top of Page