На повестке дня: модуль QuatCoreDummyUART для отображения текста во время симуляции в Quartus, "мощнейший" препроцессор для транслятора QuatCore, теперь понимающий целую строку в кавычках, добавление входа stall в АЛУ, параметризация IOselector, и 8-битный контроллер SPI.

QuatCoreDummyUART
Когда мы работаем "в железе", то любую отладочную информацию удобно отправлять по UART на компьютер, и смотреть там. Но когда ту же самую программу мы попытаемся запустить в симуляции, то UART-головная боль та ещё, он нам затормозит всё выполнение, и посмотреть на переданные символы негде. Разве что шину данных изобразить не в hex, а в ascii, и самому внимательно смотреть, когда именно подаётся символ на UART. Решение очень простое, мы штатный UART заменяем на такую хреновину:
//для удобства отладки module QuatCoreDummyUARTtx (input clk, input st, input [15:0] DataBus, output busy, output txd, output reg [7:0] Char = 8'h00); always @(posedge clk) if (st) Char <= DataBus[7:0]; assign busy = 1'b0; assign txd = 1'b1; endmodule
Она не тормозит выполнение программы и выводит последний переданный символ на отдельной шине Char. Добавим её в Vector Waveform File - и готово. В "картинке для привлечения внимания" мы отчётливо наблюдаем сообщение, которое предполагалось отправить на компьютер. Ну, оно достигло цели :)
В левой части экрана, увы, виден большой недостаток - русские символы Quartus показывает в виде чисел от 128 до 255, вот жеж буржуйская техника! Так что для такой отладки придётся использовать английский текст. Как его переубедить - понятия не имею.
Строка (больше символа) в кавычках в ассемблере
Задолбало каждую буковку одинарными кавычками выделять, вроде такого:
Row1 dw 0x1C0,'Г','а','м','и','л','ь','т','о','н',' ',' ','Р','о','д','р','и','г','е','с'
Особенно сейчас, когда попытался записать все-все сообщения по инициализации SD-карточки: CMD0 (и варианты ответа - успешно, таймаут, неверная команда), CMD8 (карточка старая или карточка новая), ACMD41 (карточка обычной ёмкости или High Capacity).
Да и затея с разными кодировками себя не сильно оправдала - в МЭЛТовском ЖК экранчике обнаружилась вполне себе стандартная Win1251, хотя позже может пригодиться.
Так что наконец-то решил ввести этап "препроцессора" - выражение в кавычках будет разбито на одинарные символы в кавычках через запятую, а дальше обработано "как и раньше". При этом две одинарные кавычки подряд внутри строки мы трогать не будем, они как раз и обозначают символ одинарной кавычки.
Так что теперь можно определять строки вот так:
CMD0str dw 'Send CMD0',13,0x800A
До чего же кайфово!
Параметризуемый селектор ввода-вывода

Если мы какой-то из модулей не применяем, то можем не удалять его со схемы, а просто поставить нолик в одном из параметров: enableLCD, enableUART, enableSPI. Вот код обновлённого селектора ввода-вывода:
module QuatCoreIOselector (input clk, input [7:0] DestAddr, input [7:0] SrcAddr, input [15:0] DataBus, output UARTtxEN, output LCD_EN, output SDtxEN, output UARTrxEN, output SDrxEN); parameter enableLCD = 1'b1; parameter enableUART = 1'b1; parameter enableSPI = 1'b1; wire isSelection = (~DestAddr[7])&DestAddr[6]; wire isIO_out = (~DestAddr[7])&(~DestAddr[6]); wire isIO_in = (SrcAddr[7:4] == 4'b1001); reg [1:0] sel = 2'b0; always @(posedge clk) if (isSelection) sel <= DataBus[1:0]; assign UARTtxEN = (sel==2'b00) & isIO_out & enableUART; assign LCD_EN = (sel==2'b01) & isIO_out & enableLCD; assign SDtxEN = sel[1] & isIO_out & enableSPI; assign UARTrxEN = (sel==2'b00) & isIO_in & enableUART; assign SDrxEN = sel[1] & isIO_in & enableSPI; endmodule
Если команда запуска никогда не проходит в модуль, то синтезатор умудряется сообразить, что там вообще ничего не может происходить - все выходы он соединяет на нули или единицы, и выкидывает все "внутренности" подчистую! По крайней мере, для устройств вывода это срабатывает. Отключать SPI ещё не пробовал, но скорее всего и там большая часть логики, если не вся, будет отброшена.
Сейчас, к примеру, мы отключили ЖК-экран.
На той же схеме видно, как мы поставили DummyUART вместо "настоящего", а вместо 16-битного SPI - 8-битный.
8-битный SPI
Хотел всё-таки реализовать свою исходную идею специализированного SPI-контроллера для работы с SD-карточкой. Там было необходимо, чтобы сам контроллер пропустил бы входные байты 0xFF и продолжал бы сам отсылать 0xFF, пока не придёт байт, начинающийся с нуля. Но если 8 байт подряд были "пустые", то делать нечего - надо вернуть 0xFFFF, а потом уже программа воспримет это как таймаут.
Но в итоге стало лениво его делать, как-то там всё немножечко не клеится. Если сдвиговый регистр может не только загружаться из входа, "стоять на месте" и сдвигаться, а ещё и загружаться во все единицы, то по 1 ЛЭ на каждый бит уже не хватает, нужно 2 ЛЭ, а это как-то неаккуратненько!
Так что решил, наоборот, подрезать наш дуплексный 16-битный модуль, чтобы "ручками" принимать по 1 байту, сравнивать его с 0xFF, и дальше либо принимать следующий байт, либо объявлять таймаут. Если мы принимаем по 16 бит, то код страшно неудобный становится - велика вероятность, что у нас всё сдвинется на полслова, а у нас нормальных команд сдвига пока вообще нет!
"Подрезанный" модуль выглядит так:
`include "math.v" module QuatCore8bitDuplexSPI ( input clk, input ce, input [15:0] D, input TXen, input RXen, inout MISO, output [15:0] Q, output reg SCK=1'b0, output MOSI, output nCS, output busy, output ceo); assign MISO = nCS? 1'b1 : 1'bz; //"неактивным" здесь считается лог. "1", на манер UART //нужно более 8 состояний localparam sIdle = 4'h0; localparam sB0 = 4'h8; localparam sB1 = 4'h9; localparam sB2 = 4'hA; localparam sB3 = 4'hB; localparam sB4 = 4'hC; localparam sB5 = 4'hD; localparam sB6 = 4'hE; localparam sB7 = 4'hF; wire [3:0] State; //вставим его отдельным модулем, чтобы задействовать режим счётчика ЛЭ reg [8:0] SR = 9'h1FF; //нужно одним битом больше, потому что пока защёлкиваем по MISO, не успеваем сдвинуть младший и затираем его нахрен //могли бы, учитывая полудуплекс, иногда защёлкивать, а иногда нет, но нужен регистр для определения - приём или передача - то на то и выходит. assign nCS = ~State[3]; //по сути, "isIdle" wire NEG_E = ce & SCK; //спад SCK, когда мы переключаем состояния и сдвигаем данные в регистре wire POS_E = ce & (~SCK); //фронт SCK, когда мы защёлкиваем входной бит wire IsFinalBit; //генерируется логикой переноса (cout) wire TXbusy = TXen & (~(NEG_E & (nCS | IsFinalBit))); //т.е ждём, пока не будет спада SCK в режиме Idle или на передаче последнего бита wire RXbusy = RXen & (~(POS_E & (nCS | IsFinalBit))); //т.е ждём, пока не будет фронта SCK в режиме Idle или на передаче последнего бита assign busy = TXbusy | RXbusy; wire start = TXen & NEG_E & (nCS | IsFinalBit); //то есть, старт всегда по TXen, а по RXen можно получить ответ, что же там было! //assign busy = ((TXen & (~NEG_E))|(RXen & (~POS_E))) & (nCS | IsFinalBit); //правда, и такой вариант не очень, т.к возникнет задержка до следующего слова. lpm_counter StateCounter (.clock (clk), .cnt_en (NEG_E & (~nCS)), .sset (start), .Q (State), .cout (IsFinalBit)); defparam StateCounter.lpm_direction = "UP", StateCounter.lpm_port_updown = "PORT_UNUSED", StateCounter.lpm_type = "LPM_COUNTER", StateCounter.lpm_width = 4, StateCounter.lpm_svalue = sB0; assign MOSI = SR[8]; assign Q = {8'h00, SR[7:1], MISO}; //экономит один такт, позволяет не накладывать огр always @(posedge clk) begin SCK <= ce? ~SCK : SCK; if (NEG_E) SR[8:1] <= start? D[7:0]: SR[7:0]; if (POS_E) SR[0] <= MISO; end assign ceo = ce & SCK; //просто сэкономили 1 бит в делителях частоты, поскольку мы ОЧЕНЬ жадные. endmodule
Синтезируется в 23 ЛЭ - вот это мне больше по душе (16-битный дуплексный синтезировался в 32 ЛЭ). Вроде бы работает, но чтобы его основательно проверить, мне нужна ещё одна приблуда - "имитатор SD-карточки". Опыт показывает, что так я быстрее всё безобразие запущу, хоть потом и покажется - "лучше бы сразу в железе стал проверять - всё же было правильно изначально!".