Шаг 4. Подключение LCD
Циферки да буковки…
В это раз мы подключим LCD-дисплей к нашей микросхемке. Нам потребуется:
- наша микросхема ATtiny2313
- ЖК-модуль Мэлт 10S1
- опционально - резистор для ресета и конденсатор для стабилизации напряжения
Подсоединяем по такой схеме:
Если подсоединяете резистор к RESET-у, то его вторым концом на питание; конденсатор же параллельно "батарейке".
Подробнее о портах: дисплей можно подключить как по 8-ми, так и по 4-хбитному интерфейсу, то есть можно передавать либо сразу по одному символу (или команде из 8 бит), либо два раза по 4 бита: сначала старший полубайт, затем младший - соответственно, не должно быть такого, что мы посылаем внезапно нечетное число полубайтов! Будем использовать как раз 4-хбитный интерфейс, поэтому и не задействуем выводы DB0 – DB3 у LCD-модуля (смотрите стр. 10 документации). Итак, выводы B0 - B3 нашей микросхемы заняты делом.
Далее: к выводу B4 подключаем вывод E дисплея. Этот вывод играет роль флага передачи сообщения, когда переходит с 1 на 0 - дисплей считывает данные с ножек DB. Впоследствии мы рассмотрим этот момент более подробно.
Вывод дисплея R/W используется для переключения режима записи/чтения. Так как мы будем только передавать модулю сообщения, то этот вывод мы использовать не будем - просто присоединим его к "земле" (если вывод R/W опущен на "землю", то дисплей будет только принимать данные)
Следующая ножка микросхемы - B5 - будет присоединена к выводу A0 дисплея. С помощью данной ножки мы будем указывать, передаём ли мы команды управления ("0") или данные для отобрадения на экране ("1").
Теперь включаем модуль в работу: порт GND - к «земле», а Ucc - питание – к 5В.
В этот момент дисплей должен подать признаки жизни - первые восемь его знакосимволов должны "загореться".
К слову, если вдруг этого не произошло, не переживайте раньше времени - когда в один прекрасный день у нас дисплей перестал включаться, опытным путём мы выяснили, что отключилась контрастность - да-да, у нас в дисплее есть недокументированная способность ножки 13 - она отвечает за контраст. Мы присоединили её через резистор 1 кОм (или 510 Ом) на ноль, и пациент ожил!
Вернёмся к задаче) Сначала напишем хоть что-то работающее, а потом будем совершенствовать)
Для начала не будем заморачиваться и используем имеющуюся в MikroE библиотеку для работы с LCD-дисплеем. Согласно справке, эта библиотека поддерживает обмен информацией с дисплеями на основе HD44780 или совместимых с ним контроллеров (в том числе и наш МТ-10S1). Что это библиотека умеет:
- Lcd_Init() - функция инициализирует дисплей; при этом необходимо, чтобы до начала работы были определены Lcd-константы, отвечающие за расположение port-ов и ddr-ов перечисленных выше выводов (DB4-DB7, A0, E).
- Lcd_Out(char row, char column, char *text) - отправка текста дисплею; при этом он будет напечатан начиная с "row" строки и "column"столбца. Сам текст передаётся как указатель на начало строки, в MikroPascal-е - как var text: string.
- Lcd_Out_Cp(char *text) - отправка текста дисплею; при этом он будет напечатан с того места, где сейчас стоит курсор; В MikroPascal-е на вход функции подается var text: string.
- Lcd_Chr(char row, char column, char out_char) - отправка одного символа дисплею в соответствующую row и column позицию.
- Lcd_Chr_Cp(char out_char) - отправка одного символа дисплею; он будет напечатан на том месте, где сейчас стоит курсор.
- Lcd_Cmd(char out_char) - отправка команды дисплею; список команд представляется в справке, но можно использовать и свои.
Для работы с дисплеем нужно будет задать константы - они также представлены в справке; так, нам потребуются константы-ссылки на биты PORT-ов и биты DDR-ов. "LCD_RS" - это наш вывод А0, "LCD_EN" - вывод Е, "LCD_D7"-"LCD_D4" - выводы DB7-DB4 соответственно. Попробуем написать простенькую программу, которая будет инициализировать дисплей и отправлять на него простенький текст - например, "Hello".
На Си:
//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello" //Определяем PORT-ы и DDR-ы выводов дисплея так, как сказано в справке sbit LCD_RS at PORTB5_bit; sbit LCD_EN at PORTB4_bit; sbit LCD_D7 at PORTB3_bit; sbit LCD_D6 at PORTB2_bit; sbit LCD_D5 at PORTB1_bit; sbit LCD_D4 at PORTB0_bit; sbit LCD_RS_Direction at DDB5_bit; sbit LCD_EN_Direction at DDB4_bit; sbit LCD_D7_Direction at DDB3_bit; sbit LCD_D6_Direction at DDB2_bit; sbit LCD_D5_Direction at DDB1_bit; sbit LCD_D4_Direction at DDB0_bit; void main() { char sHello[] = "Hello"; //инициализируем порты ввода-вывода DDRA = 0; PORTA = 0; DDRB = 0; PORTB = 0; DDRD = 0; PORTD = 0; Lcd_Init(); //инициализируем дисплей Lcd_Out(1, 1, sHello); //отправляем приветствие while(1) { Delay_ms(1); } }
На Паскале:
//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello" program LCD; //Определяем PORT-ы и DDR-ы выводов дисплея так, как сказано в справке var LCD_RS : sbit at PORTB5_bit; LCD_EN : sbit at PORTB4_bit; LCD_D7 : sbit at PORTB3_bit; LCD_D6 : sbit at PORTB2_bit; LCD_D5 : sbit at PORTB1_bit; LCD_D4 : sbit at PORTB0_bit; LCD_RS_Direction : sbit at DDB5_bit; LCD_EN_Direction : sbit at DDB4_bit; LCD_D7_Direction : sbit at DDB3_bit; LCD_D6_Direction : sbit at DDB2_bit; LCD_D5_Direction : sbit at DDB1_bit; LCD_D4_Direction : sbit at DDB0_bit; sHello: string[6]; begin sHello := 'Hello'; //инициализируем порты ввода-вывода DDRA := 0; PORTA := 0; DDRB := 0; PORTB := 0; DDRD := 0; PORTD := 0; Lcd_Init(); //инициализируем дисплей Lcd_Out(1, 1, sHello); //отправляем приветствие while (1) do begin Delay_ms(1); end; end.
Работает!
Перейдем на следующую ступень эволюции - попробуем-ка сами заставить дисплей работать!
Сначала разберемся, как вообще происходит общение между дисплеем и микроконтроллером. Вот что есть в даташите:
Рисунок 2. Диаграмма обмена.
Из этой диаграммы мы можем увидеть, как примерно происходит процесс записи и чтения данных. То, что понадобится нам: слева изображена запись команды в дисплей. Вывод R/W опущен к "0", так так мы передаём информацию дисплею. Вывод А0 также опущен, так как передаём команду. Сначала устанавливаем вывод Е на "1", затем выставляем выводы DB4-DB7 в соответствии со старшим полубайтом самой команды, и переводом вывода Е с "1" на "0" обеспечиваем передачу команды. Дальше снова устанавливаем вывод Е на "1", записываем в выводы DB4-DB7 уже младший полубайт команды, и снова переводим вывод Е c "1" на "0".
Передача не команды, а данных, происходит точно так же, но при подтянутом к "1" выводе А0.
Вроде выглядит не очень-то страшно, верно?
А вот временная диаграмма для записи:
Рисунок 3. Временная диаграмма записи.
Таблица 1. Временные характеристики дисплея.
Вот это уже выглядит более пугающе... Жесткие проверки по времени, ужас-ужас! Но на самом деле, ничего страшного: во-первых, почти все ограничения стоят на минимальные параметры - задержка должна быть не меньше ..., за исключением перевода вывода Е с "1" на "0" и обратно - а тут переживать не нужно, наша микросхема в плане перевода ножек достаточно шустра. Касательно остальных задержек - помним, что мы работаем на частоте 8 МГц, верно? Значит, один такт у нас занимает (1/8) мкс, или 125 нс. Так, значит, интерес для нас представляют только два минимальных времени: время цикла чтения/записи (500 нс, или 4 такта микросхемы), и длительность импульса разрешения чтения/записи (230 нс, или 2 такта микросхемы). Первое - это весь цикл отправки одного полубайта, второй - длительность нахождения вывода Е в "1". Что ж, просто в нужном месте поставим задержки!
Теперь напишем алгоритм отправки одной команды (или одного байта данных).
- Выставляем значение ножки, связанной с выводом А0: если отправляем команду, то А0 = 0, если отправляем данные - то А0 = 1.
- Выставляем вывод Е на "1".
- Устанавливаем на выводах DB7-DB4 значение старшего полубайта данных или команды.
- Делаем задержку - тут нам будет достаточно самой маленькой задержки - например, на одну микросекунду (к счастью, нужные нам длительности сверху не ограничиваются).
- Выставляем вывод Е на "0". Полубайт отправлен!
- Снова делаем задержку.
- Выставляем вывод Е на "1".
- Устанавливаем на выводах DB7-Db4 значение младшего полубайта данных или команды.
- Очередная задержка.
- Выставляем вывод Е на "0" - ура, байт оправлен!
- Добавим ещё достаточно большую задержку для того, чтобы дисплей успел осознать и выполнить команду, или нарисовать полученный символ.
Итак, у нас уже сформировался алгоритм для функции, которая будет отправлять байт данных дисплею. Для этой функции нам надо будет вынести расположение выводов А0, Е и DB4-DB7. При этом будем считать, что для работы с дисплеем используется один порт.
Программа на Си:
//переименование типов typedef unsigned char byte; //определяем назначение портов ввода-вывода #define portLCD PORTB #define ddrLCD DDRB //константы для дисплея const byte LCD_A0_OUT_NUMBER = 5; //номер вывода, к которому подключён вывод A0 дисплея const byte LCD_E_OUT_NUMBER = 4; //номер вывода, к которому подключён E дисплея const byte LCD_DATA_MASK = 0x0F; const byte LCD_DATA_SHIFT = 0; const byte DL_LCD_BETWEEN_E_CHANGE_MCS = 1; //время задержки перед изменением вывода Е дисплея (отправка полубайта команд) const byte DL_LCD_DATA_SEND_MS = 2; //время задержки после отправки одного байта данных или команды в миллисекундах void LCD_SendByte(byte DataByte, byte IsSymbol) { portLCD.LCD_A0_OUT_NUMBER = IsSymbol; //определяем, что отправляем - команду или данные portLCD.LCD_E_OUT_NUMBER = 1; //показываем, что сейчас будет устанавливать данные portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte >> 4); //устанавливаем на выводах DB7-DB4 значение старшего полубайта Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед отправкой portLCD.LCD_E_OUT_NUMBER = 0; //отправляем значение старшего полубайта! Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта portLCD.LCD_E_OUT_NUMBER = 1; portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte & 0x0F); Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); portLCD.LCD_E_OUT_NUMBER = 0; Delay_ms(DL_LCD_DATA_SEND_MS); //делаем задержку для того, чтобы дисплей мог обработать команду или данные }
На Паскале:
program LCD; const //константы для дисплея LCD_A0_OUT_NUMBER = 5; //номер вывода, к которому подключён вывод A0 дисплея LCD_E_OUT_NUMBER = 4; //номер вывода, к которому подключён E дисплея LCD_DATA_MASK = 0x0F; LCD_DATA_SHIFT = 0; //задержки DL_LCD_BETWEEN_E_CHANGE_MCS = 1; //время задержки перед изменением вывода Е дисплея (отправка полубайта команд) DL_LCD_DATA_SEND_MS = 2; //время задержки после отправки одного байта данных или команды в миллисекундах var //определяем назначение портов ввода-вывода portLCD: byte at PORTB; ddrLCD: byte at DDRB; procedure LCD_SendByte(DataByte: byte; IsSymbol: byte); begin portLCD.LCD_A0_OUT_NUMBER := IsSymbol; //определяем, что отправляем - команду или данные portLCD.LCD_E_OUT_NUMBER := 1; //показываем, что сейчас будет устанавливать данные portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte shr 4); //устанавливаем на выводах DB7-DB4 значение старшего полубайта Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед отправкой portLCD.LCD_E_OUT_NUMBER := 0; //отправляем значение старшего полубайта! Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта portLCD.LCD_E_OUT_NUMBER := 1; portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte and 0x0F); Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); portLCD.LCD_E_OUT_NUMBER := 0; Delay_ms(DL_LCD_DATA_SEND_MS); //делаем задержку для того, чтобы дисплей мог обработать команду или данные end;
Так, с "низним уровнем" протокола обмена информацией с дисплеем мы разобрались - теперь нужно понять, с помощью каких команд можно инициализировать его. Снова обращаемся к даташиту:
Рисунок 4. Последовательность команд при инициализации 4-хбитного режима.
Упс! Как мы видим, первые команды - четырехбитные, и после каждой из них должна быть задержка не менее 40 мкс. В принципе, можно либо написать свою функцию для конкретно инициализации, либо слегка модифицировать уже написанную "универсальную" - просто добавить бóльшую задержку после отсылки первого полубайта (на самом деле, опытным путем проверено, что функция работает и без каких-либо изменений) - тогда первые четыре команды мы запишем как два байта, и всё.
Ещё одно замечание - после того, как мы инициализировали дисплей, судя по даташиту, нам его нужно "включить" - отправить команду 0b000011__, где заместо "__" можно поставить:
- 00: на дисплее не будет курсора и ничего не будет мигать
- 01: курсора не будет, но будет мигать весь символ в позиции курсора
- 10: курсор есть, то он не мигает
- 11: есть и курсор, и весь символ; при этом символ мигает
Или же можно немножечко изменить процесс инициализации, и вместо команды "Выключить дисплей" (на рисунке 4 третья команда снизу), послать команду "Включить дисплей" с выбранными параметрами курсора (вместо команды 0b00001000 послать 0b00001110, например. Будет работать!
После же инициализации попробуем послать текст-приветствие - например, "Hello":
На Си:
//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello" //переименование типов typedef unsigned char byte; //определяем назначение портов ввода-вывода #define portLCD PORTB #define ddrLCD DDRB //константы для дисплея const byte LCD_A0_OUT_NUMBER = 5; //номер вывода, к которому подключён вывод A0 дисплея const byte LCD_E_OUT_NUMBER = 4; //номер вывода, к которому подключён E дисплея const byte LCD_DATA_MASK = 0x0F; const byte LCD_DATA_SHIFT = 0; //задержки const byte DL_LCD_SWITCH_ON_MS = 20; //задержка после подачи питания на дисплей в мс const byte DL_LCD_BETWEEN_E_CHANGE_MCS = 1; //задержка перед изменением вывода Е дисплея (отправка полубайта команд) в мкс const byte DL_LCD_DATA_SEND_MS = 2; //задержка после отправки одного байта данных или команды в мс void LCD_SendByte(byte DataByte, byte IsSymbol) { portLCD.LCD_A0_OUT_NUMBER = IsSymbol; //определяем, что отправляем - команду или данные portLCD.LCD_E_OUT_NUMBER = 1; //показываем, что сейчас будет устанавливать данные portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte >> 4); //устанавливаем на выводах DB7-DB4 значение старшего полубайта Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед отправкой portLCD.LCD_E_OUT_NUMBER = 0; //отправляем значение старшего полубайта! Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта portLCD.LCD_E_OUT_NUMBER = 1; portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte & 0x0F); Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); portLCD.LCD_E_OUT_NUMBER = 0; Delay_ms(DL_LCD_DATA_SEND_MS); //делаем задержку для того, чтобы дисплей мог обработать команду или данные } void main() { byte i; //объявление переменных и констант byte bInit[] = {0b00110011, //команды для инициализации модуля 0b00110010, 0b00101000, 0b00001110, 0b00000001, 0b00000110}; //строка на вывод char cText[] = "Hello"; //инициализация портов ввода-вывода DDRA = 0; PORTA = 0; DDRB = 0; PORTB = 0; DDRD = 0; PORTD = 0; //инициализация выводов для дисплея - на выход и на 0 ddrLCD |= (1 << LCD_A0_OUT_NUMBER) | (1 << LCD_E_OUT_NUMBER) | LCD_DATA_MASK; Delay_ms(DL_LCD_SWITCH_ON_MS); //задержка для дисплея после включения for (i = 0; i < 6; i++) //инициализируем дисплей LCD_SendByte(bInit[i], 0); for (i = 0; i < 5; i++) //отправляем текст LCD_SendByte(cText[i], 1); while(1) { Delay_ms(1); } }
На Паскале:
//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello" program LCD; const //константы для дисплея LCD_A0_OUT_NUMBER = 5; //номер вывода, к которому подключён вывод A0 дисплея LCD_E_OUT_NUMBER = 4; //номер вывода, к которому подключён E дисплея LCD_DATA_MASK = 0x0F; LCD_DATA_SHIFT = 0; //задержки DL_LCD_SWITCH_ON_MS = 20; //задержка после подачи питания на дисплей в мс DL_LCD_BETWEEN_E_CHANGE_MCS = 1; //задержка перед изменением вывода Е дисплея (отправка полубайта команд) в мкс DL_LCD_DATA_SEND_MS = 2; //задержка после отправки одного байта данных или команды в мс var //определяем назначение портов ввода-вывода portLCD: byte at PORTB; ddrLCD: byte at DDRB; procedure LCD_SendByte(DataByte: byte; IsSymbol: byte); begin portLCD.LCD_A0_OUT_NUMBER := IsSymbol; //определяем, что отправляем - команду или данные portLCD.LCD_E_OUT_NUMBER := 1; //показываем, что сейчас будет устанавливать данные portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte shr 4); //устанавливаем на выводах DB7-DB4 значение старшего полубайта Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед отправкой portLCD.LCD_E_OUT_NUMBER := 0; //отправляем значение старшего полубайта! Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта portLCD.LCD_E_OUT_NUMBER := 1; portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte and 0x0F); Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS); portLCD.LCD_E_OUT_NUMBER := 0; Delay_ms(DL_LCD_DATA_SEND_MS); //делаем задержку для того, чтобы дисплей мог обработать команду или данные end; const Init: array[6] of Byte = (%00110011, //команды для инициализации модуля %00110010, %00101000, %00001110, %00000001, %00000110); var i: Integer; sText: String[5]; begin sText := 'Hello'; //инициализация портов ввода-вывода DDRA := 0; PORTA := 0; DDRB := 0; PORTB := 0; DDRD := 0; PORTD := 0; //инициализация выводов для дисплея - на выход и на 0 ddrLCD := ddrLCD or (1 shl LCD_A0_OUT_NUMBER) or (1 shl LCD_E_OUT_NUMBER) or LCD_DATA_MASK; Delay_ms(DL_LCD_SWITCH_ON_MS); //задержка для дисплея после включения for i := 0 to 5 do LCD_SendByte(Init[i], 0); for i := 0 to 4 do LCD_SendByte(sText[i], 1); while(1) do Delay_ms(1); end.
В результате получили работающий дисплейчик! Вот прошивка.
Рисунок 5. Работающий дисплей.
Автор - Moriam
Обсудить на форуме