nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Жадность до добра не доводит

Опять небольшое ковыряние привело к снижению допустимой тактовой частоты, до 24,45 МГц. Наверняка можно это дело побороть, ещё поковырявшись в настройках фиттера, и столь же наверняка оно ещё много раз изменится (то к лучшему, то к худшему), пока буду подключать ввод/вывод, но мне стало интересно, какие именно цепи нарушают тайминги:



По большому счёту, ровно один сигнал "не поспевает", вот этот:


(обведён толстым синим)
Давайте его немножко укоротим!


Как видно, по "OR" объединяется два сигнала, и каждый из них - с регистра. Один из них виден прямо на этой схеме, тот самый inst8. Второй закопан внутри QuatCorePC, а ещё точнее, в QuatCorePCadder:

always @(posedge clk) if (~busy) begin
	DestDiscard <= SrcDiscard;
end


Сигналы SrcDiscard и SrcStall, если помните, могли действовать по-разному. Когда мы делаем выборку памяти, и появился SrcStall=1, мы невозмутимо запрашиваем значение, и по снятию SrcStall готовы двигаться дальше (нам и самим нужна была "передышка" в 1 такт). А вот с SrcDiscard так поступить было нельзя, поскольку мог уже поменяться адрес, который нам нужно прочитать.

А вот DestDiscard и DestStall, похоже, всегда будут объединяться по OR, т.к смысл всех команд DestAddr - в осуществлении каких-то конкретных действий - записи в память, или куда-то на выход, или арифметические операции, или прыжки. И всё это нельзя сделать "на всякий случай", точнее, можно, но это уже совсем другой уровень :) Типа современных интелов и AMD, с их спекулятивными вычислениями и уязвимостями SPECTRE/MELTDOWN :) Боже упаси.

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

Если поступать "в лоб", то получится что-то такое:

module QuatCoreDestStallGenerator (input clk, input DestStallReq, input SrcDiscard, input PipeStall, output reg DestStall = 1'b0);

reg DestDiscard = 1'b0;

always @(posedge clk) begin
	DestDiscard <= PipeStall? DestDiscard : SrcDiscard;
	DestStall <= PipeStall? (DestDiscard | DestStallReq) : (SrcDiscard | DestStallReq);
end

endmodule


Два регистра по-прежнему нужны, поскольку если конвейер останавливается (PipeStall = 1), значение DestDiscard должно сохраняться.

Это если мы не налагаем никаких ограничений на подающиеся на вход сигналы. Но на самом деле все эти сигналы очень "завязаны" друг на друга:

1. DestStallReq и SrcDiscard исключают друг друга, ведь если SrcDiscard=1, то команда по SrcAddr не выполняется, и "попросить подождать" она не сможет,
2. DestStallReq=1 гарантирует PipeStall=1.
3. Когда SrcDiscard=1 и DestStall=1 (команда JMP отсекла целую пару Dest-Src), заведомо получится PipeStall=0, т.е на выполнение "ничего" лишних тактов не уходит, и к следующему такту выйдет SrcDiscard=0, но по-прежнему DestStall=1,
4. Когда SrcDiscard=1 и DestStall=0, конвейер может остановиться только по требованию команды DestAddr, и она же сама не отреагирует на DestStall, так что он может включиться и в 1, если так уж хочется.

В итоге приходим к простейшему выражению:

module QuatCoreDestStallGenerator (input clk, input DestStallReq, input SrcDiscard, output reg DestStall = 1'b0);

always @(posedge clk) begin
	DestStall <= SrcDiscard | DestStallReq;
end

endmodule


Рассмотрим 3 сценария, которые должны описать все варианты работы:

1. Переходы не выполняются, команда SrcAddr занимает дополнительный такт на выборку из памяти.

SrcDiscard=0, DestStallReq=1. В этот же такт DestStall=0 (команда по DestAddr сразу начинает выполняться, пока на шине данных правильное значение), а уже в следующий DestStall=1. Если это была "длинная команда", она не обращает внимания и продолжает выполняться (при этом сама просит остановить конвейер, выдавая SrcStall, и тем самым сбрасывая сигнал DestStallReq). Если "короткая", в один такт - она не выполняется повторно. На этом такте уже DestStallReq=0, поэтому к следующему будет DestStall=0.

2. Выполняется вызов процедуры. Если она вызывается "по-нормальному", через
[SP++]  CALL0

или на худой конец
C       CALL0

или

X       CALL 0

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

Допустим, перед вызовом процедуры была какая-то длинная команда, FMA. Тогда уже на первом её такте мы меняем PC, но сигнал SrcDiscard=1 формируется только по окончании выполнения FMA. Так что в любом случае дальше у нас поступает 2 "полукоманды":

[SP++]   xxx

Команда "справа" (SrcAddr) не выполняется, поскольку SrcDiscard=1. Команда слева занимает всего один такт. И уже к следующему такту у нас будет SrcDiscard=0, и DestDiscard=1.

Там мы начнём выполнять первую команду из процедуры, куда только что прыгнули. "В худшем случае" это окажется команда выборки из памяти (в будущем это может быть чтение данных "извне", например, из UART). Она установит DestStallReq=1, так что к следующему разу опять выйдет DestStall=1, и так, пока команда SrcAddr не окончит выполнение. После этого, наконец-то, DestStall=0, и следующая команда уже будет выполнена.

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

Можно придумать экзотический вызов процедуры, где над адресом возврата мы зачем-то производим арифметические действия, типа такого:

ADD   CALL0


Не знаю, зачем это может понадобиться, но даже в таком случае всё пройдёт нормально. Да, на сложение уходит 3 такта, поэтому на выполнении

ADD   xxx

DestStall=1 установится уже по окончании 1-го такта, но АЛУ, когда оно вышло из режима ожидания, когда оно "запустило" какую-то длинную операцию, на адрес и на DestStall вообще не обращает внимания! При этом, поскольку SrcDiscard обновляется только между командами (т.е когда PipeStall=0), то у нас по-прежнему будет "гореть" SrcDiscard=1, и DestStall будет оставаться единичным всё время выполнения, и ещё 1 такт спустя. Так что и здесь у нас всё в порядке.

3. Выполняется прыжок командами JMP, JL, JGE и т.д.
В таком случае уже при исполнении команды прыжка "зажигается" SrcDiscard=1. Прыжок всегда занимает один такт, а за счёт SrcDiscard=1 и вторая "полукоманда" затянуть этого не может, т.к мы её отключили.

К следующему такту будет SrcDiscard=1 и DestStall=1, и раз ни одна команда не выполняется, затягивать некому, и уже к следующему такту SrcDiscard=0, DestStall=1. Т.е мы наконец-то начали выполнять команды с "нового места", куда прыгнули.

И как видно, дальше всё идёт ровно так же, как и при вызове процедуры.

Пытаемся это дело реализовать.


Количество ЛЭ немного уменьшилось: раньше мы использовали 2 разных регистра и объединяли их потом по OR, а сейчас регистр остался только один, притом без "разрешения записи".

А вот тайминги не выдерживаются всё равно! Точнее, либо фиттер вообще не может схему скомпоновать, либо, если заставить его "агрессивно" заниматься компоновкой - тайминги не выдерживаются, 24.6 МГц - и всё тут!

Немного поразмыслив, я, кажется, понял, почему так получилось. Базовые регистры X,Y,Z,SP могут "защёлкиваться" в два разных регистра: это регистр шины данных, который лежит в SrcMux, и регистр адреса на входе в модуль памяти SRAM (БВП, он же EAB), примерно так:


Путь от мультиплексора базового адреса к защёлке адреса гораздо короче, он состоит всего из одного сумматора (т.е к адресу прибавляются выбранные смасштабированные индексы), и поэтому и те два бита, что выбирают базовый регистр (X/Y/Z/SP) могут формироваться довольно долго. А именно, это либо DestAddr[5:4], либо SrcAddr[5:4], а вот что именно - будет определяться DestAddr[7:6] (проверяем, хотим ли мы записать что-то в память или базовые регистры), DestStall (проверяем, что эта команда сейчас "активна") и DestAddr[3:0] (проверяем, что запись именно в память, а не в регистры). В итоге, каждый из этих входов является функцией от 11 бит (!). Это значит, как минимум, делается каскадирование, а может и несколько слоёв логических функций.

А вот тот дополнительный мультиплексор, который мы ставили, чтобы выполнить
[SP++]   Y

для выбора одного из четырёх регистров использовал непосредственно SrcAddr[5:4], и поэтому на его выходе правильные данные формировались довольно быстро, чтобы успеть распространиться ещё через 2 мультиплексора до фронта тактового импульса.

В итоге, я вернул на место этот мультиплексор, и хотя по логике вещей это должно было увеличить нам схему на 16 ЛЭ, в реальности добавилось всего 2, т.е всего стало 490 ЛЭ. И теперь отсинтезировалось легко и непринуждённо, на частоту 25,77 МГц, причём такую предельную частоту имеет один-единственный путь. Ещё один: 26,81 МГц, а дальше огромное количество путей с предельной частотой 27,03 МГц:



Причём эти "критические пути" уже совсем в другом месте, в QuatCorePC, выполнение условных переходов.

В общем, несколько дней коту под хвост. Хотя, может и не совсем: в любом случае обновил под новый процессор процедуры умножения кватернионов и поворота вектора с помощью кватерниона, и вспомнил, как работают все эти блокировки. Ведь теперь к ним нужно будет подсоединить модули ввода-вывода, которые могут приостанавливать конвейер. И добавил новую функциональность в транслятор, пусть сейчас и придётся её отключить...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Так есть ли толк в ковариационной матрице?

    Задался этим вопросом применительно к своему прибору чуть более 2 недель назад. Рыл носом землю с попеременным успехом ( раз, два, три, четыре),…

  • Big Data, чтоб их ... (4)

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

  • Потёмкинская деревня - 2

    В ноябре 2020 года нужно было сделать скриншот несуществующей программы рабочего места под несуществующий прибор, чтобы добавить его в документацию.…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments