nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

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

Лучше день потерять, а потом за 500 мкс долететь!

Увы, перечисления из прошлой части по-прежнему недостаточно, "циклические ссылки", или точнее, комбинаторные обратные связи (combinatorial loops) по-прежнему зловеще нависли над этим проектом. Скажем, пришли две очередные половинки команды, одна в DestAddr, другая в SrcAddr. В одной, DestAddr, лежит FMA - Fused Multiply-Add - умножение с накоплением, и запрашивает остановить конвейер на ближайшие 17 тактов. А в другой, SrcAddr, лежит CALL0 - вызов "нулевой" процедуры (у нас их может быть 16 штук, от 0 до 15, лежат в таблице QuatCoreCallTable.v), которая "хочет" поскорее перепрыгнуть на новое место, а последующую команду забраковать.

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

Вот из-за таких хитростей у меня "самовозбуд" начался, пора разобраться. Всё не так сложно, как кажется.


Возьмём первый тестовый кусочек кода:

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

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


Сначала загрузили значение в регистр C, затем провели умножение с накоплением, а после этого вызываем процедуру. Причём мы только что перепрыгнули на первую строчку этого кода. Тогда на первом такте процессор увидит следующее:

XXX   [X+i]

В DestAddr - "неверное" значение, соответствующее команде после команды прыжка. К шинам Data, DestAddr и SrcAddr у нас очевидно добавляются ещё 2 провода: DestDiscard и SrcDiscard. Вот на данный момент DestDiscard=1, а SrcDiscard=0. (как именно они сформировались, пока что опустим). Из-за того, что DestDiscard=1, мы даже не пытаемся изучать команду по DestAddr. К примеру, если на неё поступил DestDiscard=1, она заведомо не подаст запроса на остановку процессора, типа "я не я и команда не моя".

А вот [X+i] изучаем подробнее. Это чтение из памяти, поэтому из QuatCoreMem должно поступить stall=1 - запрос на остановку конвейера. Таким образом, DestAddr и SrcAddr фиксируются на том же месте, в QuatCoreMem формируется эффективный адрес и защёлкивается на входе ОЗУ.

На следующем такте у нас ОБЯЗАТЕЛЬНО должно сохраниться значение DestDiscard=1, иначе мы всё-таки выполним эту команду. Значит, и регистр, хранящий DestDiscard, также должен останавливаться в этой ситуации. Значит, команда в DestAddr снова игнорируется и сигнала stall не выдаёт. Тем временем на шину данных наконец поступили правильные данные и защёлкнулись в мультиплексоре шины данных. stall=0, так что мы наконец-то переходим к следующей команде:

C  [X+k]


К этому времени должно установиться DestDiscard=0, т.е "неправильную" команду проехали уже. Также SrcDiscard=0, так что обеим командам "верить".

По DestAddr - команда записи в регистр C, одна из самых безобидных. Она выдаёт stall=0. Но по SrcAddr - опять идёт команда чтения из памяти, выставляющая stall=1. И вот тут мы понимаем, что команда "C" просто обязана выполниться ТОТЧАС ЖЕ! Только сейчас в шине данных лежит то, что нам нужно, значение [X+i]. К следующему такту значение может уже исказиться. Более наглядно, если вместо [X+i] там был литерал, например, 5. По окончании текущего такта в шину данных придёт другое значение, что-то из памяти, но ещё не [X+k], а какое-то прошлое значение. Итак, "C" выполняется, и [X+k] выполняется - на вход ОЗУ защелкивается правильный эффективный адрес.

Начинается следующий такт. Те же там же (конвейер был остановлен). И теперь уже мы ОБЯЗАНЫ остановить команду "C", иначе она повторно занесёт данные в регистр, только теперь это КРИВЫЕ данные! Получается, что stall, который ИНИЦИИРУЕТСЯ СО СТОРОНЫ SrcAddr, должен задерживаться на такт и только после этого управлять стороной DestAddr! Тогда всё получится... Наконец-то на шине данных появляется значение [X+k] и защёлкивается на мультиплексоре. А со стороны DestAddr всё без изменений.

Переходим к следующей команде:
FMA   CALL0


Опять вспоминаем, что Call foo преобразуется в [SP++] CALLn, где n-номер процедуры в таблице QuatCoreCallTable.v. Команда CALLn подаёт на шину данных адрес возврата и осуществляет переход, а [SP++] заносит этот адрес возврата в стек.

И тут становится отчётливо понятно: FMA имеет явный приоритет перед CALL0, ведь это ещё до конца не выполненная ПРЕДЫДУЩАЯ КОМАНДА. Она обязана быть выполнена. Поэтому, FMA имеет полное право остановить конвейер и спокойненько в течение 18 тактов выполнять умножение с накоплением. Всё это время CALL0 "отдыхает".

Наконец, на последнем такте выполнения FMA, stall устанавливается в 0, и активируется CALL0. В итоге, к следующему такту на шине данных защёлкивается адрес возврата, на счётчике инструкций (PC) - адрес процедуры, и также к следующему такту "зажигается" SrcDiscard=1. В аккумуляторе лежит корректный результат умножения с накоплением.

Переходим к следующей команде:
[SP++] CALL1


При этом "горит" SrcDiscard=1. Поэтому команду CALL1 игнорируем начисто - нет её. А будь на её месте опять какой-нибудь доступ к памяти - не позволили бы останавливать процессор, ещё чего, это команда, идущая ЗА вызовом процедуры. Она случайно, "по инерции" к нам залетела. Так что мы спокойненько заносим значение из шины данных в [SP++] - и двигаем дальше.

[SP++] [--SP]


О, шикарно! Причём сейчас DestDiscard=1, но SrcDiscard=0. То есть, в левой части "огрызок" команды Call bar, а вот в правой части уже правильная команда из процедуры foo. Небольшое дежа вю - мы выполняем [--SP] за два шага, всё это время продолжается DestDiscard=1. На шине данных появляется адрес возврата.

Переходим к следующей команде:
JMP  CALL1

Жизненно - куда бы мы не шли - попадаем в bar.
Итак, ситуация, которая раньше казалась нам происками злых хакеров, когда два разных способа прыжков соседствуют бок о бок в одном модуле. С введением конвейера это стало суровой правдой жизни. К счастью, ничего страшного здесь нет - ситуация разрешается абсолютно однозначно. Ведь мы знаем что JMP - это ПРЕДЫДУЩАЯ команда, а CALL1 - ТЕКУЩАЯ. У предыдущей очевидный приоритет. Если там прыжок, значит CALL1 недействителен. Так что уже сразу, увидев JMP, мы выставляем SrcDiscard=1, и он ТУТ же гасит нам любую команду, лежащую в SrcAddr.


Фух, что-то начинает проясняться, но мозги плавятся...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Ремонт лыжных мостиков

    Вернулся с сегодняшнего субботника. Очень продуктивно: отремонтировали все ТРИ мостика! Правда, для этого надо было разделиться, благо народу…

  • Гетто-байк

    В субботу во время Великой Октябрьской резни бензопилой умудрился петуха сломать в велосипеде. По счастью, уже на следующий день удалось купить…

  • А всё-таки есть польза от ковариаций

    Вчера опробовал "сценарий", когда варьируем дальность от 1 метра до 11 метров. Получилось, что грамотное усреднение - это взять с огромными весами…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments