nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

QuatCore: сделаем счётчик инструкций счётчиком!

Начинаем в очередной раз переделывать процессор QuatCore так, чтобы вместо "непосредственных значений" от -64 до +63, которые можно задать непосредственно "в коде", у нас получилось бы по сути 128 произвольных 16-битных констант, что сразу упростило бы нам жизнь.

В прошлый раз доработали файл конфигурации компилятора, чтобы к каждой команде указывалась "маска шины данных", а именно, какие из 16 бит реально оказываются задействованы, а какие никуда не идут. Так, при записи в регистры i,j,k поступает только 5 младших бит, а в i++/j++/k++ - и вовсе шина данных оказывается не нужна. (Можно было бы придумать команды i++/j++/k++, лежащие СПРАВА, по SrcAddr, т.е прибавление единицы является "побочным эффектом", а основное действие - выдать содержимое регистра на шину данных, чтобы с ним что-то сделала команда СЛЕВА, которая DestAddr. Но пока это было не нужно, и лишь усложнило бы модуль QuatCorePC и его взаимодействие с конвейером)

Сейчас разберёмся с относительными прыжками. Ещё одна доработка компилятора показала: в нашей самой "толстой" на текущий момент программе, в которой исполнен "аффинный алгоритм", он же алгоритм захвата, самый длинный относительный прыжок "назад": -16, а "вперёд": всего-навсего +2. В итоге, чтобы все эти прыжки выразить, хватает всего 5 бит, при ширине регистра PC: 8 бит на данный момент, и, надеюсь, не более 10 бит в дальнейшем.

Давайте попробуем в очередной раз сэкономить логические элементы, а именно, укоротить сумматор, дающий следующее значение Program Counter (PC)...



Регистр PC и его "обвязка" на данный момент вышли весьма крупными, ведь к следующему такту он может:
- сброситься в ноль по сигналу reset. Не уверен, что это реально нужно: при подаче питания на ПЛИС и её конфигурации PC и так устанавливается в ноль, а при "любой непонятной ситуации", в которой нам следовало бы вернуться "в известное начальное состояние", лучше всего было бы "перезапустить" всю ПЛИС, чтобы восстановить и содержимое оперативной памяти. Просто reset может и не помочь, если мы себе всю оперативку затёрли... Но сам сигнал reset пока идёт, я его уже "снаружи" QuatCore заземляю.
- принять значение с шины данных - это переход по абсолютному адресу, JMP. Применяется в первую очередь для возврата из процедур: ведь в стек был записан АБСОЛЮТНЫЙ АДРЕС PC, значит, теперь по нему и должны прыгнуть. Для прочих "безусловных прыжков" могли бы и относительный адрес использовать...
- принять значение из таблицы вызова процедур, QuatCoreCallTable.
- остаться "как есть", если у нас остановился конвейер. Это обеспечивается подачей на сумматор "относительного адреса" 0,
- прибавить единичку, что соответсвует нормальному выполнению программы (без переходов и вызовов процедур). Обеспечивается подачей на сумматор относительного адреса 1,
- прыжок по относительному адресу. Тогда, как и в двух предыдущих случаях, в регистр PC заносится значение из сумматора относительного адреса, на который поступает значение из шины данных.

Входов получается больше четырёх: в каждый бит PC должен идти провод из шины данных (для абс. прыжка), ещё один из таблицы адресов процедур (хотя, если повезёт и там стабильно ноль или единица - он окажется не нужен), третий - из сумматора, и хотя бы два нужны для выбора режима (сброс/абс. прыжок/вызов процедуры/всё остальное). Значит, каждый бит будет занимать минимум 2 ЛЭ, что довольно грустно.

Да и второй вход сумматора требует логических элементов, чтобы выбрать, подключаем ли мы туда шину данных, или ноль, или единицу? В результате, у нас по 2 ЛЭ уходит на каждый бит регистра PC, ещё 1 ЛЭ на бит сумматора и 1 ЛЭ - на "входной мультиплексор" этого сумматора, итого 4 ЛЭ на бит.

В принципе, это не так уж и плохо, ну 40 ЛЭ в случае 10-битного регистра PC, и что такого? В конце концов, это "сердце" процессора, и эти 40 ЛЭ дают очень нефиговую гибкость в работе: хочешь прыжки такие, хочешь эдакие, хочешь процедуру вызывай!

Но можно ещё немножко пожадничать: сумматор оставить только на младшие 4 бита, а для всех остальных поставить реверсивный счётчик. Регистр PC от этого меньше не станет: реверсивный счётчик нам входы не сэкономит. Всё равно понадобится поставить мультиплексор на вход data (для синхронной загрузки), чтобы либо брал значение с шины данных (прыжок по абсолютному адресу), либо с таблицы вызовов процедур. Если "логический элемент общего назначения" может сделать мультиплексор "внутри себя", то реверсиный счётчик (специальный режим работы логических элементов) - нет, ему подавай sload (разрешение синхронной загрузки), data (вход сихнронной загрузки), cnt_en (разрешение счёта) и updown (направление счёта).

Но экономия у нас появится на сумматоре и мультиплексоре на его входе: теперь и то, и другое ужмётся с 8..10 бит всего до 4 бит! Именно в 4 младших битах будет производиться нормальное человеческое суммирование без знака. 5-й бит, который показывает знак, мы обработаем отдельно. А именно, из него и из выхода переноса сумматора мы формируем сигналы управления реверсивным счётчиком. Возьмём такой пример: было PC=14 (9-битное значение), а мы прибавляем 2:

  0000_01110
+      00010
  ----------
  0000_10000


4-битный сумматор честно делает свою работу, получая 0000 и выход переноса: 1. Если мы получили эту единичку в переносе, а старший бит прибавляемого значения нулевой (т.е знак ПОЛОЖИТЕЛЬНЫЙ), то нужно сформировать сигнал "+1" на реверсивный счётчик, в котором хранятся старшие 5 бит. И действительно, он прибавит единицу - и результат получится правильный.

Теперь глянем, что будет, если мы прибавляем число 10010 = -14 (в 5-битном дополнительном коде):
  0000_01110
+      10010
  ----------
  0000_00000


В этот раз у нас опять сформировался сигнал переноса: 1, но и в старшем бите лежала единичка, которая интерпретируется скорее как "-1". В итоге, они друг друга погасили, и счётчик должен остаться в прежнем состоянии.

А если мы прибавляем -15 (10001), то перенос не возникает, и вот теперь счётчику надо дать сигнал "-1":

  0000_01110
+      10001
  ----------
  1111_11111

и получаем "-1", или, что то же самое, 511.

В общем, сигнал разрешения работы счётчика формируется как ИСКЛЮЧАЮЩЕЕ ИЛИ (XOR) между переносом и старшим (знаковым) разрядом поступающего числа, а направление счёта - через сам старший разряд. Если там 1, нужно вычитать, в противном случае - складывать.

Получается такая вот штуковина:


Нас интересуют два нижних блока: QuatCoreLighterPCreg и QuatCoreLighterPCadder. Появился новый параметр, RelAddrWidth = 5 - это сколько бит используется для "относительных адресов". Его должен будет выставлять компилятор, как и все остальные. Но пока выставляю я ручками, очень уж лениво упихивать весь QuatCore в "текстовый" (верилоговский) модуль верхнего уровня, в котором эти параметры будут прописаны и "правиться" компилятором.

Сигнал reset исчез - по-моему, толку от него немного. Шина RelAddr, идущая с выхода сумматора на регистр, имеет странную ширину: RelAddrWidth..0, т.е на один бит шире, чем ожидается. Так получилось, потому что старшие 2 бита - это на самом деле сигналы для реверсивного счётчика, updown и cnt_en.

Глянем, наконец, на код модулей. Вот сам регистр PC:
module QuatCoreLighterPCreg (	input clk, input [15:0] DataBus, input [RomAddrWidth-1:0] CallAddr,
				input [RelAddrWidth:0] RelAddr, input DoCall, input DoAbsAddr,
				output [RomAddrWidth-1:0] Q);

parameter RomAddrWidth = 9;
parameter RelAddrWidth = 5;
localparam CounterWidth = RomAddrWidth - RelAddrWidth + 1;

reg [RelAddrWidth-2:0] PC = 1'b0;

always @(posedge clk)
	PC <= 	DoCall? 	CallAddr[RelAddrWidth-2:0] :
		DoAbsAddr? 	DataBus[RelAddrWidth-2:0] :
				RelAddr[RelAddrWidth-2:0];
						
assign Q[RelAddrWidth-2:0] = PC;
						
lpm_counter Senior (.clock (clk),
		.data (DoCall? CallAddr[RomAddrWidth-1:RelAddrWidth-1] : DataBus[RomAddrWidth-1:RelAddrWidth-1]),
		.sload (DoCall | DoAbsAddr),
		.cnt_en (RelAddr[RelAddrWidth-1]),
		.updown (RelAddr[RelAddrWidth]),
		.Q (Q[RomAddrWidth-1:RelAddrWidth-1]),
		.cout ());
defparam
	Senior.lpm_type = "LPM_COUNTER",
	Senior.lpm_width = CounterWidth;					
						
endmodule


Штука довольно своеобразная. Если мы говорим, что для записи относительного адреса нам достаточно 5 бит, то из всей ширины регистра PC, 4 бита уйдёт на "регистр общего назначения", а все остальные: на реверсивный счётчик. Объяснение приведено выше: бит знака обрабатывается "вне" сумматора. Ну и входы updown и cnt_en реверсивного счётчика подключены к двум старшим битам шины RelAddr, что может быть не совсем хорошо, лучше было бы "отдельными проводами", но лениво.

И код сумматора:
module QuatCoreLighterPCadder (input clk, input [RomAddrWidth-1:0] PC, input [15:0] D, input DoJump, input DoCall, input DoAbsAddr, input busy, input DestStall,
				output [RelAddrWidth:0] Q, output SrcDiscard);

parameter RomAddrWidth = 9;
parameter RelAddrWidth = 5;

wire SureToJump = (~DestStall)&DoJump;
								
wire [RelAddrWidth-2:0] incr = 	busy?		1'b0:
				SureToJump?	D[RelAddrWidth-2:0]:
						1'b1;
wire cout;
		
lpm_add_sub adder (	.dataa (PC[RelAddrWidth-2:0]), 
			.datab (incr),
			.cin (1'b0),
			.result (Q[RelAddrWidth-2:0]),
			.cout (cout));
	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 = RelAddrWidth-1;

wire Neg = D[RelAddrWidth-1] & SureToJump;

assign Q[RelAddrWidth-1] = Neg ^ cout;
assign Q[RelAddrWidth] = ~Neg;

reg SrcReg = 1'b0;
wire AnyJump = SureToJump | DoAbsAddr;
assign SrcDiscard = SrcReg | AnyJump;
always @(posedge clk) if (~busy)
	SrcReg <= DoCall | AnyJump;
										
endmodule


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

Для "аффинного алгоритма" старая версия QuatCore (без периферии и пр.) компилировалась в 519 ЛЭ, а новая версия - в 507 ЛЭ, на 12 ЛЭ меньше. Так что какой-то толк в этом как будто есть, аж на 2,3% уменьшили размер...

Но надо проверить, что "ничего не поломали" - запускаем симуляцию и смотрим на поведение PC.


И увы, что-то пошло не так. Мы видим, как бит за битом в PC "заползают" значения "x" (неопределён), пока не занимают его целиком. Я надеялся, что отрабатывать счётчик инструкций "отдельно от всего" не придётся - проверю его прямо в составе процессора, и если всё работает как прежде - на этом и успокоюсь.

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

И только тогда сообразил: дело было не в этих внесённых изменениях, а в новых входах SrcStallReq и DestStallReq (запрос остановки по Src и по Dest), которые нужны для подключения модулей ввода-вывода. Их не было в моём Vector Waveform File, с помощью которого я проверял отдельно "ядро" QuatCore, вот так оно и получилось.

Добавил их, всегда нулевые, и попробовал ещё разок:


Это больше похоже на правду. Чтобы понять, что там происходит, приведём фрагмент листинга:
main proc
00  FD47              SP      StackAdr
01  8900              NOP     0       ;избежать Write Hazard: только здесь в SP запишется новое значение!
02  FDFC              SP      [SP]
AffineAlgorithm     proc
    Associate4Points    proc
        FindMDD3    proc
03  DD18                  Y       Points2D
04  F000                  [SP+1]  0   ;максимальная отдалённость, инициализируем нулём
05  A103                  j       3
06  A201          @@j_loop:   k       1   ;также от 0 до 3, чтобы все расстояния просуммировать
07  FC00                  [SP]        0   ;здесь будет храниться текущий максимум
08  A003          @@k_loop:   i       3   ;от 0 до 1, т.е значения X и Y
09  80DA          @@i_loop:   Acc     [Y+2j+k]    ;загрузили одно значение
0A  83D9                  SUB     [Y+2i+k]    ;вычитаем второе
0B  9C80                  SQRD2       Acc         ;возводим в квадрат
0C  82FC                  ADD     [SP]        ;прибавляем к пред. значению
0D  FC80                  [SP]        Acc
0E  A879                  iLOOP       @@i_loop        ;теперь то же самое для второй координаты
0F  AA77                  kLOOP       @@k_loop
10  83F0                  SUB     [SP+1]  ;можно и "пожертвовать" значением в Acc,
11  B801                  JL      @@skip
12  F0FC                  [SP+1]  [SP]    ;двухпортовая память-это зашибись!
13  CDA1                  X       j
14  A970          @@skip: jLOOP       @@j_loop
15  A0CD                  i       X
16  CD18                  X       Points2D
17  ED18                  Z       Points2D
18  F3B4                  CALL        SwapPoints  ;потёрли текущий максимум (лежал в [SP])-и хрен с ним
        FindMDD3    endp


Сигнал reset у нас больше "не работает", и запуск до сих пор немножко "с особенностями". Хотя я "просил", чтобы регистры, задерживающие DestAddr, инициализировались значением 0x89 (NOP), так делает только один регистр из двух, поэтому сначала всё равно мы видим 0x00 (OUT), на следующем такте 0x89 (NOP), а вот после этого начинается нормальное выполнение инструкций.

В остальном, пока всё как надо: PipeStall (приостановка конвейера на "медленных" командах) явно работает, и нормальный счёт работает, по крайней мере, пока нет переноса из младших 4 бит.

Наблюдаем мучительную инициализацию стека (надо потерпеть, скоро писать такой дурацкий код больше не придётся!): сначала в SP занесли значение 0xC7 - адрес в памяти, где лежит адрес стека :) Затем и этот адрес, 0x5F, занесли в SP. Потом инициализировали кучу регистров - и начали находить сумму квадратов расстояний от 3-й точки, пока что до 3-й точки.

Проматываем чуть вперёд, где начинаются относительные прыжки.


Здесь мы видим вполне успешный счёт с 0x0F до 0x10 (именно здесь задействуется реверсивный счётчик), а затем - "прыжок назад", по относительному адресу "-7", к 0x09. Опять же, здесь сработал как сумматор, так и реверсивный счётчик, теперь уже "в минус".

На следующем скриншоте мы видим ещё один "прыжок назад", в этот раз по адресу "-9", от 0x11 до 0x08. Тоже всё сработало.


За прыжок "вперёд" на значение "+1" мы спокойны, поскольку он не шибко отличается от нормального выполнения команд, кроме возникновения SrcDiscard и DestDiscard, чтобы не выполнить несколько команд, уже сидящих в конвейере.

И ещё посмотрим вызов процедуры и возврат из процедуры. Вызов нас ожидает не так уж быстро: сначала надо прокрутиться по трём вложенным циклам (для каждой из 4 точек, мы хотим найти сумму квадратов её расстояний до всех остальных, для чего нужно ещё 2 вложенных цикла, один - по номеру координаты, 0 это X, 1 это Y, и второй - по номеру точки), выполнив инструкции "посерединке" 32 раза кряду. И уже после того, как мы нашли самую удалённую от остальных точку, мы вызываем SwapPoints, чтобы поместить её в нулевую позицию:


И приведём фрагмент листинга, чтобы проверить правильность выполнения:
SwapPoints  proc
93  A201              k       1
94  8900              NOP     0   ;хотим избежать warning'ов
95  8AEA  @@swap:     C       [Z+2j+k]
96  EAC9              [Z+2j+k]    [X+2i+k]
97  C983              [X+2i+k]    C
98  AA7B              kLOOP       @@swap
99  B0FF              JMP     [--SP]
SwapPoints  endp    


Итак, мы действительно прыгнули куда надо, на адрес 0x93. Уже радостно.

Далее, здесь интересный вариант прыжка "назад", когда реверсивный счётчик сработать не должен, из позиции 0x9A переместившись в 0x95 (как видно, старшие 4 бита не поменялись). И мы видим, что происходит ровно так.

И, наконец, проверяем переход по абсолютному адресу, то бишь возврат из процедуры. Мы перепрыгиваем в 0x19, и снова это правильное поведение.


Итак, маленькая радость: ничего не сломали, сделали процессор чуточку компактнее, и теперь при относительных переходах он использует только младшие 5 бит с шины данных.

Правда, сразу ещё одна мысль появилась: а если наши "непосредственные значения" больше не ограничиваются диапазоном -64..+63, то может в относительных прыжках вообще смысл исчезает? Пусть все будут по абсолютному адресу! Тогда модуль PC ещё сильнее упростится, но боюсь, разрабатываемый QuatCoreImm тут же растолстеет...

Poll #2102747 Прыжки по относительному адресу

А нужны ли они?

Да
2(66.7%)
Нет
1(33.3%)
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

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

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

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

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: 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 

  • 1 comment