Работаем с LCD на базе HD44780 без библиотек

11 декабря 2017

Про LCD-модули на базе HD44780 (здесь и далее под HD44780 понимается как оригинальный чип, так и совместимые с ним аналоги) я писал уже дважды — в заметке Научился выводить текст на ЖК-индикатор из Arduino, а также Об использовании экранчиков 1602 с I2C-адаптером. В обоих статьях использовался принцип «подключаем экранчик так-то, берем готовую библиотеку, и магия, все работает». Магию, как вы можете помнить, я осуждаю. Поэтому сегодня мы наконец-то разберемся, как устроен протокол подобных дисплеев, и напишем нашу собственную, очень простенькую, библиотеку для работы с ними.

Спрашивается, зачем вообще разбираться в каких-то там протоколах, когда есть готовые библиотеки? Как минимум, выводить что-то на экранчик может хотеться не только с Arduino, но и с отличных от AVR микроконтроллеров (PIC, STM32, …), а также с какого-нибудь FPGA или даже Raspberry Pi через его GPIO. В этом случае готовой библиотеки у вас может и не быть. Кроме того, готовые библиотеки для Arduino, вроде той же LiquidCrystal, зачастую далеко не оптимальны в плане используемого ими объема flash-памяти, а память у микроконтроллеров не резиновая. Наконец, это просто интересно — разобраться, как же там данные передаются по проводам, что в итоге буковки выводятся.

Итак, работающий прототип у меня получился таким:

Использование LCD на базе чипа HD44780 без библиотек

Модуль подключается к Arduino точно так же, как было описано в уже упомянутой заметке Научился выводить текст на ЖК-индикатор из Arduino.

Рассмотрим код прошивки:

#include <Arduino.h>

const int PIN_RS = 7;
const int PIN_E = 8;
const int PIN_D4 = 9;
const int PIN_D5 = 10;
const int PIN_D6 = 11;
const int PIN_D7 = 12;

const int LCD_DELAY_MS = 5;

void lcdSend(bool isCommand, uint8_t data) {
    digitalWrite(PIN_RS, isCommand ? LOW : HIGH);
    delay(LCD_DELAY_MS);

    digitalWrite(PIN_D7, (data >> 7) & 1);
    digitalWrite(PIN_D6, (data >> 6) & 1);
    digitalWrite(PIN_D5, (data >> 5) & 1);
    digitalWrite(PIN_D4, (data >> 4) & 1);

    // Wnen writing to the display, data is transfered only
    // on the high to low transition of the E signal.
    digitalWrite(PIN_E, HIGH);
    delay(LCD_DELAY_MS);
    digitalWrite(PIN_E, LOW);

    digitalWrite(PIN_D7, (data >> 3) & 1);
    digitalWrite(PIN_D6, (data >> 2) & 1);
    digitalWrite(PIN_D5, (data >> 1) & 1);
    digitalWrite(PIN_D4, (data >> 0) & 1);

    digitalWrite(PIN_E, HIGH);
    delay(LCD_DELAY_MS);
    digitalWrite(PIN_E, LOW);
}

void lcdCommand(uint8_t cmd) {
    lcdSend(true, cmd);
}

void lcdChar(const char chr) {
    lcdSend(false, (uint8_t)chr);
}

void lcdString(const char* str) {
    while(*str != '\0') {
        lcdChar(*str);
        str++;
    }
}

void setup() {
    pinMode(PIN_RS, OUTPUT);
    pinMode(PIN_E, OUTPUT);
    pinMode(PIN_D4, OUTPUT);
    pinMode(PIN_D5, OUTPUT);
    pinMode(PIN_D6, OUTPUT);
    pinMode(PIN_D7, OUTPUT);

    // 4-bit mode, 2 lines, 5x7 format
    lcdCommand(0b00110000);
    // display & cursor home (keep this!)
    lcdCommand(0b00000010);
    // display on, right shift, underline off, blink off
    lcdCommand(0b00001100);
    // clear display (optional here)
    lcdCommand(0b00000001);

    lcdCommand(0b10000000); // set address to 0x00
    lcdString("Using HD44780");
    lcdCommand(0b11000000); // set address to 0x40
    lcdString("LCD directly! :)");
}

void loop() {
    /* do nothing */
    delay(100);
}

Как видите, данные передаются в модуль по четырехбитной шине D4-D7. Если на пин RS модуля подано низкое напряжение, данные воспринимаются, как команда. Если же напряжение на пине высокое, данные выводятся в виде символа. Считывание данных с шины происходит по спаду сигнала (заднему фронту) на пине E.

Наглядная табличка с описанием поддерживаемых команд была найдена мной в статье 1997-го года How to Use Intelligent L.C.D.s, part one [PDF] за авторством Julyan Ilett. Вот эта табличка:

Команды, поддерживаемые чипом HD44780

В приведенном выше коде первом делом отправляется команда 0b00110000, выбирающая четырехбитную передачу данных, двухстрочный режим, и формат символов 5x7. Заметьте, что размер шины (4 или 8 бит) передается в 4-м бите команды, а значит чип может интерпретировать команду о желаемом размере шины, не зная размера шины. Какое элегантное решение проблемы курицы и яйца, не находите?

Остальная же часть кода достаточно тривиальна, если не считать адресации символов на индикаторе. Как видите, первому символу в первой строке соответствует адрес 0x00, а первому символу во второй строке — адрес 0x40. Спрашивается, а что делать, если у экранчика, скажем, четыре строки? Ведь никакого выбора режима с четырьмя строками в командах не предусмотрено! Оказывается, такие индикаторы обычно имеют ширину 16 символов и адреса в третьей строке начинаются с 0x10, а в четвертой — 0x50. То есть, в каком-то смысле такие экранчики даже совместимы с двухстрочными. Вообще, возможных адресов всего лишь 128. То есть, определить, какой адрес какому положению на экране соответствует для данного конкретного экрана, можно обыкновенным перебором.

Размер прошивки получился равным 1438 байтам. Для сравнения, аналогичная прошивка, использующая LiquidCrystal, занимает 2164 байт. Получается, что отказавшись от LiquidCrystal, программу можно сжать на 33.5%, что очень даже неплохой результат! Думаю, что переписав программу на чистый C (см заметку Как я спаял электронные игральные кости на базе ATtiny85) результат можно еще улучшить. Если хотите, то можете проверить это утверждение в качестве домашнего задания.

Надеюсь, сей материал был для вас интересен. Полную версию кода к этой заметке вы найдете на GitHub. Как обычно, если после прочтения у вас остались вопросы или появились дополнения, не стесняйтесь воспользоваться комментариями!

Дополнение: Протокол, используемый в I2C-адаптерах к таким экранчикам, рассматривается в заметке Микроконтроллеры STM32: работа с экранчиком 1602 по I2C.

Метки: , .

Понравился пост? Узнайте, как можно поддержать развитие этого блога.

Также подпишитесь на RSS, Facebook, ВКонтакте, Twitter или Telegram.