nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Новости QuatCore

Пока что подзабросил реализацию на Verilog, пока не определюсь до конца со всеми необходимыми "командами", в смысле, с адресами Dest и Src, поскольку команда у нас только одна: MOV.

Вместо этого параллельно пишу программу, которая должна на нём выполняться, и под неё "подгоняю" эти адреса, чтобы не получалось "пустого" кода, а каждая строка получалась сколько-нибудь информативной.

И потихоньку улучшаю компилятор ассемблера, чтобы и сам код, и информация "дебаггера" (пока что эмулятора - дебаггер будет, когда подключу реальное железо) были осмысленными.



На данный момент программа насчитывает целых 98 слов (196 байт), и состоит из двух частей. Первая - это уже начало "целевой программы" - мы имеем координаты 4 точек на фотоприёмной матрице, и мы должны сопоставить их с 4 "мишенями", то есть расположить в правильном порядке в массиве, от самой отдалённой точки и затем против часовой стрелки. На это уходит 55 слов.

Вторая часть - демонстрация кватернионной арифметики. У нас есть кватернион ориентации BigQuat, который исходно равен -1 (т.е показывает нулевой поворот). И есть кватернион "малого поворота" SmallQuat, равный -1-0,0872i, т.е кватернион поворота на 10 градусов, построенный наиболее "грубым способом" - скалярная часть единичная, а в векторную засунут половинный угол поворота.

Мы делаем 18 итераций, где "интегрируются" эти повороты, а также поддерживается единичная норма нашего кватерниона. Результат весьма неплох: на каждой итерации мы ошибаемся на 18 угл. секунд, тогда как "теоретическая точность" 16-битного представления кватерниона - 12 угл. секунд (я-таки пожадничал и принял формат 1.15, хоть это и требует введения насыщаемой арифметики). Применённый сейчас метод "интегрирования с поддержанием нормы" оказался весьма подходящим для такой архитектуры - экономичным и на удивление точным. Надо будет и про него рассказать в "ликбезе" - ждите обновления после очень долгого перерыва :)

Процедуры умножения кватернионов, поворота вектора с помощью кватерниона и нормировки занимают в итоге 32 слова, или 64 байта. Оставшиеся слова - небольшая инициализация и тестовый код, который запускает всё это дело.


Под спойлером - объявление области данных, оно довольно громоздкое, поскольку я попытался объявить все-все переменные, которые нужны в полной "бортовой программе". Их набралось примерно на 128 слов (256 байт), что, учитывая размер одного блока памяти ПЛИС в 512 байт - весьма неплохо, "инженерный запас" 2 - то что надо :)

[Spoiler (click to open)]
;Бортовая программа ВИПС для QuatCore
;версия от 08.09.2019
;пока что тестируем нашу "экосистему": транслятор и эмулятор

.data
;чтобы задействовать всю память, "достигаемую" через Imm-значения,
;начнём с "отрицательных" адресов
ORG -64

;выходные данные
;"в максимальной комплектации" - ковариационная матрица шума измерений (21 элем.),
;кватернион взаимной ориентации (4 слова),
;вектор параллельного переноса (3 слова),
;значение скорости (1 слово)
;сюда же вклинивается вектор 1х6 (для алгоритма сопровождения),
;и остаются вакантные места на 6 слов и 5 слов.
Matrix:
a11     Int16   ?           ;адр. -64
vel     Int16   ?   ;скорость сближения
K1      Int16   ?  ;вектор 1х6, для алгоритма сопровождения
K2      Int16   ?
K3      Int16   ?
K4      Int16   ?
K5      Int16   ?
K6      Int16   ?

a12     Int16   ?           ;адр -56
a22     Int16   ?
pad32   dw      ?           ;пока не используется (6 слов)
pad42   dw      ?
pad52   dw      ?
pad62   dw      ?
pad72   dw      ?
pad82   dw      ?

a13     Int16   ?           ;адр -48
a23     Int16   ?
a33     Int16   ?
pad43   dw      ?   ;пока не используется (5 слов)
pad53   dw      ?
pad63   dw      ?
pad73   dw      ?
pad83   dw      ?

a14     Int16   ?           ;адр -40
a24     Int16   ?
a34     Int16   ?
a44     Int16   ?
QuatA   Int16   ?   ;кватернион взаимной ориентации
QuatX   Int16   ?
QuatY   Int16   ?
QuatZ   Int16   ?

a15     Int16   ?           ;адр -32
a25     Int16   ?
a35     Int16   ?
a45     Int16   ?
a55     Int16   ?
Tx      dw      ?   ;вектор параллельного переноса
Ty      Int16   ?   ;(надо только вспомнить, не обращаемся ли к нему "по-кватернионному" где-нибудь?
Tz      Int16   ?

a16     Int16   ?           ;адр -24
a26     Int16   ?
a36     Int16   ?
a46     Int16   ?
a56     Int16   ?
a66     Int16   ?   ;всё, этого нам хватит!

;адр -18

;константы, которые держатся весь сеанс (если только по МКО не поступил массив полётного задания, который их переопределяет!)

;заранее посчитанная матрица для нахождения коэф. аффинного преобразования
;пока она неправильно задана, в порядке МДД1-МДД2-МДД3-МБД, а нам удобнее МДД3-МДД1-МБД-МДД2
;и с центром на стыковочном узле, а нам лучше центр в МБД, иначе будет "стык" при переходе на ближнюю дистанцию
AffineMat:
AfMat00 Int16 11928     ;адр -18
AfMat01 Int16 -17105
AfMat02 Int16 4817
AfMat03 Int16 360

AfMat10 Int16 10191     ;адр -14
AfMat11 Int16 -2359
AfMat12 Int16 -16548
AfMat13 Int16 8716

AfMat20 Int16 4584          ;адр -10
AfMat21 Int16 10355
AfMat22 Int16 11764
LongRangeRefPoints: ;поскольку вектор для нас - кватернион с произв. скал. частью, а жаба душит, мы ставим метку на одно слово раньше.
MDD3a:
AfMat23 Int16 6029          ;адр -7

;координаты мишеней дальней дистанции (т.е когда смотрим издалека, и отдельные точки сливаются в одну)
;единица измерения - метры, 
;диапазон представимых значений: от -2 до примерно +2 метров

MDD3x   Int16   -4751   ;адр -6
MDD3y   Int16   11108
MDD3z   Int16   -31588  ;здесь же MDD1a

MDD1x   Int16   -4751   ;адр -3
MDD1y   Int16   13664
MDD1z   Int16   -819    ;здесь же MBDa

MBDx    Int16   0       ;адр 0
MBDy    Int16   0
MBDz    Int16   0       ;здесь же MDD2a

MDD2x   Int16   -4751   ;адр 3
MDD2y   Int16   -18678
ShortRangeRefPoints:    ;метка координат ближней дистанции перенесена сюда, на 1 слово назад
MBD1a:
MDD2z   Int16   -9486   

;координаты отдельных отражателей мишени ближней дистанции, в увеличенном масштабе
;единица измерения - метры,
;диапазон представимых значений - от -0,125 до примерно +0,125 метров

MBD1x   Int16   24904   ;адр 6
MBD1y   Int16   -22282
MBD1z   Int16   0       ;здесь же MBD2a

MBD2x   Int16   24904   ;адр 9
MBD2y   Int16   22282
MBD2z   Int16   0       ;здесь же MBD3a

MBD3x   Int16   0       ;адр 12
MBD3y   Int16   -28836
MBD3z   Int16   11796   ;здесь же MBD4a

MBD4x   Int16   0       ;адр 15
MBD4y   Int16   -28836
MBD4z   Int16   -13107  ;здесь же MBD5a

MBD5x   Int16   0       ;адр 18
MBD5y   Int16   -8651
MBD5z   Int16   0       ;здесь же MBD6a

MBD6x   Int16   0       ;адр 21
MBD6y   Int16   9699
MBD6z   Int16   0       ;здесь же MBD7a

MBD7x   Int16   0       ;адр 24
MBD7y   Int16   28836
MBD7z   Int16   13107   ;здесь же MBD8a

MBD8x   Int16   0       ;адр 27
MBD8y   Int16   28836
MBD8z   Int16   -13107

;получается 12 + (4+8)*3 = 48 слов на хранение констант

;прореха в 5 и 6 слов, в остальном "плотненько"

;и ещё нужно данные с фотоприёмной матрицы - по кр. мере 8 точек X,Y, т.е ещё 16 слов
;координаты точек, обнаруженных на матрице (gx, gy - временные значения, для идентификации точек)
Points2d:
Fx0     Int16 53    ;адр 30
Fy0     Int16 -7

Fx1     Int16 480   ;адр 32
Fy1     Int16 -30

Fx2     Int16 -539  ;адр 34
Fy2     Int16 -302

Fx3     Int16 400   ;адр 36
Fy3     Int16 -997

Fx4     Int16   ?   ;адр 38
Fy4     Int16   ?

Fx5     Int16   ?   ;адр 40
Fy5     Int16   ?

Fx6     Int16   ?   ;адр 42
Fy6     Int16   ?

Fx7     Int16   ?   ;адр 44
Fy7     Int16   ?

;коэф. аффинного преобр
;конечно, можно их куда-нибудь ещё запихать (все ковар. коэф. в этот момент нахрен не нужны)
AfTransf:
Txx     Int16 ? ;адр 46
Tyx     Int16 ?
Txy     Int16 ?
Tyy     Int16 ?
Rx      Int16 ?
Ry      Int16 ? ;адр 51

;два кватерниона для теста
SmallQuat:
squat0a Int16   -32768  ;адр 52
squat0x Int16   -2859   ;поворот на 10 градусов
squat0y Int16   0
squat0z Int16   0

BigQuat:
bquat1a Int16   32767   ;адр 56
bquat1x Int16   0
bquat1y Int16   0
bquat1z Int16   0       ;адр 59




;и стек нужен какой-никакой!

;хотя бы на вызов RotateVecByQuat

;стек, решили, наконец-то, что он растёт вверх!
;зарезервируем память для него
Stack   dw  ?   ;1 ячейка для адреса возврата RotateVecByQuat ;адр 60
st[1]   dw  ?   ;для хранения X внутри RotateVecByQuat
st[2]   dw  ?   ;1 ячейка для адреса возврата QuatMultiply
st[3]   dw  ?   ;для хранения ijk внутри QuatMultiply
st[4]   dw  ?   ;промежут. значение кват (0)
st[5]   dw  ?   ;(1)    
st[6]   dw  ?   ;(2)    
st[7]   dw  ?   ;(3)        ;адр 67

;вот так как-то... 
;но поскольку один блок памяти ПЛИС - это 512 байт, или 256 слов,
;можно так уж не ужиматься!
;то есть, у нас около 128 слов под временные переменные...


Комментарии идут в стиле ассемблера - они отделяются точкой с запятой.

Мы видим директиву .data - таких директив в коде может быть сколько угодно, т.е сегменты данных и кода можно перемежать - компилятор попросту хранит текущий адрес для каждого из сегментов, и продолжает заполнять его как ни в чём не бывало.

Далее используется директива ORG (Origin), чтобы начать заполнение памяти не с нулевого адреса, а с того, который мы укажем. Мы делаем финт ушами и указываем отрицательный адрес, -64. Так сделано, поскольку мы можем указывать в коде "непосредственные значения" (Immediate values) от -64 до +63, что необходимо для "безболезненного" выполнения условных переходов (они работают с относительными адресами), а когда указываешь "отрицательные адреса" при обращении к шине данных, то естественным образом (когда шину данных воспринимаешь как беззнаковую и отрезаешь верхние, незадействованные биты) обращаемся к последним 64 ячейкам памяти. Именно 64 первых и 64 последних ячеек надо использовать для глобальных переменных, т.к доступ к ним получить проще всего. А стек и какие-нибудь длинные массивы должны располагаться посерединке. Чтобы шибко не задумываться об этой укуренной адресации, мы просто начинаем с адреса -64, заполняем все "последние" ячейки и переходим в начало. Любой массив можно "завернуть" таким же образом, и обращение к нему пройдёт "бесшовно", потому как за FFFF естественным образом идёт 0000 :)

Компилятору приходится слегка "заморочаться" для обеспечения такой функциональности, поскольку он ещё не знает, как много памяти нам понадобится, и какой ширины будет адресная шина, поэтому сначала он заносит все эти значения в специальный массив на 64 элемента, а уже под самый конец, когда размер памяти становится понятен, перемещает их на нужное место.

Поскольку адресация осуществляется по словам (16 бит), а не по байтам (строки текста нам вообще неинтересны,мы общаемся с внешним миром по МКО, которое также составлено из 16-битных слов), то мы пока что реализовали только директиву dw (define word), но чтобы подсказать дебаггеру, как лучше всего показать содержимое памяти, ввели ещё и int16, которая ничем не отличается от dw конечным результатом (память "не знает", как мы используем 16 бит, это должен "знать" сам программный код!), но отлаживать это дело становится куда приятнее :)

Имя, стоящее слева от dw или int16, становится "основным" именем этой переменной, именно оно будет отображено в окне памяти в дебаггере. Справа мы можем перечислить несколько значений через запятую - и они будут занесены по идущим последовательно адресам.

Если мы напишем такую строку:
Quat int16 -32768, 0, 0, 0


то в окне памяти первое (в смысле нулевое) значение будет названо Quat, а последующие: Quat[1], Quat[2] и Quat[3].

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

Мы не стали пока заморачиваться с более сложными структурами данных (аналогов struct и union), поскольку чаще всего будем иметь дело с векторами и матрицами, через регистры косвенной адресации (X,Y,Z) и индексные регистры (i,j,k).

Если вместо конкретного числа мы напишем знак вопроса, это означает неинициализированное слово. Для "прошивки" ПЛИС в этом нет никакого смысла, конфигуратор всё равно захочет инициализировать каждое значение, и по умолчанию заполнит их нулями, эти вопросики нужны для отладки. Дебаггер может предупредить нас, что мы обращаемся к "неопределённым" значениям, что скорее всего означает ошибку в коде.

Результатом обработки сегмента данных становится файл DataSegment.hex, которым инициализируется соответствующий блок памяти ПЛИС. Также составляется список меток данных, которые подставляются в программный код. При этом, некоторые метки могут оказаться "недоступны" через Immediate-значения, тогда компилятор грязно выругается. Кроме того, подготавливается более полная структура данных для дебаггера, в которой отмечено имя каждой ячейки памяти, и какие ячейки не инициализированы, они будут отмечены как "????".

Под следующим спойлером - код программы.
[Spoiler (click to open)]
.code
;основная программа
main proc
;эта команда может вращаться в бесконечном цикле, пока подана единица на reset
;и поэтому PC остаётся нулевым. Но она не страшная, ничего не испортит!
            SP          Stack   ;инициализировали стек
;возможно, при занесении значения в SP сделаем сдвиг влево на пару позиций,
;чтобы разместить стек туда, куда мы не достаём через Immediate значения

;"реальный код" - запустим "алгоритм захвата"
;в действительности, такое деление на процедуры может оказаться лишним, но для отладки это довольно удобно
;по F8 выполняем всю процедуру, а по F7 входим в неё
            CALL        AffineAlgorithm

;умножим кватернион Quat0 на Quat1 справа, в цикле, 4 раза
            k           17
            Z           BigQuat 
            X           SmallQuat
            Y           BigQuat ;[Z] = [X]*[Y]
            Inv         0       ;сопряженный нам не нужен           
@loop:      CALL        QuatInvNorm ;в скал. компоненту SmallQuat запихиваем величину, обратную норме нашего кват, для нормировки
            CALL        QuatMultiply
            kLOOP       @loop
;и завершим выполнение попаданием в бесконечный цикл
;как ни странно, в таком режиме энергопотр. минимально, поскольку нет переключений лог. значений
@endless:   JMP         @endless
main endp

;Аффинный алгоритм, он же алгоритм захвата
;содержание регистров произвольное, сохранять его не требуется
;исходные данные - Points2D - координаты 4 точек на фотоприёмной матрице,
;неупорядоченные.
AffineAlgorithm     proc

    ;алгоритм переупорядочивает точки "на месте", в порядке МДД3-МДД1-МБД-МДД2 (т.е против часовой стрелки от МДД3)
    Associate4Points    proc
    
        FindMDD3    proc
        ;перво-наперво, находим МДД3
        ;как точка, сумма квадратов расстояний до которой максимальна (т.е самая отдалённая)
        ;для каждой точки считаем сумму квадр. расстояний до всех остальных, в т.ч до самой себя (это 0, зато код упрощается)
        ;внешний цикл (по строкам) - перем. j
        ;внутр. цикл (по столбцам) - перем. i
        ;самый-самый внутр. цикл - по индексу перем (X или Y) - перем k
                    X           Points2D
                    Y           Points2D    ;пригодится при сортировке
                    [SP+1]      0   ;максимальная отдалённость
        ;а в [SP+2] - индекс точки с макс. отдаленностью. Её иниц. не надо-заведомо на одной из итераций запишется

                    j           3   ;от 0 до 3, т.е 4 наши точки
        @j_loop:    i           3   ;также от 0 до 3, чтобы все расстояния просуммировать
                    [SP]        0   ;здесь будет храниться текущий максимум
        @i_loop:    k           1   ;от 0 до 1, т.е значения X и Y
        ;а теперь непосредственно прибавляем к акк. ([X+2i+k]-[X+2j+k])^2
        @k_loop:    Acc         [X+2i+k]    ;загрузили одно значение
                    SUB         [X+2j+k]    ;вычитаем второе
                    SQR         Acc         ;возводим в квадрат
                    ADD         [SP]        ;прибавляем к пред. значению
                    [SP]        Acc
                    kLOOP       @k_loop     ;теперь то же самое для второй координаты
                    iLOOP       @i_loop
        ;хорошо, нашли сумму расстояний от точки j до всех остальных
        ;она лежит в Acc и в [SP]
        ;нужно сравнить с самым большим, и если больше - заменить
        ;самое большое значение мы храним в [SP+1],
        ;а его индекс - в [SP+2]
                    SUB         [SP+1]  ;можно и "пожертвовать" значением в Acc,
        ;т.к в [SP] лежит точно такое же!
                    JL          @skip
        ;дистанция оказалась больше - надо обновить максимум и его индекс
                    [SP+1]      [SP]    ;двухпортовая память-это зашибись!
                    [SP+2]      j
        @skip:      jLOOP       @j_loop
        ;по окончании этих циклов, у нас в [SP+2] лежит индекс точки, которая является МДД3
        ;осталось эту точку поменять местами с точкой под индексом 0...
                    i           [SP+2]
                    k           1
        @swap:      Acc         [Y+k]
                    [Y+k]       [X+2i+k]
                    [X+2i+k]    Acc     ;ага, поменяли местами одну из координат
                    kLOOP       @swap   ;а теперь и обе
            
        ;на этом первая часть марлезонского балета закончена.
        FindMDD3    endp
        
        SortCCW     proc
        ;Далее: нужно расставить оставшиеся точки против часовой стрелки
        ;Первый шаг к этому: переместить начало координат в МДД3, который сейчас по нулевому индексу
                    X           Fx1 ;чтобы индекс от 2 до 0 соотв. точкам (Fx1,Fy1) ... (Fx3, Fy3)
        ;потом вернём как ни в чём не бывало, у нас фикс. точка, все правила арифм. соблюдаются
                    Inv         1
                    CALL        ShiftOrigin
        ;за счёт введения этой процедуры сэкономили аж 2 слова, т.е 4 байта, мы молодцы :)
        ;правда, ввели новый Dest: PM (Plus-Minus), но скорее всего пригодится и позже!
        ;вот теперь пора сортировать!
                    Z           Fx2 ;чтобы иметь сдвинутую на 1 адресацию
                    j           1   ;всего три точки, 0,1,2 :)
        ;на этом моменте считаем, что точки j-1..2 уже отсортированы
        ;и берёмся за следующую, под индексом j
        ;это получается сортировка простым выбором
        @j_loop:    i           j
        ;теперь надо сравнить точку j и i, кто из них больше? это по сути умножение комплексных чисел
        ;здесь заведомо k=0, так что X+2i / X+2j не нужны!
        @i_loop:    Acc         0
                    B           [Z+2j+1]
                    FMS         [X+2i+k]
                    B           [Z+2j+k]
                    FMA         [X+2i+1]    ;нахождение "векторного произведения"
                    JL          @skip
        ;меняем местами, как обычно
                    k           1
        @swap:      Acc         [Z+2j+k]
                    [Z+2j+k]    [X+2i+k]
                    [X+2i+k]    Acc
                    kLOOP       @swap
        @skip:      iLOOP       @i_loop
                    jLOOP       @j_loop
        
        ;возвращаем на место, как ни в чём не бывало
                    Inv         0
                    CALL        ShiftOrigin
        SortCCW     endp
        
    Associate4Points endp


    ;по уже идентифицированным (переставленным в нужном порядке) точкам
    ;сначала находит коэф. аффинного преобразования,
    ;а потом - интересующие нас параметры,
    ;т.е кватернион взаимной ориентации и вектор параллельного переноса
    Compute4PointAffine proc



    Compute4PointAffine endp

            JMP         [--SP]
AffineAlgorithm     endp

;по адресу [X], [X+1] лежат коорд X,Y на которые надо передвинуть 3 точки,
;лежащие по адресу [Y], [Y+1], и так далее
;нужно ли их прибавить или вычесть - определяется регистром Inv
ShiftOrigin proc
                i           2
@i_loop:        k           1   ;как всегда, чтобы Y и X
@k_loop:        Acc         [X+2i+k]
                PM          [Y+k]
                [X+2i+k]    Acc
                kLOOP       @k_loop
                iLOOP       @i_loop
                JMP         [--SP]
ShiftOrigin endp

;Процедура нахождения обратной нормы кватерниона
;Состояние регистров при вызове:
;Y - адрес кватерниона, норму которого хотим взять
;X - адрес слова, в которое мы положим эту норму
;остальные регистры - любые
;после вызова, значения регистров не изменятся.
QuatInvNorm proc
            [SP++]  ijk ;сохраняем переменную, т.к применим её
            k       3
            ZAcc    1          ;присвоить -3/2
@loop:      SQRAD2  [Y+k]
            kLOOP   @loop
            [X]     Acc         ;то ли без индекса, то ли с индексом i. Ставить индекс j не хочется - он не инициализирован. Хотя можно...
            ijk     [--SP]
            JMP     [--SP]
QuatInvNorm endp


;Процедура поворота вектора с помощью кватерниона.
;Состояние регистров при вызове:
;X – адрес исходного вектора (вектор – это кватернион с произвольной скалярной частью)
;Y – адрес кватерниона
;Z – адрес результата
;i,j,k, Inv – любые.
;Адрес исходного и результирующего вектора может совпадать, вычисления будут корректными. 
;После вызова, значения регистров не изменятся, кроме Inv, который станет нулевым (мы могли бы и его сохранять, но скорее всего нет необходимости)

;использует 2 слова стека (адрес возврата + значение X) + вызывает QuatMultiply, так что в целом нужно 8 слов. 
RotateVecByQuat proc
            Inv     1               ;чтобы было умножение на сопряжённый
            CALL    QuatMultiply    ;вызов процедуры
;теперь по адресу Z лежит X * Conj(Y)
;осталось домножить его слева на Y
;для чего нужно поставить X=Y, Y=Z
;но X сохраним, чтобы потом восстановить
            [SP++]  X
            Inv     0                   ;сопряжение больше не нужно  
            X       Y
            Y       Z
            CALL    QuatMultiply
            ;восстанавливаем Y
            Y       X
            ;и наконец, X
            X       [--SP]
            JMP     [--SP]          ;RET
RotateVecByQuat endp

;Процедура перемножения кватернионов.
;Состояние регистров при вызове:
;X – адрес левого операнда
;Y – адрес правого операнда
;Z – адрес результата
;i,j,k - произвольное значение
;Inv=1, если правый операнд нужно взять сопряжённым, =0 в противном случае.
;На адреса операндов и результата не накладывается ограничений (допустимо Z=X или Z=Y, или X=Y, вычисления будут корректными). 
;Значения регистров после вызова не изменятся.

;эта процедура не вызывает других, но требует 6 слов на стеке (включая адрес возврата)
QuatMultiply proc
            [SP++]      ijk         ;заносим в стек все индекс. регистры разом
            i           3           ;i-номер компонента, который вычисляем в данный момент
@row_loop:  ZAcc        0           ;обнуляем аккумулятор
            k           3           ;k-номер слагаемого
@col_loop:  B           [X+i^k]     ;загрузили левый операнд
            FMPM        [Y+k]       ;помножили на правый, с нужным знаком
            kLOOP       @col_loop

            [SP+i]      Acc         ;храним ответы на стеке
;потому что если X=Z или X=Y (обновляем кватернион),
;то занесение сразу в Z исказит последующие вычисл.
            iLOOP       @row_loop
            i           3
@out_loop:  [Z+i]       [SP+i]  ;помещаем ответ куда надо
            iLOOP       @out_loop
            ijk         [--SP]      ;восстанавливаем исх. значения регистров
            JMP         [--SP]      ;возврат из процедуры
QuatMultiply endp


Мы видим директиву .code - куда же без неё :)

Далее следуют директивы Proc / Endp. На данный момент, они решают 3 задачи:
- обозначить область действия локальных меток, то есть меток, начинающихся со знака @. Это позволяет не шибко задумываться о том, как назвать эти метки,
- имя процедуры само по себе является меткой,
- данные строки, хоть и не транслируются в программный код, но попадают в "финальный" листинг программы, отображаемый дебаггером, что упрощает отладку.

Такие "процедуры" могут быть вложенными, что мы наблюдаем на примере "общей" процедуры AffineAlgorithm, внутри которой находятся Associate4Points, а внутри неё - FindMDD3 и SortCCW. В первую очередь это сделано для разделения локальных меток, но также может повысить читаемость кода, как самого по себе, так и в отладчике. Возможно, я также введу "макрос" VAR для введения "локальных переменных", имена которых будут попросту заменяться на [SP], [SP+1] и [SP+2]. Пока мне хватает 3 локальных переменных, в дополнение ко всем регистрам. Если понадобится, добавлю ещё. Если такой "макрос" появится, то вложенные процедуры также помогут правильно задействовать стек для локальных переменных, но это пока "в процессе".

На данный момент наш процессор очень прост: вход Reset лишь сбрасывает счётчик инструкций PC (Program Counter) в ноль. Остальные регистры остаются какими угодно, поскольку особенного смысла в полном сбросе нет - всё равно их нужно инициализировать. Сейчас даже ожидается, что процессор во время сброса может не остановиться, а продолжать выполнять команду нулевому адресу. Это всегда будет команда инициализации стека, так что ничего страшного :)

Когда мы заканчиваем работу программы, достаточно засунуть её в бесконечный цикл - во время такого цикла ни один регистр не меняется, поэтому сквозных токов (и перезарядки ёмкостей переходов) не происходит, и ПЛИС потребляет достаточно малый ток. Ещё лучше было бы целиком прекратить подачу тактовой частоты, но такое возможно только в более новых ПЛИС, где есть контуры ФАПЧ и аппаратные коммутаторы тактовых импульсов.

Но чтобы не мучать компьютер, в эмуляторе мы всё-таки ввели флаг HALT, который сбрасывается в ноль по Reset, и устанавливается в единицу, если инструкция JMP указывает "сама на себя", т.е мы явно определили бесконечный цикл. Если такое происходит, выполнение прекращается, и мы видим "финальное" содержание памяти и регистров.

И ещё один "макрос" - это CALL. В первых версиях QuatCore для вызова процедуры требовалось 2 команды - сначала занесение PC в стек, затем команда безусловного перехода, причём если программа длинная (более 128 слов), то опять могут возникнуть проблемы с Immediate-значениями, тогда придётся городить огород с формированием адреса перехода в несколько заходов (сначала верхние биты, потом нижние), в общем, как-то всё нескладно.

Поэтому сейчас у меня выделено 16 адресов Src в модуле PC, которые предназначены для вызова 16 процедур. Эти адреса лежат в диапазоне 176..191 (т.е от 0xBO до 0xBF). Чтобы вызвать первую процедуру, мы пишем

 [SP++] 176

"источником" выбран адрес 176 - получив этот запрос, модуль PC осуществит переход по "вшитому" адресу, а на шину данных выдаст значение PC+1. Это значение будет помещено в стек, т.е по адресу [SP], с инкрементом указателя стека после этого.

Назначение "макроса" CALL - добавить новую запись в таблицу вызовов процедур, грязно выругаться, если их стало более 16, и сформировать правильный код команды.

Одним из выходных файлов компилятора является verilog-файл CallTable.v, в нашем случае он выглядит так:
//адреса для вызова процедур, "вшитые" в модуль QuatCorePC
module QuatCoreCallTable (input [3:0] index, output [6:0] addr)
	assign output = 
		(index==0)? 7'd11:
		(index==1)? 7'd66:
		(index==2)? 7'd84:
		(index==3)? 7'd58:
		7'bxxxxxxx;
endmodule


Данные значения соответствуют процедурам AffineAlgorithm, ShiftOrigin, QuatInvNorm и QuatMultiply. Иными словами, в таблицу попали лишь те процедуры, которые хоть раз вызываются через CALL.

На этом укуренные директивы ассемблера заканчиваются, всё остальное - непосредственно команды, записанные в форме
Dest Src


Dest - адрес получателя, Src- адрес источника данных, и то, и другое - по сути 8-битное число. Но всем корректным адресам присвоены мнемонические имена, которые делают этот язык хоть сколько-нибудь читаемым :)

Если кому-нибудь интересно, могу в следующий раз привести полную таблицу этих адресов, хотя она пока что до конца не "устоялась": сейчас задействуется 44 адреса Src из 256 возможных (22 - на Immediate-значения, ещё 22 - на всё остальное) и 35 адресов Dest, и "пустые поля" будут потихоньку заполняться по мере написания программы.

Когда-нибудь можно будет пойти ещё дальше, и сделать автоматическую генерацию verilog-кода процессора, исходя из тех команд, которые мы написали в программе. Скажем, сложная адресация наподобие [X+2i+k] позволяет добиться очень компактного кода, но если бы мы захотели иметь ПОЛНЫЙ набор команд такого типа (регистр X/Y/Z/SP в качестве базового, i/j/k/0 с множителем 2, i/j/k/0 с множителем 1), это уже получится 43 = 64 команды, тогда как сейчас именно столько выделено на весь модуль MEM, а ещё 32 - на ALU и 32 - на PC. А захотим ещё и множитель 4 - и уже точно ничего не влезет! Но с другой стороны, в реальности мы вряд ли применим прямо все эти
команды, это маловероятно, скорее всего хватит 10-20 разных, если не меньше :)

А пока приведу лог компилятора, который он пишет по окончании работы:
[Spoiler (click to open)]
Компиляция завершена успешно
Ширина адреса сегмента кода (и регистра PC): 7
Ширина адреса сегмента данных (и регистров X,Y,Z,SP): 8
Количество инициализированных слов данных: 196
Количество инициализированных слов кода: 98
Количество адресов процедур: 4

Адреса процедур:
AffineAlgorithm=11(поле Src = 176)
QuatInvNorm=66(поле Src = 177)
QuatMultiply=84(поле Src = 178)
ShiftOrigin=58(поле Src = 179)

Список используемых меток:
a11=192  (данные)
a12=200  (данные)
a13=208  (данные)
a14=216  (данные)
a15=224  (данные)
a16=232  (данные)
a22=201  (данные)
a23=209  (данные)
a24=217  (данные)
a25=225  (данные)
a26=233  (данные)
a33=210  (данные)
a34=218  (данные)
a35=226  (данные)
a36=234  (данные)
a44=219  (данные)
a45=227  (данные)
a46=235  (данные)
a55=228  (данные)
a56=236  (данные)
a66=237  (данные)
AffineAlgorithm=11  (код)
AffineMat=238  (данные)
AfMat00=238  (данные)
AfMat01=239  (данные)
AfMat02=240  (данные)
AfMat03=241  (данные)
AfMat10=242  (данные)
AfMat11=243  (данные)
AfMat12=244  (данные)
AfMat13=245  (данные)
AfMat20=246  (данные)
AfMat21=247  (данные)
AfMat22=248  (данные)
AfMat23=249  (данные)
AfTransf=46  (данные)
Associate4Points=11  (код)
BigQuat=56  (данные)
bquat1a=56  (данные)
bquat1x=57  (данные)
bquat1y=58  (данные)
bquat1z=59  (данные)
Compute4PointAffine=57  (код)
FindMDD3=11  (код)
FindMDD3::i_loop=17  (код)
FindMDD3::j_loop=15  (код)
FindMDD3::k_loop=18  (код)
FindMDD3::skip=29  (код)
FindMDD3::swap=32  (код)
Fx0=30  (данные)
Fx1=32  (данные)
Fx2=34  (данные)
Fx3=36  (данные)
Fx4=38  (данные)
Fx5=40  (данные)
Fx6=42  (данные)
Fx7=44  (данные)
Fy0=31  (данные)
Fy1=33  (данные)
Fy2=35  (данные)
Fy3=37  (данные)
Fy4=39  (данные)
Fy5=41  (данные)
Fy6=43  (данные)
Fy7=45  (данные)
K1=194  (данные)
K2=195  (данные)
K3=196  (данные)
K4=197  (данные)
K5=198  (данные)
K6=199  (данные)
LongRangeRefPoints=249  (данные)
main=0  (код)
main::endless=10  (код)
main::loop=7  (код)
Matrix=192  (данные)
MBD1a=5  (данные)
MBD1x=6  (данные)
MBD1y=7  (данные)
MBD1z=8  (данные)
MBD2x=9  (данные)
MBD2y=10  (данные)
MBD2z=11  (данные)
MBD3x=12  (данные)
MBD3y=13  (данные)
MBD3z=14  (данные)
MBD4x=15  (данные)
MBD4y=16  (данные)
MBD4z=17  (данные)
MBD5x=18  (данные)
MBD5y=19  (данные)
MBD5z=20  (данные)
MBD6x=21  (данные)
MBD6y=22  (данные)
MBD6z=23  (данные)
MBD7x=24  (данные)
MBD7y=25  (данные)
MBD7z=26  (данные)
MBD8x=27  (данные)
MBD8y=28  (данные)
MBD8z=29  (данные)
MBDx=0  (данные)
MBDy=1  (данные)
MBDz=2  (данные)
MDD1x=253  (данные)
MDD1y=254  (данные)
MDD1z=255  (данные)
MDD2x=3  (данные)
MDD2y=4  (данные)
MDD2z=5  (данные)
MDD3a=249  (данные)
MDD3x=250  (данные)
MDD3y=251  (данные)
MDD3z=252  (данные)
pad32=202  (данные)
pad42=203  (данные)
pad43=211  (данные)
pad52=204  (данные)
pad53=212  (данные)
pad62=205  (данные)
pad63=213  (данные)
pad72=206  (данные)
pad73=214  (данные)
pad82=207  (данные)
pad83=215  (данные)
Points2d=30  (данные)
QuatA=220  (данные)
QuatInvNorm=66  (код)
QuatInvNorm::loop=69  (код)
QuatMultiply=84  (код)
QuatMultiply::col_loop=88  (код)
QuatMultiply::out_loop=94  (код)
QuatMultiply::row_loop=86  (код)
QuatX=221  (данные)
QuatY=222  (данные)
QuatZ=223  (данные)
RotateVecByQuat=74  (код)
Rx=50  (данные)
Ry=51  (данные)
ShiftOrigin=58  (код)
ShiftOrigin::i_loop=59  (код)
ShiftOrigin::k_loop=60  (код)
ShortRangeRefPoints=5  (данные)
SmallQuat=52  (данные)
SortCCW=36  (код)
SortCCW::i_loop=42  (код)
SortCCW::j_loop=41  (код)
SortCCW::skip=53  (код)
SortCCW::swap=49  (код)
squat0a=52  (данные)
squat0x=53  (данные)
squat0y=54  (данные)
squat0z=55  (данные)
st[1]=61  (данные)
st[2]=62  (данные)
st[3]=63  (данные)
st[4]=64  (данные)
st[5]=65  (данные)
st[6]=66  (данные)
st[7]=67  (данные)
Stack=60  (данные)
Tx=229  (данные)
Txx=46  (данные)
Txy=48  (данные)
Ty=230  (данные)
Tyx=47  (данные)
Tyy=49  (данные)
Tz=231  (данные)
vel=193  (данные)

Используемые Immediate-значения:
0
1
2
3
6
10
17
30
32
34
52
56
60
-14
-13
-11
-7
-6
-5
-3
-2
-1
(задействовано 22 из 128)

Используемые адреса Src:
Acc(128)
j(161)
ijk(164)
CALL AffineAlgorithm(176)
CALL QuatInvNorm(177)
CALL QuatMultiply(178)
CALL ShiftOrigin(179)
X(192)
Y(193)
Z(194)
[X+i^k](224)
[Y+k](225)
[SP+i](227)
[Z+2j+k](230)
[--SP](231)
[X+2i+k](232)
[Z+2j+1](234)
[X+2j+k](236)
[X+2i+1](240)
[SP](243)
[SP+1](247)
[SP+2](251)
(задействовано 22 из 128)

Используемые адреса Dest:
Acc(128)
ZAcc(129)
ADD(130)
SUB(131)
PM(132)
B(133)
FMA(134)
FMS(135)
FMPM(136)
SQR(137)
SQRAD2(138)
i(160)
j(161)
k(162)
Inv(163)
ijk(164)
iLOOP(176)
jLOOP(177)
kLOOP(178)
JMP(180)
JL(181)
X(192)
Y(193)
Z(194)
SP(195)
[Y+k](225)
[Z+i](226)
[SP+i](227)
[X](228)
[Z+2j+k](230)
[SP++](231)
[X+2i+k](236)
[SP](243)
[SP+1](244)
[SP+2](245)
(задействовано 35 из 256)




Продолжение следует...
Tags: ПЛИС, кватернионы-это просто (том 1), программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments