nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

Счётчик инструкций QuatCore зафурычил!

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

И каждая команда процессора - это просто пара (DestAddr, SrcAddr), т.е инструкция "взять данные с адреса SrcAddr и записать их по адресу DestAddr.

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

В прошлый раз мы разбирались с АЛУ (общая схема, модуль управления). В качестве "источника данных" он ведёт себя "тихо" - просто выдаёт либо значение аккумулятора, либо значение регистра C. А вот почти все обращения по адресам "получателя" запускают вычисления, и в зависимости от адреса вычисления разные.

Следующий важный модуль, возможно самый важный - это счётчик инструкций, он же Program Counter (PC). У нас именно в нём также содержится 3 индексных регистра i,j,k (по которым можно организовывать циклы) и однобитный регистр Inv. В очередной раз немного отступив от идеалов TTA, удалось получить довольно компактный набор инструкций с широкими возможностями условных переходов и с вызовом функции всего за одну 16-битную команду.

Схема пока не очень причёсанная, но рабочая :)

В упрощённой "конфигурации" (без команды ijk, которая на удивление усложняет реализацию), эта штука занимает 107 ЛЭ, в "полной" - 131 ЛЭ.


Выпишем для начала таблицу адресов SrcAddr ("источники данных")



Все адреса лежат в диапазоне от 0xA0 до 0xBF, именно этот диапазон выделен под данный модуль.

Первые адреса - наиболее простые для понимания. Обращаемся по адресу 0xA0 ("i") - на шину данных поступает значение регистра i. Аналогично - регистр j, k, Inv.

Когда мы обращаемся по адресу 0xA4 ("ijk") - все 4 этих регистра объединяются в одно 16-битное значение, как описывалось здесь:

Inv  k4 k3 k2 k1 k0 i4 i3 i2 i1 i0 j4 j3 j2 j1 j0


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

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

Самое приятное, что можно научить компилятор самому заменять код, где такая команда есть, эквивалентным более "толстым" кодом без неё. Так что программу пишем с расчётом на ijk, ну а если окажется, что мы не влезаем на ПЛИС в плане логических элементов, а памяти лишней сколько-нибудь есть - то отключим эту команду. Память мы можем брать очень "дискретно" - кусками по 512 байт. Т.е мучались-мучались, всю программу в 512 байт уместить не смогли, заняла она 640 байт - всё, теперь можно не жадничать, и растягивать её до 1024.

Наконец, адреса "CALL x" - это вызов одной из 16 процедур, адреса которых "зашиты" в модуль CallTable - это один из модулей, который будет генерироваться компилятором, для конкретной "бортовой программы". Здесь мы приводили пример, но оказалось, что верилоговский код там нерабочий - писался впопыхах и не проверялся, там очевидные ошибки типа недостающей точки с запятой и неправильного имени переменной :) Но это дело поправимое.

Может показаться странным, почему вызовы процедур записаны в "адрес источника". Дело в том, что выбор этих адресов заставляет модуль PC выдать на шину данных значение PC+1, т.е адрес возврата, в надежде, что адресом получателя (DestAddr) мы поставим [SP++], т.е этот адрес пойдёт напрямую в стек. И в качестве "побочного эффекта" будет произведён прыжок на адрес, указанный в таблице CallTable.

Эту функциональность тоже можно было бы "подсократить" - вызывать функцию в 2 команды. Сначала занести в стек адрес возврата, а затем совершить безусловный прыжок куда надо. Правда, в большинстве случаев понадобится 3 команды, т.к адрес функции не влезет в Immediate-значение (от -64 до +63), поэтому придётся хранить его в памяти, и сначала занести в один из регистров адрес ячейки, где хранится адрес функции, чтобы потом получить его через косвенную адресацию (а у нас другой и нет, по сути). Но "ломать не строить" - надо будет, тоже отдельным параметром всё это выкинем.

Теперь выпишем таблицу DestAddr (адреса получателя):


Первые строки снова весьма просты для понимания: записать содержимое шины данных в регистры i,j,k, Inv соответственно.

Затем идут команды инкремента - они вообще игнорируют содержимое шины данных, там может быть что угодно. На данный момент реализован только k++ (только он был нам нужен), но ничто не мешает сделать и остальные - похоже, каждая из этих команд добавляет всего 1 ЛЭ, для декодирования соотв. входа в реверсивный счётчик.

Дальше идёт знакомый нам ijk - занести данные в i,j,k, Inv РАЗОМ.

И затем - всевозможные команды переходов ("прыжков").
iLOOP - если i>0, уменьшает его на единицу и совершает переход,
jLOOP - если j>0, уменьшает его на единицу и совершает переход,
kLOOP - если k>0, ну вы поняли.
Jik - совершает переход, если i=k. Очень здорово для работы с симметричными и треугольными матрицами. А мы практически только с такими и работаем :)
JL - Jump if LESS (перейти, если "меньше") - проверяет флаг АЛУ isNeg
JGE - Jump if GREATER or EQUAL (перейти, если "больше или равно") - то же самое, но с другим знаком,
JO - Jump if Overflow (перейти, если переполнение) - проверяет флаг АЛУ isOFLO
JNO - Jump if Not Overflow (перейти, если нет переполнения) - то же самое, с другим знаком,

Во всех этих инструкциях "условного перехода" по шине данных поступает ОТНОСИТЕЛЬНЫЙ адрес, т.е насколько нужно прыгнуть ОТ ТЕКУЩЕГО. Это очень здорово для организации циклов и ветвлений, т.к диапазона "непосредственных значений" -64..+63 скорее всего хватит с лихвой.

JMP (jump) - безусловный переход. Он использует АБСОЛЮТНЫЙ АДРЕС. Это необходимо, поскольку именно эта инструкция используется для возвращения из процедуры:
 JMP [--SP]

а мы занесли в стек именно абсолютный адрес возврата, иначе нельзя.

Серым в этих таблицах мы отметили "недокументированные" инструкции, которые срабатывают, поскольку при малой населённости этих таблиц нам не хочется каждый раз проверять все 8 бит, хочется обойтись меньшим числом, зная, что мы будем пользоваться только правильными инструкциями. Там, где написано н/д, происходит какая-то дичь: инкремент конфликтует с командами условных переходов, в итоге переменная не убавляется, а прибавляется, а проверяем мы равенство её 31, т.е максимального значения. Может, и пригодится где-нибудь. А команда на строчку ниже Jik является симбиозом Jik и ijk - осуществляет прыжок, если i=k, а затем, все эти регистры заполняет из шины данных. Тоже действие довольно странное, учитывая, что на шине данных должен лежать относительный адрес для прыжка :)

Теперь, когда понятно, что мы хотим, пора рассказать, как это реализовано.

В модуле QuatCorePCreg хранится регистр PC (program counter). Он всегда выдаётся "как есть", и можно загрузить его с 3 различных входов - либо напрямую из шины данных (абсолютный прыжок), либо с выхода сумматора (нормальная работа, останов или относительный прыжок), либо из таблицы вызовов CallTable. Также он может обнулиться по сигналу Reset. Собственно, он единственный реагирует на Reset - инициализировать все остальные регистры в известное состояние - забота программиста :)

Код очень простой:
module QuatCorePCreg (	input clk, input [15:0] DataBus, input [RomAddrWidth-1:0] CallAddr,
			input [RomAddrWidth-1:0] RelAddr, input DoCall, input DoAbsAddr, input reset,
			output reg [RomAddrWidth-1:0] Q = 1'b0);

parameter RomAddrWidth = 9;

always @(posedge clk)
	Q <= 	reset? 		{RomAddrWidth{1'b0}} :
		DoCall? 	CallAddr :
		DoAbsAddr? 	DataBus[RomAddrWidth-1:0] :
						RelAddr;
endmodule



Довольно простая штука, а если выкинуть CallTable - станет ещё проще. Управляющие воздействия DoCall и DoAbsAddr декодируются из SrcAddr и DestAddr, соответственно. Модули для этого совсем простые, может и не стоило выделять их отдельно, но мне кажется, так понятнее, что происходит.

Модуль QuatCorePCisFuncCall:
module QuatCorePCisFuncCall (input [7:0] SrcAddr, output Q);

assign Q = (SrcAddr[7:4] == 4'b1011);

endmodule


И модуль QuatCorePCisJMP:
module QuatCorePCisJMP (input [7:0] DestAddr, output Q);

assign Q = (DestAddr[7:3] == 5'b10111);

endmodule


Модуль CallTable, пока что поправленный вручную, выглядит так:
module CallTable (input [7:0] index, output [8:0] addr);
	wire [2:0] i = index [2:0];
	assign addr = 
		(i==0)? 9'd10:		//AffineAlgorithm
		(i==1)? 9'd301:		//CopyTranslVector
		(i==2)? 9'd255:		//NormSiCo
		(i==3)? 9'd285:		//QuatMultiply
		(i==4)? 9'd275:		//RotateVecByQuat
		(i==5)? 9'd247:		//ShiftOrigin
		(i==6)? 9'd241:		//SwapPoints
		(i==7)? 9'd149:		//TrackingAlgorithm
		9'bxxxxxxxxx;
endmodule


Пока что мой компилятор ассемблера QuatCore расставляет процедуры в алфавитном порядке - не лучший выбор! ПЛИС не даёт большого пространства для оптимизации - любой 4-битный ГФ (LUT) совершенно равнозначен. Но может получиться так расположить адреса функций, чтобы некоторые выходные биты вообще не требовали декодирования, а совпадали с одним из 4 входных битов. На этом можно сэкономить 1-2 ЛЭ, может потом этим займёмся.

Чтобы сформировать "относительный адрес", а также обработать "нормальный ход операции" и останов, и ещё сформировать адрес возврата, используется сумматор QuatCorePCadder. Проще показать его код, чем описывать словами:
module QuatCorePCadder (input [RomAddrWidth-1:0] PC, input [15:0] D, input DoJump, input busy,
			output [RomAddrWidth-1:0] Q);

parameter RomAddrWidth = 9;
/*
assign Q = 	busy?		PC :
		DoJump? 	PC + D[RomAddrWidth-1:0] :
				PC + 1'b1;
										*/
								
wire [RomAddrWidth-1:0] incr = busy? 	{RomAddrWidth{1'b0}}:
				DoJump?	D[RomAddrWidth-1:0]:
				{{(RomAddrWidth-1){1'b0}},1'b1};
		
lpm_add_sub adder (	.dataa (PC), //full acc width
			.datab (incr), //this one extended as well
			.cin (1'b0),
			.result (Q));
	defparam
		adder.lpm_direction = "ADD",
		adder.lpm_hint = "ONE_INPUT_IS_CONSTANT=NO,CIN_USED=NO",
		adder.lpm_representation = "UNSIGNED",
		adder.lpm_type = "LPM_ADD_SUB",
		adder.lpm_width = RomAddrWidth;
										
endmodule


Лучше всего работу объясняет закомментированный код - ровно его хватило бы для этого модуля.

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

Закомментировать его пришлось, поскольку он генерировал два отдельных сумматора - один для прибавления D, и ещё один для прибавления единицы, и всё это безобразие синтезировалось в 38 ЛЭ. После добавления сумматора в явном виде, модуль "ужался" до 19 ЛЭ - уже лучше.

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

Модуль QuatCorePCrelJumpDecision определяет - нужно ли нам совершать "относительный прыжок". Это чисто комбинаторная штука, довольно сложная, но синтезируется в 6 ЛЭ - не так уж и плохо:

module QuatCorePCrelJumpDecision (	input isNeg, input isOFLO, input iZ, input jZ, input kZ, input iEQk,
					input [7:0] DestAddr,
					output DoJump);

wire isOurOp = (DestAddr[7:5] == 3'b101)&(DestAddr[4]^DestAddr[3]);

wire isLOOP = isOurOp & DestAddr[3];

wire DoiLOOP = isLOOP & (DestAddr[1:0] == 2'b00) & (~iZ);
wire DojLOOP = isLOOP & (DestAddr[1:0] == 2'b01) & (~jZ);
wire DokLOOP = isLOOP & (DestAddr[1:0] == 2'b10) & (~kZ);
wire DoJik	 = isLOOP & (DestAddr[1:0] == 2'b11) & iEQk;

wire isFlags = isOurOp & (~DestAddr[3]);
wire DoJL	= isFlags & (~DestAddr[1]) & (isNeg ^ DestAddr[0]);
wire DoJO	= isFlags & DestAddr[1] & (isOFLO ^ DestAddr[0]);

assign DoJump = DoiLOOP | DojLOOP | DokLOOP | DoJik | DoJL | DoJO;

endmodule


Как видно, мы убеждаемся, что задан именно адрес одной из команды условного перехода, и что соответствующее условие выполнено. Флаги АЛУ попросту идут отдельными "проводами", а условия для регистров i,j,k - из нашего же модуля QuatCorePCregisters.

Этот модуль содержит в себе регистры i,j,k, Inv, и управляет их загрузкой и инкрементом/декрементом. Он один из наиболее сложных здесь. Вот его код:

//registers i,j,k, Inv
//first 3 are 5 bit, last is 1 bit

//one of them, k, allows both incrementing and decrementing
//all of them allow synchronous loading and decrementing if they're more than 0.
//that's needed to organize loops.

//1010 0000 - i
//1010 0001 - j
//1010 0010 - k
//1010 0011 - Inv

//1010 0100 - i++ (optional)
//1010 0101 - j++ (optional)
//1010 0110 - k++
//1010 0111 - ijk (optional)

//1010 1000 - iLOOP
//1010 1001 - jLOOP
//1010 1010 - kLOOP
//1011 xxxx - other jump instructions, ignoring i/j/k/Inv

module QuatCorePCregisters (input clk, input [7:0] DestAddr, input [15:0] DataBus,
			output [4:0] ireg, output [4:0] jreg, output [4:0] kreg, output reg invreg = 1'b0,
			output iZ, output jZ, output kZ, output iEQk);

parameter isize = 5;
parameter jsize = 5;
parameter ksize = 5;
parameter ijkEnabled = 1'b1;
parameter ippEnabled = 1'b0;
parameter jppEnabled = 1'b0;
parameter kppEnabled = 1'b1;

wire isOurOp = (DestAddr[7:4] == 4'b1010); //excludes non-PC ops and unrelated JMP ops
wire isIJK = ijkEnabled & DestAddr[2] & DestAddr[1] & DestAddr[0]; //good command, but probably pretty annoying for HW (lots of MUXes!)
wire isWrite = isOurOp & (~DestAddr[3]); //excludes also LOOP ops, but still leaves increments

//so far no i++
wire WriteI = isWrite & (((DestAddr[1:0] == 2'b00)&(~(DestAddr[2]&ippEnabled)))|isIJK);
//wire Iup = isOurOp
//so far no j++

wire WriteJ = isWrite & (((DestAddr[1:0] == 2'b01)&(~(DestAddr[2]&jppEnabled)))|isIJK);

//we want k++ 
wire WriteK = isWrite & (((DestAddr[1:0] == 2'b10)&(~(DestAddr[2]&kppEnabled)))|isIJK);

wire WriteInv = isOurOp & (DestAddr[1:0] == 2'b11);

wire DecI = isOurOp & DestAddr[3] & (DestAddr[1:0] == 2'b00) & (~iZ);
//wire CountI = isOurOp & 
wire DecJ = isOurOp & DestAddr[3] & (DestAddr[1:0] == 2'b01) & (~jZ);
//wire DecK = isOurOp & DestAddr[3] & DestAddr[1] & (~kZ);
wire CountK = isOurOp & ((DestAddr[3] & (~kZ))|(DestAddr[2]&kppEnabled)) & (DestAddr[1:0] == 2'b10);
wire Kup = (DestAddr[2]&kppEnabled);

lpm_counter cnt_i (	.clock (clk),
			.data (isIJK? DataBus[4+isize:5] : DataBus[isize-1:0]),
			.sload (WriteI),
			.cnt_en (DecI),
			.Q (ireg[isize-1:0]),
			.cout (iZ));
defparam
	cnt_i.lpm_direction = "DOWN",
	cnt_i.lpm_port_updown = "PORT_UNUSED",
	cnt_i.lpm_type = "LPM_COUNTER",
	cnt_i.lpm_width = isize;

lpm_counter cnt_j (	.clock (clk),
			.data (DataBus[jsize-1:0]),
			.sload (WriteJ),
			.cnt_en (DecJ),
			.Q (jreg[jsize-1:0]),
			.cout (jZ));
defparam
	cnt_j.lpm_direction = "DOWN",
	cnt_j.lpm_port_updown = "PORT_UNUSED",
	cnt_j.lpm_type = "LPM_COUNTER",
	cnt_j.lpm_width = jsize;

lpm_counter cnt_k (	.clock (clk),
			.data (isIJK? DataBus[9+ksize:10] : DataBus[ksize-1:0]),
			.sload (WriteK),
			.cnt_en (CountK),
			.updown(Kup),
			.Q (kreg[ksize-1:0]),
			.cout (kZ));
defparam
	//cnt_k.lpm_direction = "DOWN",
	//cnt_k.lpm_port_updown = "PORT_UNUSED",
	cnt_k.lpm_type = "LPM_COUNTER",
	cnt_k.lpm_width = ksize;

always @(posedge clk) if (WriteInv)
	invreg <= isIJK? DataBus[15] : DataBus[0];
	
assign iEQk = (ireg == kreg);

endmodule


Собственно, по обилию комментариев и закомментированных строчек можно судить, сколько времени уходило на тот или иной модуль :)

В целом, ничего сложного и здесь, просто долго и громко думал, как расположить команды по адресам, чтобы они декодировались наиболее логичным образом. Всегда в таких случаях хочется поразмашистее, чтобы отдельные биты напрямую чем-нибудь управляли, но тогда в 8 битах (из них 3- выбор модуля PC) становится как-то уж слишком тесно!

И наконец, модуль выходного мультиплексора - очень прост концептуально:
module QuatCorePCsrcMux (	input [4:0] ireg, input [4:0] jreg, input [4:0] kreg, input invreg, 
				input [RomAddrWidth-1:0] PC, input [7:0] SrcAddr,
				output [15:0] Q, output [15:0] ijk);
							
parameter RomAddrWidth = 9;
parameter ijkEnabled = 1'b1;
localparam LeadingZeros = RomAddrWidth - 5;
localparam InvLeadingZeros = RomAddrWidth - 1;
wire [1:0] rg = SrcAddr[1:0];
wire isIJK = SrcAddr[2]&ijkEnabled;

assign ijk = {invreg, kreg, ireg, jreg};

assign Q = 	SrcAddr[4]? 			PC :
			isIJK? 					ijk :
			(rg == 2'b00)&(~isIJK)? {{LeadingZeros{1'b0}}, ireg} :
			(rg == 2'b01)&(~isIJK)?	{{LeadingZeros{1'b0}}, jreg} :
			(rg == 2'b10)&(~isIJK)? {{LeadingZeros{1'b0}}, kreg} :
									{{InvLeadingZeros{1'b0}}, invreg};
endmodule


Один из выходов, Q - через общий мультиплексор поступает на шину данных, а выход ijk - на модуль QuatCoreMEM, чтобы можно было обращаться к памяти с разными индексами. Это та функциональность, которую я ни в коем случае выкидывать не буду, потому как без всех этих режимов адресации с индексами программный код легко вырастет в 2-3 раза и по ходу дела потеряет точность.

Для проверки функционирования я нарисовал такую вот схемку:


Ничего хитрого - 11-битный счётчик перебирает все адреса, причём младшие перебирают внешние флаги - isNeg, isOFLO и busy, и также 8 младших идёт на SrcAddr. А выходы 10:3 идут на DestAddr. Всё это целиком - на шину данных, в общем, хотелось, чтобы по каждой команде что-то изменялось на входах и мы могли бы проверить все "ветви" кода. Покрытие не получилось 100% - я смог увидеть всё, кроме перехода по условию i=k. Должно сработать - с чего бы ему не сработать, проверю это уже наверное "в составе процессора".

А так, имеем простыню навроде такого:


Здесь мы видим команду i: действительно загружает в регистр i. А ещё наблюдаем, как меняются выходы на Q, при том, что там вообще "чужой" адрес задан на SrcAddr, не относящийся к QuatCorePC. Но это правильно - "общий" мультиплексор всё равно нас отсечёт!

Дальше идут команды j, k и Inv - все работают. Ещё можно видеть, как счётчик инструкций прибавляет по единице, кроме случаев busy = 1 - тогда он стоит на месте.

А здесь мы можем наблюдать команды iLOOP/jLOOP/kLOOP:


Сначала наблюдаем за регистрами - по соответствующей команде они начинают уменьшаться на единицу. Регистр k был равен единице - он уменьшается до нуля и "застревает" на нуле - так и задумано!

Далее наблюдаем за счетчиком инструкций PC - пока идёт уменьшение, он безумно "прыгает", а затем начинает мерно отсчитывать по единице. Всё как задумано.


Остался небольшой должок - написать модулёк, который из регистров Inv, i, k образует сигнал PM (Plus-Minus) для АЛУ. И затем - огромный модуль QuatCoreMEM, для обращения к оперативной памяти.

Ну и заставить всё это дело корректно исполнять программы :) Интересно - до ближайшего нового года успею? Ответ может дать гадание по картам Карно!
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 

  • 3 comments