Для начала необходимо подключить дисплей к контроллеру. Подключаем по схеме:
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 это моя библиотека для работы с задержками на основе системного таймера. Здесь ее описывать не буду, вот ее код:
#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_ */
#include "delay.h"/* Функции задержек на микросекунды и миллисекунды*/void delay_us(uint32_t us){ // до 233016 мксif (us > US_MAX_VALUE || us == 0)return;SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // запретить прерывания по достижении 0SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // ставим тактирование от процессораSysTick->LOAD = (US * us-1); // устанавливаем в регистр число от которого считатьSysTick->VAL = 0; // обнуляем текущее значение регистра SYST_CVRSysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // запускаем счетчикwhile(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // ждем установку флага COUNFLAG в регистре SYST_CSRSysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;// скидываем бит COUNTFLAGSysTick->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#defineLCD_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); // шлем в одном байте два 0011delay_us(100);sendByte(0x32, 0); // шлем в одном байте 00110010delay_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; // если данные поднимаем RSelse 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); // ставим курсор на начало нужной строки в DDRAMdelay_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);
И получаем результат:
В заключение приведу полный листинг файлов:
#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#defineLCD_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_ */
#include "lcd_20x4.h"// посылка байта в порт LCDvoid sendByte(char byte, int isData){//обнуляем все пины дисплеяLCD_ODR &= ~LCD_PIN_MASK;if(isData == 1) LCD_ODR |= PIN_RS; // если данные ставмим RSelse 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;// поднимаем пин ELCD_ODR |= PIN_EN;LCD_ODR &= ~PIN_EN; // сбрасываем пин Е//обнуляем все пины дисплея кроме RSLCD_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;// поднимаем пин ELCD_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); // шлем в одном байте два 0011delay_us(100);sendByte(0x32, 0); // шлем в одном байте 00110010delay_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); // ставим курсор на начало нужной строки в DDRAMdelay_ms(4);while(*str != '\0'){sendByte(*str, 1);str++;//delay_ms(100);}// while}