nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

QuatCore: и немедленно выпил!

Решил внести серьёзные изменения в самый первый из "компонентов" QuatCore, в модуль QuatCoreImm. Самым первым он был, поскольку именно его было легче всего написать, буквально в две строки и в НОЛЬ логических элементов:

module QuatCoreIMM (input [7:0] SrcAddr, output [15:0] Q);

assign Q[6:0] = SrcAddr[6:0];
assign Q[15:7] = {9{SrcAddr[6]}}; //sign extension

endmodule


Imm - сокращение от Immediate, то бишь "непосредственные" значения. Когда нужно не из памяти значение извлечь, не из регистра, не со входа - а просто использовать заранее известную константу, например, 3, то с помощью данного "модуля" это становится возможным. То есть, 128 адресов "источника данных" (SrcAddr) из 256 отданы именно на эти непосредственные значения, и при данной реализации они соответствуют числам от -64 до +63 (производится расширение знака до 16 бит).

Увы, этого мало. Вот хочу сейчас для обработки изображения устроить цикл по строкам, от 719 до нуля, и чтобы это сделать, приходится городить такое:
.rodata
  ImageHeight  dw 719
.code
  X      ImageHeight
  i      0
  [SP+1] [X+i]

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

И как будто это недостаточно уродливо, компилятор в таком коде обнаруживает Hazard между строками
i      0


и
[SP+1] [X+i]

дескать, из-за конвейера запись нуля в i и чтение [X+i] происходит одновременно, RAW (Read After Write) hazard, поэтому он туда вставляет ещё пустую инструкцию NOP 0.

Это уже начало причинять физическую боль, когда с подобными проблемами сталкиваешься раз за разом. И ладно, если бы это был процессор, в который уже "не влезешь" - уж раз купил его, раз решил конкретно эту архитектуру использовать - ТЕРПИ! Но это ж мой собственный драндулет, soft core, который я могу как угодно модифицировать!

Так что всё, хватит мучать самого себя этими костылями, пора научиться "хранить" в QuatCoreImm "произвольные" 16-битные константы, причём их количество может даже немножко превысить 128 штук :)


Идея в целом проста до безобразия: вместо прямого транслирования 7 младших бит адреса в шину данных и расширении знака, мы скорее делаем "массив констант", эдакий Imm[SrcAddr], 128 значений по 16 бит. Поначалу мне совсем не хотелось так делать, поскольку я полагал, что это чрезвычайно затратно. Потому как если ввести массив "в общем виде", и пихать в него константы "как попало", например, в порядке их появления в коде, то модуль получится весьма объёмным. Грубо говоря, в один LUT (Look-up table) логического элемента "помещается" 16 бит информации, т.е каждой из 24=16 комбинаций четырёх 1-битных входов сопоставляется один 1-битный выход. Значит, нам понадобится 128 ЛЭ только на "хранение", и ещё чуть больше 64 ЛЭ - на мультиплексирование. Если помните, до сих пор ВЕСЬ QuatCore КАК-ТО УМЕЩАЛСЯ в 500 ЛЭ, и одним махом его утолщать на 40% ради экономии пары строк мне не хотелось.

Но после возни с таблицей адресов процедур QuatCoreCallTable.v, когда обнаружилось, что используя свободу в нумерации процедур, удаётся обойтись 1-2 логическими элементами (см раз, два), а то и вовсе без них, причём компилятор можно научить выполнять всю "грязную работу" автоматически и довольно разумно, у меня появилась уверенность, что и здесь, за счёт правильного выбора адресов, где будут "храниться" константы, удастся этот "массив в общем виде" заменить на гораздо более простую логику. Забегая вперёд: младшие биты передавать "как есть", а старшие где-то будут дублировать младших, а где-то выражаться через логическую функцию, но задействовать не все 7 бит, а лишь несколько.

Масочный режим
И очень мощным подспорьем будет, если компилятор начнёт понимать, действительно ли нам нужно все 16 бит на шине данных задать верно? Если мы какое-то число кладём в аккумулятор Acc или в 16-битный регистр C - то так оно и есть. А вот регистры X,Y,Z,SP редко оказываются 16-битными, их ширина соответствует ширине адреса ОЗУ, это до сих пор было 8 бит, возможно, подрастёт до 9 бит, вряд ли больше (очень надеюсь, что 1024 байта оперативной памяти мне хватит :)). Регистры i,j,k и того меньше: 5 бит, а регистр Inv вообще однобитный!

Также ограниченную ширину имеет регистр PC, в который заносится новое значение при прыжке по абсолютному адресу JMP, либо прибавляется значение при условных прыжках по относительному адресу JGE/JL, JO/JNO. Пока что он был 8-битным, но когда добавится алгоритм сопровождения, а также алгоритмы по обработке изображений (на пару с видеопроцессором), скорее всего он станет 9 или 10-битным.

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

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

В описание каждой из команд QuatCore надо будет добавить новое поле: DataMask, которое по умолчанию равно 0xFFFF (все 16 бит важны!), в описании i,j,k маска будет 0x001F (только 5 младших), для Inv это будет 0x0001 (всего 1 бит), для каких-нибудь i++/j++/k++ - и вовсе 0x0000 (они игнорируют шину данных!), в ACQ это может оказаться 0xC3FF, т.е два старших бита задают режим синхронизации, 10 младших - новое значение X, до которого нужно обработать строку, а остальные оказываются "ни к чему". Понятно, что для регистров X/Y/Z/SP, для JMP/JGE/JL..., компилятору придётся самому сосчитать маски уже исходя из ширины адресных шин на ОЗУ и ПЗУ, и исходя из максимальных относительных прыжков. Это уже сделано, благо, оно не "ломает" текущую функциональность.

Вот, к примеру, непосредственные значения, которые используются в "алгоритме захвата": нахождение самой отдалённой точки, сортировка оставшихся "против часовой стрелки", нахождение матрицы аффинного преобразования, нахождение крена, нахождение масштаба, нахождение вектора. Плюс ещё процедура поворота вектора с помощью кватерниона и процедура умножения кватернионов. Это пока самая длинная наша программа, занимающая 204 слова кода (408 байт). Из 128 возможных непосредственных значений, оказалось задействовано лишь 32:

0   (16 раз) 
1   (21 раз) 
2   (4 раз)  
3   (12 раз) 
7   (1 раз)  
12  (1 раз)  
24  (3 раз)  
26  (1 раз)  
28  (1 раз)  
30  (1 раз)  
49  (1 раз)  
51  (1 раз)  
-57 (1 раз)  
-56 (1 раз)  
-52 (1 раз)  
-50 (1 раз)  
-29 (1 раз)  
-28 (1 раз)  
-17 (1 раз)  
-16 (1 раз)  
-15 (1 раз)  
-13 (3 раз)  
-11 (2 раз)  
-10 (3 раз)  
-9  (3 раз)  
-8  (3 раз)  
-7  (2 раз)  
-6  (2 раз)  
-5  (2 раз)  
-4  (6 раз)  
-3  (2 раз)  
-2  (1 раз) 


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

Ноль - штука полезная, тут без вопросов. Маленькие положительные значения используются для инициализации регистров i,j,k, опять же для организации циклов.

И наконец, значения побольше, как в плюс, так и в минус, нужны для обращения к памяти, к различным массивам.

Допустим, теперь мы в дополнение к этим значениям хотим использовать и число 719 = 0x2CF = 0010_1100_1111. Если взять только 7 младших бит (столько помещается в команду), получится значение 0x4F= -49, которое мы, кстати, вообще здесь не использовали! Мне что-то кажется, что если те старшие биты, что должны здесь быть единичными, будут просто повторять один из младших бит, то всё у нас и сложится. Эти старшие биты попросту не попадут ни в регистры i,j,k, ни в сумматор для отработки относительных переходов, разве что в аккумулятор, когда мы его инициализируем нулём.

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

SP   StackAdr
SP   [SP]

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

как не захочется хранить в памяти "нулевое приближение" для метода Ньютона, магическое число 43648 (см. раз, два).

Потом ещё добавятся "одиночные константы", как-то посылки ACQ (Acquire, команда для видеопроцессора) с синхронизацией по строчным и кадровым импульсам.

В общем, задача очень похожа на ту, что мы решали для модуля QuatCoreCallTable, но наличие масок несколько усложняет её.

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


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

  • Ремонт лыжных мостиков

    Вернулся с сегодняшнего субботника. Очень продуктивно: отремонтировали все ТРИ мостика! Правда, для этого надо было разделиться, благо народу…

  • Гетто-байк

    В субботу во время Великой Октябрьской резни бензопилой умудрился петуха сломать в велосипеде. По счастью, уже на следующий день удалось купить…

  • А всё-таки есть польза от ковариаций

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

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 3 comments