nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: знак четырёх


Вот это 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 раза, а потом уже начинается настройка в выбранном режиме.
Tags: ПЛИС, математика, программки, работа, странные девайсы
Subscribe

  • Ещё котики и поезда

    Первый раз встретил котика в Лосином острове, до этого встречал лишь гиену и павиана лосей и пятнистых оленей. Но этот прям дикий - только…

  • Королёвские котики и тепловоз

    Акынский пост - "что вижу, о том пою!" Кот-консьерж ушёл в отпуск: в кои-то веки можно не стоять у входа на проходную, а лежать на солнышке!…

  • Как бы Штиль

    Мне тут отдали даром бензопилу, один из Сафроновцев. Когда он её из сумки начал вытаскивать, я немножко прифигел, неужто новенького Штиля отдаёт??…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 37 comments

  • Ещё котики и поезда

    Первый раз встретил котика в Лосином острове, до этого встречал лишь гиену и павиана лосей и пятнистых оленей. Но этот прям дикий - только…

  • Королёвские котики и тепловоз

    Акынский пост - "что вижу, о том пою!" Кот-консьерж ушёл в отпуск: в кои-то веки можно не стоять у входа на проходную, а лежать на солнышке!…

  • Как бы Штиль

    Мне тут отдали даром бензопилу, один из Сафроновцев. Когда он её из сумки начал вытаскивать, я немножко прифигел, неужто новенького Штиля отдаёт??…