nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Модуль управления видеопроцессором

Почти на неделю "утонул" в документации на свой будущий прибор, жуткая бюрократия, вот возвращаюсь в прекрасный мир ассемблера и ПЛИС :)

В прошлый раз мы сделали буфер между выходом видеопроцессора и входом QuatCore, через который будут идти результаты обработки изображения.

Сейчас ставим последнее, недостающее звено - модуль, принимающий команды от QuatCore, заносящий их "внутри себя" в очередь и управляющий модулями, которые непосредственно "перемалывают" поступающие пиксели: первый модуль, второй модуль.

До сих пор именно такие "модули управления" тяжело мне даются: Data Path мне как-то роднее :) Может, мне надо обрести веру в FSM, и наступит просветление, но учитывая что Quartus не понимает значений Don't care, а пока предпочту по-старинке, ручками. Но по крайней мере ни разу ещё не приходилось "опускаться" до микрокода, обходились обычной логикой, притом довольно простой.

Так выглядит модуль "на схеме", это его "интерфейс", если можно так выразиться:



Входов на удивление мало:
- тактовая частота clk, куда же без неё,
- синхроимпульсы Hsync (строчный синхроимпульс, от Horizontal) и Vsync (кадровый, от Vertical). Для наших целей надо, чтобы их длительность была 1 такт;
- стандартная шина QuatCore: 8-битный "адрес получателя" DestAddr, сигнал приостановки DestStall и 16-битная шина данных D.

А больше нам ничего не надо - до пикселей как таковых модулю управления никакого дела нет :)

Да и выходов скромное количество:
- номер пикселя на строке X - нужно, чтобы "защёлкивать" координату самой яркой точки,
- start - работает одновременно как сброс модулей видеопроцессора, чтобы они начали очередной отрезок "с чистого листа", и как сигнал для записи результатов в выходную очередь;
- flush - сигнал для опустошения выходной очереди по окончанию обработки отрезка со строчным синхроимпульсом;
- isSum - выбор режима работы "первого сумматора" - либо нахождение максимума, либо суммирование. Первое нужно в режиме захвата, второе - в режиме сопровождения;
- SrcStallReq - сигнал для QuatCore, чтобы он остановился при переполнении входного буфера заданий видеопроцессора. Таким образом, будет обеспечиваться синхронная работа QuatCore и видеопроцессора;
- UFLO (Underflow) - отладочный сигнал, который поначалу хочу вывести через "защёлку" на светодиод на отладочной плате. Будет зажигаться, если видеопроцессор уже отработал все поступившие задания, а нового не поступило. При нормальной работе такого происходить не должно - он всё время должен либо обрабатывать очередной отрезок, либо ждать прихода синхроимпульсов. Если UFLO будет срабатывать, это скорее всего означает слишком короткий буфер заданий - пока у QuatCore есть запас времени, он ничего не может сделать, поскольку застрял на отправке очередного задания, не влезшего в буфер, а потом, когда будут идти коротенькие отрезки один за другим - не будет поспевать за видеопроцессором, который будет щёлкать их как орешки.

Кроме того, введено 4 параметра:
- XregWidth - ширина числа, представляющего X-координату пикселя. По умолчанию 10, то есть пиксели у нас нумеруются от 0 до 1023. Захочется задействовать хотя бы все 1280 пикселей, которые предоставляет нам аналоговая камера - надо будет поставить 11, и столько же хватит для 1920х1080;
- ElemCount - размер входного буфера. Пока 4, а там видно будет;
- UseRowCount - нужно ли вообще в модуле управления подсчитывать, на какой мы сейчас строке? Изначально я хотел, чтобы видеопроцессор вообще "не заморачивался", а счёт мы уже будем вести внутри QuatCore, благо смена строк процесс очень медленный. Но алгоритм захвата в его текущей реализации будет правильно работать только если мы продолжим обрабатывать "фиктивные" строки, в которых видеопроцессор не будет обнаруживать никаких ярких точек, причём срабатывать будет "мгновенно", чтобы успеть всё обработать до начала следующего кадра. Это позволит не объединять списки ActivePoints и AllPoints "вручную", уже написанная программа в итоге перенесёт все точки в AllPoints, причём строго в порядке возрастания координаты Y, что может оказаться полезным для режима сопровождения;
- RowCount - количество полезных строк после кадрового синхроимпульса. Когда мы досчитаем до RowCount (и если UseRowCount=1), модуль управления будет исполнять каждое последующее задание за 1 такт - сразу же посылать нулевые результаты, и так до тех пор, пока не придёт следующий кадровый синхроимпульс.

Вот код этого модуля:
//сначала у нас все адреса 0xxx_xxxx были отданы под OUT
//затем мы ужались до 00xx_xxxx, поскольку 01xx_xxxx отдали под SRAM 
//и было 000x_xxxx: OUT,
//       001x_xxxx: SIO
//но давайте ещё пару адресов подкинем для GPU!
//и будет
//0000_xxxx: OUT,
//0001_xxxx: SIO,
//0010_xxxx: ACQ (Acquire, захват)
//0011_xxxx: TRK (Tracking, сопровождение)

//содержание шины данных
//старшие 2 бита: режим синхронизации
//чтобы случайно затесавшиеся МАЛЕНЬКИЕ отрицательные значения не портили малину, надо так (укуренность, но что делать):
//00, 11 - запускаемся по завершении предыдущего отрезка,
//10 - запускаемся по кадровому синхроимпульсу,
//01 - запускаемся по строчному синхроимпульсу
//кстати, это позволяет заодно обнаружить отрицательное значение, так что 2 лишних бит нам не надо!

//младшие биты (от 8 до 10, пока громко думаем) - длина очередного отрезка

`include "math.v"
module QuatCoreFullGPUinput (	input clk, input Hsync, input Vsync, input [7:0] DestAddr, input DestStall, input [15:0] D,
				output [XregWidth-1:0] X, output start, output flush, output isSum, output SrcStallReq, output UFLO);

parameter XregWidth = 10;
parameter ElemCount = 4;	//ставим буфер FIFO, чтобы 
parameter UseRowCount = 1'b1;
parameter RowCount = 720;	//остальное надо автоматом определить

localparam YregWidth = `CLOG2(RowCount);
localparam YinitVal = (1 << YregWidth) - RowCount;	//возможно, на единичку ошиблись, потом проверим
wire Yfinished;

wire isOurAddr = (~DestStall)&(DestAddr[7:5]==3'b001); //ACQ либо TRK
wire [XregWidth:0] BufOut;	//очередное задание на выполнение
wire [1:0] SyncOut;				//режим синхронизации на данный момент
wire rdreq;						//когда заканчиваем обработку очередного задания

	FIFO_on_LE_for_GPU buff (.clk(clk),
				.D({D[15:14],DestAddr[4],D[XregWidth:0]}),	//старшие биты: режим синхронизации, младшие 12: коорд. X, до которой надо идти
				.rdreq(rdreq),
				.wrreq(isOurAddr),
				.Vsync(Vsync),
				.Hsync(Hsync),
				.Q({SyncOut,isSum,BufOut}),
				.wr_stall(SrcStallReq),	//QuatCore останавливается и ждёт, пока мы освободимся
				.rd_stall(UFLO)			//видеопроцессор всегда должен быть занят. Если ему нечего делать - это подозрительно!
							);
	defparam
		buff.DataWidth = XregWidth+4,	//1 бит добавляем для "граничных условий", ещё 2 на синхроимпульс и 1 на тип команды (Acq или Trk)
		buff.ElemCount = ElemCount;				//пока не можем сообразить, сколько их надо

wire XCout;	//счётчик по координате X досчитал до упора
//старт по сигналу с компаратора (но ещё и с учётом "переполнения")
wire start_neg = SyncOut[1]&SyncOut[0];	//если попало отрицательное число, выдаём незамедлительно!
wire start_oflo = (~SyncOut[1])&(~SyncOut[0])&(BufOut[XregWidth] & XCout);	//число положительное, но ООЧЕНЬ большое, больше нашей разрядности!
wire start_normal = (~SyncOut[1])&(~SyncOut[0])&(BufOut[XregWidth-1:0] == X);	//нормальное число, и совпало с номером пикселя

assign start = start_neg | start_oflo | start_normal | (Yfinished & UseRowCount);	//хоть что-нибудь из этого выполнится - и мы стартуем
assign rdreq = start; //похоже, вышло одно и то же
//хотим, чтобы flush запустился уже ПОСЛЕ прихода строчного синхроимпульса
//для этого нужен дополнительный регистр, запоминающий, что именно строчных синхроимпульсов мы и ждали
reg FrontPorch = 1'b0; //"признак" передней полочки перед началом строки
always @(posedge clk)
	FrontPorch <= (SyncOut[0] & Hsync)? 1'b1 : start? 1'b0 : FrontPorch;
	//в момент прихода синхроимпульса, если его мы и ждали, защёлкивается, а когда отсчитали сколько хотели - сбрасывается
	
assign flush = start & FrontPorch;

lpm_counter Xcounter (	.clock (clk),		//не мудрствуя лукаво, считаем пиксели на строке, слева направо. Сбрасываемся только по синхроимпульсу
			.cnt_en (~XCout),		//(хотя для аналогового сигнала может захотеться ещё по окончанию обработки задания с с.и повторно сбросить)
			.sclr (Hsync | flush),		
			.sload (1'b0),
			.data (),
			.Q (X),
			.cout (XCout));			//в кои-то веки использовать не будем, вместо этого компаратор. Хотя для граничных случаев может и пригодиться?
	defparam
		Xcounter.lpm_direction = "UP",
		Xcounter.lpm_port_updown = "PORT_UNUSED",
		Xcounter.lpm_type = "LPM_COUNTER",
		Xcounter.lpm_width = XregWidth;

//счётчик строк нужен только если мы пытаемся упростить программу и прогнать дополнительные строки "вхолостую" (с выдачей сплошных нулей) чтобы все активные точки
//"автоматом" перешли в список обнаруженных точек
		
lpm_counter Ycounter (	.clock (clk),		
			.cnt_en (Hsync & (~Yfinished)),		
			.sset (Vsync),		
			.sload (1'b0),
			.data (),
			.Q (),
			.cout (Yfinished));			
	defparam
		Ycounter.lpm_direction = "UP",
		Ycounter.lpm_port_updown = "PORT_UNUSED",
		Ycounter.lpm_type = "LPM_COUNTER",
		Ycounter.lpm_width = YregWidth,
		Ycounter.lpm_svalue = YinitVal;

endmodule


Прежде всего, рассмотрим формат "задания", которое поступает по шине данных:
- старшие два бита, [15:14] - это режим синхронизации. 00 или 11 означает отсутствие синхронизации, т.е задание запускается сразу же по завершении предыдущего. 01 означает ожидание строчного синхроимпульса, 10 означает ожидание кадрового синхроимпульса. Дальше мы объясним, почему выбрали именно такие значения :)
- младшие XregWidth+1 бит, в нашем случае [10:0] - X-координата, до которой надо вести обработку. Мы взяли на 1 бит больше, чтобы упростить программу захвата: ей не нужно специально проверять выход за диапазон 0..1023. Если точка будет найдена на самом краю диапазона, и потом мы прибавим радиус точки и получим, к примеру, 1035 - это вполне влезет в 11 бит. И единичный бит [10] сразу же укажет, что работать надо до самого упора!

Собственно, тут и становится понятно, почему на синхронизацию мы выделили либо 10, либо 01. Если мы случайно выйдем за диапазон и уйдём в отрицательные числа, то у нас все старшие биты будут единичками, и мы сможем эту ситуацию отличить от всех остальных.

В буфер же мы заносим ещё один бит, взятый из DestAddr, тот самый, который отличает команду ACQ (ACQuire) от TRK (TRacK), итого, на данный момент: 14 бит (10 бит на координату + 1 бит дополнительный, отличить выход за диапазон вправо + 2 бита на синхронизацию и выход за диапазон влево + 1 бит выбор захват/сопровождение).

Поначалу у меня была идея сделать специальные "ведущие" и "ведомые" буферы, чтобы не плодить управляющую логику, но в итоге сделал ещё проще - поставил ОДИН широкий буфер, и там на входе и на выходе конкатенация разнородных сигналов.

Можно заметить, что здесь вместо модуля FIFO_on_LE, который мы разрабатывали, допиливали и тестировали в прошлый раз, здесь используется некий FIFO_on_LE_for_GPU. Это специализированная версия, чуть отличающаяся от FIFO_on_LE, вот её код:

//укуренная версия FIFO, где один выходной бит отдельным сигналом можно сбросить!
//используется в нашем GPU, т.к единичка означает "ждём синхроимпульс", а приход этого синхроимпульса должен единичку сбросить
//самый старший бит: Vsync,
//второй по старшинству: Hsync

module FIFO_on_LE_for_GPU (input clk, input [DataWidth-1:0] D, input rdreq, input wrreq, input Vsync, input Hsync,
			output [DataWidth-1:0] Q, output wr_stall, output rd_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

wire empty = CR[0];
wire 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
	//первый элемент, то бишь самый правый, тоже отдельно, чтобы сделать "сброс бита"
	if (ena[0]) begin
		DR[0][DataWidth-1] <= Vsync? 1'b0 : CR[1]? D[DataWidth-1] : DR[1][DataWidth-1];	//вот он!
		DR[0][DataWidth-2] <= Hsync? 1'b0 : CR[1]? D[DataWidth-2] : DR[1][DataWidth-2];
		DR[0][DataWidth-3:0] <= CR[1]? D[DataWidth-3:0] : DR[1][DataWidth-3:0];
	end;
	for(i=1; 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 wr_stall = (~nfull) & wrreq & (~rdreq);
assign rd_stall = empty & rdreq;

endmodule


Мы решили, что выход буфера будет задействоваться на всём протяжении отрезка: младшие биты будут подаваться на компаратор, чтобы понять, когда отрезок завершается, бит isSum будет подаваться на одноимённый вход "первого сумматора", чтобы выбрать режим работы, а старшие биты будут заставлять модуль замереть, пока не придёт нужный синхроимпульс.

В FIFO_on_LE_for_GPU, кадровый синхроимпульс сбрасывает старший бит в первом элементе буфера, а строчный синхроимпульс - сбрасывает второго по старшинству. Так мы практически автоматом, и очень малой кровью реализуем логику ожидания синхроимпульсов. Можно возразить, что если бы у нас стояло 11 (отрицательное число), то оно не должно сбрасываться в 10 или 01 за счёт прихода синхроимпульса, но такой ситуации в принципе возникать не должно! Единственный момент, когда у нас может появиться отрицательное число - это непосредственно после задания с ожиданием строчного импульса, и выполнится это задание с отрицательным числом ровно за такт (выдаст нули - и всё!). Так что никаких проблем.

Также в FIFO_on_LE_for_GPU убран не нужный в данном случае вход sclr - этот буфер опустошать нет никакого смысла!

Вход буфера завязан на QuatCore: как только дешифруется команда для видеопроцессора (и получается isOurAddr=1), информация с шины данных и 1 бит с шины адреса помещаются в буфер с помощью wrreq=1, а если там всё занято - возникает SrcStallReq=1, который останавливает процессор до лучших времён.

Обработка кадра начинается с того, как мы посылаем задание с ожиданием кадрового синхроимпульса. Оно защёлкивается в буфере и к следующему такту поступает на выход: SyncOut = 2'b10, isSum = 0 (если это захват), BufOut = 0 (хотя здесь мы можем придумать и что-нибудь другое).

Нам нет дела, что в этот момент будет накапливаться на выходе видеопроцессора, главное, ничего не помещать в выходной буфер, т.к кто знает - вдруг QuatCore уже оттарабанил все команды и ждёт ответ с выхода, искренне веря, что это будут результаты по первой строке изображения! Ни в коем случае нельзя его обламывать :) Так что выход start должен держаться в нуле, а если и переключаться в единицу, то только на пару с flush :) У flush приоритет :)

Но когда кадровый синхроимпульс всё-таки придёт, мы всё-таки должны подать start=1, что сбросит наши обработчики, подготовив их к следующему отрезку, а также вдвинет на выход нашего буфера очередное задание (отрезок).

Следующим заданием будет ожидание строчного синхроимпульса, а при использовании аналоговой камеры это же задание позволит пропустить "полочку" между строчным синхроимпульсом и началом видимой строки (front porch). Та же история: сначала замираем до прихода синхроимпульса, его приход обнуляет нам счётчик Xcounter, а затем, как и в "нормальном" режиме (без синхроимпульсов), мы ждём, пока Xcounter не досчитает до значения, переданного в этом задании. Когда это происходит, мы подаём start=1, чтобы перейти к следующему заданию и сбросить наши обработчики, а также flush=1, чтобы и сейчас не сбить с толку QuatCore, который ожидает результатов именно по видимой строке! И ещё мы хотим повторно обнулить Xcounter, чтобы пиксели действительно отсчитывались от нуля. Не нужно нам лишних забот на уровне софта - и так несладко...

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

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

И наконец, в конце строки может последовать "выход за пределы" вправо, и это мы должны воспринять так же, как и задание обработать до значения 1023.

И мы видим в коде логику запуска, растянутую аж на 4 строки. В первой мы формируем сигнал start_neg - запуск по отрицательному значению. Во второй: запуск start_oflo по "переполненному" значению - в этом случае мы дожидаемся сигнала переноса cout со счётчика. В третьей строке: start_normal - нормальный запуск по компаратору. Тут можно было бы ввести дополнительное условие: (~BufOut[XregWidth]), т.е как раз-таки отсутствие переполнения. Формально, оно здесь нужно. Допустим, мы в самом начале обработки строки дали задание обработать до числа 1030. Без этого условия, компаратор, использующий лишь 10 бит, сработает уже на X=6, и работа закончится гораздо раньше, чем требовалось. Но на деле, если уж на выход пойдёт переполненное значение, это будет означать точку, найденную в самом краю изображения, и задание на обработку отрезка [Xp-R, Xp+R], со значением R, которое не может превышать примерно сотни. Так что проблемы особой случиться не должно.

Наконец, четвёртая строка объединяет эти 3 условия для старта по логическому "ИЛИ". И здесь же добавляется ещё одно условие - если мы уже перешли на "фиктивные" строки.

Для сброса счётчика после прохождения "передней полочки" front porch после синхроимпульса, а также для подачи сигнала flush мы применяем 1-битный регистр FrontPorch. Он нужен, чтобы запомнить, что обработка текущего отрезка началась с ожидания строчного синхроимпульса. В тот момент, когда приходит синхроимпульс и сбрасывает соответствующий флажок, мы устанавливаем FrontPorch в единицу. А когда досчитываем сколько надо пикселей и появляется сигнал start - сбрасываем FrontPorch в ноль.

Сигнал flush формируется очень легко - как раз в момент сброса FrontPorch. Довольно щадящий вариант - даём QuatCore максимум времени, чтобы успеть прочитать все результаты обработки предыдущей строки.

Теперь разберёмся, как работает счётчик XCounter.
Считает он каждый такт, пока не дойдёт до упора, после чего останавливается на максимальном значении, на данный момент это 1023. Эта остановка необходима - наверняка у нас будет одно задание на обработку крайней правой точки, а следующее - на обработку всего, что осталось справа за ней.

Сбрасывается он один раз за строку по приходу строчного синхроимпульса, а потом повторно после обработки "полочки" front porch, чтобы счёт реальных, видимых пикселей начался с нуля.

И наконец, у нас есть счётчик YCounter. Он здесь стоит немножко особняком - сам "обнуляется" по кадровому синхроимпульсу, сам прибавляет по единице на каждый строчный синхроимпульс и точно так же застревает на самом большом значении, с выдачей сигнала YFinished. Точнее, по кадровому синхроимпульсу в него загружается константа, которая делает его из "двоичного" (от 0 до 1023) "произвольным". При работе с аналоговой камерой у нас будет 720 строк - маловато, но уж что есть...


Всё это безобразие синтезируется в 107 ЛЭ. Как бы всё по делу - два счётчика нужны, хранилище 4х14 бит для входного FIFO и 4х2+1 ЛЭ на управляющую логику FIFO, а ещё компаратор - обычно я стараюсь без них обойтись, предпочитая им синхронную загрузку и счёт вниз до нуля, но тут он всё-таки необходим, без него существенно усложнилась бы программа, ей бы пришлось помнить, какой пиксель мы обработали последним, и подсчитывать, сколько пикселей нужно обработать на очередном отрезке, а потом ещё восстанавливать координаты самой яркой точки. Я конечно жадный, но не настолько :)

Максимально допустимая частота для этого драндулета: 49,5 МГц, неплохо. Критический путь - от счётчика XCounter к нему же самому через логику формирования start, а затем ещё и flush. Хотя когда-нибудь нам захочется разогнаться до 50 МГц, но это будет чуточку позже...

Сейчас ещё нужно протестировать этот модуль.
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 

  • 0 comments