QuatCore: подключаем ЖК-экранчик

Feb 01, 2020 02:53

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

Мне достался ЖК-экран МЭЛТ MT-20S4A-2FLA-АМПЕРКА, из Линовых запасов (скупил тогда всякие электронные штуки, оставшиеся от стенда), это черно-белый символьный экран 20x4, с янтарной подсветкой.

Расскажу о нескольких подводных камнях, из-за которых он только в пятницу, в 8 вечера наконец-то подал признаки жизни.




Снова доволен как удав - писать взаимодействие с этим экраном на QuatCore - ПРОЩЕ, чем на ардуинке. "СУПЕРНАВОРОЧЕННАЯ СХЕМА" аж на 33 ЛЭ взяла на себя все низкоуровневые заботы!


Наименование дисплея, MT-20S4A-2FLA, я ввёл в поиске, и попал на описание с сайта изготовителя:
http://www.melt.com.ru/shop/mt-20s4a-2fla.html

Там указано напряжение питания: 5 вольт.
Но в даташите приведено два разных питающих напряжения: либо 3 вольта, либо 5 вольт.

Также есть система обозначений, согласно которой, в названии "MT-20S4A-2FLA":

MT - название производителя, МЭЛТ,
20S4A - функциональное обозначение (20 символов и 4 строки явно проглядывают, а что буквы обозначают - точно не знаю, могут ничего не значить),
2 - диапазон рабочих температур от минус 20° до +70°C (и хранения от минус 30° до +80°С),
F - тип панели, FSTN positive (серый фон, черные символы, высокий контраст и широкий угол обзора)
L - тип подсветки, светодиодная (LED),
A - цвет подсветки, янтарный (Amber).

А вот дальше, через ещё одно тире, должно было следовать напряжение питания, 3V0 либо 5V0. Но у меня вместо этого лишь слово "АМПЕРКА":



Можно предположить, что они делают специальную модификацию для той самой "Амперки". Ищем у них:
http://wiki.amperka.ru/%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D1%8B:text-lcd-20x4

и там находим напряжение питания, 3,3-5 В.

Отлично, он УНИВЕРСАЛЬНЫЙ. Мне удобнее работать с 3,3 вольтами, т.к от них же питается периферия ПЛИС, и таковы её логические уровни. Поэтому я включил её в 3,3 вольта.

Как было указано в даташите, вывод контрастности (третий) оставил неподключённым (для питания от 5 вольт его надо замкнуть на землю, либо поставить регулировочный резистор 10 кОм).

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

Также я подсоединил выводы A0 (выбор между командами и символами), R/W (чтение/запись), E ("тактовая частота") и DB4-DB7.

Там вообще шина данных в 8 бит, DB0-DB7, но есть режим передачи по 4 бита в два захода. Поначалу я пожадничал и оставил DB0-DB3 неподключёнными. Потом, правда, пустил отдельный шлейф и для них, чтобы начать с более простой логики управления. R/W мне пока так и не пригодился - я его в ПЛИС пока что замкнул на ноль, что означает - всегда производится запись в ЖК.

На этом электрическая часть была как будто завершена - наставала пора научиться подавать на экран правильные сигналы. На самом низком уровне, порядок подачи сигналов изображён здесь:




До сих пор меня эти эпюры вводят в ступор и уныние. Конкретные значения я вписал из таблицы, приведённой в даташите, для напряжения 3 вольта (на 3 вольтах всё должно происходить несколько медленнее, чем на 5 вольтах).

Получается, что мы должны несколько заблаговременно выставить A0 (0 для команд, 1 для данных), и чтобы два раза не ходить - может сразу же и сигналы D все зафиксировать.

Затем через 60 нс (или больше) переключить сигнал E ("тактовая частота") из нуля в единицу.

Ещё через 500 нс (или больше) переключаем сигнал E из единицы в ноль, и удерживаем в нуле хотя бы 500 нс. На этом передача байта завершена.

Но затем у нас есть таблица команд, из которой выходит: между любыми двумя посылками должно проходить хотя бы 40 мкс, а в случае команды очистки экрана мы должны и вовсе выждать 1,5 мс. Можно убыстриться, если заморочится с чтением/записью и запрашивать флаг занятости. Тогда, как только он переключится в ноль, можно начинать следующее действие. Мы пока не стали этого делать - если мы торопимся, чтобы освободить процессор побыстрее, мы лучше смонстрячим FIFO, чтобы он быстренько забросил все необходимые данные, и они потихоньку поступали бы.

Я уже не раз и не два видел, как всю эту катавасию осуществляют непосредственно в процессоре: переключить бит здесь, подождать столько, переключить бит здесь, ещё подождать, и так далее. Но как-то мне это не шибко нравится. Поэтому написал модуль, который освобождает нас всех проблем с таймингами. Он предназначен для QuatCore, и пока что занимает те же адреса, что и UART, хотя нет особых проблем их потеснить. Нужно лишь поменять строчку

wire IsOurAddr = ~DestAddr[7];

Например, можно дать UART адреса 0x00..0x3F, а LCD: 0x40..0x7F. Тогда первому нужна строка

wire IsOurAddr = ~DestAddr[7]&(~DestAddr[6]);

а второму:

wire IsOurAddr = ~DestAddr[7]&DestAddr[6];

Это на потом, пока что совместно их использовать не планировал. Вот код модуля:

`include "math.v"
module QuatCoreLCD (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;

localparam EDivBy = (ClockFreq + 2_222_221) / 2_222_222;
localparam EDivWidth = `CLOG2 (EDivBy);

localparam ActualEFreq = ClockFreq / EDivBy;

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),
.sset (e_set),
.cout (ce) );
defparam
EDivider.lpm_direction = "UP",
EDivider.lpm_port_updown = "PORT_UNUSED",
EDivider.lpm_type = "LPM_COUNTER",
EDivider.lpm_width = EDivWidth,
EDivider.lpm_svalue = (1 << EDivWidth) - EDivBy;

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;

always @(posedge clk) begin
zLCD_E <= b_set? 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
LCD_A0 <= b_set? ~DataBus[8] : LCD_A0;
LCD_data <= b_set? DataBus[7:0] : LCD_data;
end

endmodule

Весь модуль состоит из двух счётчиков и выходных регистров.

Первый счётчик, EDivider - это делитель частоты, чтобы обеспечить длительность импульса E свыше 500 нс. Но также он используется совместно со вторым счётчиком (управляет его входом cnt_en, разрешением счёта), чтобы второй вышел хоть чуточку компактнее.

Второй счётчик, BusyDivider - отсчитывает необходимую паузу, прежде чем мы сможем подать следующую команду. В одном-единственном случае, когда мы подаём команду "очистить экран", он отсчитает 1,5 миллисекунды, в противном случае - 40 мкс.

Благодаря нашему старому макросу `CLOG2, т.е Ceil of LOG2, удаётся ширину этих счётчиков сконфигурировать автоматически, пользователю данного модуля нужно лишь задать тактовую частоту процессора, в нашем случае 4 МГц.

Счётчик EDivider считает "вверх", а BusyDivider - "вниз", и это неслучайно. При включении ПЛИС и окончании её конфигурирования, все счётчики инициализированы нулями. При этом выход окончания счёта cout в EDivider получается нулевым ("счёт не окончен"), а cout в BusyDivider - единичный ("нам нужно было сделать обратный отсчёт до нуля - мы уже в нуле!"). Именно такое начальное состояние нам и нужно.

выход cout в BusyDivider мы назвали idle ("режим ожидания"). Пока idle=1, также e_set=1, то есть счётчик EDivider непрерывно устанавливает в начальное состояние, поэтому счёта как такового не происходит, и ce остаётся нулевым. Благодаря этому и счётчик BusyDivider стоит на одном месте. Сигнал busy, который останавливает процессор, заведомо будет нулевым, поскольку мы вовсе не заняты :) Полная идиллия.

Она нарушается, когда нам дали команду, т.е IsOurAddr = 1. Именно сейчас, когда мы были в режиме ожидания (idle=1) и нам дали команду, b_set = 1. Это заставляет защёлкнуть 9 бит из шины данных в регистры LCD_data[7:0] и LCD_A0. Таким образом, мы можем задать любую команду или любой символ. Эти регистры непосредственно подключаются к дисплею.

И также защёлкивается в единицу регистр zLCD_E. Если бы мы его напрямую подсоединили на выход LCD_E, то не обеспечили бы задержку в 60 нс, которую требует эпюра. Чтобы исправить положение, мы задерживаем этот регистр на один такт, и уже этот задержанный сигнал отправляется на дисплей:

LCD_E <= zLCD_E; //1 clk delay to ensure t_AS (Address set-up time) > 60 ns

Это немножко косой код, он будет работать при тактовой частоте процессора вплоть до 19 МГц, потом времени начнёт немножко не хватать, желательно будет ввести несколько тактов задержки, но давайте решать проблемы по мере их поступления!

И наконец, сигнал b_set вызывает синхронную загрузку в счётчик BusyDivider. В зависимости от команды, это будет или быстрая задержка в 40 мкс, или долгая задержка в 1,5 мс.

К следующему такту (когда загрузка в счётчик возымела эффект) пропадает сигнал idle, поэтому теперь ничто не удерживает счётчик EDivider. Он отсчитывает 500 нс или чуть больше, после чего зажигается сигнал ce. Он заставляет регистр zLCD_E переключиться назад в нолик, завершая "строб". И также это вызывает счёт в BusyDivider. И ещё, ce=1 заставляет счётчик EDivider начать по-новой, что он и будет делать раз за разом, пока BusyDivider не досчитает до конца, из-за чего снова будет idle=1, и EDivider перестанет считать.

В момент поступления команды, если модуль был в режиме ожидания, сигнала busy не возникнет - уже на следующем такте процессор начнёт исполнять следующую команду. А вот если мы снова запросили LCD, а он ещё не отсчитал паузы от предыдущей посылки, тут-то возникнет busy=1, при этом сигнал IsOurAddr не будет воздействовать на модуль, пока пауза не закончится. Тут-то на один такт возникнет idle=1, благодаря чему мы "отпустим" процессор (возникнет busy=0 - и он возьмётся за следующую команду) и по-новой запустим посылку.

Мы покажем всё это на симуляции, но для начала нужно показать тестовую программу:

;Hello, world на ЖК-экранчике
%include "LCD_code_page_1.inc"
.rodata
InitLCD dw 0x130,0x130,0x130,0x13A,0x10C,0x101,0x8106
Wolf Int16 'П','р','и','в','е','т',' ','л','у','н','а','т','и','к','а','м','!',' ',' ',' ','о','т',' ','Q','u','a','t','C','o','r',f'e'
.data
Stack dw ?,?
.code
main proc
SP Stack
X InitLCD
CALL print
X Wolf
CALL print
@@endless: JMP @@endless
main endp

print proc
[SP++] i
i 0
@@start: LCD [X+i]
Acc [X+i]
i++ 0
SUB 0
JGE @@start
i [--SP]
JMP [--SP]
print endp

Процедура print нам уже знакома. Единственное отличие - адрес UART0 был заменён на LCD. Остальное - как было. Нам даётся адрес строки, и мы посылаем эту строку, слово за словом. Только в случае UART мы посылали 8 бит, а здесь посылаем 9. Девятый бит - это A0, только мы здесь для упрощения сделали инверсию. В наших строках если девятый бит нулевой, значит это обычный символ, а если единичный - это команда. Самый старший бит используется для индикации конца строки.

Получается замечательно: инициализация ЖК-экрана - это всего лишь передача на него специальной строки. А глядя в даташит, этого и не скажешь:




Приведём также листинг программы, чтобы потом понимать, что творится на симуляции:

main proc
0 FD43 SP Stack
1 CD00 X InitLCD
2 F3B0 CALL print
3 CD24 X Wolf
4 F3B0 CALL print
5 B805 @@endless: JMP @@endless
main endp
print proc
6 F3A0 [SP++] i
7 A000 i 0
8 01C4 @@start: LCD [X+i]
9 80C4 Acc [X+i]
A A400 i++ 0
B 8300 SUB 0
C B17C JGE @@start
D A0FF i [--SP]
E B8FF JMP [--SP]
print endp

Мы в компиляторе указали адрес для UART0: 0x00, а для LCD: 0x01. Но пока что модуль UART0 мы вытащили, а LCD поставили:




(там сейчас куча отладочных выводов, чтобы показать "потроха" модуля LCD, да и не заработал он у меня с первого раза, пришлось уйти глубоко в отладку).

Итак, вот что происходит на симуляции:



Как всегда, инициализируем стек, заносим в регистр X адрес строки для инициализации ЖК-экранчика, и вызываем процедуру print. Та, в свою очередь, сохраняет в стек регистр i, в данной программе это было не нужно, но в более сложных будет полезно, когда print не затирает нам все регистры подряд. Затем присваивает i=0 и тут же отправляет на ЖК-экран первую команду.

Как и в случае UART, процессор не блокируется до завершения посылки - он продолжает заниматься своими делами - убеждается, что это ещё не последний символ, инкрементирует i и начинает следующую итерацию. И только тогда, снова обратившись к LCD, останавливается, потому что ещё не прошло 40 мкс от первой посылки.

Теперь глянем, что происходит в модуле LCD. Как и обещалось, изначально он в состоянии idle. Из-за этого сохраняется e_set=1, то естьсчётчику непрерывно присваивается начальное значение, поэтому он не считает. И никак не может досчитать до ce=1. Соответственно, и второй счётчик стоит.

Всё меняется, когда процессор обращается к LCD. Теперь возник запускающий импульс b_set. Уже к следующему такту на выходах LCD_data и LCD_A0 защёлкнулись правильные значения, а ещё спустя такт установилось LCD_E=1. Он держится в единице в течение двух полных тактов, что при 4 МГц соответствует 500 нс - всё как положено.

И потом продолжается счёт делителя частоты, и больше мы никакой активности не видим.

Смотрим, что происходит через 40 мкс:



Как и обещалось, на один такт включается idle, чего достаточно, чтобы "отпустить" процессор - он трогается с мёртвой точки и быстренько убеждается, что и этот символ ещё не последний в строке - и быстренько возвращается опять к LCD.

В то же время запускается следующая посылка, ровно такая же. Почему-то в даташите прописано отправить одну и ту же посылку ТРИ РАЗА. Ну написано - давайте сделаем. К сожалению, глубинной подоплёки я не знаю.

И ещё глянем на команду очистки экрана:



Всё то же самое, но в счётчик загружается значение 2999 вместо обычного 80, так что теперь он застревает всерьёз и надолго - на 1,5 миллисекунды. И если прокрутить дальше - мы увидим, что так оно и есть.

Осталось запустить всё это "в железе"

И тут меня ждал полнейший облом - экран не подавал никаких признаков жизни. Подсветка работала, и больше ничего. И причин этому может быть великое множество. Не так спаял, не так сконфигурировал пины (если помните, есть таблица соответствия между пинами flex10k и 5576ХС4Т), или неправильно понял эпюры, и управлять этой штукой надо совсем по-другому. Или сам экранчик горелый.

Уже приготовился с осциллографом проверять каждую ножку, но сначала расчхлил тестер. И увидел ровно последнюю конфигурацию, соответствующую на тот момент восклицательному знаку. A0=1, т.е мы передаём символ, а не команду. D = 0010 0001, что и есть восклицательный знак, 0x21. Это вселило некоторую надежду, что на самом деле всё работает правильно.

Другое подозрение было, что нельзя было ножку контраста оставлять болтаться в воздухе, а надо всё-таки подключить подстроечный резистор и его подкрутить. Но подстроечного под рукой не было, поэтому просто опробовал тестером в режиме амперметра замкнуть на ноль и на плюс питания - никакого эффекта. Нашёл резистор 9 кОм, поковырялся через него - никакого эффекта. Причём экран абсолютно чистый, не видно даже намёка на площадки для символов.

Это меня и насторожило. Какую-то структуру точек мы просто обязаны наблюдать, когда экран включён! Хоть под каким-то углом, сильно сбоку!

И я попробовал подать на него 5 вольт вместо 3,3. Подсветку отключил (она бы сгорела, пока подходящий ограничительный резистор не впаяю), цифровые выходы оставил как есть, в надежде, что 3,3 вольта его "раскачают" как надо. Включаю - опять ничего.

Но этому было объяснение - ведь в даташите сказано, что при питании от 5 вольт ножку контраста надо замкнуть на землю. Сделал это щупами тестера в режиме амперметра - и увидел следующее:




УРА!!! Он явно инициализировался почти как надо, и явно работает, выдал мою строчку "ПрЮвет, ВОЛКУ!!!"

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

Продолжая тематику "Ну, погоди!" и инопланетные надписи, решил переправить фразу на "Привет лунатикам!", но слегка поторопился:



тоже сойдёт :)

И наконец, поставил ту фразу, что в картинке для привлечения внимания. Я хотел задействовать две строки, чтобы убедиться, что нумерация действительно "чересстрочная" - за первой строкой идёт третья, а уже потом вторая и четвёртая.

ВЫВОД: эти индикаторы АМПЕРКА - СТРОГО НА 5 ВОЛЬТ. То, что они написали у себя про 3,3-5, они вычитали из даташита, им показалось, что это ОДИН И ТОТ ЖЕ дисплей может работать по-разному, а на самом деле речь шла о разных моделях. От 3,3 вольт он вообще не работает. Зато логические уровни 3,3 вольта он переваривает нормально. Да и в режиме чтения пожечь ПЛИС он не должен, по даташиту там при любом напряжении уровни вполне себе ТТЛные, они с 3,3 вольтами КМОП дружат неплохо.

UPD. Осознал, что самый простой способ проверить на симуляторе работу отдельного модуля - это не вручную для него рисовать Vector Waveform File, а просто написать короткую программку для QuatCore - и запустить их совместно :)

странные девайсы, ПЛИС, программки, работа

Previous post Next post
Up