Вот это BUS, на ней сидят,
А это STALL, на нём стоят!

В сравнении с прошлым разом, мы научились обходиться 4-битной шиной данных для подключения ЖК-экранчика, а также перемещаться на нужные позиции и рисовать свои собственные символы.
По ходу дела упёрлись в ограничения модуля QuatCoreMem, и снова доработали его напильником. Не путать с наклеиванием заплаток - после доработки его размер уменьшился!
А также совершили полнейшее кощунство с регистром SP...
Первый шлейф для подключения к ЖК-экрану, который я спаял, содержал 10 проводов: плюс и минус питания, катод подсветки, входы E ("тактовая частота", или скорее строб), RW (Read/Write, его мог бы просто на землю посадить, но тогда ещё был в раздумьях и на всякий случай притащил), A0 (переключение команда/данные) и 4 вывода DB7..DB4. Ещё 4 вывода DB3..DB0 паять не хотелось, а в документации описывался режим, в котором обращение ведётся "в два захода", по 4 бита.
Но затем меня начали терзать смутные сомнения - я не понимал из даташита, как же экран "выправится", если хотя бы один раз за всё время случайно передать лишнюю посылку? Скорее всего, там прописан какой-то "таймаут", что если отправили первые 4 бита, то надо достаточно быстро отправить оставшиеся 4, но как именно быстро - указано не было.
Да и порядок инициализации как-то не внушал доверия:

Получается, что нужно 3 раза подряд сказать экрану, что мы используем 8-битный режим, затем на четвёртый раз - что используем 4-битный, а потом уже начать "сдвоенные" посылки и настроить всё остальное.
В итоге, "для первого раза" я спаял ещё один проводок:

и подключил недостающие 4 бита.
Пожалуй, это было правильное решение, иначе у меня было бы ещё один подозреваемый, когда экранчик вообще ничего не показывал!
Но теперь, когда всё заработало как надо, пришло время разобраться и с 4-битным режимом. Похоже, что существует великое множество таких символьных экранов от разных производителей, но с совершенно одинаковым алгоритмом работы (видел такие у 8-bit guy и недавно у Ben Eater, где он показывал, как сделать компьютер на 6502).
Как следует из порядка инициализации, мы должны уметь подавать "одиночные" посылки в начале работы, а потом перейти на "сдвоенные":

Нда, как бы меня ни печалили подробнейшие эпюры с десятком параметров, но здесь вообще ни одного не указано. Ладно, сделаем длительность одного "такта" передачи близко к минимальной допустимой, а именно, 500..1000 нс (меньшее время для 5-вольтовых дисплеев, большее-для 3-вольтовых).
Режим передачи - одиночный или сдвоенный - мы зададим 9-м битом на шине данных. Пока мы задействовали биты 7..0 для непосредственных данных (или кода команды), бит 8 - выбор между командой и данными, бит 15 - признак окончания строки. Теперь ещё 9-й нам пригодится. Если он нулевой - делаем сдвоенную передачу, если единичный - одиночную. Я выбрал такой порядок, чтобы обычные строки текста не содержали лишних "единичных" флажков.
Доработаем наш модуль для работы с ЖК-экраном:
`include "math.v" module QuatCoreLCD4wire (input clk, input [7:0] DestAddr, input [15:0] DataBus, output busy, output reg LCD_A0 = 1'b0, output reg LCD_E = 1'b0, output reg [7:0] LCD_data = 1'b0); parameter ClockFreq = 4_000_000; parameter is5volts = 1'b1; //1: 5 volts (works faster), 0: 3,3 volts localparam EDivBy = is5volts? ((ClockFreq + 3_999_999) / 4_000_000) : ((ClockFreq + 1_999_999) / 2_000_000); localparam EDivWidth = `CLOG2 (EDivBy); localparam ActualEFreq = ClockFreq >> EDivWidth; //so we always divide by 2**EDivWidth, for smaller 'busy' counter and to avoid problems with 'divide by 1' localparam ShortBusyDiv = (ActualEFreq + 24_999) / 25_000; //for almost all commands localparam LongBusyDiv = (ActualEFreq + 666) / 667; //for CLS localparam BusyWidth = `CLOG2 (LongBusyDiv); wire IsOurAddr = ~DestAddr[7]; wire ce; //clock enable wire e_set; //keeps EDivider inoperative when idle, also restarts it after each ce. //freq divider for 'E' clock output (it should count 500 ns) lpm_counter EDivider ( .clock (clk), .sclr (e_set), .cout (ce) ); defparam EDivider.lpm_direction = "UP", EDivider.lpm_port_updown = "PORT_UNUSED", EDivider.lpm_type = "LPM_COUNTER", EDivider.lpm_width = EDivWidth; wire [BusyWidth-1:0] Duration = (DataBus[8:0]==9'h101)? LongBusyDiv : ShortBusyDiv; wire b_set; wire idle; //freq divider for 'busy' signal (it should count 40 us or 1,5 ms for CLS) //should stay at final count until started once again lpm_counter BusyDivider ( .clock (clk), .cnt_en (ce), .sload (b_set), .data (Duration), .cout (idle) ); defparam BusyDivider.lpm_direction = "DOWN", BusyDivider.lpm_port_updown = "PORT_UNUSED", BusyDivider.lpm_type = "LPM_COUNTER", BusyDivider.lpm_width = BusyWidth; //let's describe logic of all of that... assign b_set = idle & IsOurAddr; assign busy = ~idle & IsOurAddr; //it stalls CPU only if we want to use LCD once more when it didn't finish previous work assign e_set = idle | ce; reg zLCD_E = 1'b0; reg DoItAgain = 1'b0; always @(posedge clk) begin zLCD_E <= (b_set | (~zLCD_E&ce&DoItAgain))? 1'b1 : ce? 1'b0 : zLCD_E; //RS-trigger basically LCD_E <= zLCD_E; //1 clk delay to ensure t_AS (Address set-up time) > 60 ns DoItAgain <= b_set? ~DataBus[9] : (ce & (~zLCD_E))? 1'b0 : DoItAgain; LCD_A0 <= b_set? ~DataBus[8] : LCD_A0; LCD_data [7:4] <= b_set? DataBus[7:4] : (DoItAgain & ce & (~zLCD_E))? LCD_data[3:0] : LCD_data[7:4]; LCD_data [3:0] <= b_set? DataBus[3:0] : LCD_data[3:0]; end endmodule
Мы чуть изменили настройки делителей частоты. Во-первых, теперь появился параметр is5volt: мы задаём, каким именно экранчиком мы пользуемся. Если он 5-вольтовый, то у него тайминги чуть короче, хотя 20 мкс и 1,5 мс, которые в наибольшей степени определяют скорость работы с ним, остались теми же самыми. Возможно, это излишнее, можно было остаться с 3-вольтовыми настройками...
Кроме того, нашему модулю просто НЕОБХОДИМО деление частоты хотя бы в два раза, иначе просто накроется логика работы, будет непрерывно "гореть" ce, которая будет по чём зря вычитать единичку из счётчика BusyDivider - и привет.
Поэтому тут же мы "отыграли назад" - теперь частота будет делиться обязательно в степень двойки, начиная с первой (достигли этого, заменив вход sset на sclr в делителе EDivider). То есть, даже если мы высчитаем, что необходимо деление в 1 раз, маленький 1-битный счётчик у нас всё-таки поставится! Мотивация: если мы "выжмем" максимальное деление частоты из EDivider, то есть шанс, что BusyDivider уместится в меньшее число бит.
В целом это пока ни на что не повлияло, остались при своих.
Дальше изменения серьёзнее: появился регистр DoItAgain, в который запоминается 9-й бит из шины данных. Он заставляет нас выдать дополнительный импульс LCD_E, если этот бит был нулевым.
Регистр LCD_data[7:0] из простого стал сдвиговым.
Ну и чуточку усложнилась логика управления. В целом, размер этого модуля вырос с 33 до 38 ЛЭ, при тактовой частоте 4 МГц. Не так уж плохо.
Посмотрим на работу этого модуля на симуляторе. При передаче первой, "одинарной" посылки, он ведёт себя как и прежде:

А вот как выглядят "сдвоенные" посылки:

Сначала "защёлкиваются" выводы A0 и D7..D4, затем следует импульс E, затем он сбрасывается, на выводы D7..D4 подаются новые значения - и следует новый импульс E. Всё как положено.
Для проверки мы написали новую программу, HelloLCD4wire.asm. Взглянем на её самое начало - на строки инициализации ЖК-экрана по 4-битной шине:
;Hello, world на ЖК-экранчике, через 4-проводной интерфейс %include "LCD_code_page_1.inc" .rodata InitLCD dw 0x330 ;установить разрядность интерфейса (одиночная посылка, команда, 0011) Init1 dw 0x330 ;установить разрядность Init2 dw 0x330 ;установить разрядность Init3 dw 0x320 ;установить разрядность 4 бита Init4 dw 0x12A ;установка параметров (двойная посылка, команда, 0010 1010 = 4 бита, страница 1) Init5 dw 0x10C ;включение дисплея (двойная посылка, команда, 0000 1100 = включить, курсора нет, ничего не мигает) Init6 dw 0x101 ;очистка дисплея (двойная посылка, команда, 0000 0001) Init7 dw 0x106 ;установка режима ввода данных (двойная посылка, команда, 0000 0110 = курсор вправо, дисплей не двигается)
Единица в старшем разряде из показанных сообщает, что перед нами команда для ЖК-экрана, а не просто очередной символ. Тройка - что впридачу нам нужна лишь "одиночная" посылка. Как видно, одиночными являются первые 4 посылки, все остальные уже "сдвоенные".
Всё это замечательно заработало, причём нет разницы, был ли экран инициализирован заранее (возможно, в 8-битном режиме) или только включился - всё работает.
Свои собственные символы.
Как видно из фотографии в начале поста, мы добавили два новых символа - половинки большой буквы H с двойным начертанием. Такой буквой обозначается множество кватернионов, в честь Гамильтона (Hamilton).
Знак четырёх: 4 провода, 4 компоненты кватерниона, 4 фамилии причастных к этому безобразию :)
Чтобы добавить эти символы, мы сначала выбрали область CGRAM (Character Gen RAM), нулевой адрес. Затем начали заносить байт за байтом, каждый отвечает за свою строку, сверху вниз. Старшие 3 бита игнорируются, т.к ширина буквы всего 5 пикселей. Восьмая строка, хоть и используется для мигающего курсора, может содержать часть символа. При мигании курсора она инвертируется. Именно такую высокую букву H я и поставил. Вот строки инициализации, отвечающие за добавление 2 новых символов:
Init8 dw 0x140 ;выбор области CGRAM, установка на нулевом адресе Init9 dw 0x00E ;нулевой символ, 0-я строка InitA dw 0x00A ;1-я строка InitB dw 0x00A ;2-я строка InitC dw 0x00B ;3-я строка InitD dw 0x00B ;4-я строка InitE dw 0x00A ;5-я строка InitF dw 0x00A ;6-я строка Init10 dw 0x00E ;7-я строка (для курсора вообще, но мы его не исп) Init11 dw 0x00E ;1-й символ, 0-я строка Init12 dw 0x00A ;1-я строка Init13 dw 0x00A ;2-я строка Init14 dw 0x01A ;3-я строка Init15 dw 0x01A ;4-я Init16 dw 0x00A ;5-я Init17 dw 0x00A ;6-я Init18 dw 0x00E ;7-я Init19 dw 0x8180 ;выбор области DDRAM,нулевая позиция
Старшая цифра в последнем слове - признак окончания строки инициализации. Также, как указано в комментарии, мы обязаны были снова выбрать область DDRAM, куда будет заносится наше сообщение, которое хотим отобразить на экране.
Переход на нужную строку
Как показал предыдущий пример с лунатиками, за первой строкой внезапно идёт третья, а затем и вовсе наступает огромная пауза - если последний символ третьей строки расположен по адресу 0x27, то первый символ второй строки - по адресу 0x40. Так что, если мы хотим отобразить сообщение строка за строкой, лучше в начале каждой строки явным образом задать адрес. Это мы и сделаем, чтобы отобразить "Знак четырёх" и четыре фамилии:
Row0 dw 0,1,'-','з','н','а','к',' ','ч','е','т','ы','р','ё','х' Row1 dw 0x1C0,'Г','а','м','и','л','ь','т','о','н',' ',' ','Р','о','д','р','и','г','е','с' Row2 dw 0x194,'Б','р','а','н','е','ц',' ',' ','Ш','м','ы','г','л','е','в','с','к','и',f'й'
значения 0 и 1, с которых начинается верхняя строка - это как раз "пользовательские" символы, которые мы задали при инициализации.
Значение 0x1C0 - это команда перехода на вторую строку (единица означает команду, затем C0 = 1100_0000 - команда выбрать DDRAM и перейти по адресу 0x40)
Значение 0x194 - это команда перехода на третью строку (единица означает команду, затем 94 = 1001_0100 - команда выбрать DDRAM и перейти по адресу 0x14).
Вывод длинных строк
Ранее написанная процедура print использовала индексный регистр i, чтобы пройтись по строке. Увы, у нас пока задана ширина всех индексных регистров: 5 бит, поэтому такую процедуру можно использовать для строк не более 32 слов, далее мы уйдём в бесконечный цикл, т.к до посинения будем гонять i по кругу и ждать, что каким-то чудом у нас появится признак окончания строки.
Можно, в принципе, уширить эти регистры, но в "штатном коде" это скорее всего не понадобится, поэтому хочется обойтись "тем, что есть". И наиболее компактным пока вышел следующий КОЩУНСТВЕННЫЙ код:
;затирает регистр Y. Если раскомментировать 2 строки - не будет затирать print proc ;[SP++] Y Y SP SP X ;BLASPHEMY!!! КОЩУНСТВО!!! @@start: LCD [SP] Acc [SP++] SUB 0 JGE @@start SP Y ;Y [--SP] JMP [--SP] print endp
Здесь мы запоминаем значение SP (stack pointer) в регистр Y, а сам SP начинаем использовать для доступа к элементам строки! Нечасто такое встретишь - если в коде может возникать прерывание, так поступать в принципе нельзя! Но у нас пока нет прерываний как таковых, поэтому можно :)
Приведём, наконец, полный текст программы:
;Hello, world на ЖК-экранчике, через 4-проводной интерфейс %include "LCD_code_page_1.inc" .rodata InitLCD dw 0x330 ;установить разрядность интерфейса (одиночная посылка, команда, 0011) Init1 dw 0x330 ;установить разрядность Init2 dw 0x330 ;установить разрядность Init3 dw 0x320 ;установить разрядность 4 бита Init4 dw 0x12A ;установка параметров (двойная посылка, команда, 0010 1010 = 4 бита, страница 1) Init5 dw 0x10C ;включение дисплея (двойная посылка, команда, 0000 1100 = включить, курсора нет, ничего не мигает) Init6 dw 0x101 ;очистка дисплея (двойная посылка, команда, 0000 0001) Init7 dw 0x106 ;установка режима ввода данных (двойная посылка, команда, 0000 0110 = курсор вправо, дисплей не двигается) Init8 dw 0x140 ;выбор области CGRAM, установка на нулевом адресе Init9 dw 0x00E ;нулевой символ, 0-я строка InitA dw 0x00A ;1-я строка InitB dw 0x00A ;2-я строка InitC dw 0x00B ;3-я строка InitD dw 0x00B ;4-я строка InitE dw 0x00A ;5-я строка InitF dw 0x00A ;6-я строка Init10 dw 0x00E ;7-я строка (для курсора вообще, но мы его не исп) Init11 dw 0x00E ;1-й символ, 0-я строка Init12 dw 0x00A ;1-я строка Init13 dw 0x00A ;2-я строка Init14 dw 0x01A ;3-я строка Init15 dw 0x01A ;4-я Init16 dw 0x00A ;5-я Init17 dw 0x00A ;6-я Init18 dw 0x00E ;7-я Init19 dw 0x8180 ;выбор области DDRAM,нулевая позиция Row0 dw 0,1,'-','з','н','а','к',' ','ч','е','т','ы','р','ё','х' Row1 dw 0x1C0,'Г','а','м','и','л','ь','т','о','н',' ',' ','Р','о','д','р','и','г','е','с' Row2 dw 0x194,'Б','р','а','н','е','ц',' ',' ','Ш','м','ы','г','л','е','в','с','к','и',f'й' .data Stack dw ?,? .code main proc SP Stack X InitLCD CALL print X Row0 CALL print @@endless: JMP @@endless main endp ;затирает регистр Y. Если раскомментировать 2 строки - не будет затирать print proc ;[SP++] Y Y SP SP X ;BLASPHEMY!!! КОЩУНСТВО!!! @@start: LCD [SP] Acc [SP++] SUB 0 JGE @@start SP Y ;Y [--SP] JMP [--SP] print endp
Я был наказан за кощунства: эта программа не заработала. Точнее, дисплейчик она инициализировала, а затем застряла в бесконечном цикле, выдавая какой-то мусор.
В эмуляторе всё работало как надо, а в симуляторе я увидел то же самое "зацикливание" и увидел, где начинаются проблемы. На строчке
Y SP
Оказалось, что вместо "Y=SP" у нас здесь получилось "Y=Y" - на шине данных вместо значения 0xD2 (текущее значение SP) стоял нолик.
И тут я наконец-то вспомнил, что модуль QuatCoreMem, внутри которого сейчас хранятся базовые регистры X,Y,Z,SP, и который управляет ими и доступом к памяти через комбинацию базовых и индексных регистров, имеет БЕЗУМНО ЖЕСТКОЕ ПРАВИЛО, что этот модуль может быть применён либо как источник данных, либо как получатель, но не всё сразу! Мы уже правили его один раз, а потом ещё один раз, но этого мало!
И ладно бы двухпортовая память - но просто один регистр присвоить другому - это совершенно законная операцию, которую запрещать не стоило бы...
Внимательный осмотр модуля показал: такое ограничение было по сути искусственным. Да, нам не хотелось делать два формирователя эффективного адреса, т.к эта штука довольно-таки громоздкая. Но все прочие варианты (из памяти в базовый регистр, из базового регистра в память или из регистра в регистр) этого и не требуют, нужно лишь чуточку поправить управляющую логику в модуле QuatCoreMemDecoder
module QuatCoreMemDecoder (input [7:0] DestAddr, input [7:0] SrcAddr, input stall, output MemWrite, output SrcSquareBrac, output WriteX, output WriteY, output WriteZ, output WriteSP, output CountSP, output SPup, output [1:0] BaseAddr, output [1:0] FirstIndex, output [1:0] SecondIndex ); wire isDest = (DestAddr[7:6] == 2'b11); wire DestSquareBrac = (DestAddr[3:0] != 4'b1101); assign MemWrite = isDest & DestSquareBrac; assign BaseAddr = MemWrite? DestAddr[5:4] : SrcAddr[5:4]; assign FirstIndex = MemWrite? DestAddr[1:0] : SrcAddr[1:0]; assign SecondIndex = MemWrite? DestAddr[3:2] : SrcAddr[3:2]; assign SrcSquareBrac = (SrcAddr[3:0] != 4'b1101); assign WriteX = isDest & (~DestSquareBrac) & (DestAddr[5:4] == 2'b00); assign WriteY = isDest & (~DestSquareBrac) & (DestAddr[5:4] == 2'b01); assign WriteZ = isDest & (~DestSquareBrac) & (DestAddr[5:4] == 2'b10); assign WriteSP = isDest & (~DestSquareBrac) & (DestAddr[5:4] == 2'b11); wire isSource = (SrcAddr[7:6] == 2'b11); assign CountSP = (~stall) & (isDest | isSource) & (BaseAddr == 2'b11) & (FirstIndex == 2'b11); assign SPup = ~SecondIndex[0]; endmodule
В общем, регистры, используемые для формирования эффективного адреса и "прямого" (без памяти) выхода на шину теперь будут браться из SrcAddr, за единственным исключением, что в DestAddr у нас прописана запись в память. А вот мультиплексор, который коммутирует на шину данных либо значение из памяти, либо значение из регистра - теперь начисто управляется из SrcAddr, потому иное не имеет смысла.
Мы чуть изменили название одного из выводов, он назывался просто SquareBrac ("квадратная скобка"), теперь SrcSquareBrac, т.е это признак, что в источнике данных (SrcAddr) фигурирует квадратная скобка, говорящая - нужно брать данные из памяти, по адресу, указанному в этих скобках.
Всё: теперь львиная доля операций внутри модуля QuatCoreMem становится разрешённой. Исключение - скопировать данные из одной области в другую область памяти, такая операция не возымеет эффекта, разве что "побочные эффекты" вроде инкремента/декремента SP.
Но чтобы процедура print заработала, нам пришлось внести и ещё одно изменение: добавить вход stall. Вся проблема в строке:
Acc [SP++]
Помещение в аккумулятор требует 3 такта, и на каждом из тактов у нас к SP прибавлялась единица, что явно не соответствовало нашим намерениям. Я подумал было сделать так:
Acc [SP] LCD [SP++]
но вспомнил, что LCD во все разы, кроме самого первого, останавливает процессор ещё серьёзнее, на 40 мкс, а иногда и на 1,5 мс, так что ничего не исправится.
Так что теперь выходы busy с АЛУ и LCD (и UART) поступают на элемент OR, выход с него мы гордо называем "stall" и подаём не только на QuatCorePC, но и на QuatCoreMem. У последнего stall запрещает инкремент/декремент SP.
Так теперь выглядит QuatCore:

Всё, отступать некуда, пора заняться обработкой видео в реальном времени...
UPD. Не уточнил о доработке напильником: модуль QuatCoreMem занимал 130 ЛЭ (при ширине адреса RAM 8 бит), а после доработки - 127 ЛЭ, даже после добавления stall.
UPD2. Пролистал даташит на управляющую микросхемку К1013ВГ6, вычитал, как они обеспечивают восстановление после ошибочно переданных 4 бит. Оказывается, счётчик половинок сбрасывается каждый раз, как меняется логический уровень на A0 (переключение между командами и данными) или RW (чтение/запись). Не так уж и плохо: у меня, получается, на каждой строке будет восстановление происходить. А вот причину хитрючей инициализации с подачей одной и той же команды 3-4 раза я там не нашёл. Мало того, там и в 8-битном, и в 4-битном режиме команда подаётся 3 раза, а потом уже начинается настройка в выбранном режиме.