====== Работа с ЖК-дисплеем 128х64 через низкоуровневое программирование ====== Я расскажу о том, как работать с жидкокристаллическим дисплеем MT-12864J. Для тех, кто хочет разобраться в сути управления этим экраном, будет расписан процесс записи/чтения данных, а для ленивых будет готовая библиотека. Для демонстрации возможностей сделаю небольшой проект, в котором будет строиться график показаний с потенциометра. ===== Список компонентов ===== В нашем проекте мы используем: -[[amp>product/arduino-uno|Arduino Uno]] -[[amp>product/display-lcd-graphic-128x64-fla|ЖК-дисплей 128×64]] -[[amp>product/multi-potentiometer-module|Потенциометр]] -[[amp>product/breadboard-mini|Breadboard Mini]] -[[amp>product/wire-ff|Соединительные провода «мама-мама»]] ===== ЖК-матрица ===== Матрица выглядит следующим образом: {{ :дисплеи-и-индикаторы:matrix-view.jpg?700 |}} Легко увидеть, что для подключения выведено аж 20 контактов. Чтобы разобраться с тем, что куда подключается, необходимо обратиться к официальному сайту за [[http://www.melt.com.ru/shop/mt-12864j-2ylg-2.html|документацией]]. Из нее можно получить таблицу пинов: ^ Вывод ^ Обозначение ^ Назначение ^ | 1 | Ucc | Питание цифровой части модуля | | 2 | GND | Общий вывод (0 В, земля) | | 3 | Uo | Вход питания ЖК-панели | | 4-11 | DB0---DB7 | Шина данных | | 12 | E1 | Выбор кристалла 1 | | 13 | E2 | Выбор кристалла 2| | 14 | RES | Сброс (начальная установка) | | 15 | R/W | Выбор: Чтение/Запись | | 16 | A0 | Выбор: Команда / Данные | | 17 | E | Стробирование данных | | 18 | Uee | Выход DC-DC преобразователя | | 19 | A | + питания подсветки | |20 | K | - питания подсветки| ==== Некоторые особенности распиновки ==== - Цифровая часть, ЖК-панель и подсветка запитываются отдельно друг от друга. - Вход питания ЖК-панели (3) необходимо соединить с выходом DC-DC преобразователя (18) через потенциометр для возможности настройки контраста. Либо, если потенциометра под рукой нет, то через резистор ~4.7 КОм. - Внутри ЖК-панели находятся два контроллера KS-0108. Поскольку размер матрицы 128х64, то каждый контроллер отвечает за свою половину экрана и активизируется соответствующим пином E1 и E2. Необходимо учитывать, что запись или чтение сразу в/из обоих контроллеров **не допускается**! ==== Память ==== Подробная картинка, содержащая в себе распределение ОЗУ находится на странице 4 [[http://www.melt.com.ru/docs/MT-12864J.pdf|даташита]] Модуль содержит ОЗУ для хранения данных, выводимых на ЖКИ, размером 64х64х2 бит (по 64х64 бит на каждый кристалл). Для выбора нужного кристалла используются выводы E1,E2. ОЗУ разбито на 8 страниц размером по 64х8 бит каждая. Каждой светящейся точке на ЖКИ соответствует логическая «1» в ячейке ОЗУ модуля. ==== Команды ==== Контроллер дисплея понимает 7 команд: - Включение / выключение дисплея вне зависимости от данных в ОЗУ и внутреннего состояния - Установка номера строки ОЗУ, которая будет отображаться в верхней строке дисплея - Установка номера страницы ОЗУ - Установка адреса ОЗУ для последующих обращений - Чтение статуса состояния - Запись данных - Чтение данных ==== Программирование ==== Все низкоуровневое взаимодействие с контроллерами будет содержаться в трех функциях * ''void waitForLcdReady(byte state)'' * ''byte readByte(byte state)'' * ''void writeByte(short int data, byte state)'' В каждая функция принимает аргумент ''state''. Он содержит в себе информацию о том, с каким чипом необходимо работать и будут это команды или данные. Делается это за счет трех констант ''#define CHIP1 0x01'', ''#define CHIP2 0x02'', ''#define DATA 0x80''. К примеру, нам необходимо записать байт в дисплей, тогда мы вызываем функцию: ''writeByte(data, CHIP1 + DATA);''. Теперь перейдем к реализации функций: void LcdKs0108::waitForLcdReady(byte state) { //Линия RW = 1 - мы читаем с индикатора digitalWrite(RW, 1); //Читаем команду digitalWrite(CD, 0); //Включаем необходимый чип if (state && CHIP1) { digitalWrite(E1, 1); } else { digitalWrite(E2, 1); } //Необходимая задержка delayMicroseconds(1); //Выставляем строб digitalWrite(E, 1); //Необходимая задержка delayMicroseconds(1); //Седьмой пин данных делаем входом pinMode(dataPins[7], INPUT); //Необходимая задержка delayMicroseconds(1); //Дожидаемся сброса сигнала занятости while (digitalRead(dataPins[7])) { ; } //Сбрасываем строб digitalWrite(E, 0); //Вновь пин - на выход pinMode(dataPins[7], OUTPUT); //Необходимая задержка delayMicroseconds(1); //Сбрасываем сигналы управления чипами digitalWrite(E1, 0); digitalWrite(E2, 0); } byte LcdKs0108::readByte(byte state) { //Все пины - вход for (int i = 0; i < 8; ++i ) { pinMode(dataPins[i], INPUT); } //Ждем готовности waitForLcdReady(state); //Будем считывать digitalWrite(RW, 1); digitalWrite(CD, 1); //Выбираем чип (state & CHIP1)?digitalWrite(E1, 1):digitalWrite(E2, 1); //Необходимая задержка delayMicroseconds(1); //Устанавливаем строб digitalWrite(E, 1); //Ждем delayMicroseconds(1); byte readedByte = 0; //Считываем данные с шины for (int i = 0; i < 8; ++i) { readedByte |= (digitalRead(dataPins[i]) << i); } //Убираем строб digitalWrite(E, 0); //Ждем delayMicroseconds(1); //Восстанавливаем направление шины for (int i = 0; i < 8; ++i ) { pinMode(dataPins[i], OUTPUT); } return readedByte; } void LcdKs0108::writeByte(short int data, byte state) { //Ждем готовности waitForLcdReady(state); //На RW=0 - мы будем писАть в индикатор digitalWrite(RW, 0); //Команду или данный digitalWrite(CD, state & DATA); //Выбираем необходимый чип (state & CHIP1)?digitalWrite(E1, 1):digitalWrite(E2, 1); //Выставляем данные на шину for (int i = 0; i < 8; ++i ) { digitalWrite(dataPins[i], (data & (1 << i))); } //Необходимая задержка delayMicroseconds(1); //Устанавливаем строб digitalWrite(E, 1); //Ждем delayMicroseconds(1); //Сбрасываем строб digitalWrite(E, 0); //Снова ждем delayMicroseconds(1); //Сбрасываем сигналы управления чипами digitalWrite(E1, 0); digitalWrite(E2, 0); } Вполне резонный вопрос:"А что это за массив такой ''dataPins''?" Данные участки кода --- функции библиотеки, написанной специально для этого дисплея. Библиотеку я делал с учетом того, чтобы можно было использовать любые пины Arduino, необязательно расположенные последовательно. Заполнение этого массива происходит в функции ''Init''. void LcdKs0108::Init(int _DB0, int _DB1, int _DB2, int _DB3, int _DB4, int _DB5, int _DB6, int _DB7, int _E1, int _E2, int _RES, int _RW, int _CD, int _E) { Serial.begin(115200); //Serial.println("init"); //Заполняем массив пинами с линиями данных dataPins[0] = _DB0; dataPins[1] = _DB1; dataPins[2] = _DB2; dataPins[3] = _DB3; dataPins[4] = _DB4; dataPins[5] = _DB5; dataPins[6] = _DB6; dataPins[7] = _DB7; //Все пины - выход for (int i = 0; i < 8; ++i ) { //Serial.println(dataPins[i], DEC); pinMode(dataPins[i], OUTPUT); } pinMode(_CD, OUTPUT); CD = _CD; pinMode(_E, OUTPUT); E = _E; pinMode(_E1, OUTPUT); E1 = _E1; pinMode(_E2, OUTPUT); E2 = _E2; pinMode(_RES, OUTPUT); RES = _RES; pinMode(_RW, OUTPUT); RW = _RW; //Сбрасываем модуль digitalWrite(E, 0); digitalWrite(RES, 0); delay(2); digitalWrite(RES, 1); delay(2); //Верхняя строчка отображения - 0 writeByte(0xC0, CHIP1); writeByte(0xC0, CHIP2); //Включить контроллер! writeByte(0x3F, CHIP1); writeByte(0x3F, CHIP2); } В эту функцию необходимо передать много параметров, но зато в дальнейшем библиотека возьмет на себя всю работу с дисплеем. Однако, эти функции не являются доступными для пользователей библиотеки. Ибо ни к чему грузить пользователя этими чтениями и записями. Лучше, чтобы библиотека делала все самостоятельно. ==== Принцип работы библиотеки ==== Библиотека способна выводить линии и окружности. Из этого вытекает необходимость зажигать ли гасить отдельные пикселы дисплея. Посмотрим код, который раньше этим занимался: void putPixel(short int x, short int y, bool bit) { byte inByte = 0; byte outByte = 0; short int offset = 0; byte state = 0; if ((x > 127) || (y > 63)) { return; } if (x < 64) { state += CHIP1; } else { state += CHIP2; offset = 64; } writeByte(0xB8 | (y >> 3), state); writeByte(0x40 | (x - offset), state); inByte = readByte(state); inByte = readByte(state); outByte = 1 << (y & 0x07); if (bit == 1) { outByte = inByte | outByte; } else { outByte = inByte & (~outByte); } writeByte(0x40 | (x - offset), state); writeByte(outByte, state + DATA); } Легко видеть, что для зажигания одного пиксела необходимо выполнить 4 операции записи и 2 операции чтения. А это время. И отрисовка линии будет занимать его приличное количество. Поэтому необходим метод, способный избежать этого. И он довольно прост: мы отказываемся от записи отдельных байтов в память экрана и пишем всю ее только целиком. Для этого в библиотеке имеется массив ''byte lcd_picture[8][128];'', в котором находится содержимое экрана. Все манипуляции с экраном производятся как раз с этм массивом. И код для зажигания пиксела становится вот таким: void LcdKs0108::putPixel(short int x, short int y, bool bit) { if (bit) { lcd_picture[y / 8][x] |= 1 << (y % 8); } else { lcd_picture[y / 8][x] &= 0 << (y % 8); } } Массив ''lcd_picture'' выводится целиком и использует такую возможность контроллера дисплея, как автоинкремент адреса, что позволяет писать данные в страницу непрерывно, без явного указания адреса. ==== Пример работы ==== #include "lcd_ks0108.h" LcdKs0108 lcd; void setup() { //Инициализация дисплея lcd.Init(2, 3, 4, 5, 6, 7, 8, 9, 10, A2, A3, A4, A0, A1); //Очистка lcd.clearScreen(); //Добавляем линию lcd.addLine(0, 0, 127, 63); lcd.addLine(0, 63, 127, 0); //Добавляем окружность lcd.addCircle(63, 32, 30); //Выводим на экран lcd.draw(); } void loop() { } Результат: {{ :дисплеи-и-индикаторы:matrix-demo1.jpg? |}} ===== Потенциометр ===== На этом теория создания библиотеки окончена. Переходим к практике. Будем мы делать устройство, которое будет примитивнейшим "осциллографом" и будет выводить график изменения значения на каком-либо аналоговом входе Arduino. Для этого нам и потребуется упомянутый в списке компонентов потенциометр. //Подключаем библиотек #include "lcd_ks0108.h" LcdKs0108 lcd; //Используемый аналоговый вход #define ANALOG_PIN A5 //Массив, в котором содержится история значени с АЦП short analogValues[127]; //Функция добавляет новое измерение в конец массива void appendValue(int value) { for (int i = 1; i < 127; ++i) { analogValues[i-1] = analogValues[i]; } analogValues[126] = value; } void setup() { //Инициализируем библиотеку lcd.Init(2, 3, 4, 5, 6, 7, 8, 9, 10, A2, A3, A4, A0, A1); //Обнуляем массив for (int i = 0; i < 127; ++i) { analogValues[i] = 0; } //Делаем пин входом pinMode(ANALOG_PIN, INPUT); } void loop() { //Перед каждым обновлением очищаем массив lcd.clearScreen(); //Добавляем новое считанное значение appendValue(analogRead(ANALOG_PIN)); //Добавляем график-гистограмму. Не забыв, что диапазон значений //АЦП - 0..1024, а размер дисплея по оси Y - 0..63. //11 строк оставляем под строку со значением for (int i = 0; i < 127; ++i) { lcd.addLine(i, 54, i, 54 - int(float(analogValues[i]) / 1024.0 * 54)); } //Добаляем посдледнее значение на экран lcd.addNumber(analogValues[126], 0, 7); //Рисуем lcd.draw(); } ====== Fin ====== Мы получили вот такой простой "осциллограф". {{ :дисплеи-и-индикаторы:matrix-result1.jpg? |}} Если заменить потенциометр, к примеру, барометром или термометром, то можно сделать погодный монитор и отслеживать динамику изменения метеорологической обстановки.