nabbla (nabbla1) wrote,
nabbla
nabbla1

QuatCore+GPU: начинается самое интересное

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

T=1,081 мс - мы заказали на обработку вторую строку и прыгнули в начало цикла по строкам. Как и в первый раз, застряли на команде GPUL, поскольку выходной буфер GPU опять пуст. Мы наблюдаем, как отработало задание по синхроимпульсу (ACQ HSync), хотя на "осциллограмме" на это почти нет никаких намёков. Теперь начинается задание ACQ WholeRow. Наконец, оно завершилось, мы получили свои данные: яркость 0x0FFF (то есть, по нулям) и координата 0x0000. Сравнили её с пороговой, не впечатлились и прыгнули в середину цикла, на проверку, остались ещё яркие точки в нашем списке? А их там и в помине не было, поэтому прыгаем совсем в конец цикла, где снова даём задания на обработку теперь уже третьей строки, и затем на строчный синхроимпульс за ней. Интересный факт: от получения ответа с GPU до отправки задания на обработку сейчас проходит 23 такта, что должно будет соответствовать 920 нс, когда выйдем на свои 25 МГц.

Если помните, мы боялись, что нам не хватит обратного хода в 200 с чем-то тактов. По крайней мере, когда ничего на строке не обнаружено, мы соображаем относительно шустро :)

Примерно на T=1,095 мс мы опять застреваем на команде GPUL, теперь уже по третьей строке изображения. На T=1,107 мс получаем результат: нулевая яркость и нулевая координата. Делать нечего - завершаем цикл, заказывая всю четвёртую строку за раз.

На T=1,121 мс получаем ответ по четвёртой строке: нулевая яркость, нулевая координата. Скука! Заказываем всю пятую строку за раз.

На T=1,134 мс мы видим, что наконец-то началась какая-то движуха на входе! В смысле, ненулевые отсчёты:


Это уже интереснее!


В первую очередь, проверим, что у нас с X-координатой, потому как возня с регистрами способствует появлению задержек и "рассинхрону". Согласно самой картинке (открыл её в фотошопе), первый ненулевой пиксель имеет координату 13 (счёт от нуля). Отладочный регистр X в генераторе тестового изображения (внизу "осциллограммы") показывает уже 0x0F = 15, бежит на 2 такта впереди паровоза, поскольку ПЗУ тестовой картинки имеет защёлку и на входе, и на выходе. А вот регистр GPU XReg показывает 0x0B = 11, то есть на два такта отстаёт. Это не страшно, просто я ошибся с величиной 240 тактов, которые мы просим GPU отсчитать от синхроимпульса до начала строки. Поставим 238 - и всё пройдёт.

Смотрим теперь, что нам выдал GPU. Максимальная яркость составляет 0x0E4A, что при инверсии младших 12 бит даст 0x1B5 = 437. И похоже, что с порогом 512 мы тоже перегнули палку - самое яркое значение на этой строке до порога не дотягивает! Хотя ничего сильно криминального в этом нет, это действительно самый край пятна...

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

Наконец, на T=1,149 мс мы получаем результат по 6-й строке: яркость 0x041F, что при инверсии даст 0xBE0 = 3040 - это уже заявка! Координата 0x000C = 12, что вполне ожидаемо.

И рассмотрим, что же там происходит дальше:


Чтобы понимать происходящее, приведём листинг кода (нужное место):
0B  F288      @@ActPointsStart:   [SP+2j+1]   GPUL            ;яркость самой яркой точки на отрезке
0C  F08A                          [SP+1]  GPUH            ;соотв. координата
0D  807E                          Acc     Threshold       ;поставили задом наперёд, чтобы избежать Hazard'а   
0E  83F2                          SUB     [SP+2j+1]       ;вычитаем порог, положит. значение свидетельствует о новом найденном "пятне"                        
0F  B845                          JL      @@MidCycle      ;пятна нет, не торопимся заказывать отрезок
10  EDCD                          Z       X       ;Чтобы в Merge всегда использовать Z, независимо от того, левая или правая точка оказались
11  A040                          i       1
12  80F0          @@checkCycle:   Acc     [SP+1]
13  83EA                          SUB     [Z+2j+k]
14  8480                          ABS     Acc
15  FC80                          [SP]    Acc     ;модуль разности - это и будет новый "диаметр" точки, если сработает слияние
16  8F83                          DIV2S   C       ;не будем мудрить пока что, всё "в лоб"
17  8FE0                          DIV2S   [Z+1]                       
18  B871                          JL      @@DoMerge
19  EDC8                          Z       [X+k]
1A  A836                          iLOOP   @@checkCycle
1B  DD65                          Y       Heap                                    
1C  F3B6                          CALL    ListOp  ;добавит в X новую, пока что пустую точку.
1D  CDED                          X       Z       ;для дальнейшей работы (после MidCycle)... Чтобы мы не взялись за только что добавленную точку!
1E  E083                          [Z+1]   C       ;гориз. размер точки (будет расти при слияниях, а поначалу D, т.к мы сюда включаем и )                                                                                                                      
1F  F3B8                          CALL    AcqQueue
20  B023                          JMP     @@MidCycle  ;пропускаем слияние точек, 


Загружаем в аккумулятор порог, вычитаем только что полученную яркость, и впервые не делаем условный переход в @@MidCycle, т.е ТОЧКУ (ПЯТНО) МЫ ОБНАРУЖИЛИ!

Помещаем в регистр Z значение из регистра X, 0x0010 - это адрес "текущей яркой точки". В данный момент это у нас "левая фиктивная точка" с X-координатой "-32768".

Затем присваиваем i=1, переменную для цикла "от 1 до 0".

Загружаем в аккумулятор только что найденную X-координату яркой точки, 0x000C = 12. Хотим убедиться, что эта точка лежит не слишком близко к ранее обнаруженным. Вычитаем X-координату текущей яркой точки, т.е "левой фиктивной".

Результат, притом ещё и "насыщенный", т.е насильно упихнутый в прокрустово ложе -32768..+32767 (в данном случае, 12 - (-32768) дало насыщенный результат +32767), сразу же берём по модулю, и приходим к тому же результату, 32767.

Это значение, "растояние между точками", мы заносим в [SP], в нашу самую первую и самую "недолговечную" локальную переменную (при вызове процедуры она затирается адресом возврата!). Это расстояние может пригодиться, если мы всё-таки сделаем "слияние" точек, чтобы записать оценку радиуса получившегося пятна. Затем мы вычитаем расчётный диаметр, делёный на 2, и потом диаметр пятна слева от нас (также делёный на 2), чтобы понять, остаётся ли просвет? В данном случае диаметр "левой фиктивной точки" составляет "-32768", и при вычитании получится заведомо положительное значение.

Следующий слайд!


Да, как и ожидалось, условный переход на @@DoMerge не осуществляется (фиктивные точки на то и фиктивные, что нужны лишь чтобы не проверять всё время - а есть ли точка слева, а есть ли точка справа? ЕСТЬ!), поэтому теперь в Z заносим адрес следующей точки, 0x0012. Если помните, это мой очередной приступ жадности, заставивший сделать левую и правую фиктивные точки "сиамскими близнецами", делящими одну область памяти, ибо по значениям всё устраивает, а участвовать в "перецепках" они всё равно принципиально не будут.

И прыгаем в начало цикла @@checkCycle, на PC=0x12, всё верно. Опять загружаем в аккумулятор X-координату только что найденной яркой точки, 0x000C. В этот раз вычитается координата яркой точки СПРАВА от неё, в нашем случае ПРАВОЙ ФИКТИВНОЙ ТОЧКИ. Её X-координата: 0x7FFF = 32767, скорее для драматического эффекта, чем из надобности, ну пущай будет. Результат получился 0x800D, взяли его по модулю и получили 0x7FF3 = 32755 (логично).

Теперь мы должны были вычесть "расчётный" диаметр точки, делёный на два, он хранился в регистре C и равнялся 3. Увы, сейчас там появилось значение 0x8000. Очередная ошибка. Похоже, команда DIV2S даёт побочный эффект в виде занесения значения из шины данных в регистр C. Я знал об этом 17 января 2020 года, но уже забыл!

Наши текущие проблемы: расхождение на 2 пикселя по горизонтали (нужно отсчитывать 238 тактов от HSync, а не 240), и "побочный эффект" команды DIV2S. Пока это не фатально - на ход выполнения программы не влияет, давайте продолжим!


Далее мы вычитаем диаметр точки правее нашей, в данном случае "фиктивной правой точки". Её диаметр "налезает" на указатель Heap на первый "свободный" элемент списка, 0x0015. Мы знаем, что памяти у нас ожидается то ли 256 слов, то ли 512, вряд ли больше, поэтому какой бы там указатель не стоял, учитывая "фиктивную X-координату" в 32767, мы знаем, что коллизии у нас не будет, результат всяко выйдет положительным.

Следующий слайд!



Действительно, условный переход на @@DoMerge мы и здесь не совершаем. В Z повторно записывается адрес "точки правее текущей", это действие нам не нужно, но без него не удалось бы две проверки (слева и справа) упаковать в цикл, и программа удлинилась бы на 4 слова, впрочем, выполнилась бы за меньшее количество тактов.

Наконец, цикл завершается (iLOOP не даёт прыжка в начало цикла), и раз мы не "выпрыгнули" из него на метку @@DoMerge, значит точка действительно стоит "обособленно", а значит, пора добавить её в список активных точек.

Регистру Y мы присваиваем адрес указателя Heap. Непосредственное значение 0x65 преобразуется в 0x8C13, а поскольку регистр 8-битный, в него попадает значение 0x13 - совершенно верно!

Теперь идёт вызов процедуры ListOp. Пора вспомнить, что CALL ListOp на самом деле превращается в [SP++] CALL6, где CALL6 - команда SrcAddr, осуществляющая прыжок на 6-ю запись в таблице QuatCoreCallTable, выдавая на шину данных текущее значение PC (Program Counter, счётчик инструкций), а [SP++] подбирает значение из шины данных и укладывает в стек - это и будет наш адрес возврата.

Всё происходит "как в аптеке", что не может не радовать - хоть это мы не поломали.

Приведём листинг кода процедуры ListOp, чтобы ориентироваться в происходящем:
    ListOp proc         
3E  80D8              Acc     [Y+k]
3F  ED80              Z       Acc         
40  8300              SUB     0           
41  D8E8              [Y+k]       [Z+k]
42  B861              JL      @@OutOfMem          
43  E8C8              [Z+k]       [X+k]
44  C8ED              [X+k]       Z
45  8957              NOP     0   ;избежать Warning'а. Как убрать этот Hazard - понятия не имею.. Разве что для вызова функции вместо стека применить регистр C? Возможно...
46  B0FF              JMP     [--SP]
47  FFB4      @@OutOfMem: [--SP]  Call(OutOfMemory)   ;SP возвращается на место, но выпрыгиваем в другое место, на обработку ошибки
    ListOp endp


первым делом загружаем в аккумулятор адрес свободной ячейки, т.е значение указателя Heap. Это значение 0x0015. Его же заодно запихиваем в регистр Z.

Далее вычитаем нолик, чтобы выставить флаг знака. Так мы проверяем на NULL/nil, который у нас равен -32768, а вообще подошло бы любое отрицательное число (поскольку у нас нет флага нуля и соответствующего зоопарка команд условного перехода типа JZ/JNZ/JG/JLE и т.д, то так оно сподручнее).

Заставляем Heap указывать на следующий свободный элемент, это 0x1A, поскольку текущий (0x15) мы сейчас применим.

Вот только сейчас у нас условный прыжок на @@OutOfMem, если список свободных элементов закончился. Расположили в таком порядке, чтобы снизить количество Hazard'ов и соответственно NOP'ов, которые вставил бы компилятор. На столь активную работу с памятью QuatCore не очень расчитан пока (формирователь эффективного адреса всего один, хоть сама память и двухпортовая).

Всё в порядке, памяти пока хватает, поэтому теперь правим ссылку с только что выделенного элемента. Она вела к следующему свободному элементу, теперь ведёт на элемент "правее текущей точки", в данный момент это "правая фиктивная точка" с адресом 0x12.

И наконец, точка "левее текущей" изменяет ссылку на точку правее себя. Раньше это была "правая фиктивная точка" 0x12, а теперь будет новая точка с адресом 0x15.

Затем следует "пустая операция" NOP, а за ней: возвращение из процедуры, на адрес 0x1D, всё верно.

Там мы "продвигаем" регистр X на одну точку вправо. Он указывал на "левую фиктивную точку", а теперь будет указывать на только что добавленную точку, т.е на адрес 0x15.

До сих пор добавленная точка не была инициализирована, в её полях лежит мусор (не считая ссылки на следующую, об этом ListOp позаботился на славу!). В первую очередь, указываем, её диаметр, и пока что он должен равняться "расчётному", хранящемуся у нас в регистре C. Увы, палёная команда DIV2S затёрла это значение, и по удивительному совпадению засунула туда 0x15, адрес этой самой точки :) Пускай, это мы будем расхлёбывать потом.

Следующий слайд!


Мы вызываем процедуру AcqQueue. Вот листинг её кода:
    AcqQueue proc   
48  A023              i       2   ;даже если не нужно обновить точку, не навредит... (это мы Hazard убираем мучительно!)
49  B840              JL      @@queue
4A  C6F4      @@updCycle: [X+2j+i]    [SP+i]
4B  A85F              iLOOP       @@updCycle
4C  80CA      @@queue:    Acc     [X+2j+k]
4D  8FC0              DIV2S       [X+1]   ;теперь в аккмуляторе у нас X - Ceil(D/2)
4E  2080              ACQ     Acc     ;первый отрезок
4F  82C0              ADD     [X+1]   ;а вот теперь X + Floor(D/2)
50  2080              ACQ     Acc     ;второй отрезок
51  B0FF              JMP     [--SP]
    AcqQueue endp   


В первую очередь, убеждаемся что перешли куда надо, на PC=0x48, и при этом на шине данных лежал наш старый PC, который теперь занесён в стек как адрес возврата.

Наблюдаем удивительную картину: непосредственное значение из случайно затесавшейся команды JMP @@MidCycle, которое должно было превратиться в 0x22, совпало с непосредственным значением из команды i 2. И то и другое - 0x23, которое превратилось в 0xCC22, и для 8-битного значения PC, и для 5-битного i всё в порядке.

Далее идёт команда условного прыжка JL @@queue, и мы заранее знаем, что она не выполнится, поскольку у нас последний раз проверялась нехватка памяти, и раз мы всё-таки продолжили работу, с памятью всё в порядке! (По-другому выйдет, когда мы вызываем эту процедуру из другого места!)

И действительно, переход не происходит. Вместо него мы попадаем в коротенький цикл для инициализации нашей точки. Последовательно мы должны занести в память яркость точки, затем её Y-координату и, наконец, X-координату.

Да, первой идёт 0x041F, наша яркость. Затем 0x22 - это номер строки, если считать "снизу". Кстати, и это не лучшее решение, учитывая наши "фиктивные нижние строки". Пора взять и по-нормальному считать с нуля, это ничего не поменяет в данном конкретном случае... А так понятно: первый раз было 0x28 = 40, и это как бы самая верхняя строка. А сейчас уже 6-я итерация идёт, вот и вышла она под номером 34 = 0x22...

Наконец, идёт X-координата точки, 0x000C. Всё верно. Цикл оканчивается, пора выдать задания видеопроцессору по обработке следующей строке, по крайней мере, той её части, которую мы уже поняли.

Первым делом загружаем в аккумулятор X-координату точки, 0x000C. Далее вычитаем из неё половину её диаметра. К сожалению, из-за "побочного эффекта" DIV2S диаметр здесь неправильный, 0x0015=21. В итоге, 12 - 21/2 = 1 (при округлении "до ближайшего целого", НЕ БАНКИРА). Именно эта единица отправляется на видеообработчику, т.е задание "просмотреть строку с нуля до единицы".

Во время отправки этого задания видеопроцессор успел отсчитать 0x6A = 106 тактов от синхроимпульса. Ещё 134 такта у нас осталось в запасе. А мы затратили 118 тактов от получения результатов до отправки нового задания, или 4,72 мкс, когда всё же разгонимся до 25 МГц. Для фотоприёмной матрицы 1204ХВ014 так не пойдёт, увы, там время обратного хода 8 тактов. Для неё придётся придумать более сложный алгоритм, но давайте решать проблемы по мере их поступления. Думаю, для большинства как аналоговых камер, так и цифровых с "нормальным" параллельным интерфейсом данный алгоритм подойдёт.

Следующий слайд! (осталось уже немного...)


Теперь к аккумулятору прибавляем диаметр точки, то есть выходит X+D/2 - правая граница нашей точки. Эту координату мы посылаем на видеопроцессор. По сути, мы запрашиваем отрезок от X-D/2 до X+D/2. Когда получим по нему результаты, они будут использованы для УТОЧНЕНИЯ координат и яркости текущей точки.

И успешно возвращаемся из процедуры, чтобы сразу же попасть на прыжок в @@MidCycle. Там мы уже были ранее, пройдёмся ещё разок:

22  DDCD              @@MidCycle:     Y       X
23  CDC8                          X       [X+k]                               
24  8000                          Acc     0
25  83C8                          SUB     [X+k]
26  BC39                          JGE     @@FinalRange    ;наткнулись на NULL, значит, все точки обработали...                        
27  F288                          [SP+2j+1]   GPUL        ;яркость
28  8088                          Acc     GPUL


На всякий случай сохраняем текущее значение регистра X, он указывает на только что обработанную точку, 0x15. А регистр X "прыгает" на следующую за ней точку, 0x12. Это наша "правая фиктивная точка".

Наконец, проверяем указатель на следующую точку. В нём NULL, значит, наша работа здесь завершена, мы прыгаем в @@FinalRange, где отправляем ещё два задания на обработку. Одно - до конца строки, до 0x1F, и ещё одно - на строчный синхроимпульс.

В самом "низу" входного буфера GPU лежит исполняемое прямо сейчас задание ACQ HSync (мы сейчас находимся на обратном ходу, следующая строка ещё не началась), затем ACQ 1 (обработать отрезок до единицы), ACQ 16 (обработать отрезок до числа 16) и ACQ 31 (обработать до конца строки).

Поэтому ещё одно задание ACQ HSync пока не влезает, и мы застреваем на этом самом месте.


В целом я доволен: мы уже просмотрели весь код, кроме 16 строчек, т.е "покрытие 80%". И всё сработало практически как надо. Да, нужно команду DIV2S подрихтовать, уменьшить задержку после синхроимпульса с 240 до 238 тактов, начать отсчёт строк с нуля, "как у всех нормальных людей", поставить D1=6, а не 3. Но ведь работает же!
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

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

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

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

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: 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 

  • 0 comments