Графический и текстовый режим дисплеев Winstar
Графические дисплеи, в том числе и типа OLED, больше всего представленные на нашем рынке фирмой Winstar, имеют куда меньший спрос по отношению к строчным и публикаций по их применению также намного меньше. Между тем, именно графические OLED-дисплеи из-за отсутствия привязки к таблицам шрифтов предопределенного рисунка, предоставляют наилучший способ для получения эргономичных индикаторных устройств для самых разных надобностей. Причем оказалось, что графический режим в контроллере WS0010 иницируется проще и работает стабильнее, чем текстовый.
Прежде чем перейти к рассмотрению собственно графических дисплеев, рассмотрим вечнозеленую проблему с проблемами включения текстового режима контроллера WS0010, которая получила неожиданное и очевидное решение (ах, где были мои глаза!).
Решение проблем текстового режима WS0010
Общеизвестно, что строчные дисплеи Winstar имеют проблемы со стабильностью при инициации. Выяснилось, кстати, что характерно это совсем не только для «проклятых китайцев»: добытые мной с большими трудностями образцы Newhaven Display 16×2, находящейся на другой стороне земного шара, внешне являются полной копией Winstar, за исключением расположения некоторых надписей и названия фирмы на нашлепке (той же формы и с тем же шрифтом):
Содержащие, как написано в datasheets, некий «LCD comparable» контроллер, эти дисплеи ведут себя совершенно идентично китайским и имеют те же недостатки. Очевидно, на проверку других фирм, вроде Midas, времени тратить не стоит: судя вот по этой публикации, и там без международной кооперации не обошлось. Глобализированная экономика рулез!
Трудности текстового режима выражаются в том, что при запуске (например, при перезагрузке или ручном ресете программы управляющего контроллера) на дисплеях может появляться мусор, а строки 0 и 1 произвольно меняются местами. Эксперименты показали, что от метода включения (8-ми или 4-х битный) это не зависит. Особенно остро стоит этот вопрос при необходимости периодической софтовой перезагрузки, например, по Watchdog-таймеру.
Частично проблему решает аккуратное отношение к питанию (от отдельного источника, и ни в коем случае не от USB Arduino), и отдельная перезагрузка через выключение-включение питания дисплея после запуска управляющей программы (см. предыдущую публикацию автора). Как выяснилось, автор этих строк не единственный, кто предлагал подобное решение проблемы: автор надстройки над LuquidCrystal под названием WinstarOLED также включил в нее специальный pw_pin, с помощью которого в момент запуска программы передергивается питание дисплея.
Но это все, конечно, самодеятельность и полумеры. На радикальный способ натолкнулся некто SeregaB (см. его публикацию на easyelectronics.ru — благодарю Tomasina за наводку). Он вообще-то ставил совсем другую задачу: научиться работать как раз с графическим, а не текстовым режимом. Попытавшись попереключаться между режимами, он быстро обнаружил, что »переключение в графический режим происходило нормально, а из графического в «текстовый» — очень коряво». Тогда он вспомнил, что »когда-то, давным-давно, когда ДШ еще печатали на бумаге, в каком-то из ДШ на HD44780 я читал, что переключение режимов надо делать только при выключенном экране». И все заработало.
Из цитировавшейся публикации я тут просто воспроизведу две процедуры переключения, несколько адаптировав их под использование совместно с LuquidCrystal (экземпляр класса здесь называется OLED1).
Переключение в графический режим:
OLED1.command(0x08);//выключили экран
OLED1.command(0x1F);//переключение в графику
OLED1.command(0x01);//очистили от мусора ОЗУ (т.с. что clear())
OLED1.command(0x08|0x04);//включили экран
Переключение в текстовый режим:
OLED1.command(0x08);//выключили экран
OLED1.command(0x17);//переключение в текстовый режим
OLED1.command(0x01);//очистили от мусора ОЗУ (т.с. что clear())
OLED1.command(0x04 | 0x08);//включили экран
Как мы увидим далее, первая процедура не очень-то и нужна: WS0010 переключается в графический режим с полпинка, достаточно послать в него команду 0×1F. А вот вторая последовательность команд оказалась очень по делу. Для пробы она включалась прямо в скетч с использованием LuquidCrystal в таком виде:
void reset_textmode() //функция для установки графического режима
{
OLED1.command(0x08);//выключили экран
OLED1.command(0x17);//переключение в текстовый режим
OLED1.command(0x01);//очистили от мусора ОЗУ
OLED1.command(0x04 | 0x08);//включили экран
}
Затем эта функция вызывалась в setup прямо сразу после инициации библиотеки:
. . . . .
OLED1.begin(16,2); //16 символов 2 строки
reset_textmode(); // вместо clear()
. . . . .
Если перед этим еще вставить какой-нибудь delay (500), то демонстрация оказывается очень наглядной: после нажатия кнопочки ресета платы Arduino на экране, как обычно появляется мусор, но только на мгновение: после срабатывания функции экран очищается и все строки оказываются на своих местах.
Функция работает и так, но для удобства я заменил этой последовательностью команд содержимое функции LiquidCrystalRus: clear () в файле модернизированной библиотеки LiquidCrystalRus_OLED.cpp, о которой шла речь ранее (напомню, что скачать ее можно с сайта автора). Ожидания выполнения команды в библиотеке не предусмотрено, потому для надежности после каждой команды там в общем стиле библиотеки вставлены задержки 100 мкс. В скетчах, использующих этот вариант LiquidCrystalRus_OLED, в начале setup обязательно надо вызывать функцию clear (), при этом она заодно и почистит экран.
С очисткой экрана связана одна непонятка: в таблице команд в даташите отмечено, что команда 0×01 может продолжаться целых 6,2 мс «when fsp or fosc = 250KHz». Какое «fsp or fosc» на самом деле в конкретных контроллерах, оне-с написать поленились, но в любом случае, даже если это мегагерцы, задержка для этой команды должна быть значительная (и автор LiquidCrystal об этом упоминает). Однако, на практике оказывается, что команда очистки вполне себе работает и если задержки нет вовсе. Так что я не стал разбираться, а поступил по известному программистскому правилу: «работает — не трогай!».
Теперь займемся, наконец, графическим режимом.
Графический режим в текстовых дисплеях WEH001602
Для начала я попробовал имевшийся у меня текстовый дисплей WEH001602BG переключить в графический режим. Отметим, что у графического 100×16 и текстового (конфигурации 20×2, у 16×2 просто меньше точек по горизонтали) дисплеев идентичные матрицы, только у текстового они разделены промежутками на знакоместа. Это сильно ограничивает применение графического режима в текстовых дисплеях, и еще больше текстового режима в графических. Но для проверки, как это работает, можно использовать любой их них.
Дисплей вместе с часами DS1307 подключался к Arduino Nano по следующей схеме:
По этой же схеме будем подключать в дальнейшем и графические дисплеи. Серым цветом на схеме показано подключение второго дисплея, если он необходим.
Для переключения в графический режим можно использовать и усовершенствованную процедуру из предыдущего раздела, но вполне работает простая функция из одной команды:
. . . . .
#define LCD_SETGRAPHICMODE 0x1f
LiquidCrystal lcd(9, 4, 8, 7, 6, 5);
void setGraphicMode(){
lcd.command(LCD_SETGRAPHICMODE);
}
. . . . .
Никакой русской таблицы нам здесь не потребуется, потому применяется стандартная (неотрихтованная) LiquidCrystal, которая в графическом режиме работает безупречно. Чтобы не возиться с отладкой всех вариантов библиотеки, в случае, когда включены параллельно текстовый и графический варианты дисплеев, то для каждого я применяю свою библиотеку (для текстового модернизированную Rus_OLED, для графического обычную). Подключение при этом можно все равно делать к одним и тем же ножкам контроллера, за исключением выводов разрешения E, согласно вышеприведенной схеме.
Далее я частично использовал наработки автора упоминавшейся библиотеки WinstarOLED (сама по себе эта надстройка над LuquidCrystal, на мой взгляд, недоработана, и применять ее as is нецелесообразно). Он ввел удобную функцию установки графического курсора (здесь исправлена ошибка оригинала в части максимального значения x):
void setGraphicCursor( uint8_t x, uint8_t y ){
if( 0 <= x && x <= 99 ){
lcd.command(LCD_SETDDRAMADDR | x);
}
if( 0 <= y && y <= 1 ){
lcd.command(LCD_SETCGRAMADDR | y);
}
}
Константа LCD_SETDDRAMADDR определена в библиотеке LiquidCrystal. Дисплей 100×16, как и текстовый, делится на две строки 0 и 1, потому y здесь может принимать только два значения. А горизонтальная координата x варьируется от 0 до 99. По установленной координате командой lcd.write () посылается байт, отдельные биты которого определяют светящиеся позиции вертикальной линии длиной в 8 точек. Крайняя левая позиция в верхней строке имеет координаты 0,0, крайняя правая в нижней — 99,1. Причем верхней точке будет соответствовать младший бит, а нижней точке — старший.
Для удобства кодирования картинок я расчертил табличку, в которой можно быстро создать нужный код вручную. Для полных таблиц шрифтов, конечно, целесообразно применять специальные редакторы (которых не меньше миллиона разной степени самодеятельности), но 10 цифр с нужным порядком бит быстрее обработать вручную, тем более, что автоматически создаваемые шрифты часто все равно приходится допиливать руками. В соответствии со сказанным выше, глиф, например, цифры 2 шрифтом 10×16 будет кодироваться следующим образом:
Все это записывается в двумерный массив вида:
const byte Data2[2][10]={{0x06,0x07,0x03,0x03,0x03,0x83,0xc3,0x63,0x3f,0x1e},
{0xf0,0xf8,0xcc,0xc6,0xc3,0xc1,0xc0,0xc0,0xc0,0xc0}};
Для каждой цифры 0–9 создается отдельный такой массив Data0, Data1, Data2 и так далее. Для часов, кроме цифр, потребуется еще двойная точка. Ее можно сделать покороче:
const byte DataDP[2][2]={{0x70,0x70},
{0x1c,0x1c}};//двойная точка
Так как в графическом режиме контролер «блинкать» не умеет, то мигать двоеточием придется программно. Гасить двойную точку можно и просто выводом нулей в соответствующие позиции, но для единообразия я сделал отдельный массив
const byte DataDPclr[2][2]={{0x00,0x00},
{0x00,0x00}};//очистка дв. точки
Для вывода каждой цифры и отдельно для двойной точки пишется отдельная функция:
void draw2 (byte x/*горизонтальная позиция*/) //вывод "2”
{
for (byte i = x; i
Все функции одинаковые, но используют разные массивы, а для двойной точки и другие пределы цикла. Получилось не слишком экономично в части объема кода (см. об этом далее), зато наглядно и легко править ошибки. Учет промежутков между символами производится на стадии вывода, указанием соответствующей позиции (для чтения часов применяется библиотека RTClib):
void loop() {
DateTime clock = RTC.now();
if (clock.second()!=old_second)
{
uint8_t values;
values=clock.hour()/10; //десятки часов
drawValPos(values,0);
values=clock.hour()%10; //единицы часов
drawValPos(values,12);
values=clock.minute()/10; //десятки минут
drawValPos(values,28);
values=clock.minute()%10; //end единицы минут
drawValPos(values,40);
if (clock.second()%2) drawDP(24); else drawDPclr(24);
old_second=clock.second();
}//end if clocksecond
}
Десять цифр по 20 байт займут в памяти 200 байт — около 10% ее объема (а широкий шрифт 16×16, как в примере ниже, и все 16%). Полный одноязычный шрифт такого размера вместе с цифрами, без учета всяческих знаков препинания и спец. символов, содержит от 62 (английский) до 74 (русский без Ё) символов, значица, займет почти половину оперативной памяти ATmega328. Потому фокусы с массивами и функциями вывода раздельно для каждого символа придется отменить, и делать, как положено. То есть шрифты оставлять в программной памяти и загружать через PROGMEM, а все рисунки глифов оформлять в виде единого массива шрифта, и загружать для вывода по номеру символа в единой таблице. В противном случае и памяти не хватит и код программы раздуется до неуправляемого объема. Здесь мы на этом останавливаться не будем, потому что в наших простых примерах все это не потребуется — мы каждый раз будем ограничиваться небольшим строго необходимым количеством символов.
Из-за большого размера полного текста скетча GraphicOLED_DC1307 я его не привожу, скачать его можно здесь. В тексте сохранена функция resetOLED, которая передергивает питание дисплея при перезагрузке контроллера (через pwrPin D2), но она ни разу не понадобилась, так что ее можно спокойно удалить. Результат работы программы показан на фото:
К сожалению, одновременное пребывание в текстовом и графическом режиме исключено, потому, если вы хотите использовать оставшееся место, то придется рисовать свои шрифты (там остается место примерно на 7 символов шрифта 5×7 в каждой строке).
Графический дисплей WEG010016A
Когда, наконец, приехали заказанные графические дисплеи WEG010016AL, я начал с того, что попробовал их ввести в текстовый режим с целью посмотреть, что из этого выйдет.
Для проверки текстового режима была загружена программа имитации дисплея часов-календаря с датчиком внешней температуры, описанная в предыдущей публикации (https://geektimes.ru/post/284712/). Полученный результат заставил меня вспомнить, что разные дисплеи Winstar могут быть по разному ориентированы относительно разъема (в данном случае у WEG010016A разъем вверху, у текстовых WEH001602B, которые мы применяли выше — внизу, у типа С — вообще сбоку):
С ориентацией дисплея будем разбираться далее, а пока так посмотрим, что получилось. А получилось ничего хорошего: текстовый режим (разумеется, снабженный костылем, о котором речь шла в начале статьи) работает безупречно, но на практике его применять бессмысленно из-за отсутствия промежутков между символами. Потому не будем на нем задерживаться, а перейдем к рассмотрению графического режима.
Сами процедуры установки графического режима те же самые, что разбирались выше для текстового варианта. Осталось разобраться с переворотом дисплея, если у него разъем вверху относительно экрана. Конечно, можно, просто перевернуть дисплей, но положение с обращенным вниз разъемом мне кажется более естественным и удобным. Кроме того, при использовании типа с разъемом сбоку может понадобиться ориентировать разъем вправо, а не влево. Для ориентации «вверх ногами» необходимо преобразовать картинку — то есть поменять местами первую и последнюю позиции по горизонтали, строки, а также реверсировать порядок бит в байтах, составляющих массив (при этом младший бит будет соответствовать нижней точке).
Так как у меня уже были разрисованы десять цифр для предыдущего случая, то для последней задачи оставалось ввести процедуру программной реверсии:
byte reverse(byte x)
{
byte result=0,i;
for(i=0;i<8;i++)
{
if (x & (1 << i))
{
result |= 1 << (7-i);
}
}
return result;
}
Поменять порядок следования координат по горизонтали и строк по вертикали можно внесением изменений в функцию setGraphicCursor:
void setGraphicCursor( uint8_t x, uint8_t y ){
if( 0 <= x && x <= 99 ){
lcd.command(LCD_SETDDRAMADDR | (99-x));
}
if( 0 <= y && y <= 1 ){
lcd.command(LCD_SETCGRAMADDR | (1-y));
}
}
Функции вывода массива каждой цифры при этом остаются теми же самыми, только добавляется реверсия бит:
void draw2 (byte x/*горизонтальная позиция*/) //цифра 2
{
for (byte i = x; i
Полный скетч вывода часов GraphicOLED_DC1307_100×16 можно скачать отсюда, а результат для дисплея WEG010016AL представлен на фото:
А вот на этом фото шрифт другого типа (16×16) на дисплее WEG010016CG (дисплей также перевернут):
Если вы создадите шрифт заново, поменяв порядок бит вручную, то реверсии делать не надо и программа будет выполняться быстрее (хотя на глаз особых задержек и так не заметно). Но приведенная процедура переворота бит пригодится в любом случае — для отображения различных картинок. Например, из одной стрелки, направленной вверх-вправо, программным путем можно получить сразу четыре направления.
const byte DataATR[2][8]={{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x01,0x02,0x04,0x28,0x30,0x78,0x60,0x80}};
Функции вывода разнонаправленных стрелок:
. . . . .
void drawSW (byte x) //стрела вправо вверх (направление ветра ЮЗ)
{
for (byte i = x; i
На фото ниже представлен результат программы-заготовки дисплея датчика скорости и направления ветра. Как видите, здесь оказалось очень просто реализовать в одной строке шрифты разных размеров совместно с картинками:
В заключение добавлю, что вот тут находится очень интересная библиотека для работы с WS0010 в графическом и текстовом режимах по SPI. В текстовом она большей частью копирует Liquid Crystal (а что там еще можно придумать?), а в графическом имеет функции рисования графических примитивов, встроенные шрифты (толстый, как у меня, и обычный 5×7) и еще много всего другого.