====== Работа с ЖК-дисплеем 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? |}}
Если заменить потенциометр, к примеру, барометром или термометром, то можно сделать погодный монитор и отслеживать динамику изменения метеорологической обстановки.