nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Буфер для результатов видеообработки

У нас появился параметризованный модуль буфера "первый вошёл-первый вышел", с выходами, "приспособленными" к QuatCore. Его уже можно использовать как буфер заданий для видеопроцессора. Благо, "задание" вполне себе влезает в 16 бит (в одно слово) и состоит, по наихудшему случаю, из 12-битного числа - X-координаты, до которой нужно вести обработку, и ещё 2 бит - режим синхронизации (без синхронизации, по строчному импульсу или по кадровому). 12 бит - чтобы можно было выползти за границы 0..1023 с обеих сторон, и обработчик понял бы, с какой именно стороны мы выползли. Если попросили обработать ДО отрицательной координаты - сразу формируем нули. А если попросили обработать ПОСЛЕ конца строки - обрабатываем строго до конца строки - и выдаём. (А вообще склоняюсь к мысли поставить "схему насыщения" ещё ПЕРЕД этим буфером, ну это ещё надо подумать, как проще)

С передачей измеренных значений от видеопроцессора на QuatCore дело обстоит немножко хуже, ведь у нас как минимум два различных выхода. В режиме захвата, из первого идёт максимальная яркость, достигаемая на отрезке, из второго - координата X, при которой эта яркость была достигнута. Пока я предполагал, что они будут заноситься "в шахматном порядке" - сначала яркость, потом координата, потом снова яркость (уже на следующем отрезке) и снова координата, и QuatCore будет прочитывать значения одно за другим.

Но уже не уверен, что это лучшее решение. Мне казалось, что удастся обойтись без лишнего мультиплексора: одни значения будут заноситься по "чётным" регистрам, а другие по "нечётным", но учитывая, что QuatCore при чтении ОДНОГО значения заставит весь остальной буфер сдвинуться НА ОДНУ позицию, чётные с нечётными поменяются местами, так что без мультиплексора обойтись будет сложно, а то и парочки.

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

В целом, это безобразие выглядит вот так:



На вход D поступают данные с модуля PixelSumMax, который в зависимости от режима может находить либо максимальную яркость и координату, где она была достигнута, либо сумму яркостей всех пикселей. Мы его описывали здесь, а потом ещё дорабатывали.

На вход wrreq поступает запрос на запись результатов в буфер. Пока мне представляется, что можно обойтись одним управляющим сигналом и на сброс модуля PixelSumMax, и на wrreq здесь: как только с одним отрезком заканчиваем, тут же, "бесшовно", начинаем следующий, каждый пиксель важен для нас, независимо от яркости :)

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

SrcAddr и SrcStall - это уже родная шина от QuatCore - адрес команды и сигнал чтобы мы "ничего не делали пока".

Выходы тоже уже родные: 16-битный Q для присоединения к входному мультиплексору шины данных, DestStallReq - запрос на остановку нашего "соседа" и конвейера в целом. OFLO немножко особняком - его я пока воспринимаю как "отладочный" вывод, который через какую-нибудь "защёлку" хочу вывести тупо на светодиод на отладочной плате. Если в процессе работы он так и не загорится - значит, длины буфера нам хватает, в противном случае надо его удлинять, или искать, почему возникает "затор". Мы не можем приказать матрице "остановиться" - она работает в своём темпе, и нам нужно не отставать! Для этого нужно, чтобы QuatCore успевал худо-бедно разгребать те данные, что на неё сыплются.

Окромя входов и выходов, у нас целый ворох параметров. По сути, это опять ширина буфера, но ещё и то, как его нужно разбивать на две части. Для китайской камеры наблюдения я могу захотеть ширину пикселя 8 бит (АЦП у меня на макетке всего 8-битная), а для реального изделия: 12 бит (там АЦП в самой матрице и "помощнее" немножко). С шириной регистра, представляющего координату, тоже пока не определился. С 10 битами удобно возиться, но впоследствии можно будет попробовать сократить до 8 бит, поэтому решил повсюду параметров поставить.

ElemCount - сколько элементов вмещается в буфер. И наконец, LongClr - очередной результат моей жадности. Подумалось, что нам вовсе не обязательно обнулять буфер прямо за один такт, может просто будем подавать сигнал sclr подлиннее, и тогда можно не городить специальные цепи сброса, а просто воспринимать sclr как rdreq - аккуратно "выдавить" значения штатным путём, одно за другим. Особенно, если мы не предполагаем, что там вообще будут оставаться значения, тогда одно случайно затесавшееся можно за один раз опустошить :)

Так выглядит код данного модуля:
//буфер FIFO, через который результаты работы видеопроцессора поступают в QuatCore. Также здесь логика общения по шине
//резервируем 2 команды:
//GPUL (luminance): 1000_10xx
//GPUH (horiz. coord):1000_11xx
//т.е на каждую из них выделено по 4 адреса, чтобы поменьше декодировать
//GPUL остановится, если данные ещё не поступили, равно как и GPUH 
//между тем, GPUL не трогает буфер (только читает из него),
//а GPUH выталкивает последние значения из него

module QuatCoreGPUout(input clk, input [PixSumWidth-1:0] D, input wrreq, input sclr, input [7:0] SrcAddr, input SrcStall, output DestStallReq, output [15:0] Q, output OFLO);

	parameter PixSumWidth = 22;	//сколько отводится на совместное хранение координаты и яркости или на хранение суммы всех яркостей на отрезке
	parameter PixWidth = 12;	//разрядность АЦП по сути
	parameter XregWidth = 10;	//сколько бит на координату. Теоретически могли бы сократить до 8, но довольно муторно, пока не жадничаем...
	parameter ElemCount = 4;
	parameter LongClr = 1'b1;	//1: сигнал sclr достаточно долгий, чтобы тупо "вытолкнуть" элементы по одному, или считаем что по одному лишнему оттуда выпихивать вполне хватит
								//0: обнуление за один такт

	wire IsOurAddr = (~SrcStall)&(SrcAddr[7:3] == 5'b1000_1); 	//используем при формировании Stall
	wire rdreq = IsOurAddr & SrcAddr[2] | (LongClr & sclr);			//выталкиваем только по GPUH
	
	wire i_sclr = (~LongClr) & sclr;	//наша жадность не знает границ
	wire empty;							//нужен для формирования stall
	wire [PixWidth-1:0] LumOut;			//"широкий" выход буфера
	wire [XregWidth-1:0] HorOut;

	FIFO_on_LE buff (.clk(clk),
			 .D(D),
			 .rdreq(rdreq),
			 .wrreq(wrreq),
			 .sclr(i_sclr),
			 .empty(empty),
			 .nfull(),
			 .Q({HorOut,LumOut}),
			 .wr_stall(OFLO),	//для начала "защёлкнем" и выведем на светодиод, чтобы посмотреть, не возникает ли переполнения
			 .rd_stall()		//у нас логика чуть сложнее, сами сделаем на основе empty
			 );
	defparam
		buff.DataWidth = PixSumWidth,
		buff.ElemCount = ElemCount;				//пока не можем сообразить, сколько их надо
	
	assign Q = SrcAddr[2]? HorOut : LumOut;
	assign DestStallReq = empty & IsOurAddr;	//т.е когда запрашиваем любую из частей, а выдать нечего
	
endmodule


Тут же в комментариях указано, на какие адреса должен реагировать модуль. Вместо одной команды GPU, вводим две команды, GPUL и GPUH, здесь L и H означает Low и High, как это было когда-то в регистрах x86: AX, состоящий из AH и AL, и т.д. Но ещё это Luminance (яркость) и Horizontal coord (координата по горизонтали), так что есть шанс не запутаться :)

Команда GPUL не активирует rdreq, т.е данные в буфере в этот момент не смещаются вправо, что и понятно: мы же ещё не все данные прочитали! Но если никаких данных не поступило, то и GPUL должна "застрять", пока они не появятся. Собственно, львиную долю времени наш процессор будет проводить именно в таких ожиданиях.

Такая логика работы позволяет чуть улучшить нашу "программу захвата" - там возникал Hazard, поскольку мы боялись потерять яркость пикселя, считали (и небезосновательно), что прочитать его из GPU можно лишь единожды, тут же запихивали в стек, и тут же перетаскивали из стека в аккумулятор, чтобы сравнить яркость с яркостью этой точки с прошлой строки, и "перегружали" память тем самым. Теперь же мы знаем, что яркость от нас никуда не денется, пока мы не запросили GPUH, так что код
	[SP+2j+1]	GPU		;яркость
	[SP+1]		GPU		;пока не забыли - сразу второе слово запрашиваем (коорд точки), чтобы не сбить весь FIFO 
	NOP		0		;УБРАТЬ WARNING компилятора. Не знаю, как здесь обойтись без Hazard'а...
	Acc		[SP+2j+1]
	SUB		[X+2j+1]	;сравниваем по яркости
	JL		@@NotSoBright	;похоже, на спад пошло


превращается в такой:
	[SP+2j+1]	GPUL		;яркость
	Acc		GPUL
	SUB		[X+2j+1]	;сравниваем по яркости						
	[SP+1]		GPUH		;пока не забыли - сразу второе слово запрашиваем (коорд точки), чтобы не сбить весь FIFO 
	JL		@@NotSoBright	;похоже, на спад пошло


Вот у нас Hazard'ов и не осталось почти. Только один, в процедуре ListOp "перецепки" списков, но там неудивительно - с памятью работаем только так.

Но вернёмся к нашим баранам...
Модуль QuatCoreGPUout при нынешних параметрах (12 бит разрядность АЦП, то бишь пикселя, 10 бит на координату, 4 элемента в буфере) синтезируется в 114 ЛЭ. Из них 88 ЛЭ абсолютно неизбежны, они хранят те самые 4х22 бита данных, ещё 12 ЛЭ образуют выходной мультиплексор, 4х2 ЛЭ - управляют работой буфера, ещё 2 для определения, "наш ли это адрес", парочка для формирования сигналов OFLO и DestStallReq - уже получается 112 ЛЭ. В общем, ложится модуль хорошо, не думаю, что можно его сделать компактнее, но всё равно удивляет, что весь видеопроцессор занимает меньше одного этого буфера! (на комбинированный модуль PixelSumMax, модуль для нахождения "второй суммы" PixelAdder и счётчик для координаты X у нас уходит 107 ЛЭ!)
А это мы ещё пресловутую "вторую сумму" никак не задействовали - она тоже по-хорошему должна "отдельным буфером" у нас идти, уже для режима сопровождения.

Если подумать, с современными процессорами ровно та же фигня: львиную долю транзисторов и площади кристалла отжирают кэши всех уровней!

А ведь у нас должно быть два видеопроцессора (когда вместо китайской камеры наблюдения будет использоваться матрица 1205ХВ014), и к каждому свои буферы на вход и выход. И если один-единственный QuatCore будет управлять обоими - задержек будет ещё больше, так что буферы, возможно, придётся ещё удлинить...

Ну да ладно, проблемы будем решать по мере поступления.


UPD. С FIFO на блоках внутренней памяти тоже всё не так просто, как может показаться. Один блок имеет максимальную ширину входов и выходов 16 бит (для той ПЛИС, в которую я пытаюсь впихнуться), а у нас на одном такте может образоваться 22 бита "первая сумма" и 26 бит "вторая сумма", т.е 48 бит за раз. Потребуется 3 такта, чтобы всё это пропихнуть в FIFO, но за это время могут уже новые данные прийти, мы рассматривали "вырожденный случай", когда отрезок имеет длину 1 пиксель, зажатый между двумя отрезками побольше. Так что буфер на ЛЭ, совсем крохотный, всё равно нужен, хорошо, что мы в этой теме разобрались! Возможно, в итоге будет у меня адский гибрид - FIFO на памяти общая для двух видеопроцессоров, и ещё "очередь на входе" в него, реализованная на ЛЭ :)
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