Что радует, достаточно взять лишь "вычислительное ядро" QuatCore, без периферии - и уже многое понять. Все команды, завязанные на периферию (IN, OUT, SIO, ACQ, GPUH, GPUL) при этом исполняются за один такт "вхолостую". Некому останавливать конвейер, пока не появятся данные или пока не отправятся, и если логику работы это не нарушает (а для процедур print и SetClock - НЕ НАРУШАЕТ), то всё в порядке.
И вот она проблема, в самом конце процедуры print:

Приведём листинг процедуры print:
print proc 98 F380 [SP++] Acc 99 FDCD SP X 9A CDFD X SP 9B 8861 @@start: ZAcc RoundZero 9C 83FC SUB [SP] 9D BC05 JGE @@finish ;увы, теперь из процедуры так просто не выпрыгнешь 9E 00F3 OUT [SP++] 9F B06D JMP @@start A0 FDCD @@finish: SP X A1 CDFD X SP A2 80FF Acc [--SP] A3 B0FF JMP [--SP] print endp
А вызывалась она первый раз с адреса 0x0A:
07 FD31 SP Stack ;инициализация стека. 08 104D SIO LCD 09 CD4D X InitLCD 0A F3BB CALL print
На print перешли правильно, и выдали всю запрошенную строку, вот только вернуться куда надо, на адрес 0B, так и не смогли.
Как видно по листингу, мы в неописуемой наглости использовали указатель стека, чтобы пройтись по строке, временно сохраняя его в регистре X (мы меняли их местами, как описано здесь).
Тем не менее, исходное значение 0x148 мы возвращаем (см листинг памяти в этом месте):
Elem1F: 141 0x8000 Elem1F[1]: 142 ???? Elem1F[2]: 143 ???? Elem1F[3]: 144 ???? Elem1F[4]: 145 ???? Stack: 146 ???? Stack[1]: 147 ???? Stack[2]: 148 ???? Stack[3]: 149 ???? 14A ???? 14B ???? 14C ????
Изначально 0x146, но при вызове print смещается на один (в 0x146 идёт адрес возврата), потом сохраняем аккумулятор в 0x147, вот и выходит 0x148, всё как в аптеке.
И по значению SP, вынесенному в результаты симуляции (внизу) мы тоже видим, что всё правильно присвоилось.
Только вот по строке MemAddr видно, что когда мы пытаемся извлечь из стека сначала сохранённое значение Acc, а потом и адрес возврата - мы вместо 0x147 и 0x146 обращаемся к 0x47 и 0x46, что заканчивается эпик фейлом, мы выпрыгиваем вообще куда-то в "пустырь", и там исполняем нулевые команды, пока не доползём своим ходом до адреса 0, после чего всё начинается заново!
И только сейчас я вспомнил, что [--SP] реализован немного своеобразно: разумеется, приходит сигнал "вычесть единичку" на реверсивный счётчик, в котором и хранится SP. Но это-то будет к следующему такту, т.е [SP--]. Поэтому мы ещё и честно вычитаем единичку с помощью наших сумматоров индексных регистров:

Вот код QuatCoreFirstIndexMux:
module QuatCoreFirstIndexMux (input [15:0] ijk, input [1:0] Base, input [1:0] adr, output [7:0] Q); wire [4:0] i = ijk[9:5]; wire [4:0] j = ijk[4:0]; wire [2:0] smj = j[2:0]; //for TreugNum wire [3:0] TreugNum; assign TreugNum[3] = smj[2]; assign TreugNum[2:0] = (smj == 3'd0)? 3'd0 : (smj == 3'd1)? 3'd1 : (smj == 3'd2)? 3'd3 : (smj == 3'd3)? 3'd6 : (smj == 3'd4)? 3'd2 : (smj == 3'd5)? 3'd7 : 3'bxxx; assign Q = (adr == 2'b00)? 8'b00000000 : //0 (adr == 2'b01)? {2'b0, i, 1'b0} : //2i (adr == 2'b10)? {2'b0, j, 1'b0} : //2j (~Base[0])? {1'b0, j, 2'b0} : //4j for X or Z Base[1]? 8'b11111111 : //-1 for SP {4'b0, TreugNum}; //TreugNum for Y endmodule
Укуренный код, и последний раз он редактировался 6 января 2020 года, в 2:56 ночи :) Это многое объясняет!
Хотя, определённая система тут есть. Нам здесь нужны "треугольные числа" Treug[j] = j(j+1)/2, но только первые 6:
0, 1, 3, 6, 10, 15.
(для адресации треугольной матрицы 6х6)
Помнится, я их выписал в бинарном виде:
j Treug[j] 000 0000 001 0001 010 0011 011 0110 100 1010 101 1111
И понял, что 3-й бит Treug[] можно соединить напрямую со 2-м битом j. А вот для трёх младших битов сколько-нибудь простой закономерности не нашлось - и я выписал их "в лоб".
Интереснее другое: почему здесь 8-битный выход. Самые крупные значения получаются для варианта "4j", ведь сам j может быть от 0 до 31, значит 4j - от 0 до 124, влезает в 7 бит.
Очевидно, чтобы не спутать большие положительные значения вроде 124, и отрицательное "-1", я и ввёл сюда 8-й бит. Это по сути получился ДОПОЛНИТЕЛЬНЫЙ КОД. Определённая логика прослеживается...
Далее, посмотрим модуль QuatCoreMemSecondIndexMux:
module QuatCoreMemSecondIndexMux (input [15:0] ijk, input [1:0] Base, input [1:0] adr, output [4:0] Q); wire [4:0] i = ijk[9:5]; wire [4:0] j = ijk[4:0]; wire [4:0] k = ijk[14:10]; assign Q = (adr == 2'b00)? 5'b00001 : (adr == 2'b01)? i : (adr == 2'b10)? k : (Base[0]&Base[1])? 5'b00000 : i^j; endmodule
Здесь всё поскромнее: умножать на 2 или 4 ничего не надо, в крайнем случае два регистра объединить через XOR, и 5 бит хватает "за глаза".
Теперь посмотрим на QuatCoreMemIndexAdder, который складывает два результата, полученных ранее:
module QuatCoreMemIndexAdder (input [7:0] First, input [4:0] Second, output [7:0] Q); assign Q = First + Second; //will it be kind enough to synthesize this to 7 LE? /* lpm_add_sub adder ( .dataa (First), //full acc width .datab ({2'b00, Second}), //this one extended as well .result (Q[6:0]), .cout (Q[7])); 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 = 7; */ endmodule
В кои-то веки не пришлось вводить сумматор "в явном виде", и без этого нормально. Очень частая ситуация, когда мы здесь складываем "-1" и "+1", получая в итоге ноль (см. магические квадраты).
Если так подумать, возможна проблема с выражением [X+4j+i] или подобным. Если j=31 (максимально возможное) и i=31, то 4j+i = 155, что уже не влезет в 8-битное ЧИСЛО СО ЗНАКОМ.
Так что впору здесь сделать 9-битный выход, но для этого нужно расширить знак у 8-битного входного числа:
module QuatCoreMemIndexAdder (input [7:0] First, input [4:0] Second, output [8:0] Q); assign Q = {First[7],First} + Second; endmodule
Мой брат вовсю юзает директиву signed - и не заморачивается на такие вот совсем "низкоуровневые" вещи как расширение знака. Может, и мне стоило бы, но чего-то не доверяю я квартусу, мне спокойнее, когда оно вот так в лоб прописано...
И наконец, поглядим, что такое QuatCoreMemMainAdderBanded:
module QuatCoreMemMainAdderBanded (input [RamWidth+1:0] Base, input [7:0] Index, output [RamWidth-1:0] Q); parameter RamWidth = 8; assign Q = Base[RamWidth-1:0] + Index; endmodule
Собственно, именно здесь ошибка и закралась: мы забыли расширение знака сделать!
Пока у нас размер оперативной памяти был 256 слов или меньше - всё работало правильно, поскольку расширения знака и не требовалось. А на 512 словах (9-битная адресация) и пришла к нам хана. К 0x147 прибавляется 0xFF - и выходит 0x246, но старший бит отбрасывается - и выходит 0x46. Вот и вся проблема...
Напишем как-то так:
module QuatCoreMemMainAdderBanded (input [RamWidth+1:0] Base, input [8:0] Index, output [RamWidth-1:0] Q); parameter RamWidth = 9; localparam ExtensionBits = RamWidth - 9; wire [RamWidth-1:0] ExtIndex = (ExtensionBits > 0)? {{ExtensionBits{Index[8]}}, Index} : Index[RamWidth-1:0]; assign Q = Base[RamWidth-1:0] + ExtIndex; endmodule
То есть, когда надо, делаем расширение знака, в противном случае можем вообще старшие биты отрезать.
Снова отсинтезируем QuatCore (процессорное ядро без периферии) и запустим симуляцию:

Фух, работает!
Вот теперь можно попробовать и на "железе":

Ну, уже получше :)
К следующему разу так:

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