nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Подправляем видеопроцессор QuatCore

Я его "собрал" в конце июля 2020 года, и с тех пор толком не трогал. Ну, размер входного и выходного буфера увеличил (циферку поменял), чуть изменил логику ожидания синхроимпульсов, вот не так давно заставил его не пропускать пикселей на стыках отрезков.

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

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

//GPUPL и GPUPH также останавливаются, но не выталкивают, считываем их пораньше

module QuatCoreFullGPUout(input clk, input [PixSumWidth-1:0] D, input [PixMulWidth-1:0] Prod, 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 PixMulWidth = 26;	//сколько бит на "вторую сумму"
	parameter ElemCount = 4;
	parameter LongClr = 1'b1;	//1: сигнал sclr достаточно долгий, чтобы тупо "вытолкнуть" элементы по одному, или считаем что по одному лишнему оттуда выпихивать вполне хватит
								//0: обнуление за один такт

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

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


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


В первую очередь, название модуля. QuatCore - понятно, GPUout - понятно. Full означало, что данные поступают сразу с двух сумматоров - "первого" и "второго". В режиме сопровождения первый из них даёт сумму всех пикселей на отрезке, а второй - суммирует эти суммы по мере их поступления, благодаря чему самый последний пиксель суммируется с весом 1, предпоследний - с весом 2, перед ним - с весом 3, и т.д. Поделив "вторую сумму" на "первую", мы и получим "центр тяжести" с субпиксельной точностью. На самом деле, нужно их ещё просуммировать по всем строкам, но этим когда-нибудь займётся QuatCore, я надеюсь.

Первый сумматор подключается к входу D, а второй - к входу Prod, сокращённо от "Product", то есть произведение. Я далеко не сразу сообразил, что здесь можно обойтись одним лишь суммированием, и на полном серьёзе придумывал, как на лету (по одному за такт) делать перемножение в ПЛИС, не имеющей аппаратного умножителя. В общем, уже всё было готово, когда наконец-то "осенило", что это нафиг не надо! С тех пор и осталось название Prod :)

Когда нужно "защёлкнуть" новые результаты, поступает wrreq = 1 (Write Request). И есть многострадальный вход sclr (Synchronous CLeaR) для сброса всех результатов, да ещё и параметр LongClr, с помощью которого я надеялся чуть сэкономить на логических элементах в этом буфере.

Все остальные входы и выходы - интерфейс с QuatCore. Шина SrcAddr[] и её извечный спутник SrcStall, но почему-то без SrcDiscard, из-за чего пришлось поставить элемент OR "снаружи". 16-битный выход Q, а также DestStallReq, и прерывание OFLO ("переполнение очереди результатов работы").

Основную работу выполняет параметризуемый буфер FIFO на логических элементах. Он поставлен ровно один, но очень ШИРОКИЙ. При выставленных сейчас параметрах выходит аж 48 бит.

Этот буфер так устроен, что на его выходе всегда находится первый вошедший элемент, т.е чтобы он появился, не нужно обязательно давать rdreq. Поэтому в первую очередь у нас просто стоит мультиплексор, который из этого широкого 48-битного выхода выбирает соответствующие части, в зависимости от команды: GPUL, GPUH, GPUPL, GPUPH.

rdreq нужен для того, чтобы "вытолкнуть" первый элемент. Это происходит только при подаче команды GPUH. Мне так показалось удобно, хотя скорее всего мы с тем же успехом могли возложить эту почётную обязанность на GPUL. Но только не на GPUPL и GPUPH, т.к они на этапе захвата нафиг не нужны!

Наконец, у буфера есть выходы empty, nfull (negative full, т.е активный уровень - низкий), rd_stall и wr_stall. empty=1, ВНЕЗАПНО когда очередь пуста. nfull=0, когда очередь полна. rd_stall = 1, когда очередь пуста, и в то же время мы пытаемся ВЫТОЛКНУТЬ из неё один элемент, т.е подали rdreq = 1. Наконец, wr_stall = 1, когда очередь полна, но мы пытаемся ЗАПИХАТЬ в неё ещё один элемент, т.е подали wrreq = 1, и при этом rdreq=0. Если одновременно wrreq=1 и rdreq=1 и nfull=0 - ВСЁ ХОРОШО, очередь была заполнена, но один элемент мы вытолкнули, и один сразу же запихали - всё чётко!

Здесь, как мы видим, сигнал empty был применён для формирования DestStallReq - если мы запросили результат работы видеопроцессора, а результатов ещё нет - мы останавливаемся и ждём, когда же они появятся. Мы не могли использовать напрямую rd_stall (хотя изначально такое намерение явно было), потому что из 4 команд лишь одна даёт rdreq = 1, это GPUH. Но оставшиеся 3 тоже должны дождаться появления данных, поэтому и пришлось логику rd_stall оставить неподключённой, и "ручками" сформировать DestStallReq.

Вроде всё понятно и логично. Пора понять, что нам тут нужно поменять. В первую очередь, добавляем вход isOdd, указывающий, какую строку мы сейчас обрабатываем, чётную (even) или нечётную (odd). И поменяем имя модуля (не будем трогать имеющийся, жалко, вдруг пригодится), в этот раз QuatCoreOddGPUout, ничего лучше пока не придумал. А вход sclr уберём нафиг:

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


Соответственно, убираем параметр LongCLR, за ненадобностью:
	parameter PixSumWidth = 22;	//сколько отводится на совместное хранение координаты и яркости или на хранение суммы всех яркостей на отрезке
	parameter PixWidth = 12;	//разрядность АЦП по сути
	parameter XregWidth = 10;	//сколько бит на координату. Теоретически могли бы сократить до 8, но довольно муторно, пока не жадничаем...
	parameter PixMulWidth = 26;	//сколько бит на "вторую сумму"
	parameter ElemCount = 4;


Обнаружил, что если из окна Quartus II копируешь текст с русскими буквами, то если раскладка выбрана русская - всё копируется нормально. А если английская - они превращаются в буквы с диакритическими знаками. Забавно!

Ширину буфера увеличиваем на единичку, туда будет заноситься "чётность" (тот самый isOdd):
	wire outOdd;

	FIFO_on_LE buff (.clk(clk),
			 .D({odd, Prod, D}),
			 .rdreq(rdreq),
			 .wrreq(wrreq),
			 .sclr(1'b0),
			 .empty(empty),
			 .nfull(),
			 .Q({outOdd, ProdOutH, ProdOutL, HorOut, LumOut}),
			 .wr_stall(OFLO),	
			 .rd_stall()		//у нас логика чуть сложнее, сами сделаем на основе empty
					 );
	defparam
		buff.DataWidth = PixSumWidth+PixMulWidth+1,
		buff.ElemCount = ElemCount;


И продумаем интерфейс с QuatCore. Как видно из комментариев в начале модуля, пока у нас вообще есть "лишний" нулевой бит адреса, т.е мы зарезервировали 8 адресов от 0x88 до 0x8F, а в действительности используем только 4, игнорируя младший бит. Вот его мы и можем применить для переключения бита чётности. Потом, при необходимости, ужмёмся, а сейчас почему бы и нет:

	reg ParityRequired = 1'b0;
	always @(posedge clk) if (IsOurAddr & SrcAddr[0])
		ParityRequired <= ~ParityRequired;


Тем самым, мы как бы вводим ещё 4 команды, что-то типа BGPUH, BGPUL, BGPUPL, BGPUPH, где B значит Begin - начало новой строки. Тем самым мы дадим столь ценную информацию, что с предыдущей строкой мы закончили, и если раньше просили результаты по чётной строке, сейчас это будут результаты по нечётной.

Ну и самое последнее: поставить логику проверки чётности.

Первая идея была - сделать вот так:

wire WrongParity = ParityRequired ^ outOdd;
	
wire rdreq = (IsOurAddr & (~SrcAddr[2]) & SrcAddr[1]) | (WrongParity & (~empty));


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

Но здесь мы можем "с разбегу" налететь на очередную проблему! Мы чуть замотались, сняли последнее задание по текущей строке в тот момент, когда уже начали прибывать результаты по следующей строке - и опомниться не успели, как все последующие результаты оказались стёрты! Потому как мы ещё не успели выставить новую чётность - и видеопроцессор проявил излишнюю прыть и всё вытолкнул наружу!

Чтобы такого не происходило, мы должны проверять чётность непосредственно во время запроса из QuatCore, так что строки чуточку изменяются:
wire WrongParity = (ParityRequired ^ outOdd) & (~empty);
wire rdreq = IsOurAddr & (((~SrcAddr[2]) & SrcAddr[1]) | WrongParity );


В коде программы добавим такую строку:

				;НАЧАЛО ЦИКЛА ПО СТРОКАМ
@@newRow:		NOP		BGPUL			;поставить GPU в известность, что мы начали новую строку
			[SP+2j]		Acc
			X		ActivePoints	;начинаем цикл по всем активным точкам
				
@@ResetPending:		C		0			;уж явно не 0x8000			
;даже если список пуст, первую половину цикла мы пройдём, она касается отрезка, предшествующего точке из списка
@@ActPointsStart:	[SP+2j+1]	GPUL			;яркость самой яркой точки на отрезке
			[SP+1]		GPUH			;соотв. координата


То есть, когда начинаем обрабатывать следующую строку, мы посылаем эту новую команду 0x89. Она заставляет к следующему такту поменять "требуемую чётность". Только вот незадача: сейчас-то чётность ещё старая, а команда поступила (isOurAddr = 1), и если в очереди уже лежал результат по следующей строке, он будет ошибочно вытолкнут!

Поэтому ещё небольшая правка:
wire WrongParity = (ParityRequired ^ outOdd ^ SrcAddr[0]) & (~empty);


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

На этом издевательства над QuatCoreOddGPUout завершены:
//буфер FIFO, через который результаты работы видеопроцессора поступают в QuatCore. Также здесь логика общения по шине
//резервируем аж 4 команды:
//GPUL (luminance):   	1000_1000
//GPUH (horiz. coord):	1000_1010
//GPUPL (Product Low):	1000_1100
//GPUPH (Product High): 1000_1110
//т.е на каждую из них выделено по 2 адреса, чтобы поменьше декодировать
//GPUL остановится, если данные ещё не поступили, равно как и GPUH 
//между тем, GPUL не трогает буфер (только читает из него),
//а GPUH выталкивает последние значения из него

//GPUPL и GPUPH также останавливаются, но не выталкивают, считываем их пораньше

//новые команды
//BGPUL:  1000_1001
//BGPUH:  1000_1011
//BGPUPL: 1000_1101
//BGPUPH: 1000_1111
//то же самое, но переворачивает чётность строки. B - от слова Begin. С них нужно начинать новую строку

module QuatCoreOddGPUout(input clk, input [PixSumWidth-1:0] D, input [PixMulWidth-1:0] Prod, input wrreq, input odd,
						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 PixMulWidth = 26;	//сколько бит на "вторую сумму"
	parameter ElemCount = 4;

	wire IsOurAddr = (~SrcStall)&(SrcAddr[7:3] == 5'b1000_1); 	//используем при формировании Stall
	
	reg ParityRequired = 1'b0;
	always @(posedge clk) if (IsOurAddr & SrcAddr[0])
		ParityRequired <= ~ParityRequired;
	
	wire outOdd;	
	
	wire WrongParity = (ParityRequired ^ outOdd ^ SrcAddr[0]) & (~empty);
	wire rdreq = IsOurAddr & (((~SrcAddr[2]) & SrcAddr[1]) | WrongParity );			//выталкиваем только по GPUH
	
	
	wire empty;							//нужен для формирования stall
	wire [PixWidth-1:0] LumOut;			//"широкий" выход буфера
	wire [XregWidth-1:0] HorOut;
	wire [15:0] ProdOutL;				//предполагаем, что 16 бит нужно как минимум
	wire [PixMulWidth-17:0] ProdOutH;	//всё, что осталось
	

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



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

Recent Posts from This Journal

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

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

  • Ай да Пафнутий Львович!

    Решил ещё немного поковыряться со своим арктангенсом. Хотел применить алгоритм Ремеза, но начал с узлов Чебышёва. И для начала со своего "линейного…

  • atan(y/x) на двух умножениях!

    Чего-то никак меня не отпустит эта тема, всё кажется, что есть очень простой и эффективный метод, надо только его найти! Сейчас вот такое…

  • Таблица коэффициентов для atan1

    Пора уже добить этот несчастный арктангенс, что-то слишком долго его ковыряю. Мы упоминали, что в оперативной памяти будет лежать таблица в 15 слов:…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 1 comment