HD44780-совместимый LCD дисплей и STM32.

HD44780 совместимые LCD алфавитно-цифровые модули очень удобны для применения во многих хобби-проектах. Они недороги и бывают разных размеров и цветов. Наиболее распространены 2-х строчные модули с 16 символами в строке (16х2). Другими популярными размерами являются: 8х2, 16х1, 20х2, 20х4 и 40х2.

Для моего проекта «Агат-7» в качестве дисплея эмулятора дисковода больше всего подошел 20х4 модуль. Его и будем использовать в качестве примера. Большинство модулей рассчитаны на напряжение 5В, хотя бывают и 3.3В модули. Последние как правило вдвое дороже и реже встречаются, поэтому я предпочитаю использовать 5В модели и ставить буферную микросхему,  если управляющий микроконтроллер работает на 3.3В (как например STM32L), т.к. она стоит всего порядка $1. Для управления контроллером используется 3 пина: RS, R/W и E. Данные передаются по 8-битной шине, хотя есть опция работы по 4-битной шине (байт передается в два приема). Таким образом, от микроконтроллера требуется от 7 до 11 ног для управления модулем. 8-битная шина имеет выше скорость и как следствие меньше загружает микроконтроллер. Кроме того, она проще в програмировании.

Так же есть два способа соблюдения интервалов между командами — таймер и запрос готовности. Во втором случае модуль передает сигнал готовности по старшему биту шины данных. При использовании 3.3В микроконтроллеров первый способ предпочтительнее, т.к. позволяет использовать дешевые однонаправленные буферы-преобразователи «3.3В -> 5В» для сигналов. Кроме того, при первом способе нам не требуется использование R/W сигнала, что экономит одну ногу. Я предпочитаю использовать первый способ в сочетании с 8-битной шиной, что требует 10 ног микроконтроллера.

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

Расположение пинов может отличаться в модулях различных производителей, но наиболее типичным является такое:

    1. GND
    2. Vcc (+5V)
    3. Настройка контрастности (Vo)
    4. RS
    5. R/W
    6. Enable (E)
    7. Bit 0 (младший для 8-ми битного интерфейса)
    8. Bit 1
    9. Bit 2
    10. Bit 3
    11. Bit 4 (младший для 4-х битного интерфейса)
    12. Bit 5
    13. Bit 6
    14. Bit 7 (старший)
    15. Питание подсветки для дисплеев с подсветкой (анод)
    16. Питание подсветки для дисплеев с подсветкой (катод)

 

Обычно подсветку следует подключать через резистор 50-100 ом. Настройка контрастности осуществляется подстроечным резистором (обычно 10-20К), подключенным как делитель напряжения (A и B к питанию 5В, а W к пину Vo управления контрастностью).

Я покажу пример подключения такого дисплея к микроконтроллеру STM32L152R6, который я использую в своем проекте «АГАТ-7» в качестве эмулятора дисковода. Схема подключения такова:

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

#include "stm32l1xx.h"
#include "stm32l1xx_conf.h"
#include "stm32l1xx_gpio.h"
#include "stm32l1xx_exti.h"
#include "stm32l1xx_syscfg.h"
#include "misc.h"

void delay_us(uint16_t usv)                 // Delay in us
{
  TIM6->PSC=32-1;
  TIM6->ARR=usv;
  TIM6->DIER = TIM_DIER_UIE;
  TIM6->SR &= ~TIM_SR_UIF;
  TIM6->CR1 &= ~TIM_CR1_ARPE;
  TIM6->SR = 0x00;
  TIM6->CR1 |= TIM_CR1_CEN;

  while (TIM6->SR == 0)
  {}
}

void delay_ms(uint16_t msv)            // Delay in ms
{
 uint16_t i;
 for(i=0; i<msv; i++) delay_us(1000);
}

void stm_init()                                   // A microcontroller initialisation
{
  int i;
  // System clock initialisation
  RCC->CR|=RCC_CR_HSION;
  while (!(RCC->CR & RCC_CR_HSIRDY));             // Wait until it's stable
  RCC->CFGR|=RCC_CFGR_PLLDIV_0 | RCC_CFGR_PLLMUL_0;   // Switch to HSI as SYSCLK
  RCC->CR|=RCC_CR_PLLON;                              // Turn PLL on
  while (!(RCC->CR & RCC_CR_PLLRDY));             // Wait PLL to stabilise

  //Setting up flash for high speed
  FLASH->ACR=FLASH_ACR_ACC64;
  FLASH->ACR|=FLASH_ACR_LATENCY;
  FLASH->ACR|=FLASH_ACR_PRFTEN;

  RCC->CFGR|=RCC_CFGR_SW_1 | RCC_CFGR_SW_0;           // Set PLL as SYSCLK
  RCC->CR&=~RCC_CR_MSION;                         // Turn off MSI

  //Enabling clock for GPIOB & GPIOC
  RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
  GPIOC->MODER = (uint32_t) 0x5555000;
  GPIOC->OTYPER &= (uint16_t) 0xFF;
  GPIOC->OSPEEDR = (uint16_t) 0xAAAA000;
  GPIOC->ODR = (uint32_t) 0;

  RCC->AHBENR|=RCC_AHBENR_GPIOBEN;
  GPIOB->MODER = (uint32_t) 0x155000;
  GPIOB->OTYPER&=~(GPIO_OTYPER_OT_8 | GPIO_OTYPER_OT_9 | GPIO_OTYPER_OT_10 | GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_7);
  GPIOB->OSPEEDR|=(GPIO_OSPEEDER_OSPEEDR8_1 | GPIO_OSPEEDER_OSPEEDR9_1 | GPIO_OSPEEDER_OSPEEDR10_1);
  GPIOB->ODR = (uint16_t) 0;

  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;                 // TIM6 timer clock
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
}

Довольно часто при общении с модулем требуется коротко «мигнуть» сигналом E, поэтому удобно написать подпрограмму “lcd_nybble”, которая это делает. Следует иметь ввиду, что длительность импульса в вашем проекте может отличаться от моей. Это зависит от выбранного модуля и скорости работы микроконтроллера:

void lcd_nybble()
{
 char i;
 GPIOB->BSRRL=GPIO_BSRR_BS_10;
 for(i=0; i<5; i++);
 GPIOB->BSRRH=GPIO_BSRR_BS_10;         	//Clock enable: falling edge
}

Для подачи команды в модуль надо выставить команду на пины данных, пины RS и R/W обнулить и «мигнуть» сигналом E. Команды требуют время для выполнения во время которого модуль недоступен. Поэтому используются задержки. Как я упоминал выше, вместо програмных задержек можно ждать сигнала готовности модуля. Возможно задержки придется изменить если используется другой модуль. Надо учитывать, что некоторые команды требуют больше времени на исполнение. Моя функция “lcd_command” выглядит так:

void lcd_command(uint16_t lcmnd)
{
  uint16_t temp_port;

  delay_us(40);
  temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
  GPIOC->ODR = temp_port | (lcmnd << 6);   //put data on output Port
  GPIOB->BSRRH=GPIO_BSRR_BS_8;             //RS=LOW : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;             //R/W=LOW : Write
  lcd_nybble();
  if (lcmnd < 4) delay_us(1700);           // It takes longer to complete some commands
}

Для инициализации LCD модуля используется следующая последовательность. Я думаю, что она объясняет себя сама:

void lcd_init();
{
  GPIOC->BSRRH = (uint16_t) 0x3FC0;                    // Reset all ports
  GPIOB->BSRRH = (uint16_t) 0x7C0;                     // Reset all ports
  delay_us(40000);                                     // Wait >15 msec after power is applied
  GPIOC->BSRRL = GPIO_BSRR_BS_10 | GPIO_BSRR_BS_11;    // put 0x30 on the output port
  delay_us(5000);                                      // must wait 5ms, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up
  delay_us(160);                                       // must wait 160us, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up #2
  delay_us(160);                                       // must wait 160us, busy flag not available
  lcd_nybble();                                        // command 0x30 = Wake up #3
  delay_us(160);
  GPIOC->BSRRL = (uint16_t) 0xE00;
  lcd_nybble();
  delay_us(160);
  lcd_command((uint16_t) 0x10);
  lcd_command((uint16_t) 0x0C);
  lcd_command((uint16_t) 0x06);
}

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

Еще нам понадобятся команды для вывода строки и символа. Мои команды «lcd_write» и «lcd_char» выглядят так:

/**********************************************************/
void lcd_write(char ldata[])
{
  uint16_t temp_port;
  char k=0;
 
  GPIOB->BSRRL=GPIO_BSRR_BS_8;                 //RS=HIGH : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;                 //R/W=LOW : Write
 
  while(ldata[k] != 0)
  {
    delay_us(40);
    temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
    GPIOC->ODR = temp_port | (ldata[k] << 6);  //put data on output Port
    lcd_nybble();
    k++;
  }
}

/**********************************************************/
void lcd_char(char ldata)
{
  uint16_t temp_port;
 
  GPIOB->BSRRL=GPIO_BSRR_BS_8;                //RS=HIGH : send data
  GPIOB->BSRRH=GPIO_BSRR_BS_9;                //R/W=LOW : Write
  delay_us(40);
  temp_port = GPIOC->ODR & ((uint16_t) 0xC03F);
  GPIOC->ODR = temp_port | (ldata << 6);      //put data on output Port
  lcd_nybble();
}

Ну и напоследок не помешает сделать команду перемещения курсора в произвольную точку экрана. Следует иметь ввиду, что адресация памяти в модулях не линейная. Так, после первой строчки идет третья, а только потом вторая. Это надо учитывать при программировании. Я написал такую функцию «lcd_goto» для этой цели:

void lcd_goto(uint8_t x, uint8_t y)
{
  uint8_t addr;

  switch (y)
  {
  case 0:
    addr = 0x80;
    break;
  case 1:
    addr = 0xC0;
    break;
  case 2:
    addr = 0x94;
    break;
  default:
    addr = 0xD4;
    break;
  }
  addr = addr + x;
  lcd_command((uint16_t) addr);
}

Надеюсь она достаточно понятна.

Теперь у нас есть все инструменты для работы с LCD модулем. Давайте напишем простенькую программу, которая выводит текст на дисплей:

int main(void)
{
  stm_init();
  lcd_init();                                // LCD initialisation
  lcd_command((uint16_t) 0x01);              // Clear screen
  delay_us(1700);
  lcd_command(0x40);                         // Start recording a cyrillyc symbol "Г" to the memory to zero place
  lcd_char(0x1f); lcd_char(0x10); lcd_char(0x10); lcd_char(0x10);
  lcd_char(0x10); lcd_char(0x10); lcd_char(0x10); lcd_char(0x00);   // The character recorded
  lcd_command(0x01);                         // Clear screen
  lcd_goto(0,0); lcd_write("********************");
  lcd_goto(0,1); lcd_write("*      A"); lcd_char(0x00); lcd_write("AT-7      *");
  lcd_goto(0,2); lcd_write("*electronicsfun.net*");
  lcd_goto(0,3); lcd_write("********************");
  while (1)
  {}
}

Она инициализирует дисплей, создает новый символ «Г» с нулевым кодом в области пользовательских символов и выводит заставку используя написанные ранее функции. Обратите внимание, что для вывода символа с кодом 0, приходится пользоваться функцией «lcd_char» вместо «lcd_write«. Вот что получилось:

Очень много дополнительной информации об этих модулях вы сможете найти в этом документе: http://www.gaw.ru/data/lcd/lcd.pdf.

Надеюсь я помог вам сделать первый шаг в использовании этого удобного и недорого дисплея. Оставляйте ваши вопросы. До встречи!

 

Leave a Reply