Подключение LSD1602A V3 к микроконтроллеру.
(Никаких Arduino и I2C. Только хардкор.)

Подключение LSD1602A V3 к микроконтроллеру.

О подключении этого дисплея написано уже много, но и я хочу добавить свои пять копеек к имеющийся информации надеясь на то, что именно моё изложение материала будет восприниматься более легко и поможет начинающим радиолюбителям быстро разобраться в этом вопросе. Как написано в заголовке мы будем подключать дисплей, не применяя никаких дополнительных устройств и сторонних библиотек. Всё сами, до полного понимания принципов его работы.


Начнем с того, что LSD1602A это алфавитно-цифровой дисплей, представляющий из себя матрицу из двух строк по шестнадцать видимых символов в каждой. Что, собственно, и отражено в номере дисплея 1602. Каждое знакоместо состоит из матрицы 5х8 которая в принципе может отображать любой символ. Некоторые буквы русского алфавита выглядят не очень, но вполне читаемы. Дисплей содержит внутренний генератор символов и в зависимости от прошивки может отображать разные алфавиты, есть даже русифицированные дисплеи. Чаще всего дисплеи содержат английский и китайские алфавиты (если китайские иероглифы можно так назвать), а также основные знаки препинания и некоторые символы ну и бонусом немного букв греческого алфавита. Всего 189 оригинальных символов расположенных в 192-х активных ячейках (некоторые символы повторяются). Наличие этой библиотеки является самым серьезным аргументом при выборе дисплея, так как она позволяет сэкономить более килобайта flash памяти микроконтроллера. Также есть возможность поместить 8 своих символов в оперативную память дисплея CGPAM, которые можно изменять по ходу программы.

Для общения LSD1602A с микроконтроллером необходимо выбрать один из трех вариантов подключения. Первый, прямое подключение всех восьми линий передачи данных к управляющему устройству. Такое подключение самое скоростное. Но из-за того, что помимо линий передачи данных надо задействовать еще четыре линии управления, оно становится нецелесообразным. А если учесть то, что сам дисплей еще и не шибко быстрый, то основное достоинство полного подключения сводится на «нет». Второй вариант использует только четыре старших линии (D4…D7) для передачи данных, что совместно с линиями управления занимает целиком один порт МК. Третий вариант – подключение через конвертер I2C разработанный специально для этого дисплея. Данное подключение можно назвать самым оптимальным потому как использует всего две линии контроллера и позволяет подключать по мимо дисплея еще множество разных устройств, а также существенно упрощается настройка МК для работы с дисплеем. Но и такое подключение не идеально потому как конвертор имеет цену равную половине стоимости дисплея и по мимо подпрограмм работы с LSD1602A необходимо программировать еще и протокол передачи данных через последовательную асимметричную шину (i2C).

Но если вы не хотите копаться в «кишках» этого дисплея, то можно не заморачиваться, взять уже готовые модули, программное обеспечение и на этом реализовать свой проект.

Мы же рассмотрим подключение этого дисплея по второму варианту с нуля. Для этого необходимо разобраться что и куда надо подключить. Дисплей имеет шестнадцать выводов:
1. Vss – общий (GND);
2. Vdd – питание дисплея (+5V);
3. V0 – контрастность;
4. RS – сигнал: команда (0)/данные (1);
5. R/W – сигнал: запись (0)/чтение (1);
6. Е – тактовый сигнал передачи данных;
7…10. D0…D3 – младшая тетрада данных (в 4-х битной передаче данных не участвует);
11…14. D4…D7 – старшая тетрада данных;
15. А – подача питания на подсветку +5V (Aнод);
16. К – общий для питания подсветки GND (Катод).

Необходимо отметить то, что 1 и 16, а также 2 и 15 выводы не связаны между собой. Поэтому подавать питания на них приходится отдельно.

Я подключал свой дисплей к уже готовому самодельному модулю ATmaga8 так, как мне было удобно, поэтому данное подключение не только не оптимально, но и добавляет проблем к организации передачи данных. Для того чтобы проблем с подключением было меньше лучше всего подключать все линии передачи данных, а также линии управления к одному порту. Более того линии передачи данных должны быть подключены к старшей или, если не получается, к младшей тетраде порта в порядке возрастания, а не как у меня в перевернутом виде, когда седьмой бит приходится передавать с нулевого вывода порта. Для компенсации этого недочета мне пришлось добавить специальную программную вставку, которая переворачивает тетрады для корректной передачи данных в дисплей.

Немного о подсветке. Без нее этот дисплей использовать затруднительно. С другой стороны, она такая яркая, что в темное время может служить в качестве хорошего ночника. Поэтому если не жалко энергии и по ночам не будет дискомфорта от работающего прибора, то 16 вывод можно смело соединить с землей (GND) и тем самым получить еще один свободный выход МК. С другой стороны, подключив подсветку дисплея через транзистор у вас появляется возможность не только отключать ее, но и регулировать яркость, а также использовать как сигнал для привлечения внимания.

Необходимо упомянуть о том, что при подаче питания, дисплей проходит процедуру самотестирования, при которой отображается верхняя строка. На этом этапе необходимо включить подсветку и вращая переменное сопротивление R1 настроить контрастность. Если вы все сделали правильно, то дисплей должен будет иметь вот такой вид.

Теперь можно начать оживлять нашего подопечного.

Первым делом необходимо скачать описание к драйверу S6A0069 который управляет нашим дисплеем, а также на сам дисплей. При первом знакомстве с этими документами я был слегка озадачен тем, что алгоритмы инициализации разнятся от версии дисплея. Поэтому процедура инициализации от 1602a V2 может не подойти к третьей версии. Но перед тем, как написать последовательность инициализации необходимо разобраться с тем, какие команды в принципе воспринимает дисплей и как осуществляется процедура передачи данных.

Начнем с обзора команд управления, передачи и приема данных:
1. RS – 0, R/W – 0 код: 0b0000 0001 (0x01) Очистка экрана и перевод курсора в ну-левую позицию (верхнее левое знакоместо).
2. RS – 0, R/W – 0 код: 0b0000 0010 (0x02) Возвращение курсора в нулевую пози-цию, а также сдвигает окно дисплея в левую сторону.
3. RS – 0, R/W – 0 код: 0b0000 01(ID)(SH) Управление сдвигом. Влияет на сдвиг при работе с CGRAM.
      a. ID – 0 уменьшает счетчик сдвига курсора; ID – 1 увеличивает счетчик сдвига курсора.
      b. SH – 0 сдвиг дисплея не выполняется; SH – 1 сдвиг дисплея выполнятся в соответствии с установками ID.
4. RS – 0, R/W – 0 код: 0b0000 1(D)(C)(B) Управление дисплеем и курсором.
      a. D – 0 дисплей выключен (данные сохраняются); D – 1 дисплей включен.
      b. С – 0 курсор не отображается; С – 1 курсор отображается.
      c. B – 0 курсор не мигает; B – 1 курсор мигает.
5. RS – 0, R/W – 0 код: 0b0001 (S/C)(R/L)00 Управление сдвигом дисплея или кур-сора.
      a. S/C – 0 сдвиг курсора; S/C – 1 сдвиг дисплея.
      b. R/L – 0 сдвиг влево; R/L – 1 сдвиг вправо.
6. RS – 0, R/W – 0 код: 0b001(DL) (N)000 Настройки интерфейса и отображения.
      a. DL – 0 разрядность интерфейса 4 бита; DL – 1 разрядность интерфейса 8 бит.
      b. N – 0 однострочный дисплей; N – 1 двустрочный дисплей.
7. RS – 0, R/W – 0 код: 0b01XX XXXX Установка адреса CGRAM. Где (Х) адрес CGRAM.
      В эту область памяти можно записывать свои знаки.
8. RS – 0, R/W – 0 код: 0b1XXX XXXX Установка адреса DDRAM. Где (Х) адрес DDRAM.
      Адрес верхней строки может при-нимать значение от 0x00 до 0x27, а адрес нижний строки от 0x40 до 0x67.
9. RS – 0, R/W – 1 код: 0b(BF)XXX XXXX Чтение флага занятости и текущего адре-са счетчика.
      a. BF – 1 Контроллер занят и выполняет внутренние операции. BF – 0 контроллер готов к приему данных.
10. RS – 0, R/W – 1 код: 0bХXXX XXXX Запись данных в DDRAM или в CGRAM.
11. RS – 1, R/W – 1 код: 0bХXXX XXXX Считывания данных из DDRAM или CGRAM.

Вот эти одиннадцать инструкций, это все что у нас есть для организации работ с дисплеем. Но перед тем, как что-то отобразить на экране, необходимо проинициализировать дисплей передав в его контроллер данные в определенной последовательности.

Время процесса передачи информации в дисплеях третьей версии, судя по временным интервалам, представленным в описании, ускорилось в два раза и составляет 500 ns. При частоте МК в 16 МГц это около 6 тактов. Но время выполнения команд осталось на прежнем уровне. Большая часть команд будет выполнятся за 37 us (микросекунд), это около 400 тактов, а вот команды очистки экрана и сброса курсора за 1. 52 ms (миллисекунды), при указанной частоте это уже около 16500 тактов. Эти цифры говорят о том, что после передачи команды или данных в дисплей необходимо ждать какое-то время пока он переварит полученную информацию. Для контроля занятости предусмотрен специальный флаг, но организация его получения и обработки сопряжено с нюансами, о которых мы поговорим позднее. А вначале, для того чтобы можно было запустить дисплей, мы попросту организуем банальную задержку чтобы с гарантией дать ему время для исполнения внутренних преобразований. Учитывая этот нюанс составляем алгоритм передачи данных двумя тетрадами в дисплей.

//передача данных/команд в дисплей 
//data – передаваемые данные 
//c_d – флаг команда (0)/данные (1)
    void send_1602a (unsigned char data, unsigned char c_d) {  
         
        //Проверка занятости дисплея
        pausa_1602a(PAUSA_4MS);    //пауза  
        
        //Передача данных 
        RS_PORT = c_d;             //RS в 0(команда) или 1(данные)        
        RW_PORT = WRITE;           //R/W установить в 0 (запись) 
        h_nibbl_out(data);         //передача старшего ниббла  
        data <<= NIBBL;            //сдвигаем для передачи младшего ниббла
        h_nibbl_out(data);         //передача младшего ниббла     
    } 

Я не очень люблю библиотеку <delay.h>, хотя в некоторых местах она просто необходима, поэтому оформляем обыкновенный настраиваемый пустой цикл для организации паузы.

#define NOP #asm("nop")				
				
//пауза
    void pausa_1602a (unsigned int c_pausa) {
        for (c_pausa; c_pausa > ZERO; c_pausa--) NOP;    
    } 

Передача одной тетрады в контроллер дисплея будет устроена таким образом:

//передача одного (старшего) ниббла в дисплей
    void h_nibbl_out(unsigned char data_port){ 	//пересылается старший ниббл из байта!!! 
        unsigned char step = ONE_BYTE;         	//счетчик для конвертации данных (8)

        //конвертер данных для передачи в порт  
        data_port &= 0xf0;                     	//очистка младшего ниббла
        for(step; step > NIBBL; step--){       	//Просматриваем старшую тетраду входящего байта
            if(data_port & (UP << (step - ONE_STEP))){  //если бит данных 1 
               data_port |= (UP << (ONE_BYTE - step));  //устанавливаем биты для передачи в порт 
            }; 
        };
        data_port &= 0x0f;                  	//очистка старшего ниббла 
        
        //передача данных в дисплей
        PORT_DATA &= MASKA_DATA;            	//очистка порта перед передачей данных  
        E_PORT = UP;                        	//поднимаем строб E  
        PORT_DATA |= data_port;             	//передаем тетраду в порт  
        NOP; NOP; NOP;                      	//три такта ничего не делаем
        E_PORT = DOWN;                      	//опускаем строб E  
    }  

Здесь, на мой взгляд, надо остановиться на том, что для правильной передачи данных мне пришлось сделать небольшую вставку, которая конвертирует данные для отправки в порт. Т.к. нумерация выводов порта не совпадает с нумерацией принимающего устройства. Об этом я писал ранее. Ну и за одно кодировщик стирает старшую тетраду для того, чтобы остаточные данные не смогли испортить установленные флаги RS и RW.

Теперь можно проинициализировать дисплей. Для третьей версии процесс инициализации имеет следующий алгоритм:

//инициализация дисплея 1602А      
    void init_1602a(void){         
        //Инициализация потров
        RW_DDR = UP;                //пин чтение/запись на выход
        RS_DDR = UP;                //пин дата/команда на выход
        E_DDR = UP;                 //пин строб на выход
        DDR_DATA_OUT;               //пины порта на выход 
        pausa_1602a(PAUSA_15MS);    //пауза    
        //инициализация дисплея     
        RS_PORT = COM;              //установить RS в 0(команда)       
        RW_PORT = WRITE;            //R/W установить в 0 (запись)  
        h_nibbl_out(0b00110000);    //1 пересылаем одиночный ниббл инициализации 0b0011 
        pausa_1602a(PAUSA_4MS);     //пауза  
        h_nibbl_out(0b00110000);    //2 пересылаем одиночный ниббл инициализации 0b0011           
        pausa_1602a(PAUSA_100US);   //пауза    
        h_nibbl_out(0b00110000);    //3 пересылаем одиночный ниббл инициализации 0b0011 
        h_nibbl_out(0b00100000);    //4 пересылаем одиночный ниббл инициализации 0b0010  
        send_1602a(0b00101000, COM);//5 Настройка дисплея 0b001(DL)(N)000	
        send_1602a(0b00001110, COM);//6 Настройка дисплея 0b00001(D)(C)(B) 
        send_1602a(0b00000110, COM);//8 Настройки сдвига 0b000001(ID)(SH) 
        send_1602a(0x01, COM);	    //Очистка дисплея
    }

Если вы сделали все правильно, то на экране должна отобразиться только черточка курсора. Если все так, то дальше дело пойдет быстрее. Для того чтобы убедится в том, что все работает как надо можно сразу после процедуры инициализации вставить строку:

send_1602a(0xff, 1);

На экране должно быть отображено закрашенное первое знакоместо, а курсор должен передвинуться вправо.

Если и это прошло удачно значит процесс передачи данных работает стабильно. Теперь, чтобы все было по фэн-шую необходимо вместо нашей задержки организовать проверку занятости дисплея.

//проверка занятости дисплея
    unsigned char read_busy_1602a (void){   
        unsigned char temp_busy = CLEAN;    //временная переменная для передачи флага занятости         
        DDR_DATA_IN;                        //переводим выводы данных МК для приема 
        PORT_DATA |= (UP<<D4)|(UP<<D5)|(UP<<D6)|(UP<<D7); //подтягиваем резисторы
        RS_PORT = COM;                      //RS в 0(команда)       
        RW_PORT = READ;                     //R/W в 1 (чтение) 
        E_PORT = UP;                        //поднимаем строб 
        NOP; NOP;                           //задержка
        if(PIN_DATA & (UP<<D7))  temp_busy = TRUE;  //если бит D7 поднят(контроллер занят) 
        E_PORT = DOWN;                      //опускаем строб  
        NOP; NOP;                           //задержка
        E_PORT = UP;                        //поднимаем строб для чтения младшего ниббла 
        NOP; NOP;                           //задержка, но младший ниббл не читаем
        E_PORT = DOWN;                      //опускаем строб E         
        PORT_DATA &= MASKA_DATA;            //отключение подтягивающих резисторов
        DDR_DATA_OUT;                       //переводим выводы данных МК для передачи
        return (temp_busy);                 //передаем флаг занятости 
    } 

Тут необходимо отметить то, что для возможности получения данных от дисплея необходимо, после перевода пинов порта для чтения данных, включить подтягивающие резисторы. В противном случае ничего не получится. Теперь подпрограмма передачи данных будет выглядеть вот так.

//передача данных/команд в дисплей
    void send_1602a(unsigned char data, unsigned char c_d){  
         
        //Проверка занятости дисплея
        while (read_busy_1602a()); //выходим, если флаг занятости снят
        
        //Передача данных 
        RS_PORT = c_d;             //RS в 0(команда) или 1(данные)        
        RW_PORT = WRITE;           //R/W установить в 0 (запись) 
        h_nibbl_out(data);         //передача старшего ниббла  
        data <<= NIBBL;            //сдвигаем для передачи младшего ниббла
        h_nibbl_out(data);         //передача младшего ниббла     
    }

Теперь мы имеем все основные подпрограммы для общения с дисплеем. Следую-щий шаг научится выводить на дисплей необходимые данные. Т.к. нумерация хранящихся данных в памяти дисплея соответствует таблице ASCII, то можно подключив библиотеку для работы со строковыми переменными выводить необходимые строки прямо на дисплей, а можно и не прибегая к библиотекам, либо по буквам, либо организовав массивы выводить нужную информацию на экран.

К примеру, для вывода цифры «три» в четвертом знакоместе нижнего ряда необходимо сначала установить курсор, а затем передать номер в памяти для генерации этой цифры.

send_1602a(0x80 + 0x43, COM);	//курсор в нужную позицию
send_1602a(0x30 + 3, DAT);	//ссылка на область памяти генератора знаков 

После записи этих строк после инициализации мы видим цифру 3 в нужном месте. Более того курсор сам перешел на следующую позицию. Поэтому если вводить сразу несколько последовательных символов. То устанавливать позицию курсора необходимо только в самом начале. Таблицу знаков нужно будет посмотреть в описании на дисплей.

Осталась еще одна нераскрытая тема вывода дополнительных знаков которых нет во встроенном генераторе. Например, надо отобразить заряд батарейки. Без использования дополнительных символов здесь не обойтись. Изучая раздел взаимосвязи между адресами CGRAM, кодами символов DDRAM и шаблонами символов можно увидеть, что каждый символ в CGRAM занимает первые 64 байта. По 8 байт на символ. При этом адрес первой (она же верхняя) строки каждого символа будет кратен восьми (0x00, 0х08, 0х10, 0х18, 0х20, 0х28, 0х30, 0х38). Для записи своего символа в память необходимо используя инструкцию «7» установить адрес, а затем инструкцией «10» поочередно ввести восемь байт записываемого символа.

//Русский шрифт
    flash const unsigned char rus_1602a [] [8] = {
	{0b00011110,0b00010000,0b00010000,0b00011110,0b00010001,0b00010001,0b00011110,0b00000000}, // 0 Буква "Б"
	…
	{0b00001111,0b00010001,0b00010001,0b00001111,0b00000101,0b00001001,0b00010001,0b00000000}  // 17 Буква "Я"
    };

//загрузка русского шрифта в память CGRAM
    void load_CGRAM_1602a_rus (unsigned char simbol, unsigned char position){
        unsigned char c_caunt_temp = ZERO;	//счетчик для ввода символов
        
        //устновка позиции строки ввода  
        position *= ONE_BYTE;			//Умножаем номер места размещения символа на 8			
        pos_CDRAM_1602a(position);   		//устанавливаем позицию вехней строки символа
        
        //загружаем знак во временную память 
        for(c_caunt_temp; c_caunt_temp < ONE_BYTE; c_caunt_temp++){
            send_1602a(rus_1602a[simbol] [c_caunt_temp], DAT);    
        };
    }     

Необходимо сказать о том, что когда мы загружаем знак в CGRAM, то счетчик позиции отображения информации на дисплее обнуляется. Также имеется еще одно ограничение. Мы не можем, записывая в одну и туже область памяти CGRAM разные символы и выводить их на экран. Как только мы записываем свой новый символ в CGRAM, то все символы, на дисплее которые были выведены на экран с этой области памяти будут заменены на новый. Например, мы записали букву «Б» в область памяти 0х00… 0х07 CGRAM и вывели его на дисплей в слове БОБЫ, а затем в эту же область хотим загрузить букву «Д», то, как только мы это сделаем, то БОБЫ автоматически превратятся в ДОДЫ. Этой фишкой можно пользоваться, например, для изменения знака заряда батарейки.

Чтобы вывести свой знак на экран необходимо действовать также, как и при выводе знака из знакогенератора, с той лишь разницей, что вводимые нами знаки располага-ются в первых восьми ячейках (0…7) памяти таблицы знаков.

Ну вот вроде и все, что на первых порах надо знать для того, чтобы использовать этот дисплей в своих проектах. Конечно, рассмотрены не все возможности, но функции чтения данных из дисплея если и понадобятся, то очень и очень нескоро.

В приложенных файлах схема, проект сделанный в CV_AVR и прошивка.

А на сегодня всё. Удачи.

15.04.2023


Если вдруг найдете в статье неточности или заблуждения. Напишите мне об этом. Я подправлю.

Приложения:
Скачать схему, проект и прошивку