STM32 и LCD2004A без I2C интерфейса

от автора

Недавно начал изучать STM32 контроллеры и понадобилось взаимодействие с LCD дисплеем. Из дисплеев нашел у себя только 2004A, причем без I2C интерфейса. О нем и пойдет речь в этой статье.

Для начала необходимо подключить дисплей к контроллеру. Подключаем по схеме:

image

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 есть система команд, а так же тайминги выполнения этих команд.

image

Перепишем нужные команды в макроопределения в заголовочном файле:

 // 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() — это функция инициализации дисплея. Напишем и ее. Она основана на блок-схеме инициализации дисплея из даташита:

image

 //--------------------- инициализация дисплея----------------------------------------------------------- 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). Напишем ее реализацию. Она основана на временной диаграмме из даташита:

image

 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 соответствует таблице:

image

 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); 

И получаем результат:

image

В заключение приведу полный листинг файлов:

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 }  

ссылка на оригинал статьи https://habr.com/ru/post/522938/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *