У меня так произошло с адресацией i^k (взять XOR, искл. ИЛИ между регистрами i, k). Именно с неё началась сколько-нибудь реальная разработка QuatCore. Потом пошла тоже "привычная" адресация 2i+k и 2j+k, и как-то так оказалось, что нужно ещё и реализовать i^j, как меньшее из зол, потому что у нас так повелось, что регистр i наиболее универсальный - может входить в выражение как одинарный, так и удвоенный, а вот регистр j всегда удваивается (либо вообще учетверяется), а регистр k - всегда одинарный. И если остаться с этими правилами и адресацией i^k, то "красиво" умножить матрицу 2х2 на матрицу поворота, которая "на лету" строится из значений co, si (синус и косинус угла крена) не выходит ну никак!
В итоге родился компромисс: для регистра Y мы используем одно выражение, для регистров X,Z - другое:

А вот что можно распрощаться с i^k - как-то и в голову не пришло...
И не пришло бы, не клюнь жареный петух: сообразил вдруг, что мне же тогда и сигнал PM (plus-minus) для команды АЛУ FMPM (Fused Multiply-Plus-Minus) тоже придётся формировать иногда по значениям i,k, а иногда по значениям i,j. И вроде бы и можно, но как-то совсем неаккуратненько получается! Сейчас этот сигнал формируется в модуле QuatCorePC (program counter), поскольку именно там сидят индексные регистры и регистр Inv. Но даже сейчас приходится брать один бит из DestAddr, чтобы определить, какая команда АЛУ сейчас исполняется - "длинная" (например, FMPM) или "короткая" (например, PM). А если теперь придётся ещё и декодировать SrcAddr, чтобы понять, какой из видов адресации мы сейчас используем, это выйдет совсем через задницу.
Сначала решил ещё разок "помытариться" с умножением матриц 2х2:
Умножение можно описать 4 выражениями:
Это исполняется с помощью трёх вложенных циклов, по i, j, k:

Скажем, мы ищем элемент Bjk = B[2j+k], для чего устраиваем внутренний цикл по i от 0 до 1. Обзовём co = R[0], si = R[1]. Тогда при конкретных i,j,k, мы должны взять R[i^j] и помножить на Aik=A[2i+k].
Вот и приходим к тому, с чего начали: тот из индексов (i,j,k), который не может умножаться на 2, НЕ ДОЛЖЕН входить в выражение для R[]. У нас это индекс k, поэтому мы и были вынуждены взять выражение i^j.
А вот с умножением кватернионов картина ГОРАЗДО ПРОЩЕ: там всего 2 вложенных цикла! Выражение выглядит так:
И "традиционно" (этой традиции около полгода) я обозначал номер элемента, который мы в данный момент считаем, i, а внутренний цикл шёл по k. Отсюда индексация NU[i], MU[k], Lambda[i^k]. Да, здесь оба индекса должны становиться "одинарными", чего по нашей таблице адресации происходить не может.
Незадача, однако. Более-менее стройная таблица индексаций рушится напрочь.
Но что хорошо: теперь у нас есть варианты индексации [--SP] и [SP++] как для источника данных (для извлечения из стека), так и для приёмника (помещения в стек), т.е мы можем его крутить в любую сторону.
А при умножении кватернионов мы используем именно стек, т.к в практических случаях умножение должно производиться "на месте", т.е адрес одного из операндов и адрес результата указан один и тот же. Чтобы не исказить данные "раньше времени", мы заносим результат на стек, а потом уже копируем его в память.
Поэтому мы всё-таки можем применить вариант адресации i^j. Получается MU[i], Lambda[i^j], а вместо несуществующей NU[j] получается [--SP] для предварительно сдвинутого куда надо указателя стека. Может, чуть эффективнее будет в кои-то веки пройтись по j не "вниз" (от 3 до 0), а "вверх", с помощью j++. А для организации цикла "параллельно" вести счёт по k "вниз", потому что иначе это замучаешься - сначала перегонять j в аккумулятор, потом вычитать 3, и затем по признаку отрицательного числа оканчивать итерации.
Но и это всё блажь. На самом деле, мы ТУПО ДОБАВЛЯЕМ ОДНУ СТРОКУ в программный код:
k j
и обращаемся как [SP+k].
Всё. Причём эта строка нужна во внешнем цикле, т.е добавляется целых 4 такта к общему времени выполнения. И потеряли на всё про всё 2 байта.
Нам нужно подправить код "мультиплексера для второго индекса" QuatCoreMemSecondIndexMux. Таким он был:
module QuatCoreMemSecondIndexMux (input [15:0] ijk, input [1:0] Base, input [1:0] adr, output [4:0] Q); wire [4:0] i = ijk[9:5]; wire [4:0] j = ijk[4:0]; wire [4:0] k = ijk[14:10]; assign Q = (adr == 2'b00)? 5'b00001 : (adr == 2'b01)? i : (adr == 2'b10)? k : (~Base[0])? i^k: Base[1]? 5'b00000 : i^j; endmodule
и занимал 20 ЛЭ.
А таким становится:
module QuatCoreMemSecondIndexMux (input [15:0] ijk, input [1:0] Base, input [1:0] adr, output [4:0] Q); wire [4:0] i = ijk[9:5]; wire [4:0] j = ijk[4:0]; wire [4:0] k = ijk[14:10]; assign Q = (adr == 2'b00)? 5'b00001 : (adr == 2'b01)? i : (adr == 2'b10)? k : (Base[0]&Base[1])? 5'b00000 : i^j; endmodule
и занимает 12 ЛЭ.
Весь QuatCore, при ширине адреса кода в 7 бит, занимал 453 ЛЭ, а теперь - 445 ЛЭ. Но надо понимать, что исходный вариант был почти бесполезен, т.к при адресации i^j он по-прежнему задавал знак для FMPM, опираясь на i, k, и исправление этого рушило бы нашу логику даже сильнее, и добавило бы ещё десяток ЛЭ.
Завтра испытаем это безобразие на процедуре нахождения крена и его кватерниона...
Жаль только, что процедура умножения кватернионов, с которой всё и началось, с каждым разом становится всё длиннее и длиннее.
[Spoiler (click to open)]
Помните, первая была такой:
Procedure QuatMultiply Acc 0 loop: B [Y+i^j] FMPM [X+i++] JiNZ loop [SP+j++] Acc JjNZ QuatMultiply output: [Z+j] [SP+j++] JjNZ output JMP [--SP] End procedure
и занимала 18 байт. При этом предполагалось, что ещё будет некая "цикличность" для всех индексов, что дойдя до 3, они сразу же "перевернутся" до 0, а для других назначений мы как-то будем их "конфигурировать". И ещё мы почему-то считали, что все эти индексы при входе в процедуру должны быть нулевыми.
А сейчас она такая:
QuatMultiply proc [SP++] C [SP++] i ;заносим в стек все индекс. регистры разом [SP++] j [SP++] k j 3 ;j-номер компонента, который вычисляем в данный момент @@row_loop: ZAcc RoundZero ;обнуляем аккумулятор i 3 ;k-номер слагаемого @@col_loop: C [X+i^j] ;загрузили левый операнд FMPM [Y+i] ;помножили на правый, с нужным знаком iLOOP @@col_loop k j [SP+k] Acc ;храним ответы на стеке ;потому что если X=Z или X=Y (обновляем кватернион), ;то занесение сразу в Z исказит последующие вычисл. jLOOP @@row_loop i 3 @@out_loop: [Z+i] [SP+i] ;помещаем ответ куда надо iLOOP @@out_loop k [--SP] j [--SP] i [--SP] ;восстанавливаем исх. значения регистров C [--SP] JMP [--SP] ;возврат из процедуры QuatMultiply endp
Здесь уже 21 слово, или 42 байта. Ну да ладно.