nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

"Знак четырёх" на быстром QuatCore

Передатчик UART подключили, теперь давайте подключим ЖК-экранчик.


Вот наш замечательный модуль для работы с экраном по 4-проводному интерфейсу (в смысле, что 4 бита данных, ещё один провод - направление передачи, ещё один - "тактовая частота", и ещё и выбор "команда/данные", но так уж повелось):
//upgraded module, mostly for sending data by 4 wires instead of 8,
//though it is actually universal.
//It has just one address on DestAddr, so far it's 0xxx_xxx (we have plenty space)
//but all the information is sent by DataBus.
//senior bit (bit 15) used by 'print' routine to indicate last symbol, we don't use it here
//least significant 8 bits (7..0) are symbol/command to send to LCD
//bit 8 is A0 - data/command select
//and now bit 9 is flag of 4-bit operation.
//when 0, we send 4 bit and then 4 bit again
//when 1, we send 8 bits all at once.
//(in fact, we always send 8 bit at once, just 4 outputs are disconnected, so we have to send these 4 bits again...)

//also, we now set timing constraints for 5-volt LCD, they are faster. 
//so far, it would work reliably with 3,3-volts LCD for clock freq up to 16,67 MHz, or with 5-volt LCD for clock freq up to 25 MHz
//(though it is fixed easily)

`include "math.v"
module QuatCoreLCD4wire (input clk, input st, input [15:0] DataBus,
			output busy, output reg LCD_A0 = 1'b0, output reg LCD_E = 1'b0, output [3:0] LCD_data);
					
parameter ClockFreq = 4_000_000; //most understood.
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 = st;

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;
	reg [7:0] rLCD_data = 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;
		rLCD_data [7:4] <= b_set? DataBus[7:4] : (DoItAgain & ce & (~zLCD_E))? rLCD_data[3:0] : rLCD_data[7:4];
		rLCD_data [3:0] <= b_set? DataBus[3:0] : rLCD_data[3:0];
	end
	
	assign LCD_data = rLCD_data [7:4];

endmodule

К нашей радости, его корректировать вообще не требуется! Чтобы он не откликался на "недействительные" команды (когда DestStall=1) - это забота QuatCoreIOselector, и там это уже реализовано. А сигнал busy формируется вполне себе как надо, по аналогии с UART, никаких претензий.

Как видно по комментариям к коду, на высокой тактовой частоте могут нарушаться тайминги, но у нас ЖК-экранчик работает от 5 вольт, и для него тайминги нарушатся только после 25 МГц. Пока всё хорошо.

Подключаем его на "общую схему":


По "аппаратной части", кажется, всё. Теперь нужно "стряхнуть пыль" с программы:

;Hello, world на ЖК-экранчике, через 4-проводной интерфейс
%include "QuatCoreConsts.inc"
%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,нулевая позиция
	Init19	dw	0x0180	;выбор области DDRAM, нулевая позиция, продолжаем выводить строку
	Row0	dw	0,1,'-знак четырёх'
	Row1	dw	0x1C0,'Гамильтон  Родригес'
	Row2	dw	0x194,'Бранец  Шмыглевски',f'й'
.data
	Stack	dw	?
.code
	main proc
				SP		Stack
;				SIO		LCD
				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:	OUT		[SP]	
				Acc		[SP++]		
				SUB		0
				JGE		@@start
				SP		Y
				;Y		[--SP]
				JMP		[--SP]	
	print	endp
	


Порезали эту программу как могли: в исходной версии процедура print вызывалась дважды, первый раз для инициализации, второй раз - для вывода строки. Сейчас, из вредности, сократил вызов до одного. Ещё и SIO убрал, хочу проверить, что при отключённом UART и включённом ЖК, именно он будет выбран "автоматом".

Поскольку процедура print из Hello,world умеет выводить строки длиной до 32 символа (регистры i,j,k у нас 5-битные), то здесь она не годится. В итоге мы совершаем "кощунство": сохраняем указатель на стек в переменную Y, после чего пользуемся им для удобного перемещения по строке, поскольку только у SP есть автоинкремент. Можно было бы и вовсе процедуру убрать, зачем она нужна, если вызывается ровно один раз, но пусть будет :)

Запускаем компиляцию:
Загружаем файл конфигурации транслятора
Файл конфигурации прочитан, готовы к работе
Обрабатываем файл HelloLCD4wire.asm
Конфликт (Hazard) между командами SP и [SP], процедура print, строки:
				SP		X	;BLASPHEMY!!! КОЩУНСТВО!!!
		@@start:	OUT		[SP]	
Вставляем NOP

Конфликт (Hazard) между командами SP и [--SP], процедура print, строки:
				SP		Y
				JMP		[--SP]	
Вставляем NOP

Пытаемся оптимизировать таблицу вызова процедур
print = 0004 

Бит 3 адреса
Всегда ноль...
Бит 2 адреса
Всегда единица...
Бит 1 адреса
Всегда ноль...
Бит 0 адреса
Всегда ноль...
4 входных бит оказались не сопоставленными битам адреса
Извините, данный код ещё не готов...
Компиляция завершена успешно

Ширина адреса сегмента кода (и регистра PC):           4  
Ширина адреса сегмента данных (и регистров X,Y,Z,SP):  7  
Количество инициализированных слов данных:             82 
Количество инициализированных слов кода:               14 
Количество адресов процедур:                           1  

Адреса процедур:
print = 0004(поле Src = B0)


Возникли Hazard'ы, компилятор всунул туда NOP'ы - а пускай! Это тестовая программа, уж как-нибудь перетерплю, что её длина 14 слов вместо возможных 12 :) Это в рабочих алгоритмах хотелось ужаться как можно сильнее.

Вот листинг кода:
    main proc
0  FD51                 SP      Stack
1  CD00                 X       InitLCD
2  F3B0                 CALL        print
3  B003     @@endless:  JMP         @@endless
    main endp
    print   proc
4  DDFD                 Y       SP
5  FDCD                 SP      X   ;BLASPHEMY!!! КОЩУНСТВО!!!
6  8900      NOP  0 ;AUTOMATICALLY INSERTED BY TRANSLATOR TO PREVENT HAZARD
7  00FC     @@start:    OUT     [SP]    
8  80F3                 Acc     [SP++]      
9  8300                 SUB     0
A  BC7B                 JGE     @@start
B  FDDD                 SP      Y
C  8900      NOP  0 ;AUTOMATICALLY INSERTED BY TRANSLATOR TO PREVENT HAZARD
D  B0FF                 JMP     [--SP]  
    print   endp


Копируем файлы QuatCoreCode.mif, QuatCoreData.mif и QuatCoreCallTable.v в папку с проектом для ПЛИС. И для начала запускаем синтез и симуляцию. Синтез успешен, даже с кучей "отладочных выводов", и тайминги выдерживаются, хоть и "с трудом": 25,32 МГц. Посмотрим, что там происходит на симуляции.


Опять у нас "фальстарт": как только ПЛИС выходит на нормальную работу, как на ЖК-экранчик посылается нулевой символ. Это не страшно: за ним последует полная его инициализация с очисткой экрана. Но неприятно...

А дальше уже совсем криминал: к указателю стека не прибавляется единичка. Так что мы застреваем на передаче самого первого "символа" до скончания времён. Причём в вызове процедуры [SP++] срабатывает как надо, а вот здесь не хочет.

Надо понимать: это две РАЗНЫХ КОМАНДЫ. По-моему, ДО СИХ ПОР мы применяли [SP++] на стороне DestAddr, т.е мы ЗАНОСИМ ДАННЫЕ В СТЕК, и двигаем указатель на слово вперёд. А сейчас не сработала команда на стороне SrcAddr, то есть ИЗВЛЕЧЕНИЯ ДАННЫХ ИЗ СТЕКА. Хотя [--SP] ранее срабатывала как надо. Значит, мы опять что-то поломали, а точнее, не доделали.

Вот такая вот офигительная строчка управляет счётчиком стека:
assign CountSP = ((isDest & DestAddr[5] & DestAddr[4] & DestAddr[1] & DestAddr[0]) | (~fetching & isSource & SrcAddr[5] & SrcAddr[4] & SrcAddr[1] & SrcAddr[0]));


Нам уже приходилось несколько раз её исправлять. Итак, часть, отвечающая за запись (т.е на стороне DestAddr), пока нареканий не вызывала. А вот на чтение творится какая-то дичь... Вот эта часть:

(~fetching & isSource & SrcAddr[5] & SrcAddr[4] & SrcAddr[1] & SrcAddr[0]));


В первую очередь, сигнал isSource, он определён так:
wire isSource = (~SrcDiscard)&(~SrcStall)&(SrcAddr[7:6] == 2'b11);



Адрес должен принадлежать модулю QuatCoreMem (т.е 11xx_xxxx). Команда должна быть действительной (SrcDiscard=0), и в данный момент активна (SrcStall=0). В конце выполнения пары команд
OUT   [SP++]

всё это выполняется.

Далее, мы проверяем единичные биты 5,4,1,0, т.е требуем команду вида 1111_xx11. Сейчас у нас команда 0xF3 = 1111_0011 - подходит. Должно бы сработать.

Похоже, всё дело в сигнале fetching. Вот здесь он вообще со стеком никак не был связан. В код для инкремента стека он появился здесь, когда оказалось, что [--SP] делает вычитание ДВАЖДЫ, поскольку ОН САМ задерживает конвейер на один шаг, что не приводит к формированию ВНЕШНИХ сигналов SrcStall / SrcDiscard, а потому на следующем такте он повторно вычитает единичку.

Хочется иногда себя из прошлого пнуть как следует: КАК ТАК МОЖНО БЫЛО? Вот ведь наглец, так и написал в том посте: "По крайней мере, ЗДЕСЬ работает". Вообще, сволочь, о будущем не задумывается.

А ведь правильное решение было так близко: нужно заменить ~fetching просто на fetching!
assign CountSP = ((isDest & DestAddr[5] & DestAddr[4] & DestAddr[1] & DestAddr[0]) | (fetching & isSource & SrcAddr[5] & SrcAddr[4] & SrcAddr[1] & SrcAddr[0]));


Запускаем повторно. Всё отсинтезировалось без проблем. Смотрим симуляцию:


Да, теперь всё верно: к указателю стека SP единичка прибавляется РОВНО ОДИН РАЗ. Промотаем к самому концу работы:


Видно, как делается пересылка "в два этапа", по 4 бита за раз. И как мы выходим из цикла, и затем из процедуры ([--SP] мы не поломали) - и входим в бесконечный цикл, завершив работу.

Выглядит многообещающе.


Увы, в этот раз "с нахрапу" на железе не заработало. Подозреваю, это потому что я игрался с подсветкой ЖК-экрана и оставил вход R/W "висеть в воздухе". Завтра попробую вернуть провод на место, должно бы заработать.
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

    Мишка такой человек — ему обязательно надо, чтоб от всего была польза. Когда у него бывают лишние деньги, он идёт в магазин и покупает какую-нибудь…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 2 comments