Шаг 4. Подключение LCD

Циферки да буковки…

В это раз мы подключим LCD-дисплей к нашей микросхемке. Нам потребуется:

  • наша микросхема ATtiny2313
  • ЖК-модуль Мэлт 10S1
  • опционально - резистор для ресета и конденсатор для стабилизации напряжения

Подсоединяем по такой схеме:

Рисунок 1. Схема

Рисунок 1. Схема подключения.

Если подсоединяете резистор к 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". Что ж, просто в нужном месте поставим задержки!

Теперь напишем алгоритм отправки одной команды (или одного байта данных).

  1. Выставляем значение ножки, связанной с выводом А0: если отправляем команду, то А0 = 0, если отправляем данные - то А0 = 1.
  2. Выставляем вывод Е на "1".
  3. Устанавливаем на выводах DB7-DB4 значение старшего полубайта данных или команды.
  4. Делаем задержку - тут нам будет достаточно самой маленькой задержки - например, на одну микросекунду (к счастью, нужные нам длительности сверху не ограничиваются).
  5. Выставляем вывод Е на "0". Полубайт отправлен!
  6. Снова делаем задержку.
  7. Выставляем вывод Е на "1".
  8. Устанавливаем на выводах DB7-Db4 значение младшего полубайта данных или команды.
  9. Очередная задержка.
  10. Выставляем вывод Е на "0" - ура, байт оправлен!
  11. Добавим ещё достаточно большую задержку для того, чтобы дисплей успел осознать и выполнить команду, или нарисовать полученный символ.

Итак, у нас уже сформировался алгоритм для функции, которая будет отправлять байт данных дисплею. Для этой функции нам надо будет вынести расположение выводов А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
Обсудить на форуме