Чуть поменяем константы:
VSync EQU 0x8000 ;ожидание кадрового синхроимпульса, значение для регистра произвольное HSync EQU 0x40EE ;ожидание строчного синхроимпульса, а затем ещё интервал back porch (между импульсом и началом картинки) в 238 тактов WholeRow EQU 31 ;для проверки на симуляторе ImgHeight EQU 40 ;вообще, 32, но вводим дополнительные строки, чтобы перенести все точки из ActivePoints в AllPoints ImgHeightP1 EQU 41 ;не обучили свой компилятор обрабатывать "constexp" Threshold EQU 0xDFF ;минимальная яркость пятен, сейчас поставили 1/8 от "насыщения" (4096), но с инверсией, так надо!
HSync переделали из 240 тактов в 238 (вместо F0 в младшем байте EE), а ещё добавили константу ImgHeightP1, то бишь Plus 1. Это недоработка с моей стороны, не умеет пока компилятор самостоятельно выражения вычислять. Было это далеко не первой необходимостью, не стал код излишне усложнять, как-нибудь сделаю.
Код в конце цикла по строкам получился таким:
Acc [SP+2j] SUB ImgHeight ADD ImgHeightP1 JL @@newRow
Забавно выглядит: загружаем текущий номер строки, вычитаем 40, а затем прибавляем 41, и только потом условный переход. Только в QuatCore такие фокусы прокатывают, он намеренно сделан асимметричным!
SUB меняет флаг знака числа, а ADD его не трогает! Ни разу мне ещё не понадобился ADD непременно с обновлением флага. Если значение непосредственное, то можно, к примеру, вместо ADD 1 написать SUB -1. Можно придумать ситуацию, что мы непременно хотим сложить два значения откуда-то из памяти, и по знаку результата принять решение, но как-то оно выглядит искусственно.
Так что да, при вычитании 40 мы проверяем, не пора ли заканчивать цикл, а потом прибавляем 41, чтобы всё закончилось попросту прибавлением единицы к номеру строки.
А уже в начале цикла значение из аккумулятора возвращается в [SP+2]:
@@newRow: [SP+2j] Acc
Конечно, и прибавление 41 можно было выполнить здесь, и не выпендриваться, но это внесло бы задержку в один такт (Read-After-Write Hazard, но хотя бы разрешаемый силами самого АЛУ), неаккуратненько!
Впрочем, когда мы от счёта "вниз до нуля" перешли к счёту "вверх", код всё-таки подрос на 1 строку, то есть на 2 байта :) Но всё-таки надо было это сделать. Одно дело, когда вместо строк 0..1023 мы получаем 1023..0 (всё то же самое, но в обратную сторону), и совсем другое - когда получаем условно 1123..100, поскольку мало того что в обратную сторону, но ещё и берём 100 лишних строк внизу, чтобы "нахаляву" перетащить все пятна из одного списка в другой. Халява - она такая, всегда оказывается не вполне бесплатной.
Ещё мы поменяли значение D1 с 3 на 6, это "расчётный диаметр" наших пятен.
Ну и самое главное: надо подрихтовать АЛУ, чтобы команда DIV2S (вычесть из аккумулятора входное значение, поделённое знаково на два) не затирала регистр C.
Очень давно я в АЛУ не залезал, "работает-не лезь". В первую очередь, посмотрим на регистр C:
module QuatCoreALUCreg (input [15:0] D, input [1:0] mode, input clk, output Senior, output reg [15:0] Q = 1'b0); localparam mNOP = 2'b00; localparam mShift = 2'b10; localparam mLoad = 2'b11; always @(posedge clk) if (mode[1]) Q <= mode[0]? D : {Q[14:0], Q[15]}; assign Senior = Q[15]; endmodule
Данный файл был последний раз изменён 20 декабря 2019 года, долгожитель! Да и сейчас не будем его трогать. Немножко раздражает, что mode[1:0] - очень "безличное" название, чтобы узнать режимы, нужно смотреть потроха этого модуля. Можно было бы не жадничать на количестве "соединительных проводов" на схеме и сделать два входа: ena (разрешение работы) и sload (синхронная загрузка). Тогда можно было бы сообразить, что при ena=0 регистр вообще ничего не делает, сколько не тыкай sload, при ena=1 и sload=1 он загружает новое значение, а при ena=1 и sload=0 должен же он что-то делать - наверное сдвигает данные циклически :)
Ну да ладно. В общем, на команде DIV2S лишний раз возникает mode[1]=1, и это надо исправить. Заглядываем в модуль управления АЛУ:
//most complicated thing so far. Should support commands: //NOP (smth else is active) //0x80: ACC //0x81: PM //0x82: ADD //0x83: SUB //0x84: ABS //0x85: ??? //0x86: ??? //0x87: ??? //0x88: ZACC //0x89: ??? //0x8A: C //0x8B: ??? //0x8C: DIV2 //0x8D: ??? //0x8E: ??? //0x8F: ??? //0x90: MUL //0x91: FMPM //0x92: FMA //0x93: FMS //0x94: MULSU //0x95: SUFMPM //0x96: SUFMA //0x97: SUFMS //0x98: MULU //0x99: UFMPM //0x9A: UFMA //0x9B: UFMS //0x9C: SQRD2 //0x9D: SQRPMD2 //0x9E: SQRAD2 //0x9F: SQRSD2 //100xxxxx = ALU commands //1000xxxx = 'short' commands //1001xxxx = 'long' commands //100xxx00 = 'clear ACC' //100xxx10 = 'add to ACC' //100xxx11 = 'sub from ACC' //100xxx01 = 'plus-minus' (depends on PM input) //100x00xx = 'signed commands' //100x01xx = 'signed B / unsigned C commands / special command (ABS)' //100x10xx = 'unsigned commands / special commands (ZAcc, C)' //100x11xx = 'div2 commands (DIV2, SQRD2, SQRAD2 etc)' //AccMode = 000 - load // 001 - FORBIDDEN // 010 - idle // 011 - clear // 100 - set -3/2 // 101 - set 2 // 110 - set 3/2 // 111 - set 1/2lsb //Bmode = 00 - shift right // 01 - shift right and invert // 10 - load 16 bit // 11 - load AccWidth bit (bypass) //Cmode = 0x - idle // 10 - shift // 11 - load 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) : //либо команда ZAcc, либо храним текущее значение (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[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] | isMult; wire MainBusy = isOurAddr & (DestAddr[4:2] != 3'b0_10)&(~isAdd)&(~finished); //если SrcAddr тоже относится к АЛУ, задерживаем ещё на такт 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
Ох жесть. Ещё и половина комментариев устарела! Последнее изменение: 17 марта 2020 года, в 2:28 утра. Да, без чёрной магии здесь явно не обошлось.
Весь АЛУ пока что синтезируется в 194 ЛЭ (при ширине аккумулятора 32 бита).
Вот строка, которая нас интересует:
assign Cmode[1] = isIdle? isOurAddr & DestAddr[3] & ((~DestAddr[4]&DestAddr[1])|(DestAddr[4]&DestAddr[2])) : isMult; //1 LE shorter still
А конкретнее, часть выражения, отвечающая за isIdle:
isOurAddr & DestAddr[3] & ((~DestAddr[4]&DestAddr[1])|(DestAddr[4]&DestAddr[2]))
isOurAddr проверяет старшие 3 бита DestAddr, т.е DestAddr[7:5], убеждается, что команда адресована имено в АЛУ, и что не "горит" Stall, и не нужно ещё на такт задержаться из-за RAW Hazard. Это мы не трогаем, работает-не лезь!
Далее, вспоминаем эту табличку:

Бит DestAddr[3]=1 выделяет нижние две строки "длинных" команд и нижние две строки "коротких" команд - только на них в регистр C надо что-то загружать. Не имеем претензий...
А дальше идёт разделение. Если DestAddr[4]=1, т.е на "длинных" командах, мы ещё требуем DestAddr[2]=1, что оставляет нам только нижнюю строку.
Если же DestAddr[4]=0, т.е на нижних двух строках "коротких" команд, мы требуем DestAddr[1]=1, что оставляет отсекает нам команды ZACC, DIV2, NOP и (зачем-то) DIV2PM, но оставляет команду C (аж в двух экземплярах) и также наши любимые DIV2A и DIV2S.
Мы можем их отсюда выкинуть, если добавить ещё один элемент, (~DestAddr[2]):
isOurAddr & DestAddr[3] & (((~DestAddr[4])&(~DestAddr[2])&DestAddr[1])|(DestAddr[4]&DestAddr[2]))
Похоже, это ничуть не увеличило размер АЛУ: так и осталось 194 ЛЭ. Оно в принципе ожидаемо: LUT (ГФ) плевать на сложность логического выражения, главное, чтобы входных бит осталось столько же!
Что ж, запускаем синтез всей связки QuatCore+GPU, с новыми файлами QuatCoreImmTable.v, QuatCoreCallTable.v, QuatCoreCode.mif и QuatCoreData.mif, то бишь после перекомпиляции нашего кода.
Как и раньше, всё вместе занимает 1004 ЛЭ. На симуляции идём сразу на T=1,149, место, где мы первый раз находим яркую точку, превышающую порог обнаружения:

Максимальная яркость на отрезке не изменилась с прошлого раза, 0x41F. А вот координата яркой точки была 0x0C, а стала 0x0E, наконец-то совпав с тем, что я смотрю в "фотошопе".
А в самом конце "слайда" мы видим, что в регистре C лежала шестёрка, наш "расчётный диаметр" пятна, не зря ж мы D1 обновили. И с замиранием сердца перематываем вперёд:

И действительно, к концу этого "слайда" завершается вторая итерация цикла, мы снова запрашиваем SrcAddr=0x83 (регистр C), и повторно получаем число 6, то есть теперь оно не затирается.
Сейчас ещё разок посмотрел на это дело, и кажется мне, что всё равно на единичку ошибся с координатой Y, надо её инициализировать значением "-1", а не нулём... А в остальном полный восторг.
Пора продолжить симуляцию, мы ещё многие "ветви" не видели в действии. Правда, "слияния" на этой тестовой картинке мы скорее всего не получим, для этого нужно будет где-нибудь сбоку повесить "Солнышко" размером 20х20 пикселей. Успеется...