nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

FIFO на ЛЭ: работа над ошибками

Почти месяц назад я предложил свою реализацию буфера FIFO на логических элементах, заточенную под небольшие размеры: выделять целый Блок Внутренней Памяти (БВП, он же EAB) жалко, а реализация scfifo (а тем более dcfifo), если поставить галочку "сделать на логических элементах" - весьма прожорливая, я углядел возможность подсократить её в 1,5 раза.

Как было замечено в комментариях, такой подход (гигантский сдвиговый регистр вместо памяти с произвольным доступом и двумя указателями, бегающими по кругу) при большой длине буфера может пожирать довольно много энергии: одновременное переключение ВСЕХ занятых регистров даёт бросок тока, может и помеха по питанию возникнуть. Согласен, поэтому такой подход хорошо годится для МАЛЕНЬКИХ буферов, а мне здесь и нужен маленький, в противном случае не пожалел бы блок внутренней памяти аж на 512 байт.

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

Пора доделать наш буфер FIFO, чтобы его работа стала корректной во всех случаях. Желательно, чтобы он при этом не шибко разжирел...


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

А вот управляющую логику надо чуточку переделать. Чтобы понять, как именно, поразмышляем, как этот буфер должен работать.

Пустой буфер
В сдвиговом регистре сплошной мусор: {x,x,x,x}, в унарном счётчике сплошные единицы: {1,1,1,1}.
При поступлении wr_req=1 (запрос на запись), мы должны записать значение в крайний правый регистр, а с остальными имеем право делать что хотим. То есть, к следующему такту должно получиться {x,x,x,D0} и {1,1,1,0} в счётчике.
Поступление rd_req=1 (запрос на чтение) в данный момент является ошибочным: извлекать-то нечего! Я вполне уверен, что ни процессор QuatCore, ни видеопроцессор не будут посылать запрос на чтение, пока empty=1, так что могу пока этот случай не рассматривать - что получится - то и ладно. Undefined Behaviour как он есть
При поступлении одновременно wr_req=1 и rd_req=1 мы могли бы сделать "байпас", который соединит вход с выходом, но придётся и выход empty тогда сделать комбинаторным - чтобы он менялся с единицы на ноль, когда что-то приходит на запись, а это всё меня очень напрягает - ПЛИС у меня МЕЕДЛЕННАЯ, могу все тайминги завалить из-за длинных комбинаторных путей. Я бы предпочёл, чтобы данные спокойно записались в пустой буфер, к следующему такту стало бы ясно, что он не пуст - и выполнился бы запрос на чтение.

Пока мало что понятно... Но наша первая реализация пока что всё делает правильно!

Один элемент в буфере
В сдвиговом регистре одно корректное значение, а остальное - условно "мусор": {x,x,x,D0}, в унарном счётчике единицы и нолик на конце: {1,1,1,0}.

При поступлении wr_req=1, мы должны записать значение во второй регистр справа. В крайний правый лезть НЕ ИМЕЕМ ПРАВА, а вот с регистрами левее можем делать что угодно. К следующему такту должно получиться {x,x,D1,D0} и {1,1,0,0} в счётчике.
При поступлении rd_req=1, мы, как ни странно, можем делать со сдвиговым регистром что угодно! Единственное значение, что в нём содержалось, будет прочитано на этом такте, и к следующему такту наш буфер считается пустым, так что в нём может содержаться что угодно: {x,x,x,x} и {1,1,1,1} в счётчике.
При поступлении одновременно wr_req=1 и rd_req=1, мы должны отправить новое значение на запись в крайний правый регистр, с остальными можем делать что угодно: {x,x,x,D1}, и счётчик остаётся в том же положении: {1,1,1,0}.

Наша первая реализация правильно исполняет случае wr_req=1 или rd_req=1, но не когда оно одновременно!

Два элемента в буфере
В сдвиговом регистре два корректных значения: {x,x,D1,D0}, в унарном счётчике два нолика на конце: {1,1,0,0}.

При поступлении wr_req=1, мы должны записать значение во второй регистр слева. В два правых лезть не имеем права, с крайним левым делаем что хотим. К следующему такту должно получиться {x,D2,D1,D0} и {1,0,0,0} в счётчике.
При поступлении rd_req=1, мы должны сдвинуть значение из второго справа регистра в крайний правый, а всё остальное - по барабану! В итоге получится {x,x,x,D1} и {1,1,1,0} в счётчике
При поступлении одновременно wr_req=1 и rd_req=1, мы должны занести новое значение на запись во второй регистр справа, а его, в свою очередь, сдвинуть вправо, с остальными можем делать что угодно: {x,x,D2,D1}, счётчик остаётся без изменения: {1,1,0,0}.

Вот начинает что-то просматриваться... Три элемента пропустим, там всё понятно.

Четыре элемента в буфере
Все значения корректные: {D3,D2,D1,D0}, в унарном счётчике все нули: {0,0,0,0} ("местов нет!")

При поступлении wr_req=1 у нас возникает "переполнение". Но я бы предпочёл, чтобы такая ситуация была допустимой. К примеру, QuatCore пытается подать новый запрос видеопроцессору, а тот ещё старые не обработал. Шина данных QuatCore - тоже вполне себе хранилище, пущай мы застрянем на выполнении команд ACQ / TRK, тщетно "пытаясь" поместить новое значение в буфер, а когда наконец-то это получится, работа конвейера QuatCore возобновится. Так мы сможем хранить одним значением больше, как бы на шине данных. В буфере должно всё оставаться "как было": {D3,D2,D1,D0} и {0,0,0,0} в счётчике.
При поступлении rd_req=1 мы читаем крайнее правое значение (как и всегда) и осуществляем сдвиг. При этом, крайний левый регистр может делать всё, что ему заблагорассудится, т.к к следующему такту он будет считаться "пустым": {x,D3,D2,D1} и счётчик {1,0,0,0}.
При поступлении одновременно wr_req=1 и rd_req=1, мы записываем значение в крайний левый регистр, все остальные заносят значение "слева" от себя: {D4,D3,D2,D1}, а счётчик остаётся на том же месте: {0,0,0,0}.

Если снова взглянуть на нашу "принципиальную схему":


то окажется: входы LOAD нужно запитать от регистров унарного счётчика НА ЕДИНИЦУ ЛЕВЕЕ! Когда LOAD=0 и ENA=1, регистр загружает значение из регистра левее себя ("сдвиг"), а при LOAD=1 и ENA=1 - параллельную загрузку. Так вот, смысл в сдвиге есть лишь в том случае, когда слева от тебя лежит что-то осмысленное, поэтому и сигналом к сдвигу должен служить регистр, указывающий занятость предыдущей ячейки! На крайний левый регистр мы тогда можем подать LOAD=1 - параллельная загрузка ВСЕГДА, потому как ничего другого ему не остаётся, регистра ещё левее попросту нет.

А вот цепи формирования ENA менять не следует! Пусть, к примеру, у нас два элемента в буфере: {x,x,D1,D0}, счётчик {1,1,0,0}.
Если мы подадим rd_req=1, то ВСЕ регистры будут активны (ENA=1), при этом для всех регистров, кроме крайнего правого будет LOAD=1 (параллельная загрузка), а для крайнего правого: LOAD=0 (загрузка из регистра левее него), поэтому в итоге мы получим {D2,D2,D2,D1}. Счётчик тем временем осуществит "сдвиг вправо": {1,1,1,0}. То, что отмечено "единицами" - считается мусором, и так оно, в сущности и есть. Всё чётко.
Если мы подадим wr_req=1, то активны (ENA=1) будут два левых регистра. А LOAD=1, как и в прошлый раз, у трёх регистров, и только у крайнего правого LOAD=0. Как результат, мы загрузим новое значение в два левых регистра: {D2,D2,D1,D0}, счётчик {1,0,0,0}. Да, всё хорошо.
Если мы подадим одновременно wr_req=1 и rd_req=1, то активны (ENA=1) будут ВСЕ регистры. LOAD=1 будет для трёх регистров, кроме крайнего правого, поэтому мы загрузим новое значение в три регистра, а в крайний правый передвинем значение левее него: {D2,D2,D2,D1}, счётчик останется на месте: {1,1,0,0}. Шикарно!

И ещё давайте посмотрим на заполненный до отказа буфер: {D3,D2,D1,D0} и {0,0,0,0}.
Если мы подадим rd_req=1, то все регистры будут активны, и все будут работать на сдвиг, кроме крайнего левого. Получим {D4,D3,D2,D1} и {1,0,0,0}. Как всегда, "не удержались", записали значение со входа, когда нас не просили, ну и ладно, оно числится как "мусорное".
Если подадим wr_req=1, то ни один из регистров не будет активным, поэтому хранящиеся данные никто не потревожит. Кроме того, не сдвинется с места и счётчик, поскольку там стоит логическое "И" с крайним левым регистром унарного счётчика. Всё ровно так, как мы хотим.

Наконец, подадим одновременно rd_req=1 и wr_req=1. Все регистры будут активны, но только крайний левый загрузит значение со входа, остальные осуществят сдвиг: {D4,D3,D2,D1}. Со счётчиком ситуация интереснее: сигнал wr_req=1 в нём окажется "замаскирован", поэтому возобладает сигнал rd_req=1, что приведёт к ENA=1 на всех регистрах счётчика. И сосчитать он захочет влево, поскольку wr_req=1. И пусть себе считает: {0,0,0,0} превратится в {0,0,0,0} - нас это вполне устраивает!

Похоже, что мы обошлись совсем малой кровью. Нарисуем, как оно по схеме:


Ну и верилоговский модуль подправим:
module FIFO_on_LE_4steps (input clk, input [DataWidth-1:0] D, input rdreq, input wrreq, output empty, output nfull, output [DataWidth-1:0] Q);

parameter DataWidth = 10;

reg [DataWidth-1:0] R1;
reg [DataWidth-1:0] R2;
reg [DataWidth-1:0] R3;
reg [DataWidth-1:0] R4; //Registers 1-2-3-4
reg U1 = 1'b1, U2 = 1'b1, U3 = 1'b1, U4 = 1'b1;				//Used 1-2-3-4

assign empty = U4;
assign nfull = U1;
assign Q = R4;

wire ena1 = rdreq | (wrreq & U1);
wire ena2 = rdreq | (wrreq & U2);
wire ena3 = rdreq | (wrreq & U3);
wire ena4 = rdreq | (wrreq & U4);
wire Uena = ((rdreq & (~empty)) ^ (wrreq & nfull));

always @(posedge clk) begin

	if	(ena1)
		R1 <= D;
	if	(ena2)
		R2 <= (U1? D : R1);
	if 	(ena3)
		R3 <= (U2? D : R2);
	if	(ena4)
		R4 <= (U3? D : R3);
	
	if (Uena) begin
		U1 <= (wrreq? U2 : 1'b1);
		U2 <= (wrreq? U3 : U1);
		U3 <= (wrreq? U4 : U2);
		U4 <= (wrreq? 1'b0 : U3);
	end;

end

endmodule


И разумеется, модуль по-прежнему успешно синтезировался в те же самые 49 ЛЭ, что не может не радовать.


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

  • Великая Октябрьская резня бензопилой

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

  • Очередная несуразность в единицах измерения

    Когда-то я написал программу PhysUnitCalc - калькулятор, умеющий работать с размерностями. Мне казалось, что я наступил уже на все грабли, которые…

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

    "В предыдущих сериях": мой прибор выдаёт 6 значений: 3 координаты и 3 угла, т.е все 6 степеней свободы твёрдого тела. Причём ошибки измерения этих 6…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 2 comments