nabbla (nabbla1) wrote,
nabbla
nabbla1

Параметризуемый FIFO на ЛЭ

Вот, по прошествии нескольких лет изучения верилога я наконец-то решил применить цикл for.

По-моему, это главная противопехотная мина языка - на ней подрываются все!

В первую очередь, новички, которым верилог кажется ещё одним языком программирования, навроде СИ, только с укуренным синтаксисом, и увидев знакомые конструкции типа if, for, while, они пытаются "написать программу", и это иногда даже получается, правда, сжирает даже крупную ПЛИС на пустяковой задаче (строится комбинаторная логика, которая решает всю задачу ЗА ОДИН ТАКТ). На этом процентов 80 людей "отваливается", навеки решив, что ПЛИС-это вообще фигня какая-то, стоит дохрена, а толку меньше чем от ардуины.

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

Но и всем остальным этот цикл преподносит сюрпризы, так как есть ЯЗЫК ВЕРИЛОГ, а есть СИНТЕЗИРУЕМОЕ ПОДМНОЖЕСТВО ЯЗЫКА ВЕРИЛОГ, причём это самое подмножество для каждого синтезатора своё, и нормально его формализовать они так и не удосужились!

Сейчас я обнаружил, что квартус терпит, если цикл for сидит внутри always-блока, а вот если always-блок загнать внутрь цикла for - выдаст ошибку, хотя концептуально это одно и то же. Ну и "непрерывные присвоения" через assign загонять в for НЕЛЬЗЯ, для этого должен применяться generate!

Тем не менее, что-то получилось:
module FIFO_on_LE (input clk, input [DataWidth-1:0] D, input rdreq, input wrreq, output empty, output nfull, output [DataWidth-1:0] Q, output stall);

parameter DataWidth = 10;
parameter ElemCount = 4;

reg [DataWidth-1:0] DR [ElemCount-1:0]; //Data Regs - регистры для данных
reg [ElemCount-1:0] CR = {ElemCount{1'b1}};			//Count Reg - регистр для "унарного счёта"
//пусть самый правый будет с индексом 0, а самый левый с индексом ElemCount-1

assign empty = CR[0];
assign nfull = CR[ElemCount-1];
assign Q = DR[0];

wire [ElemCount-1:0] ena;				//для регистров
wire cnt_ena = (rdreq & (~empty)) ^ (wrreq & nfull);	//для унарного счётчика

assign ena = {ElemCount{rdreq}} | ({ElemCount{wrreq}} & CR);	//"почленно" находим

integer i;
always @(posedge clk) begin
	for(i=0; i<ElemCount-1; i = i + 1)		//все, кроме последнего элемента
		if (ena[i])
			DR[i] <= CR[i+1]? D : DR[i+1];			
	if (ena[ElemCount-1])
		DR[ElemCount-1] <= D;
	if (cnt_ena) begin
		CR[0] 		<= wrreq? 1'b0 : CR[1];		
		for (i=1; i<ElemCount-1; i = i + 1)
			CR[i]	<= wrreq? CR[i-1] : CR[i+1];		
		CR[ElemCount-1] <= wrreq? CR[ElemCount-2] : 1'b1;
	end;			
end

assign stall = (~nfull) & wrreq & (~rdreq);

endmodule


Теперь можно задать количество элементов в буфере FIFO и их разрядность, а ещё появился новый выход stall...


И впервые я применил массив регистров. Тоже немножко странный синтаксис: слева, как обычно, указывается ширина одного элемента массива, а справа уже размерность массива. И в одной строке можно обращаться к одному-единственному элементу, т.е ввести два массива и потом где-нибудь в always-блоке один целиком присвоить другому не получится, только через тот же пресловутый цикл for. Ну, не очень-то и хотелось :)

Поначалу я хотел через for сделать выражения для ena[], ведь у нашего прототипа на 4 элемента записано было так:
wire ena1 = rdreq | (wrreq & U1);
wire ena2 = rdreq | (wrreq & U2);
wire ena3 = rdreq | (wrreq & U3);
wire ena4 = rdreq | (wrreq & U4);


Но для этого for не годится, как ни странно, выдаёт ошибку наподобие "ожидалось встретить название переменной или название блока, а нашёлся какой-то непонятный for (зарезервированное слово)". Я так понял, нужно использовать generate взамен, но мне лень, поэтому просто "размножил" сигналы rdreq и wrreq, чтобы записать всё это в виде почленной битовой операции:
assign ena = {ElemCount{rdreq}} | ({ElemCount{wrreq}} & CR);


Дальше начинается длиннющий always-блок.
Считаю, что при реализации подобных штук очень полезно использовать неблокирующие присваивания, т.е <= вместо обычного =

Потому что в таком случае попытка присвоить значение регистру НЕСКОЛЬКО РАЗ внутри always-блока сразу же даст ошибку при синтезе, и большинство ошибок с неправильно расставленными индексами ("ошибка на единицу" и иже с ними) сразу же будут обнаружены и исправлены! А вот для присваивания = считается вполне допустимым многократное присваивание. Мы можем записать
always @(posedge clk) begin
  Q = Q + 1;
  Q = Q + 1;
  Q = Q + 1;
end


И никаких проблем: синтезатор быстренько прочухает, что в конечном итоге нужно прибавить 3 - и сделает ровно так. Иногда удобно иметь такую возможность "не сразу давать окончательный результат" - вот именно что можно целую "программу" внутрь always-блока упихать, где по мере продвижения будет формироваться правильное значение. Присвоили одно, проверили, потом в цикле ещё чего-нибудь прибавили - и получили ответ :) Но за эту гибкость приходится расплачиваться - синтезатор понятия не имеет, а что нам надо, а потому не поймёт, правильно ли мы всё написали!

Ну и пару слов про stall. Как не трудно догадаться, это мы подготавливаемся к подключению этого модуля к QuatCore. Когда он будет подавать команду ACQ (ACQuire, "захват") или TRK (TRacK, "сопровождение"), это приведёт к подаче wrreq=1 на буфер FIFO. И если при этом буфер оказался забитым до отказа, мы должны "застрять" на выполнении этой команды, до тех пор, пока видеопроцессор не прочитает из буфера одно значение - как только он подаёт запрос rdreq, как мы можем с чистой совестью "трогаться с места".

Иными словами, у нас "зажигается" stall=1, если данные на запись так и не вошли в буфер.

Синтезируется вся эта штука в 50 ЛЭ при заданных параметрах (4х10 бит). А без сигнала stall: в те же самые 49 ЛЭ, как и раньше, что также вселяет оптимизм, что всё реализовано корректно. Но проверить хотя бы на симуляции всё равно необходимо!

Лениво придумывать какой-то навороченный testbench (потом всё равно понаблюдаем за всей системой "QuatCore+видеопроцессор" в сборе), так что просто накарябал Vector Wave File с входными данными в D и управляющими сигналами rdreq и wrreq:


При запуске буфер пустой, поэтому empty=1, и он не заполнен до отказа, поэтому nfull=1 (n значит negative, т.е сигнал обратной полярности). Ну и stall=0. На выходе ноль, но при empty=1 значение считается "мусорным", и это правильно - мы туда ничего не заносили.

На первом же такте мы заносим туда значение 0x42, и действительно, к следующему такту оно попадает в буфер, причём СРАЗУ НА ВЫХОД. Собственно, в этом отличие FIFO от обычного сдвигового регистра - не нужно ждать, пока значение продерётся от входа к выходу. И уже empty=0, т.е список больше не пустой.

Затем мы заносим значения 0x43, 0x44 и 0x45, но на выходе всё ещё висит 0x42, поскольку его "никто пока не забрал", а это у нас First In - First Out.

Наконец, мы пытаемся занести 0x46 - но не тут-то было: "зажигается" nfull=0 (значит список ЗАПОЛНЕН ДО ОТКАЗА) и stall=1 (значит мы пытаемся положить новое значение, а оно туда не положилось).

На следующий такт мы пытаемся занести 0x47 и в то же самое время прочитать одно значение. По-прежнему nfull=0 (ведь список всё равно полон), однако stall=0: нам как бы гарантируют, что значение 0x47 будет в списке, и мы можем заниматься своими делами. На этом такте мы должны "защёлкнуть" значение 0x42 с выхода, потому что к следующему такту его уже не будет.

И вот на выходе образовалось 0x43 - следующее значение, которое мы положили. И мы снова запрашиваем значение и кладём значение 0x48.
На следующем такте образовалось 0x44, мы запрашиваем ещё одно значение и кладём 0x49. После этого мы перестаём запихивать новые значения!

Смотрим, что же пойдёт наружу. После 0x42, 0x43 и 0x44 вышел 0x45, а сразу вслед за ним 0x47. Значение 0x46 "пропало", но нас об этом предупреждали, всё правильно. За 0x47 пришло 0x48 и, наконец, 0x49 - последнее значение, которое мы положили. Затем "зажглось" empty=1 и на выходе появилось 0x4D, которое тактом ранее было у нас на входе, но мы не присылали wrreq=1. Но это нормально: раз empty=1, то на выходе может лежать любой "мусор" :)

И наконец, мы "оставили буфер в покое" - и он остановился в своём "пустом" состоянии, с ещё одним "мусорным" значением на выходе.

Всё отработало ровно так, как мы ожидали. Конечно, мы проверили не все возможные состояния, но учитывая, как мы "прошерстили" этот модуль теоретически, я каких-то подлянок от него не ожидаю. Можно применять :)


Кстати, Timing Analyzer говорит, что этот модуль имеет предельную частоту 137 МГц, что на нашей медленной 5576ХС4Т ОЧЕНЬ недурственно! В сущности, "по построению" оно и видно, что масштабируется он очень хорошо, по крайней мере, пока fanout'а хватает (размножения сигнала на огромное количество входов).
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