nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Разгоняем QuatCore до 25 МГц, часть 4

Пора текстовые описания части 2 и части 3 "скомпилировать" в что-то похожее на принципиальную схему.

Это "фиктивная" схема, просто я понял, что "рисовать" в том же квартусе мне проще, чем в пэйнте, и даже на бумажке :)

Кстати, забавно, что квартус допускает модуль, не имеющий ни одного ВЫХОДНОГО пина (а значит, вещь в себе, никак не влияющая на схему в целом), и даже модуль ВООБЩЕ БЕЗ ПИНОВ! Спорим, вы ТАКОГО ещё не видели :)


Приятная вещь состоит в том, что все воздействия на сторону DestAddr (верхние 3 модуля), как Discard, так и Stall - поступают с регистров, т.е не зависят ни от каких сигналов, вырабатываемых НА ЭТОМ ТАКТЕ.

Далее, модули на стороне DestAddr могут КОМБИНАТОРНО воздействовать на сторону SrcAddr. Команда JMP, к примеру, тут же запустит SrcDiscard, и команда на стороне SrcAddr будет проигнорирована.

И наконец, модули на стороне SrcAddr КОМБИНАТОРНО ни на что не влияют - их влияние проявляется только к следующему такту. А именно, они могут приостановить конвейер (заморозить SrcAddr и DestAddr), могут привести к появлению SrcDiscard=1, а вслед за ним: DestDiscard=1.

Но КОМБИНАТОРНОЙ ОБРАТНОЙ СВЯЗИ (Combinatorial loops) у нас тут явно нет!

Вот вроде небольшая схема - всего 4 элемента "ИЛИ" и 3 регистра-защёлки, а ни разу не интуитивная, просто "по наитию" её нарисовать невозможно.

Давайте мысленно прогоним через неё команды из прошлой части:

C   [X+i]
FMA [X+k]
CALL foo
CALL bar

...
foo proc
  JMP [--SP]  ;пока пустая процедура - сразу возвращает выполнение
  CALL bar
foo endp


Сначала загружается команда
XXX  [X+i]

и DestDiscard=1. Для нас это просто "начальное значение регистра". Также SrcDiscard=0 и DestStall=0.

Отсюда вытекает, что все модули Dest неактивны. Также заведомо их выходы stall_req (запрос остановки конвейера) и DoJump нулевые. Следовательно, модулям Src работа разрешена.

В данном случае, поскольку команда [X+i], модуль PCSrc неактивен и его выход DoCall нулевой. Так что на оба триггера, отвечающие за SrcDiscard / DestDiscard, поступают нули.

А вот модуль MemSrc активен, поскольку команда [X+i] принадлежит именно ему. А поскольку это команда чтения из памяти, занимающая два такта, на выходе stall_req устанавливается единица. Это даёт ROMstall=1 (значения DestAddr и SrcAddr замирают на том же месте). Также ROMstall должен воздействовать на модуль PC (Program Counter).

Получается, что модуль QuatCorePC концептуально состоит из трёх частей. Это часть, отвечающая за обработку команд DestAddr, она должна отключаться по DestDiscard или DestStall. Вторая часть отвечает за обработку команд SrcAddr, она должна отключаться по SrcDiscard или SrcStall. Но есть и ТРЕТЬЯ ЧАСТЬ - это сам регистр PC, который даже когда нам запретили принимать команды, должен как ни в чём не бывало прибавлять единичку на каждом такте - или замирать по ROMstall. Эта третья часть и есть "двигатель конвейера" :)

А я изображал его большим "монолитным" модулем - и ещё удивлялся, почему не заработал мой конвейер с полпинка :) Оттого и не заработал, что три разных сигнала stall должны воздействовать на него по-разному!

Также видно, что триггер, хранящий состояние DestDiscard имеет вход разрешения записи, и во время остановки конвейера продолжает хранить своё состояние. Поэтому на первом такте исполнения нашей команды "XXX [X+i]" началась выборка данных из памяти, и больше ничего. На втором такте снова имеем DestDiscard=1, и в придачу DestStall=1 (нижний триггер), поэтому команда по DestAddr не исполняется (она попала в конвейер "по инерции"). Тем временем выборка данных закончилась, поэтому stall_req = 0. Соответственно, ROMstall = 0, поэтому PC возобновляет работу.

Теперь мы начинаем исполнять команду
C  [X+k]


Команда "С" начинает исполняться, поскольку DestDiscard=0 и DestStall=0 (нижний триггер) - так было зафиксировано по фронту тактового импульса. Это команда, которая выполняется за 1 такт, поэтому stall_req на выходе нулевой. Поэтому никто не мешает выполниться и команде [X+k]. Это опять чтение из памяти, и снова устанавливается stall_req=1 на выходе SrcMem и, соответственно, ROMstall=1.

На следующем такте устанавливается DestStall=1, поэтому команда "C" не выполняется повторно - оно и правильно, на шине данных уже могли появиться неправильные данные.

Предположим, что вместо команды "С" мы здесь применили команду "Acc", выполняющуюся 3 такта. Интересно посмотреть взаимодействие двух модулей, каждый из которых хочет остановить конвейер. Снова, именно команде Acc принадлежит приоритет - так сделано, поскольку только сейчас на шине данных лежит правильное значение. К следующему такту оно может измениться, поэтому команде в DestAddr нельзя щёлкать клювом - надо работать!

На выходе ALUdest появится stall_req=1, который пойдёт на входы stall в PCSrc и MemSrc. Заметим, что модулю памяти не стоит прямо-таки "замирать". Заморозить инкременты/декременты SP (если таковые заданы в команде) - это да. А вот выборку памяти надо начинать. Устанавливать у себя stall_req=1 тоже не стоит - ни к чему это. И нужно каким-то образом зафиксировать тот факт, что выборку мы уже начали, и уже на следующем такте сможем защёлкнуть правильные данные.

Тогда по окончании работы АЛУ, также прекратит работу и MemSrc - на шину данных попадёт правильное значение, и мы приступим к следующей команде. Если так подумать, в ответ на сигнал SrcDiscard мы могли бы сделать то же самое - сохранить внутреннее состояние (то есть, SP), а на шину данных подать то, что просят. Даже если оказывается, что данную команду выполнять не надо - оно не повредит!

Поехали дальше.

FMA   CALL0


Снова приоритет за DestAddr: в АЛУ поступает FMA (Fused Multiply-Add), одна из самых длинных (по выполнению) команд. Тут же stall_req=1. Он блокирует работу PCSrc, поэтому DoCall=0, хоть и команда CALL0. Обязательно вызовем, но позже :)

Как обычно, останавливается конвейер, все ждут выполнения команды FMA. Наконец, на последнем такте её выполнения выставляется stall_req=0, давая дорогу PCSrc. Та видит команду вызова процедуры и выставляет DoCall=1. Она защёлкивается в триггере. У этого триггера нет входа разрешения, поскольку, как мы увидели, если даже в DestAddr стояла команда, выполняющаяся долго, PCsrc запустится лишь на самом последнем такте, и сама останавливать конвейер не будет. Так что именно к следующему такту нам нужен SrcDiscard=1, и ни как иначе. Другое дело, что если команда по DestAddr на следующем шаге будет выполняться долго, то надо удерживать SrcDiscard на всё время её выполнения. Это та самая команда, которая что-то делает с адресом возврата. Так-то мы ожидаем, что это будет либо [SP++], либо C, но никак не Acc какой-нибудь (вот уж придумали, куда адрес возврата пихать!). Но для единообразия можно и поставить вход разрешения - не помешает и скорее всего обойдётся "бесплатно".

Переходим к следующей команде. (тем временем в PC устанавливается адрес процедуры)
[SP++] CALL1


К нам пришёл SrcDiscard = 1, который отключает правую часть команды, чтобы не выполнить команду, идущую сразу после команды вызова процедуры. Она случайно к нам попала. А [SP++] выполняется без проблем, за один такт. Как видим по схеме, MemDest - замечательная вещь, очень неприхотливая.

Переходим к следующей команде (в SrcAddr уже пришло правильное значение - первая команда из процедуры):
[SP++] [--SP]

А к этому моменту SrcDiscard = 0, зато DestDiscard = 1 (эта единичка у нас распространилась по цепочки триггеров слева направо). Так что игнорируем [SP++], но получаем [--SP]. На первом шаге запрашиваем память, при этом stall_req=1, на втором - вычитаем единичку и выдаём на шину данных правильное значение - ранее занесённый адрес возврата.

А к следующей команде мы наконец-то вышвырнули со своего конвейера неправильную команду:

JMP  CALL1


Здесь JMP сразу же устанавливает SrcDiscard = 1, делая команду CALL1 недействительной. Мало того, он устанавливает единичку и в триггер слева, так что на следующем такте будет SrcDiscard=1 и DestDiscard=1, а на такте за ним: SrcDiscard=0 и DestDiscard=1. Забракованными оказывается ДВЕ команды, чего и следует ожидать - слишком уж поздно мы отреагировали, что такая фигня творится.


Похоже на правду... Теперь нужно реальные модули подправить - и попробовать запустить на том же "аффинном алгоритме"...

UPD. Для модуля MemSrc есть существенная разница между входами stall и discard. Когда поступает stall=1, мы можем быть уверены, что когда он сбросится в ноль, это будет ТА ЖЕ САМАЯ КОМАНДА, поскольку конвейер был остановлен. А значит, те данные, что мы приготовили - те, что нужно. Если же discard=1, то да, мы имеем право подавать на выход всё что захотим. Но главное, когда снова будет discard=0 - мы не можем быть уверены, что прочитали ровно то, что от нас просили, поэтому нужно будет снова установить stall_req=1 и выдать данные только на следующем такте.

Именно поэтому мы не стали эти сигналы объединять по ИЛИ, как сделали на стороне DestAddr.

Да, мозги продолжают плавиться.
Tags: ПЛИС, программки, работа, странные девайсы
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 

  • 4 comments