Недавно начал изучать STM32 контроллеры и понадобилось взаимодействие с LCD дисплеем. Из дисплеев нашел у себя только 2004A, причем без I2C интерфейса. О нем и пойдет речь в этой статье.
Для начала необходимо подключить дисплей к контроллеру. Подключаем по схеме:
PB0 — PB7 — выводы контроллера.
Итак, дисплей подключили. Самое время научить микроконтроллер работать с ним. Я решил создать свою библиотеку для того, чтобы можно было ее использовать в разных проектах. Она состоит из двух файлов — lcd_20x4.h и lcd_20x4.c
Начнем с заголовочного файла.
В начале подключаем файл библиотеки CMSIS stm32f1xx.h так как у меня камень STM32F103C8T6. Следующим включением подключаем файл delay.h — это моя библиотека для работы с задержками на основе системного таймера. Здесь ее описывать не буду, вот ее код:
Дисплей 2004A основан на контроллере фирмы HITACHI HD44780. Поэтому заглянем в даташит на данный контроллер. В таблице 6 есть система команд, а так же тайминги выполнения этих команд.
Перепишем нужные команды в макроопределения в заголовочном файле:
Теперь необходимо настроить выводы контроллера для работы с дисплеем. Определяем положение битов в порте ODR контроллера. Следует обратить внимание на PIN_D4. У меня там прописан 10-й бит вместо 4. На моем контроллере не работает 4-й вывод. Не знаю с чем это связано, но в регистре ODR этот бит всегда единица, даже до начала инициализации тактирования контроллера. Не знаю с чем это связано, возможно камень не оригинальный.
Далее настраиваем управляющие регистры для выводов. Я решил это сделать в виде макросов препроцессора:
В завершении заголовочного файла определяем функции работы с дисплеем:
С заголовочным файлом закончили. Теперь напишем реализации функций в файле lcd_20x4.c
Первым делом нужно настроить выводы для работы с дисплеем. Это делает функция void portInit(void):
Что касается функции lcdInit() — это функция инициализации дисплея. Напишем и ее. Она основана на блок-схеме инициализации дисплея из даташита:
Функция инициализации использует функцию void sendByte(char byte, int isData). Напишем ее реализацию. Она основана на временной диаграмме из даташита:
Теперь мы умеем отсылать байт на дисплей по 4-битной шине. Этим байтом может быть как команда так и символ. Определяется передачей в функцию переменной isData. Пришло время научиться передавать строки.
Дисплей 2004A состоит из 4 строк по 20 символов, что отражается в названии. Дабы не усложнять функцию я не буду реализовывать обрезку строк до 20 символов. В функцию будем отправлять строку символов и строку в которой ее вывести.
Для отображения символа на экране нужно записать его в память DDRAM. Адресация DDRAM соответствует таблице:
Вот и все, библиотека для дисплея готова. Теперь настало время ее использовать. В функции main() пишем:
И получаем результат:
В заключение приведу полный листинг файлов:
Для начала необходимо подключить дисплей к контроллеру. Подключаем по схеме:
PB0 — PB7 — выводы контроллера.
Назначение выводов дисплея :
Номер вывода | Сигнал | Назначение сигнала |
1 | GND | Земля (общий провод) |
2 | VCC | Питание + 5 В |
3 | VEE | Управление контрастностью дисплея. Подключается средний вывод делителя напряжения. Обычно это подстроечный резистор 10-20 кОм, но я распаял на плате дисплея резисторы. |
4 | RS | Выбор регистра: 0 – регистр команд; 1 – регистр данных. |
5 | R/W | Направление передачи данных: 0 – запись; 1 – чтение. Как правило чтение из дисплея не используется, поэтому сажаем вывод на землю. |
6 | EN | Строб операции шины. При спадающем фронте данные, находящиеся на шине данных «защелкиваются» в регистр. |
7 | DB0 | Младшие биты восьми битного режима. При четырех битном интерфейсе не используются и обычно сажаются на землю. |
8 | DB1 | |
9 | DB2 | |
10 | DB3 | |
11 | DB4 | Старшие биты восьми битного режима или биты данных четырех битного интерфейса. |
12 | DB5 | |
13 | DB6 | |
14 | DB7 | |
15 | A | Анод питания подсветки (+) |
16 | K | Катод питания подсветки (-). Ток должен быть ограничен. |
Итак, дисплей подключили. Самое время научить микроконтроллер работать с ним. Я решил создать свою библиотеку для того, чтобы можно было ее использовать в разных проектах. Она состоит из двух файлов — lcd_20x4.h и lcd_20x4.c
Начнем с заголовочного файла.
#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_
#define LCD_LCD_20X4_2004A_LCD_20X4_H_
#include "stm32f1xx.h"
#include "delay.h"
В начале подключаем файл библиотеки CMSIS stm32f1xx.h так как у меня камень STM32F103C8T6. Следующим включением подключаем файл delay.h — это моя библиотека для работы с задержками на основе системного таймера. Здесь ее описывать не буду, вот ее код:
Файл delay.h
#ifndef DELAY_DELAY_H_
#define DELAY_DELAY_H_
#include "stm32f1xx.h"
#define F_CPU 72000000UL
#define US F_CPU/1000000
#define MS F_CPU/1000
#define SYSTICK_MAX_VALUE 16777215
#define US_MAX_VALUE SYSTICK_MAX_VALUE/(US)
#define MS_MAX_VALUE SYSTICK_MAX_VALUE/(MS)
void delay_us(uint32_t us); // до 233 мкс
void delay_ms(uint32_t ms); // до 233 мс
void delay_s(uint32_t s);
#endif /* DELAY_DELAY_H_ */
Файл delay.с
#include "delay.h"
/* Функции задержек на микросекунды и миллисекунды*/
void delay_us(uint32_t us){ // до 233 016 мкс
if (us > US_MAX_VALUE || us == 0)
return;
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // запретить прерывания по достижении 0
SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // ставим тактирование от процессора
SysTick->LOAD = (US * us-1); // устанавливаем в регистр число от которого считать
SysTick->VAL = 0; // обнуляем текущее значение регистра SYST_CVR
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // запускаем счетчик
while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // ждем установку флага COUNFLAG в регистре SYST_CSR
SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk; // скидываем бит COUNTFLAG
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // выключаем счетчик
}
void delay_ms(uint32_t ms){ // до 233 мс
if(ms > MS_MAX_VALUE || ms ==0)
return;
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;
SysTick->LOAD = (MS * ms);
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));
SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void delay_s(uint32_t s){
for(int i=0; i<s*5;i++) delay_ms(200);
}
Дисплей 2004A основан на контроллере фирмы HITACHI HD44780. Поэтому заглянем в даташит на данный контроллер. В таблице 6 есть система команд, а так же тайминги выполнения этих команд.
Перепишем нужные команды в макроопределения в заголовочном файле:
// display commands
#define CLEAR_DISPLAY 0x1
#define RETURN_HOME 0x2
#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift
#define DISPLAY_ON 0xC // non cursor
#define DISPLAY_OFF 0x8
#define CURSOR_SHIFT_LEFT 0x10
#define CURSOR_SHIFT_RIGHT 0x14
#define DISPLAY_SHIFT_LEFT 0x18
#define DISPLAY_SHIFT_RIGHT 0x1C
#define DATA_BUS_4BIT_PAGE0 0x28
#define DATA_BUS_4BIT_PAGE1 0x2A
#define DATA_BUS_8BIT_PAGE0 0x38
#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS
#define SET_DDRAM_ADDRESS 0x80
Теперь необходимо настроить выводы контроллера для работы с дисплеем. Определяем положение битов в порте ODR контроллера. Следует обратить внимание на PIN_D4. У меня там прописан 10-й бит вместо 4. На моем контроллере не работает 4-й вывод. Не знаю с чем это связано, но в регистре ODR этот бит всегда единица, даже до начала инициализации тактирования контроллера. Не знаю с чем это связано, возможно камень не оригинальный.
// положение битов в порте ODR
#define PIN_RS 0x1
#define PIN_EN 0x2
#define PIN_D7 0x80
#define PIN_D6 0x40
#define PIN_D5 0x20
#define PIN_D4 0x400
Далее настраиваем управляющие регистры для выводов. Я решил это сделать в виде макросов препроцессора:
#define LCD_PORT GPIOB
#define LCD_ODR LCD_PORT->ODR
#define LCD_PIN_RS() LCD_PORT->CRL &= ~GPIO_CRL_CNF0; LCD_PORT->CRL |= GPIO_CRL_MODE0; // PB0 выход тяни-толкай, частота 50 Мгц
#define LCD_PIN_EN() LCD_PORT->CRL &= ~GPIO_CRL_CNF1; LCD_PORT->CRL |= GPIO_CRL_MODE1; // PB1
#define LCD_PIN_D7() LCD_PORT->CRL &= ~GPIO_CRL_CNF7; LCD_PORT->CRL |= GPIO_CRL_MODE7; // PB7
#define LCD_PIN_D6() LCD_PORT->CRL &= ~GPIO_CRL_CNF6; LCD_PORT->CRL |= GPIO_CRL_MODE6; // PB6
#define LCD_PIN_D5() LCD_PORT->CRL &= ~GPIO_CRL_CNF5; LCD_PORT->CRL |= GPIO_CRL_MODE5; // PB5
#define LCD_PIN_D4() LCD_PORT->CRH &= ~GPIO_CRH_CNF10; LCD_PORT->CRH |= GPIO_CRH_MODE10; // PB10
#define LCD_PIN_MASK (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экрана
В завершении заголовочного файла определяем функции работы с дисплеем:
void portInit(void); // инициализация ножек порта под экран
void sendByte(char byte, int isData);
void lcdInit(void); // инициализация дисплея
void sendStr(char *str, int row ); // вывод строки
#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */
С заголовочным файлом закончили. Теперь напишем реализации функций в файле lcd_20x4.c
Первым делом нужно настроить выводы для работы с дисплеем. Это делает функция void portInit(void):
void portInit(void){
//----------------------включаем тактирование порта----------------------------------------------------
if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
else return;
//--------------------- инициализация пинов для LCD-----------------------------------------------------
LCD_PIN_RS();// макроопределения в заголовочном файле
LCD_PIN_EN();
LCD_PIN_D7();
LCD_PIN_D6();
LCD_PIN_D5();
LCD_PIN_D4();
lcdInit(); // функция инициализации дисплея
return ;
}
Что касается функции lcdInit() — это функция инициализации дисплея. Напишем и ее. Она основана на блок-схеме инициализации дисплея из даташита:
//--------------------- инициализация дисплея-----------------------------------------------------------
void lcdInit(void){
delay_ms(15); // ждем пока стабилизируется питание
sendByte(0x33, 0); // шлем в одном байте два 0011
delay_us(100);
sendByte(0x32, 0); // шлем в одном байте 00110010
delay_us(40);
sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 бит
delay_us(40);
sendByte(DISPLAY_OFF, 0); // выключаем дисплей
delay_us(40);
sendByte(CLEAR_DISPLAY, 0); // очищаем дисплей
delay_ms(2);
sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещается
delay_us(40);
sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсор
delay_us(40);
return ;
}
Функция инициализации использует функцию void sendByte(char byte, int isData). Напишем ее реализацию. Она основана на временной диаграмме из даташита:
void sendByte(char byte, int isData){
//обнуляем все пины дисплея
LCD_ODR &= ~LCD_PIN_MASK;
if(isData == 1) LCD_ODR |= PIN_RS; // если данные поднимаем RS
else LCD_ODR &= ~(PIN_RS); // иначе скидываем RS
LCD_ODR |= PIN_EN; // поднимаем пин E
// ставим старшую тетраду на порт
if(byte & 0x80) LCD_ODR |= PIN_D7;
if(byte & 0x40) LCD_ODR |= PIN_D6;
if(byte & 0x20) LCD_ODR |= PIN_D5;
if(byte & 0x10) LCD_ODR |= PIN_D4;
LCD_ODR &= ~PIN_EN; // сбрасываем пин Е
LCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);//обнуляем все пины дисплея кроме RS
LCD_ODR |= PIN_EN;// поднимаем пин E
// ставим младшую тетраду на порт
if(byte & 0x8) LCD_ODR |= PIN_D7;
if(byte & 0x4) LCD_ODR |= PIN_D6;
if(byte & 0x2) LCD_ODR |= PIN_D5;
if(byte & 0x1) LCD_ODR |= PIN_D4;
LCD_ODR &= ~(PIN_EN);// сбрасываем пин Е
delay_us(40);
return;
}
Теперь мы умеем отсылать байт на дисплей по 4-битной шине. Этим байтом может быть как команда так и символ. Определяется передачей в функцию переменной isData. Пришло время научиться передавать строки.
Дисплей 2004A состоит из 4 строк по 20 символов, что отражается в названии. Дабы не усложнять функцию я не буду реализовывать обрезку строк до 20 символов. В функцию будем отправлять строку символов и строку в которой ее вывести.
Для отображения символа на экране нужно записать его в память DDRAM. Адресация DDRAM соответствует таблице:
void sendStr(char *str, int row ){
char start_address;
switch (row) {
case 1:
start_address = 0x0; // 1 строка
break;
case 2:
start_address = 0x40; // 2 строка
break;
case 3:
start_address = 0x14; // 3 строка
break;
case 4:
start_address = 0x54; // 4 строка
break;
}
sendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки в DDRAM
delay_ms(4);
while(*str != '\0'){// пока не встретили конец строки
sendByte(*str, 1);
str++;
}// while
}
Вот и все, библиотека для дисплея готова. Теперь настало время ее использовать. В функции main() пишем:
portInit();// инициализация портов под дисплей
sendStr(" HELLO, HABR", 1);
sendStr(" powered by", 2);
sendStr(" STM32F103C8T6", 3);
sendStr("Nibiru", 4);
И получаем результат:
В заключение приведу полный листинг файлов:
lcd_20x4.h
#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_
#define LCD_LCD_20X4_2004A_LCD_20X4_H_
#include "stm32f1xx.h"
#include "delay.h"
// display commands
#define CLEAR_DISPLAY 0x1
#define RETURN_HOME 0x2
#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift
#define DISPLAY_ON 0xC // non cursor
#define DISPLAY_OFF 0x8
#define CURSOR_SHIFT_LEFT 0x10
#define CURSOR_SHIFT_RIGHT 0x14
#define DISPLAY_SHIFT_LEFT 0x18
#define DISPLAY_SHIFT_RIGHT 0x1C
#define DATA_BUS_4BIT_PAGE0 0x28
#define DATA_BUS_4BIT_PAGE1 0x2A
#define DATA_BUS_8BIT_PAGE0 0x38
#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS
#define SET_DDRAM_ADDRESS 0x80
// положение битов в порте ODR
#define PIN_RS 0x1
#define PIN_EN 0x2
#define PIN_D7 0x80
#define PIN_D6 0x40
#define PIN_D5 0x20
#define PIN_D4 0x400
#define LCD_PORT GPIOB
#define LCD_ODR LCD_PORT->ODR
#define LCD_PIN_RS() LCD_PORT->CRL &= ~GPIO_CRL_CNF0; LCD_PORT->CRL |= GPIO_CRL_MODE0; // PB0 выход тяни-толкай, частота 50 Мгц
#define LCD_PIN_EN() LCD_PORT->CRL &= ~GPIO_CRL_CNF1; LCD_PORT->CRL |= GPIO_CRL_MODE1; // PB1
#define LCD_PIN_D7() LCD_PORT->CRL &= ~GPIO_CRL_CNF7; LCD_PORT->CRL |= GPIO_CRL_MODE7; // PB7
#define LCD_PIN_D6() LCD_PORT->CRL &= ~GPIO_CRL_CNF6; LCD_PORT->CRL |= GPIO_CRL_MODE6; // PB6
#define LCD_PIN_D5() LCD_PORT->CRL &= ~GPIO_CRL_CNF5; LCD_PORT->CRL |= GPIO_CRL_MODE5; // PB5
#define LCD_PIN_D4() LCD_PORT->CRH &= ~GPIO_CRH_CNF10; LCD_PORT->CRH |= GPIO_CRH_MODE10; // PB10
#define LCD_PIN_MASK (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экрана
void portInit(void); // инициализация ножек порта под экран
void sendByte(char byte, int isData);
void lcdInit(void); // инициализация дисплея
void sendStr(char *str, int row ); // вывод строки
#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */
lcd_20x4.c
#include "lcd_20x4.h"
// посылка байта в порт LCD
void sendByte(char byte, int isData){
//обнуляем все пины дисплея
LCD_ODR &= ~LCD_PIN_MASK;
if(isData == 1) LCD_ODR |= PIN_RS; // если данные ставмим RS
else LCD_ODR &= ~(PIN_RS); // иначе скидываем RS
// ставим старшую тетраду на порт
if(byte & 0x80) LCD_ODR |= PIN_D7;
if(byte & 0x40) LCD_ODR |= PIN_D6;
if(byte & 0x20) LCD_ODR |= PIN_D5;
if(byte & 0x10) LCD_ODR |= PIN_D4;
// поднимаем пин E
LCD_ODR |= PIN_EN;
LCD_ODR &= ~PIN_EN; // сбрасываем пин Е
//обнуляем все пины дисплея кроме RS
LCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);
// ставим младшую тетраду на порт
if(byte & 0x8) LCD_ODR |= PIN_D7;
if(byte & 0x4) LCD_ODR |= PIN_D6;
if(byte & 0x2) LCD_ODR |= PIN_D5;
if(byte & 0x1) LCD_ODR |= PIN_D4;
// поднимаем пин E
LCD_ODR |= PIN_EN;
//delay_us(10);
// сбрасываем пин Е
LCD_ODR &= ~(PIN_EN);
delay_us(40);
return;
}
// функция тактирует порт под дисплей и задает пины на выход тяни толкай и частоту 50 Мгц
void portInit(void){
//----------------------включаем тактирование порта----------------------------------------------------
if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
else return;
//--------------------- инициализация пинов для LCD-----------------------------------------------------
LCD_PIN_RS();
LCD_PIN_EN();
LCD_PIN_D7();
LCD_PIN_D6();
LCD_PIN_D5();
LCD_PIN_D4();
lcdInit();
return ;
}
//--------------------- инициализация дисплея-----------------------------------------------------------
void lcdInit(void){
delay_ms(15); // ждем пока стабилизируется питание
sendByte(0x33, 0); // шлем в одном байте два 0011
delay_us(100);
sendByte(0x32, 0); // шлем в одном байте 00110010
delay_us(40);
sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 бит
delay_us(40);
sendByte(DISPLAY_OFF, 0); // выключаем дисплей
delay_us(40);
sendByte(CLEAR_DISPLAY, 0); // очищаем дисплей
delay_ms(2);
sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещается
delay_us(40);
sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсор
delay_us(40);
return ;
}
void sendStr(char *str, int row ){
char start_address;
switch (row) {
case 1:
start_address = 0x0; // 1 строка
break;
case 2:
start_address = 0x40; // 2 строка
break;
case 3:
start_address = 0x14; // 3 строка
break;
case 4:
start_address = 0x54; // 4 строка
break;
}
sendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки в DDRAM
delay_ms(4);
while(*str != '\0'){
sendByte(*str, 1);
str++;
//delay_ms(100);
}// while
}
AlNi89
Я бы в sendStr() сделал передачу номера строки (row 0..3) и номера стартового символа (poz 0..19). А в коде уже пересчитывать start_address = row*20 + poz. Так избавимся от громоздкого switch и от нерациональных пробелов для смещения текста, как тут
VT100
DDRAM имеет непривычную адресацию (вроде-бы — для упрощения разводки проводников на стекле). Простым "row*20 + poz" не отделаться.
RollerBob
Можно организовать буфер в ОЗУ и печатать туда как заблагорассудится. А дисплей обновлять из буфера, либо через фиксированные интервалы времени, либо по факту изменения буфера.
Хотелось бы ещё узнать как автор будет проблему кириллицы решать.
Nibiru1 Автор
Для кириллицы есть такие дисплеи с кириллическим знакогенератором. Но если его нет, то во всех таких дисплеях есть память под пользовательские 8 символов, которые можно нарисовать как хочешь. Можно попробовать динамически рисовать кириллицу в эту память а потом выводить оттуда на экран. На сколько это будет быстро работать вопрос…
fusioneer
Судя по таблице, адреса строк идут не по порядку и не монотонно. Т.е. вычислять адрес по линейной формуле не выйдет.
AlNi89
Это решается 4 строками кода:
И вызов функции будет как-то так:
Так расчет в функции вывода будет даже проще, т.к. теперь не надо будет делать умножение для вычисления строки, просто прибавить к адресу Lineх позицию начала вывода текста.
Nibiru1 Автор
Думаю так «в лоб» решить задачу не получится. Потому, что адресация первой строки продолжается на третью, а второй на четвертую. Скорее всего (но я могу ошибаться) это связано с тем, что сам контроллер дисплея умеет работать только с двумя строками, а расширение до четырех происходит добавлением еще одного контроллера на две строки и физически на дисплее строки разведены через одну. По поводу пробелов, да наверно громоздко, но если задавать начальное положение в строке, то все равно придется использовать условные операторы и перерасчет адресов. А вот если писать функцию для отображения переменных в реальном времени без перерисовки всего экрана, то там свободное позиционирование понадобится обязательно. Позже думаю реализовать возможность вывода переменных, мне нужно будет выводить температуру и мощность верхнего и нижнего нагревателя в реальном времени и само время работы.