nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore+GPU: третий заход

Продолжаем симуляцию. Пока мы увидели, как видеообработчик загрузили работой: сначала найти кадровый синхроимпульс, потом найти строчный синхроимпульс и отсчитать от него 240 тактов, и так 10 раз. Сразу 11 задач отправить было "некуда", в буфере места лишь на 4 задачи, одна из которых "текущая", поэтому процессор раз за разом останавливался и "ждал под дверью", когда же у него наконец заберут очередное задание. И вот, наконец, это случилось, и мы наконец-то взялись за "верхнюю строку", первую, на которой попытаемся найти точки. Но поскольку пока мы не знаем, где искать, мы сразу же прыгаем в самый конец цикла, где подаём команду "обработать строку до самого конца".

На ней-то мы и остановились в прошлый раз - буфер по-прежнему полон, поэтому новое задание мы положим не раньше, чем обработается очередная строка, пока ещё "бесполезная". Это был момент T=1,022 мс.

Промотаем чуть дальше, мимо строчного синхроимпульса до Xreg = 0x0F0 (T=1,035 мс)


Да, отправили одно задание, "ACQ WholeRow", как застряли на отправке следующего: "ACQ HSync". Значит, ждём ещё одну строку, там сейчас ещё 3 их осталось в буфере. Так что промотаем до следующего Xreg=0x0F0 (T=1,049 мс):


Увы, опять ошибка...

Приведём небольшую часть листинга кода, который сейчас исполняется:

36  207C          @@FinalRange:   ACQ     WholeRow
37  2006                          ACQ     HSync
38  80F2                          Acc     [SP+2j+1]
39  8340                          SUB     1
3A  BC38                          JGE     @@newRow
        ProcessFrame endp
3B  8957      OutOfMemory:    NOP     0       
3C  B01F      @@endless:      JMP         @@endless
    main endp   
    ListOp proc         
3D  80D8              Acc     [Y+k]
3E  ED80              Z       Acc         


Прыгнули в @@FinalRange, послали две команды, тем самым завершив работу по самой первой "полезной строке", а потом должны были проверить, остались ли ещё строки, и если остались - прыгнуть в начало цикла по строкам.

Наблюдаем по осциллограмме. Действительно, идёт команда 0x80 - запись в аккумулятор. Но вот записывается туда НОЛЬ, хотя заносили мы вроде бы 0x0028 = 40 (см. отладка QuatCore+GPU-вторая попытка). Затем из нуля мы вычитаем единицу, и команда JGE (Jump if Greater or Equal) не даёт прыжка, поэтому мы проскакиваем NOP 0 (ой какой интересный нолик на шине данных, 0xC1B5! Да, мы знаем, что команде NOP вообще никакой операнд не нужен, поэтому взяли первое подходящее значение :))

И попадаем в бесконечный цикл @@endless, как будто наша работа завершена.

В общем, всё правильно, кроме одного: неправильное значение мы загрузили в аккумулятор на строке 38. И прежде чем лезть в недра QuatCoreMem, искать, где же там опять ошибка, надо повнимательнее посмотреть на код.
Записывали мы ImageHeight в [SP+2j] = [SP+2], а прочитали из [SP+2j+1]=[SP+3] за каким-то лешим! Немудрено, там ноль сидит! Ладно, исправляем [SP+2j+1] на [SP+2j], перекомпилируем, повторно синтезируем, повторно запускаем симуляцию - и продолжим...

Пока шла симуляция, сообразил, что мы единичку-то из аккумулятора вычитаем, а получившееся значение назад в [SP+2j] не заносим, поэтому так и будем крутиться в бесконечном цикле! Так что надо туда ещё одну команду добавить сразу. Вот так и бывает - самый старый код, глаз замылился. Изменения в код внёс, но повторно всё компилировать пока не буду. Одну бы итерацию корректно пройти для начала!

Продолжаем с того же места:


Теперь всё верно: загрузили в аккумулятор значение 0x0028 = 40, вычитаем единичку, результат получился неотрицательным (флаг знака: ноль), поэтому осуществили переход в начало цикла. Приведём листинг начала цикла:

0A  CD05      @@newRow:           X         ActivePoints    ;начинаем цикл по всем активным точкам
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


Находим SrcAddr=0x05, это непосредственное значение ActivePoints, оно преобразуется в 0x8010. Приведём кусочек листинга оперативной памяти:

D1:              0F   0x0003
ActivePoints:    10   0x0012
AllPoints:       11   0x8000
APHeadDummyX:    12   0x8000
Heap:            13   0x0015
APTailDummyX:    14   0x7FFF
Elem0Next:       15   0x001A
Elem0X:          16   ????
Elem0Y:          17   ????
Elem0L:          18   ????
Elem0P:          19   ????


Если из 0x8010 вырезать младшие 8 бит (ширина адресной шины оперативной памяти), получим как раз 0x10 - всё как в аптеке.

К следующему такту это значение 0x10 отправляется по адресу DestAddr=0xCD, это регистр X. И тут мы основательно застреваем на SrcAddr = 0x88, это GPUL (Graphic Processing Unit - Luminance/Low word). Если мы всё правильно сделали, задачи со строчными синхроимпульсами должны по завершении сбрасывать (flush) выходной буфер, поэтому он будет пуст и мы застрянем на этой команде до тех пор, пока не исполнится задача "ACQ WholeRow", которая наконец-то определит самый яркий пиксель на строке.

Уже тот факт, что мы застряли, вселяет оптимизм. Значит, выходной буфер GPU действительно пуст несмотря на то, что уже исполнилось под десяток команд. Сейчас там должны лежать две команды "ACQ HSync", за ними "ACQ WholeRow" и снова "ACQ HSync".


T=1,063 мс. Один синхроимпульс пропустили, до 0xF0 досчитали - ничего не происходит. Это правильно, у нас остался ещё один ACQ HSync...


T=1,077 мс. Ещё один синхроимпульс, ещё раз досчитали до 0xF0 - и снова ничего не происходит. И это снова правильно, мы "сожрали" последнюю команду ACQ HSync, теперь должна пойти ACQ WholeRow. Вот оно:



WholeRow мы определили как 0x1F = 31. Именно когда Xreg = 0x1F, команда была выполнена, но ещё один такт ушёл на запись в выходной буфер. "Байпаса", позволяющего только-только появившееся значение на входе скоммутировать напрямую на выход у нас нет, он бы нам все тайминги нарушил, поэтому именно на XReg=0x20 мы наконец-то завершаем команду GPUL.

К следующему такту, на шине данных появляется значение 0x0FFF - максимальная яркость на этом отрезке, но с инверсией. То бишь, ноль. Так и есть: эта строка была целиком пустой. Это значение отправляется на DestAddr = 0xF2, то бишь [SP+2j+1], а тем временем мы запрашиваем SrcAddr = 0x8A, это GPUH (Graphic Processing Unit - Horizontal coord / High word). Тут никакого ожидания нет, поскольку пара значений для GPUL / GPUH пришла одновременно.

Как мы обнаруживаем, координата самой яркой точки оказывается 0x0000, всё верно. Если ни один пиксель не смог превысить исходной яркости 0, то и присвоения координаты не происходит. Этот нолик отправляется на DestAddr = 0xF0, то есть [SP+1]. Тем временем SrcAddr = 0x7E, это непосредственное значение, которое преобразуется в 0x0DFF. Если взять инверсию от младших 12 бит, получим 0x0200 = 512, это наш порог яркости. Всё, что ниже, мы считаем шумом.

Этот самый порог, 0x0DFF, загружается в аккумулятор, а на шину данных тем временем загружается значение из SrcAddr=0xF2, то есть из [SP+2j+1], это наша максимальная яркость на отрезке.

Да, мы видим 0x0FFF на шине данных, и выполняется команда SUB - вычитание. Тем временем SrcAddr = 0x45 - непосредственное значение, которое превращается в 0x8011.

На следующем такте выполняется команда JL @@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
29  83C2                          SUB     [X+2j+1]    ;сравниваем по яркости                      
2A  F08A                          [SP+1]  GPUH        ;пока не забыли - сразу второе слово запрашиваем (коорд точки), чтобы не сбить весь FIFO 
2B  BC40                          JGE     @@NotSoBright   ;похоже, на спад пошло


В первую очередь, мы видим, что прыгнули куда надо: PC=0x22. Уже радостно.

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

Тем временем, у нас начались операции со связанными списками! До этого мы присвоили X = ActivePoints = 0x10, это адрес в памяти, где лежит указатель на первый элемент списка.

Теперь исполняем "Y X" - сохранить это значение в регистре Y. Всё чётко, на шине данных видим 0x10.

А вот под конец этого длинного скриншота происходит какая-то хрень. Мы видим исполнение команды SrcAddr = 0xC8, что должно соответствовать [X+k] = [X]. Выборка из памяти занимает 2 такта, но почему-то мы не дожидаемся двух тактов, а тут же прём дальше. При этом на шине данных оказывается число 0x8000, что в корне не верно! По адресу 0x10 у нас лежало 0x12 - указатель на первый элемент списка. Похоже, проявился ранее не выявленный баг QuatCoreMem. Так что в регистр X уже поступает ошибочное значение, а учитывая, что эти 0x8000 ещё и обрежутся до 8 бит, там вообще будет ноль по логике вещей...

Вот эти 3 строки в QuatCoreFastMemDecoder, из-за которых весь сыр-бор:

assign busy = isSource & SrcSquareBrac & (~fetching);

//осталось с ним сообразить. Когда приходит SrcAddr[7:6] == 2'b11 и SrcDiscard = 0, то невзирая на SrcStall, мы уже начали выборку! 
//и значит, когда нам "позволят" работать, уже получим результат!
//но ещё, когда SrcStall=0 и fetching=1, он должен сброситься опять в ноль,
//чтобы идущая сразу следом команда не могла ошибочно выдать результат СРАЗУ
always @(posedge clk)
	fetching <= (~SrcDiscard)&(SrcAddr[7:6] == 2'b11)&((~fetching)|SrcStall);


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

Когда идёт команда с квадратными скобками (собственно, выборка из памяти), то на первом такте "зажигается" busy, поскольку выполнены все условия. Это нужно, поскольку на адресном входе оперативной памяти стоит "защёлка", а заранее знать, какой из адресов нам понадобится (из 60 разных вариантов) QuatCoreMem никак не может. Значит, по окончанию этого такта правильный адрес только появится на входе этой "защёлки", и только на следующем такте мы сможем получить запрошенное значение.

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

И на следующем такте действительно busy=0, а fetching = 1 к следующему такту снова превращается в fetching = 0, поэтому мы можем использовать сколько угодно операций "в квадратных скобках" подряд - и всё будет правильно.

Но мы, похоже, нашли изъян. Когда первая операция "без квадратных скобок", то есть обращение к самому регистру X, то busy=0, но к следующему такту fetching всё равно устанавливается в единицу! Нда, сколько нам открытий чудных.

Исправляем:
always @(posedge clk)
	fetching <= (~SrcDiscard)&(SrcAddr[7:6] == 2'b11)&SrcSquareBrac&((~fetching)|SrcStall);


Добавили в выражение сигнал SrcSquareBrac, так что команда "без квадратных скобок" не приведёт к ошибочному появлению fetching=1.

Вот теперь самое время всё перекомпилировать и пересинтезировать и пересимулировать :) Размер ничуть не изменился, 1002 ЛЭ, уже радостно. Продолжаем с того же места:



ДА! Исправили! Теперь команда SrcAddr = 0xC8, то бишь [X+k], занимает два такта, по завершению которых получаем свои законные 0x12. Их мы и заносим в регистр X (т.е продвинулись на один элемент в списке). Тем временем, инициализируем аккумулятор нулём, а затем вычитаем опять [X+k], но с тех пор X обновился, поэтому это наша ссылка с текущего элемента, а там уже лежит 0x8000, число, которое у нас означает Null, он же Nil.

Заметим, что на вычитание и так уходит 3 такта, так что механизм выборки мы не испортили - лишнего такта здесь не требуется.

Наконец, приходим к команде JGE @@FinalRange. Поскольку при вычитании из нуля сугубо отрицательного значения 0x8000 получилось положительное значение, прыжок осуществляется, причём на уже знакомую нам строку 0x36.

Там мы на удивление быстро, всего за один такт, отправляем команду ACQ WholeRow, на обработку всей следующей строки. Немудрено: пока мы ждали какого-то выхлопа от GPU, он уже успел переварить 3 команды и остался с последней у себя ACQ HSync, которую ковыряет прямо сейчас. Сейчас мы внесли ACQ WholeRow, и осталось ещё 2 свободных ячейки во входном FIFO.

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


Да, и следующая команда, ACQ HSync выполнилась очень быстро, за такт, но потом мы ещё на такт застряли на выборке [SP+2j], это у нас номер текущей строки. Поскольку мы добавили недостающую строку кода, то теперь у нас загрузилось 0x0027 (то есть, тогда не просто вычли единицу, но и сохранили новое значение в память), а сейчас снова вычитаем единицу, и полученное значение сохраняем назад. Кстати, мы могли бы сэкономить один такт и сделать сохранение уже вслед за прыжком, т.к обращение к аккумулятору сразу вслед за арифметической операцией - само по себе RAW Hazard, который АЛУ разрешает самостоятельно, задерживая исполнение на один такт. Ну да ладно...

И наконец, мы снова прыгаем в начало цикла.


Жизнь потихоньку начинает удаваться: исправив пару ошибок, мы успешно прошли одну "информативную" строку изображения и "заказали" обработку следующей. Правда, пока эта строка была пустой, поэтому далеко не весь код был исполнен. Пока мы увидели в действии 28 строк кода из 82, т.е "покрытие 30%". Надо продолжать...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments