nabbla (nabbla1) wrote,
nabbla
nabbla1

QuatCore+GPU: работа над ошибками

Сначала разберёмся с "софтом", то бишь с кодом на ассемблере. То чувство, когда ассемблер - это самый высокий уровень :) Ну кстати, товарищ показывал, как делает моды к DooM на языке Decorate, там нет меток, прыгать можно на определённое количество строк. Добавил новую строку - будь добр ручками пересчитай все прыжки, которые шли "через неё". Сразу почувствовал, как же ассемблер, даже мой, упрощает труд программиста! Реально все наиболее трудоёмкие вещи на себя берёт :)

Чуть поменяем константы:
	VSync			EQU	0x8000	;ожидание кадрового синхроимпульса, значение для регистра произвольное
	HSync			EQU	0x40EE	;ожидание строчного синхроимпульса, а затем ещё интервал back porch (между импульсом и началом картинки) в 238 тактов
	WholeRow		EQU	31	;для проверки на симуляторе
	ImgHeight		EQU	40	;вообще, 32, но вводим дополнительные строки, чтобы перенести все точки из ActivePoints в AllPoints
	ImgHeightP1		EQU	41	;не обучили свой компилятор обрабатывать "constexp"
	Threshold		EQU	0xDFF	;минимальная яркость пятен, сейчас поставили 1/8 от "насыщения" (4096), но с инверсией, так надо!


HSync переделали из 240 тактов в 238 (вместо F0 в младшем байте EE), а ещё добавили константу ImgHeightP1, то бишь Plus 1. Это недоработка с моей стороны, не умеет пока компилятор самостоятельно выражения вычислять. Было это далеко не первой необходимостью, не стал код излишне усложнять, как-нибудь сделаю.

Код в конце цикла по строкам получился таким:
	Acc		[SP+2j]
	SUB		ImgHeight
	ADD		ImgHeightP1
	JL		@@newRow


Забавно выглядит: загружаем текущий номер строки, вычитаем 40, а затем прибавляем 41, и только потом условный переход. Только в QuatCore такие фокусы прокатывают, он намеренно сделан асимметричным!


SUB меняет флаг знака числа, а ADD его не трогает! Ни разу мне ещё не понадобился ADD непременно с обновлением флага. Если значение непосредственное, то можно, к примеру, вместо ADD 1 написать SUB -1. Можно придумать ситуацию, что мы непременно хотим сложить два значения откуда-то из памяти, и по знаку результата принять решение, но как-то оно выглядит искусственно.

Так что да, при вычитании 40 мы проверяем, не пора ли заканчивать цикл, а потом прибавляем 41, чтобы всё закончилось попросту прибавлением единицы к номеру строки.

А уже в начале цикла значение из аккумулятора возвращается в [SP+2]:

@@newRow:  [SP+2j]	Acc


Конечно, и прибавление 41 можно было выполнить здесь, и не выпендриваться, но это внесло бы задержку в один такт (Read-After-Write Hazard, но хотя бы разрешаемый силами самого АЛУ), неаккуратненько!

Впрочем, когда мы от счёта "вниз до нуля" перешли к счёту "вверх", код всё-таки подрос на 1 строку, то есть на 2 байта :) Но всё-таки надо было это сделать. Одно дело, когда вместо строк 0..1023 мы получаем 1023..0 (всё то же самое, но в обратную сторону), и совсем другое - когда получаем условно 1123..100, поскольку мало того что в обратную сторону, но ещё и берём 100 лишних строк внизу, чтобы "нахаляву" перетащить все пятна из одного списка в другой. Халява - она такая, всегда оказывается не вполне бесплатной.

Ещё мы поменяли значение D1 с 3 на 6, это "расчётный диаметр" наших пятен.

Ну и самое главное: надо подрихтовать АЛУ, чтобы команда DIV2S (вычесть из аккумулятора входное значение, поделённое знаково на два) не затирала регистр C.

Очень давно я в АЛУ не залезал, "работает-не лезь". В первую очередь, посмотрим на регистр C:

module QuatCoreALUCreg (input [15:0] D, input [1:0] mode, input clk, output Senior, output reg [15:0] Q = 1'b0);

localparam mNOP   = 2'b00;
localparam mShift = 2'b10;
localparam mLoad  = 2'b11;

always @(posedge clk) if (mode[1])
	Q <= mode[0]? D : {Q[14:0], Q[15]};

assign Senior = Q[15];

endmodule


Данный файл был последний раз изменён 20 декабря 2019 года, долгожитель! Да и сейчас не будем его трогать. Немножко раздражает, что mode[1:0] - очень "безличное" название, чтобы узнать режимы, нужно смотреть потроха этого модуля. Можно было бы не жадничать на количестве "соединительных проводов" на схеме и сделать два входа: ena (разрешение работы) и sload (синхронная загрузка). Тогда можно было бы сообразить, что при ena=0 регистр вообще ничего не делает, сколько не тыкай sload, при ena=1 и sload=1 он загружает новое значение, а при ena=1 и sload=0 должен же он что-то делать - наверное сдвигает данные циклически :)

Ну да ладно. В общем, на команде DIV2S лишний раз возникает mode[1]=1, и это надо исправить. Заглядываем в модуль управления АЛУ:

//most complicated thing so far. Should support commands:
//NOP (smth else is active) 
//0x80: ACC
//0x81: PM
//0x82: ADD
//0x83: SUB

//0x84: ABS
//0x85: ???
//0x86: ???
//0x87: ???

//0x88: ZACC
//0x89: ???
//0x8A: C
//0x8B: ???

//0x8C: DIV2
//0x8D: ???
//0x8E: ???
//0x8F: ???

//0x90: MUL
//0x91: FMPM
//0x92: FMA
//0x93: FMS

//0x94: MULSU
//0x95: SUFMPM
//0x96: SUFMA
//0x97: SUFMS

//0x98: MULU
//0x99: UFMPM
//0x9A: UFMA
//0x9B: UFMS

//0x9C: SQRD2
//0x9D: SQRPMD2
//0x9E: SQRAD2
//0x9F: SQRSD2

//100xxxxx = ALU commands
//1000xxxx = 'short' commands
//1001xxxx = 'long' commands

//100xxx00 = 'clear ACC'
//100xxx10 = 'add to ACC'
//100xxx11 = 'sub from ACC'
//100xxx01 = 'plus-minus' (depends on PM input)

//100x00xx = 'signed commands'
//100x01xx = 'signed B / unsigned C commands / special command (ABS)'
//100x10xx = 'unsigned commands / special commands (ZAcc, C)'
//100x11xx = 'div2 commands (DIV2, SQRD2, SQRAD2 etc)'

//AccMode = 000 - load
//	    001 - FORBIDDEN
//          010 - idle
//	    011 - clear
//	    100 - set -3/2
//	    101 - set 2
//	    110 - set 3/2
//	    111 - set 1/2lsb

//Bmode   =	00 - shift right
//		01 - shift right and invert
//		10 - load 16 bit
//		11 - load AccWidth bit (bypass)

//Cmode   = 0x - idle
//	    10 - shift
//	    11 - load
module QuatCoreFastALUcontrol (input Bypass, input clk, input [7:0] DestAddr, input [7:0] SrcAddr, input Csenior, input PM, input [15:0] D,input stall,
				output [2:0] AccMode, output cin, output BSigned, output [1:0] Bmode, output [1:0] Cmode, output Sena, output Busy);
							
wire isOurAddr	   = (DestAddr[7:5] == 3'b100)&(~stall)&(~rBusy);
wire isLongCommand = DestAddr[4];
wire [1:0] PlusMinusMode = DestAddr[1:0];

wire OpSign = PlusMinusMode[0] & (PlusMinusMode[1] | PM); //0 is '+', 1 is '-'
wire isSignedC = (DestAddr[3] == DestAddr[2]);

reg SeniorDataBit = 1'b0;
always @(posedge clk) if (isIdle)
	SeniorDataBit <= D[15];

wire FirstDivSign = OpSign ^ ((isLongCommand & isSignedC & (isFirstDiv | MulFirst)) | (SeniorDataBit & (~isLongCommand) & (~DestAddr[3]) & DestAddr[2]));


//first of all, 'state machine'
localparam sIdle = 3'b000;
localparam sDiv2 = 3'b001;
localparam sFirstDiv = 3'b010;
localparam sAdd  = 3'b011;
localparam sMult = 3'b100;

reg [2:0] State = sIdle;


wire isIdle = (State == sIdle);
wire isFirstDiv = (State == sFirstDiv);
wire isDiv2 = (State == sDiv2);
wire isAdd = (State == sAdd);
wire isMult = (State == sMult);

reg MulFirst = 1'b0;
always @(posedge clk)
	MulFirst <= isFirstDiv;

	

//also, 4-bit counter for multiplier
wire finished; //when multiplication is all over
 lpm_counter	counter (
			.clock (clk),
//			.cnt_en (IsIdle),
			.sclr (~isMult),
			.cout (finished) );
  defparam
    counter.lpm_direction = "UP",
    counter.lpm_port_updown = "PORT_UNUSED",
    counter.lpm_type = "LPM_COUNTER",
    counter.lpm_width = 4;


//now combinatorial logic which depends on current state, DestAddr and other inputs
assign AccMode = 	isIdle? 		((isOurAddr & (PlusMinusMode == 2'b00))? {1'b1, D[1:0]} : 3'b010) : //либо команда ZAcc, либо храним текущее значение
			(isFirstDiv | isDiv2)?	{isLongCommand & (PlusMinusMode == 2'b00), 1'b1, (PlusMinusMode == 2'b00)}:
			isAdd?			3'b000:
						{1'b0, ~Csenior, 1'b0};
//in 'clear' commands we either don't care of contents at first step (will clear it anyway),
//or this is ZAcc command which sets one of consts. So we seek for this command
//otherwise AccMode is idle.

//so far this is only for idle
							
assign cin = FirstDivSign; //don't care at idle/FirstDiv/Div2, care at sAdd so far

assign BSigned = 	(DestAddr[3:2] != 2'b10) | (~isFirstDiv); //this row equals fully unsigned operation
//assign BSigned = (DestAddr[3:2] != 2'b10) & (isLongCommand | (~DestAddr[1])); //making one and only operation DIV2S into UDIV2S.
							
assign Bmode = 	isIdle? 	{1'b1, Bypass}:
		isFirstDiv?	{1'b0, FirstDivSign} :
				{1'b0, isSignedC & MulFirst};
					
					
assign Cmode[1] = isIdle? isOurAddr & DestAddr[3] & ((~DestAddr[4]&DestAddr[1])|(DestAddr[4]&DestAddr[2])) : isMult; //1 LE shorter still
//assign Cmode[1] = isIdle? isOurAddr & DestAddr[3] & DestAddr[2] & (DestAddr[1]|isLongCommand) : isMult;
									
assign Cmode[0] = ~isMult;

assign Sena    = 	isOurAddr & DestAddr[0] | isMult;

wire MainBusy = isOurAddr & (DestAddr[4:2] != 3'b0_10)&(~isAdd)&(~finished);	//если SrcAddr тоже относится к АЛУ, задерживаем ещё на такт
wire AuxBusy = (SrcAddr[7:4] == 4'b1000) & (isAdd | finished);
assign Busy = MainBusy | AuxBusy;

reg rBusy = 1'b0;
always @(posedge clk)
	rBusy <= AuxBusy;

always @(posedge clk)
	State <= 	isIdle? 		(Busy? ((DestAddr[3:2] == 2'b11)? sDiv2 : sFirstDiv) : sIdle):
			isDiv2?			sFirstDiv:
			isFirstDiv? 		(isLongCommand? sMult : sAdd):
			(isAdd | finished)?	sIdle : State;

endmodule


Ох жесть. Ещё и половина комментариев устарела! Последнее изменение: 17 марта 2020 года, в 2:28 утра. Да, без чёрной магии здесь явно не обошлось.

Весь АЛУ пока что синтезируется в 194 ЛЭ (при ширине аккумулятора 32 бита).

Вот строка, которая нас интересует:
assign Cmode[1] = isIdle? isOurAddr & DestAddr[3] & ((~DestAddr[4]&DestAddr[1])|(DestAddr[4]&DestAddr[2])) : isMult; //1 LE shorter still


А конкретнее, часть выражения, отвечающая за isIdle:
isOurAddr & DestAddr[3] & ((~DestAddr[4]&DestAddr[1])|(DestAddr[4]&DestAddr[2]))


isOurAddr проверяет старшие 3 бита DestAddr, т.е DestAddr[7:5], убеждается, что команда адресована имено в АЛУ, и что не "горит" Stall, и не нужно ещё на такт задержаться из-за RAW Hazard. Это мы не трогаем, работает-не лезь!

Далее, вспоминаем эту табличку:


Бит DestAddr[3]=1 выделяет нижние две строки "длинных" команд и нижние две строки "коротких" команд - только на них в регистр C надо что-то загружать. Не имеем претензий...

А дальше идёт разделение. Если DestAddr[4]=1, т.е на "длинных" командах, мы ещё требуем DestAddr[2]=1, что оставляет нам только нижнюю строку.
Если же DestAddr[4]=0, т.е на нижних двух строках "коротких" команд, мы требуем DestAddr[1]=1, что оставляет отсекает нам команды ZACC, DIV2, NOP и (зачем-то) DIV2PM, но оставляет команду C (аж в двух экземплярах) и также наши любимые DIV2A и DIV2S.

Мы можем их отсюда выкинуть, если добавить ещё один элемент, (~DestAddr[2]):

isOurAddr & DestAddr[3] & (((~DestAddr[4])&(~DestAddr[2])&DestAddr[1])|(DestAddr[4]&DestAddr[2]))



Похоже, это ничуть не увеличило размер АЛУ: так и осталось 194 ЛЭ. Оно в принципе ожидаемо: LUT (ГФ) плевать на сложность логического выражения, главное, чтобы входных бит осталось столько же!

Что ж, запускаем синтез всей связки QuatCore+GPU, с новыми файлами QuatCoreImmTable.v, QuatCoreCallTable.v, QuatCoreCode.mif и QuatCoreData.mif, то бишь после перекомпиляции нашего кода.

Как и раньше, всё вместе занимает 1004 ЛЭ. На симуляции идём сразу на T=1,149, место, где мы первый раз находим яркую точку, превышающую порог обнаружения:


Максимальная яркость на отрезке не изменилась с прошлого раза, 0x41F. А вот координата яркой точки была 0x0C, а стала 0x0E, наконец-то совпав с тем, что я смотрю в "фотошопе".

А в самом конце "слайда" мы видим, что в регистре C лежала шестёрка, наш "расчётный диаметр" пятна, не зря ж мы D1 обновили. И с замиранием сердца перематываем вперёд:


И действительно, к концу этого "слайда" завершается вторая итерация цикла, мы снова запрашиваем SrcAddr=0x83 (регистр C), и повторно получаем число 6, то есть теперь оно не затирается.


Сейчас ещё разок посмотрел на это дело, и кажется мне, что всё равно на единичку ошибся с координатой Y, надо её инициализировать значением "-1", а не нулём... А в остальном полный восторг.

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

  • Калифорния спятила

    Купил тут ключ для снятия бонок велосипеда (хитрых гаек, скрепляющих передние звёзды): Весь из себя фирменный, других в магазине не было. Но я…

  • Про интерференционный светофильтр

    В данном приборчике, видеоизмерителе параметров сближения, хочется максимально отгородиться от паразитной засветки, проще говоря от Солнца. Для…

  • Королёвские котики и тепловоз

    Акынский пост - "что вижу, о том пою!" Кот-консьерж ушёл в отпуск: в кои-то веки можно не стоять у входа на проходную, а лежать на солнышке!…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 1 comment