nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Не столь монструозное АЛУ

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

Общая схема поменялась незначительно - управляющие воздействия слегка другими стали. А вот отдельные модули претерпели изменения.




QuatCoreALUadder теперь умеет только складывать. По сути, весь модуль - лишь "обёртка" для lpm_add_sub, чтобы был параметр AccWidth:

//decided to make it as simple as possible, by delegating constants to Acc, 
//and by delegating negation to Breg.
//but second part of negation, adding 1 is here of course (cin)

module QuatCoreALUadder (input [AccWidth-1:0] A, input [AccWidth-1:0] B, input cin, output [AccWidth-1:0] Q, output SignBit);

parameter AccWidth = 32;

lpm_add_sub ALU (	.dataa (A), //full acc width
					.datab (B), //this one extended as well
					.cin (cin),
					.result (Q[AccWidth-1:0]));
	defparam
		ALU.lpm_direction = "ADD",
		ALU.lpm_hint = "ONE_INPUT_IS_CONSTANT=NO,CIN_USED=YES",
		ALU.lpm_representation = "UNSIGNED",
		ALU.lpm_type = "LPM_ADD_SUB",
		ALU.lpm_width = AccWidth;	
		
assign SignBit = Q[AccWidth-1];

endmodule


Выход SignBit попросту дублирует старший бит, чтобы общая схема была красивее, не люблю, когда приходится давать имя всей шине, а потом отдельные провода называть как провод на шине. Если бы ещё там было ГОСТовское обозначение жгута с циферкам - ещё куда не шло. Поэтому если знаю, что отдельные биты понадобятся - "ответвляю" их в верилоге.

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

К сожалению, такой подход слегка усложняет устройство управления - инверсию надо "заказать" на предыдущем шаге (регистр сделает её по фронту тактового импульса), а вот единицу прибавлять - на текущем! Ну да ладно, поставить 1-битный регистр для задержки мы сообразим :)

В первой версии, ценой 32 ЛЭ этот модуль мог инвертировать один операнд, а чтобы добру не пропадать - ещё и хранил в себе сколько-нибудь констант. Теперь эту функциональность решили переложить на другие модули.

QuatCoreALUAcc теперь хранит все константы, которые трудно было бы загрузить из памяти, целых 4 штуки: +3/2, -3/2, 2 и "одну вторую младшего разряда" для правильного округления. Поначалу я не хотел этого делать, т.к мне казалось, что необходимость выбора одного из 7 действий:

- ничего не делать,
- загрузить результат сумматора,
- сбросить регистр,
- загрузить значение +3/2,
- загрузить значение -3/2,
- загрузить значение 2,
- загрузить значение "1/2lsb"

потребует по 2 ЛЭ на бит, т.е на ровном месте у нас добавится 32 ЛЭ. Действительно: из 4 входов ЛЭ, один мы резервируем для ENA (разрешение работы триггера), ещё один - для выхода сумматора, и остаётся всего два для выбора режима, а нужно три! Значит, понадобится ещё один ЛЭ.

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

Начать с того, что 28 разрядов из 32 вообще во всех 5 "нижних" операциях заполняются нулями!
Да и остальные тоже не так плохи.

Мы выбрали следующие коды операций для Acc:

000 - загрузить результат сумматора,
001 - ЗАПРЕЩЁННОЕ СОСТОЯНИЕ (никогда не должно появляться на входе, имеем право делать что хотим, UNDEFINED BEHAVIOUR практически),
010 - ничего не делаем (idle),
011 - обнуление,
100 - загрузить -3/2,
101 - загрузить 2,
110 - загрузить 3/2,
111 - загрузить 1/2lsb.

Вот что должно получаться на выходе при каждой инструкции:

000: DDDD......D... (данные с выхода сумматора, поступающие на вход D),
001: xxxxxxxxxxx... (что угодно)
010: QQQQ......Q... (сохраняются данные, лежащие в регистре),
011: 0000......0... (сброс),
100: 1010......1... (-3/2),
101: 1000......1... (2),
110: 0110......1... (3/2),
111: 0000......1... (1/2lsb)


Сигнал ENA (разрешение работы триггеров) мы формируем "честно":
wire ENA = (mode != 3'b010);

для этого все равно понадобился отдельный ЛЭ, поэтому не пытаемся упростить.
Далее, все действия будут производиться только если ENA=1:
always @(posedge clk) if (ENA) begin
...
...
end

Так что в последующем коде мы можем не рассматривать вариант 010, его там встретиться не может! (точнее, он встретится, и логика что-то посчитает, но сохранено в регистр оно не будет). Это облегчает нам жизнь, позволяя использовать более простую логику.

28 из 32 бит обрабатываются проще всего:
Q[AccWidth-4:AccWidth-17] <= (mode[2]|mode[1])? 14'h0000 : D[AccWidth-4:AccWidth-17]; 
Q[AccWidth-19:0] <= (mode[2]|mode[1])? {trailingzeros{1'b0}} : D[AccWidth-19:0];


Если хотя бы один из двух первых битов инструкции единичный, то мы их обнуляем, в противном случае загружаем результат из сумматора. Как видно, нам хватило двух управляющих битов, поэтому дополнительные ЛЭ не нужны.

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

Q[AccWidth-18] <= mode[2]? 1'b1 : mode[1]? 1'b0 : D[AccWidth-18];

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

Самый старший бит аккумулятора также хорош:

Q[AccWidth-1] <= mode[1]? 1'b0 : mode[2]? 1'b1 : D[AccWidth-1];

Если взглянуть на таблицу, то видим, что всё как надо.

А вот со следующим битом у меня пока что-то не получается. Там одна одинокая "единица", чтобы в нужный момент её поставить, явно нужно проверить два младших управляющих бита. Но чтобы затем отличить инструкцию "000" (загрузить результат сумматора) и "100" (загрузить константу -3/2) явно нужно проверить ещё и старший бит, так что теперь все три нам задают поведение:

Q[AccWidth-2] <= (mode[1:0]==2'b10)? 1'b1 : (mode[2:1]==2'b00)? D[AccWidth-2] : 1'b0;

Эта строка очевидно синтезируется в 2 ЛЭ - ну и ладно.

С ещё одним битом (3-й с начала) всё хорошо:

Q[AccWidth-3] <= (mode[2] & (~mode[0]))? 1'b1 : ((~mode[2]) & (~mode[0]))? D[AccWidth-3] : 1'b0;

тоже 1 ЛЭ.

Вот код получившегося модуля:

//mode = 000: load from D
//mode = 001: FORBIDDEN
//mode = 010: idle,
//mode = 011: clear,
//mode = 100: -3/2   (101000...)
//mode = 101: set 2  (100000...)
//mode = 110: set 3/2 (01100...)
//mode = 111: set 1/2lsb (00000...1)

module QuatCoreALUAcc (input clk, input [AccWidth-1:0] D, input [2:0] mode, output reg [AccWidth-1:0] Q=1'b0, output [16:0] Senior17, output [AccWidth-18:0] LSB);

parameter AccWidth = 32;
localparam trailingzeros = AccWidth - 18;

wire ENA = (mode != 3'b010);

always @(posedge clk) if (ENA) begin
	Q[AccWidth-4:AccWidth-17] <= (mode[2]|mode[1])? 14'h0000 : D[AccWidth-4:AccWidth-17]; 
	Q[AccWidth-19:0] <= (mode[2]|mode[1])? {trailingzeros{1'b0}} : D[AccWidth-19:0]; 	
	Q[AccWidth-18] <= mode[2]? 1'b1 : mode[1]? 1'b0 : D[AccWidth-18]; 	
	Q[AccWidth-1] <= mode[1]? 1'b0 : mode[2]? 1'b1 : D[AccWidth-1]; 
	Q[AccWidth-2] <= (mode[1:0]==2'b10)? 1'b1 : (mode[2:1]==2'b00)? D[AccWidth-2] : 1'b0;
	Q[AccWidth-3] <= (mode[2] & (~mode[0]))? 1'b1 : ((~mode[2]) & (~mode[0]))? D[AccWidth-3] : 1'b0;
end
							
assign Senior17 = Q[AccWidth-1:AccWidth-17];
assign LSB = Q[AccWidth-18:0];

endmodule


Этот модуль синтезируется в 34 ЛЭ, как и "заказывалось" - 32-битный аккумулятор, плюс один ЛЭ на второй по старшинству бит, плюс ещё один ЛЭ на формирование сигнала ENA.

Что странно, "автоматически" генерить столь же короткую схему синтезатор по-прежнему не может, хотя казалось бы, тут чистая комбинаторика, компьютер в таких задачах должен быть непобедим! Но нет, когда я написал такой код:

	Q <= 	(mode == 3'b000)? 	D :					//load
		(mode == 3'b001)? 	{AccWidth{1'bx}} :	//forbidden
		(mode == 3'b010)? 	{AccWidth{1'bx}} : 	//we don't get this because of ce
		(mode == 3'b011)? 	{AccWidth{1'b0}} : 	//clear
		(mode == 3'b100)? 	{18'b1_0100_0000_0000_0000_1, {trailingzeros{1'b0}}}: //-3/2
		(mode == 3'b101)? 	{18'b1_0000_0000_0000_0000_1, {trailingzeros{1'b0}}}: //2
		(mode == 3'b110)? 	{18'b0_1100_0000_0000_0000_1, {trailingzeros{1'b0}}}: //3/2
					{18'b0_0000_0000_0000_0000_1, {trailingzeros{1'b0}}}; //1/2lsb

(просто говорим, каким должен быть каждый из битов аккумулятора в каждом из 8 случаев, не забывая указать, когда нам до этого нет никакого дела!)

он синтезируется в 38 ЛЭ. Хранение констант здесь ускоряет операцию ZAcc (загрузка константы в акк.) - в предыдущей реализации она занимала 2 такта (сначала обнулить, затем прибавить константу), теперь - всего один.

Поскольку логика довольно укуренная, надо бы её проверить:


Всё верно.

Регистру B у нас слишком легко жилось - всего два режима работы по сути, "загрузить" и "сдвинуть". Ну ещё сигнал BSigned, определяющий, делать арифметический (знаковый) или логический (беззнаковый) сдвиг, но он действует только на старший бит (либо он сохраняет своё значение при сдвиге, либо обнуляется). И ещё выбор - загружать ли в младшие биты нули или младшие разряды аккумулятора по "байпасу".

Теперь мы заставим его также инвертировать биты, для того, чтобы можно было сменить знак числа. Мы работаем с дополнительным кодом, смена знака эквивалентна инверсии (замене нуля на единицу и наоборот) и прибавлению единички. Единичку прибавляет сумматор, а инверсию будет делать регистр B.

Первая идея была (уже давно) в введении 4 различных режимов работы регистра:

00 - загрузить "как есть",
01 - загрузить с инверсией,
10 - сдвинуть вправо "как есть",
11 - сдвинуть вправо с инверсией.

Тогда, если мы хотим умножить два знаковых числа, мы заблаговременно загружаем второе в регистр C, а затем загружаем первое в регистр B С ИНВЕРСИЕЙ, т.к мы начинаем с умножения на старший разряд второго числа, а это разряд со знаком "минус". На следующем такте выполняем сложение и выбираем сдвиг вправо С ИНВЕРСИЕЙ, чтобы вернуться к исходному числу. Дальнейшие сдвиги уже будут без инверсии.

Можно и так поступить, но младшие разряды опять немножко "перегружаются", в каждый из них должны поступать:
- выход разряда "слева", для сдвига,
- байпас от аккумулятора,
- управляющие сигналы, задающие не только 4 режима работы, но и выбирающие, воспользоваться ли байпасом или просто обнулить. (ведь и эти биты надо уметь брать хоть "как есть", хоть "с инверсией". Тут явно необходимо 3 бита.

Итого, 5 бит, а у нас элементы имеют 4 входа, т.е на каждый из "младших разрядов" (по умолчанию их 15) будет приходиться по 2 ЛЭ. Мы по-прежнему немножко в выигрыше в сравнению с начальным вариантом, но не сильно.

Есть вариант экономичнее в плане логических элементов, но замедляющий большинство операций на 1 такт.

А именно, мы добавляем ещё один, САМЫЙ СТАРШИЙ бит, и загружаем всегда "как есть", а вот сдвигаем иногда с инверсией, иногда-нет. И один сдвиг обязательно осуществляем в самом начале.

Что интересно, этот старший бит нам тоже весьма кстати - до этого мы делали "расширение знака" до размера аккумулятора на комбинаторной логике, а теперь оно будет выполнено "автоматически". То есть здесь мы даже не теряем 1 ЛЭ - он и так был задействован.

Тем самым, у нас освобождается один "режим", как раз для обнуления младших разрядов, если байпас не нужен (данные поступают не из аккумулятора, а откуда-то извне, по 16-битной шине). Правда, если попытаться объединить 2 бита "режима" с сигналом "EnableBypass" прямо в этом модуле, он опять делает "жадный выбор" или не знаю что - не понимает, что можно сделать одну простую операцию "снаружи" и раздать её на 16 элементов, вместо этого каждый "утолщает". Так что пока я ввёл 4 режима:

00 - сдвиг вправо "как есть",
01 - сдвиг вправо с инверсией,
10 - загрузка 16 бит (байпас выключен),
11 - загрузка 32 бит (байпас включен).

Теперь формирование этих бит отдаётся в модуль управления QuatCoreALUcontrol, надеюсь, когда туда добавится дополнительная логика, синтезатор сообразит всё правильно.

А код регистра B получился таким:

//mode = 00 - shift right
//mode = 01 - shift right and negate
//mode = 10 - load 16 bit (no bypass)
//mode = 11 - load AccWidth bit (bypass enabled)

module QuatCoreALUBreg (input clk, input[AccWidth-18:0] Dsmall, input [15:0] D, input DoSigned, input [1:0] mode, output [AccWidth-1:0] Q);

parameter AccWidth = 32;
localparam SmallPartWidth = AccWidth - 16;

reg seniorBit = 1'b0;
reg [14:0] largeQ = 1'b0;
reg [SmallPartWidth-1:0] smallQ = 1'b0;

assign Q = {seniorBit, largeQ, smallQ};

always @(posedge clk) begin
	seniorBit 	<= mode[1]? D[15] : DoSigned? (seniorBit^mode[0]) : 1'b0;
	largeQ 		<= mode[1]? D[14:0] : mode[0]? {~seniorBit, ~largeQ[14:1]} : {seniorBit, largeQ[14:1]};
	smallQ		<= mode[1]? (mode[0]? Dsmall : {SmallPartWidth{1'b0}}) : mode[0]? {~largeQ[0], ~smallQ[SmallPartWidth-1:1]} : {largeQ[0], smallQ[SmallPartWidth-1:1]};
end

endmodule


Увы, эта переделка означает: логику управления надо перекраивать! Я ещё не успел доделать модуль управления по старой схеме, и существенных изменений уже не будет, но всё-таки придётся снова на бумажке расчертить нолики и единички и мой любимый символ x, то есть don't care (мне по х).


UPD. На симуляции у нас "запрещённый" режим акк. 001 вёл себя очень мирно - также загружал значение из D. Подумалось - может оба варианта, 000 и 001, допустимы? Нет, нам просто повезло, вероятность была 50%, что загрузится. Вот другой пример:



Там ровно один бит, 3-й по старшинству, в режиме 001 всегда устанавливается в ноль, а все остальные - загружаются из D. Такое вот "неопределённое поведение".
Tags: ПЛИС, математика, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Я создал монстра!

    Вот нормальная счастливая пара разъёмов ОНЦ-БС-1-10/14-Р12-2-В и ОНЦ-БС-1-10/14-В1-2-В: У розетки кроме основного выступа, отмечающего "верх",…

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 14 comments

Recent Posts from This Journal

  • Я создал монстра!

    Вот нормальная счастливая пара разъёмов ОНЦ-БС-1-10/14-Р12-2-В и ОНЦ-БС-1-10/14-В1-2-В: У розетки кроме основного выступа, отмечающего "верх",…

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…