Работа с ЖК-дисплеем 128х64 через низкоуровневое программирование
Я расскажу о том, как работать с жидкокристаллическим дисплеем MT-12864J. Для тех, кто хочет разобраться в сути управления этим экраном, будет расписан процесс записи/чтения данных, а для ленивых будет готовая библиотека. Для демонстрации возможностей сделаю небольшой проект, в котором будет строиться график показаний с потенциометра.
Список компонентов
В нашем проекте мы используем:
ЖК-матрица
Матрица выглядит следующим образом: Легко увидеть, что для подключения выведено аж 20 контактов. Чтобы разобраться с тем, что куда подключается, необходимо обратиться к официальному сайту за документацией. Из нее можно получить таблицу пинов:
Вывод | Обозначение | Назначение |
---|---|---|
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 даташита Модуль содержит ОЗУ для хранения данных, выводимых на ЖКИ, размером 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() { }
Потенциометр
На этом теория создания библиотеки окончена. Переходим к практике. Будем мы делать устройство, которое будет примитивнейшим "осциллографом" и будет выводить график изменения значения на каком-либо аналоговом входе 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(); }