nabbla (nabbla1) wrote,
nabbla
nabbla1

Весеннее обострение

Настало 1 марта - самое время в очередной раз переделать АЛУ для QuatCore, ускорить его работу, в этот раз не по предельной частоте, а по количеству тактов.

Окинем прощальным взором существующую штуку:


195 ЛЭ при ширине аккумулятора 32 бита. Выполняет 31 команду, среди которых есть NOP ("ничего не делать"), Acc и C (занести значение в соотв. регистр), ADD/SUB/PM (прибавить, отнять, ± где знак определяется регистром Inv), ABS (взять модуль числа, он же "абсолютное значение, то бишь избавиться от знака), DIV2 / DIV2S / DIV2A / DIV2PM (текущее значение поделить на 2, и либо поместить в аккумулятор, либо вычесть из аккумулятора, либо прибавить, либо ± по знаку из Inv).

Плюс к тому 24 команды умножения: с накоплением (с полной точностью) и без накопления, в том числе FMPM (Fused Multiply Plus-Minus) заточенную под умножение кватернионов, комплексных чисел, векторное умножение векторов двумерных и трёхмерных, а также умножение на матрицу поворота 2х2, построенную "на лету" из "синуса" и "косинуса". А ещё 4 команды для нахождения суммы квадратов.

Результат можно "насыщать", т.е когда он выходит за границы 16-битного числа, автоматически будет взято максимальное положительное число 32767 или максимальное отрицательное, -32768. Предусмотрено округление "до ближайшего целого", что существенно улучшает точность вычислений (см. о необходимости правильного округления), а также "байпас" (дополнительная цепь, соединяющая выход АЛУ с его входом), позволяющий провести цепочку вычислений с точностью 32 бита, что также оказалось абсолютно необходимо для той математики, которую мы собираемся выполнять (решение системы линейных уравнений 6х6 с помощью разложения Холецкого). И для точного вычисления обратной величины по методу Ньютона тоже этот байпас необходим (см. хорошая штука байпас)

Всё бы хорошо, но в задачах обработки изображения этот АЛУ слишком копается!

И что-то мне кажется, что здесь гуманнее одним точным движением отрубить хвост, чем отрубать по маленькому кусочку...


Все команды, что у нас есть, подразделяются на:
- выполняющиеся за 1 такт: NOP, C, ZAcc ("обнуление" аккумулятора),
- выполняющиеся за 3 такта: Acc, ADD, SUB, PM, все вариации ABS,
- выполняющиеся за 4 такта: DIV2 и его вариации,
- выполняющиеся за 18 тактов: умножения знаковые, беззнаковые и смешанные (12 команд),
- выполняющиеся за 19 тактов: SQRD2 и его вариации (возведение в квадрат и деление на 2, с разными вариантами накопления)

Заправляет всем хтонический модуль QuatCoreFastALUcontrol:

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) :
			(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[2])&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] & DestAddr[1] | isMult;

wire MainBusy = isOurAddr & (DestAddr[4:2] != 3'b0_10)&(~isAdd)&(~finished);	
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

Обилие комментариев и их несоответствие коду показывает: модуль рождался в муках.

В модуле управления есть регистр состояния State, который может принимать значения sIdle ("ожидание"), sDiv2 ("деление на 2"), sFirstDiv ("первое деление"), sAdd ("сложение") и sMult ("умножение"). Чтобы досчитать до 16 в состоянии sMult, дополнительно стоит 4-битный счётчик с романтическим названием Counter.

На данный момент этот модуль считает, что адрес DestAddr[], то бишь код команды, "замораживается" на всё время выполнения АЛУ, поэтому множество вещей завязано напрямую на эти значения. Мы видим isLongCommand = DestAddr[4] - признак "длинной" команды, то есть одного из умножений на 18..19 тактов. Первые два бита мы обозначаем как PlusMinusMode[1:0], где 00 означает "обнулить аккумулятор", 01 - "±", 10 - "+", 11 - "-". Есть также "провод" OpSign, который уже непосредственно называет знак операнда, исходя из PlusMinusMode и значения PM из регистра Inv.

А ещё isSignedC, который позволяет воспринимать регистр C как знаковый или беззнаковый во время умножения.

Всё это хозяйство надо наделить памятью, потому как мы больше не хотим останавливать весь процессор на время вычисления, а значит, в DestAddr[] уже пойдёт следующая команда!

Можно просто все 8 бит скопировать в какой-нибудь регистр, но наверняка будет ещё лучше сохранить непосредственно те свойства, которые нам понадобятся в дальнейшем.

А вот шину данных мы уже обрабатываем правильно, то есть всё необходимое защёлкиваем уже на первом такте - а дальше хоть трава не расти!

Так вышло, потому что в первой версии QuatCore, ещё без конвейера, шина данных напрямую коммутировала источник данных и его получателя. Если мы брали значение из памяти - всё хорошо, оно оставалось неизменным (инкремент запрещался сигналом с АЛУ, до последнего момента). Но вот когда мы брали значение с выхода самого АЛУ, начинались фокусы - вот нам и пришлось сразу же принять меры!

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

Поэтому мы видим в коде регистр SeniorDataBit, куда заносится самый старший бит данных, и может использоваться в дальнейшем для взятия модуля числа. Но куда важнее, что логика так построена, что именно на первом такте данные заносятся либо в регистр B, либо в C, либо в оба сразу. Регистр B оказался "невидимкой" - Acc мы знаем, C мы знаем, а B как такового нет, потому что любое занесение данных в него сопровождается выполнением арифметического действия, а потому все команды имеют имена типа ADD, SUB, PM и т.д.

Полностью надо переработать логику формирования busy! Изначально она была простой: как только начала выполняться длинная команда - весь процессор останавливается, пока мы не закончим, зато заведомо все результаты и все флаги будут верными.

Потом она усложнилось: кроме сигнала MainBusy (тогда просто busy) появился AuxBusy, отвечающий за интерлок (блокировку), чтобы если следующая команда источником данных использует АЛУ (а выборка данных для неё производится заранее, то есть ПРЯМО СЕЙЧАС), то мы задерживаемся ещё на такт, чтобы защёлкнуть на шину данных правильный результат.

Можно было просто определить новый Hazard и заставить компилятор вставлять повсюду NOPы, но уж слишком часто у нас используется "цепочка арифметических операций", решили чуть поэкономить память.

Сейчас же модуль QuatCorePipelineALUcontrol (который придёт на смену текущему QuatCoreFastALUcontrol, как он пришёл на смену QuatCoreALUcontrol) должен формировать сигнал SrcStallReq только если мы уже заняты делом, а нам пришла новая команда, которую прямо сейчас мы исполнить не можем.

А модуль QuatCoreALUoutput, до сих пор совсем безобидный, должен обзавестись выходом DestStallReq, куда он подаст единичку, если запрошен результат работы АЛУ, а оно ещё не закончило работать. Чтобы оно знало о работе АЛУ, у модуля управления должен появиться ещё один выход busy.

Этот же выход busy будет соединён с QuatCorePC (счётчик инструкций), чтобы команды условного перехода JL, JGE, JO, GNO также дождались окончания работы АЛУ, прежде чем выполняться. А чтобы ожидание стало возможным, они также будут формировать сигнал SrcStallReq. Можно ещё немножко пожадничать и выдать с АЛУ специальный сигнал занятости для JL/JGE, только в том случае когда текущая команда изменит флаг знака S (sign), в противном случае переход будет выполнен незамедлительно.

И как будто этого мало, нужно позволить небольшую "конвейеризацию" процесса!
А именно, все без исключения команды АЛУ позволяют на последнем такте сделать с регистром B что угодно - поэтому вполне можно загрузить в него новое значение для следующей команды! С регистром Acc ситуация ровно наоборот: во всех командах, занимающих больше такта, на первом такте можно делать с регистром Acc вообще что угодно, либо нужно сохранить его состояние, поэтому и тут конфликта с предыдущей командой не проглядывается.

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

Если же SrcAddr принадлежит какому-то другому модулю (непосредственные значения, память, ввод-вывод) и мы "озадачили" АЛУ и двинулись дальше - то следующей командой на DestAddr сможет стать либо ЕЩЁ ОДНА команда АЛУ, а может стать команда условного перехода, но НЕ ОБЕ СРАЗУ.

Поэтому, размышляя о блокировке выполнения, можем не думать о ещё одной команде, которая уже начала исполняться. А размышляя о нескольких командах АЛУ подряд, можно не думать о блокировке - её не будет, потому что иначе она заблокировала бы появление новой команды, пока не выполнилась предыдущая!


Продолжение следует...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Лестница для самых жадных

    В эти выходные побывал на даче, после 3-недельной "самоизоляции". Забавно, как будто зима началась! Особенно грязные галоши остались на улице, в…

  • Возвращаемся к макету

    Очень давно макетом видеоизмерителя параметров сближения не занимался: сначала "громко думал" по поводу измерения его положения на аппарате, а потом…

  • Минутка живописи

    В процессе разгребания содержимого квартиры (после нескольких ремонтов) дошёл, наконец, и до картин. В кои-то веки их повесил. Куда их вешать -…

  • Костыль ноутбуку и кабели Франкенштейна (это не я!)

    Продолжаю разгребать хлам, накопившийся за много лет, раз уж всё равно на карантине сижу. Довольно продуктивный день: починил ноутбук (грубо, но…

  • Атомный файлсервер, убитые петухи и харакири андроида

    Продолжаю совершать хаотические движения по квартире, хвататься то за одну железяку, то за другую, с желанием каждую куда-нибудь "пристроить". "По…

  • Ещё про яркий светильник и ИБП

    Всё-таки применил лежащее дома барахло наиболее простым образом: А ещё наконец-то замерял температуру теплоотвода яркого светильника на кухне,…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments