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 ног микроконтроллера.
Модули поставляются с различными наборами символов, поэтому при покупке следует иметь это ввиду, если планируется использовать кирилицу. Кодировка в таких модулях не совпадает со стандартной, поэтому придется потратить некоторое время на перекодирование строк выводимых сообщений. В интернете есть программы перекодировщики. Так же в модулях есть возможность запрограмировать несколько собственных символов.
Расположение пинов может отличаться в модулях различных производителей, но наиболее типичным является такое:
-
- GND
- Vcc (+5V)
- Настройка контрастности (Vo)
- RS
- R/W
- Enable (E)
- Bit 0 (младший для 8-ми битного интерфейса)
- Bit 1
- Bit 2
- Bit 3
- Bit 4 (младший для 4-х битного интерфейса)
- Bit 5
- Bit 6
- Bit 7 (старший)
- Питание подсветки для дисплеев с подсветкой (анод)
- Питание подсветки для дисплеев с подсветкой (катод)
Обычно подсветку следует подключать через резистор 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.
Надеюсь я помог вам сделать первый шаг в использовании этого удобного и недорого дисплея. Оставляйте ваши вопросы. До встречи!