- Регистрация
- 12.04.17
- Сообщения
- 19.095
- Реакции
- 107
- Репутация
- 0
Привет, Хабр!
В этой статье я хотел бы рассказать о своем опыте подключения LCD дисплеев к микроконтроллеру STM32 с использованием библиотеки HAL по I2C шине.
Подключать буду дисплей 1602 и 2004. Они оба имеют припаянный I2C адаптер на основе чипа PCF8574T. Отладочной платой выступит Nucleo676ZI, а средой разработки – STM32CubeIDE 1.3.0.
Про принцип работы I2C шины подробно рассказывать не буду, советую заглянуть
Создаем проект, выбираем отладочную плату:
Указываем, что будем использовать I2C1. Также я подключу UART5 для общения с платой, это нужно для получения информации от платы об адресе дисплея.
В этом же окне можно посмотреть номера ножек, к которым подключается дисплей, в моем случае получилось так:
Для начала подключим всего один дисплей, я начну с 1602. Также я подключу известный бывалым ардуинщикам адаптер USB-UART CH340 для получения данных с платы.
Обратите внимание, адаптер подключается RX к TX и TX к RX, перемычка на адаптере стоит на 3.3В
Рассмотрим подробнее работу с микросхемой PCF8574T и дисплеем. Ниже приведена принципиальная схема модуля с дисплеем:
Микросхема PCF8574T по функционалу схожа с регистром сдвига 74hc595 – она получает по I2C интерфейсу байт и присваивает своим выводам (P0-P7) значения соответствующего бита. Рассмотрим какие выводы микросхемы соединены с дисплеем и за что отвечают:
• Вывод Р0 микросхемы соединен с выводом RS дисплея, отвечающего за то, принимает дисплей данные (1) или инструкции по работе дисплея (0);
• Вывод Р1 соединен с R\W, если 1 – запись данных в дисплей, 0 – считывание;
• Вывод Р2 соединен с CS – вывод, по изменению состояния которого идет считывание;
• Вывод Р3 – управление подсветкой;
• Выводы Р4 — Р7 служат для передачи данных дисплею.
К одной I2C шине может быть подключено несколько устройств одновременно. Для того, чтобы можно было обращаться к конкретному устройству, каждое из них имеет свой адрес, для начала выясним его. Если контакты А1, А2 и А3 на плате адаптера не запаяны, то адрес будет скорее всего 0х27, но лучше проверить. Для этого напишем небольшую функцию, которая покажет адреса всех устройств, которые подключены к I2C шине:
void I2C_Scan ()
{
// создание переменной, содержащей статус
HAL_StatusTypeDef res;
// сообщение о начале процедуры
char info[] = "Scanning I2C bus...\r\n";
// отправка сообщения по UART
HAL_UART_Transmit(&huart5, (uint8_t*)info, strlen(info), HAL_MAX_DELAY);
/* &huart5 - адрес используемого UART
* (uint8_t*)info - указатель на значение для отправки
* strlen(info) - длина отправляемого сообщения
* HAL_MAX_DELAY - задержка
*/
// перебор всех возможных адресов
for(uint16_t i = 0; i < 128; i++)
{
// проверяем, готово ли устройство по адресу i для связи
res = HAL_I2C_IsDeviceReady(&hi2c1, i / если да, то
if(res == HAL_OK)
{
char msg[64];
// запись адреса i, на который откликнулись, в строку в виде
// 16тиричного значения:
snprintf(msg, sizeof(msg), "0x%02X", i);
// отправка номера откликнувшегося адреса
HAL_UART_Transmit(&huart5, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
// переход на новую строчку
HAL_UART_Transmit(&huart5, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}
else HAL_UART_Transmit(&huart5, (uint8_t*)".", 1, HAL_MAX_DELAY);
}
HAL_UART_Transmit(&huart5, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}
Данная функция опрашивает все адреса от 0 до 127 и если с этого адреса поступил ответ, она отправляет номер этого адреса в 16-тиричной форме в UART.
Для общения с платой я использую программу Termite. По умолчанию скорость UART у микроконтроллера устанавливается в значении 115200, необходимо установить такую же в термите. Вызываем функцию в основном теле программы, прошиваем плату и коннектимся в термите к нашему микроконтроллеру:
Точками отображаются все адреса, с которых ответ не был получен. Адрес у моего дисплея 0х26, так как я запаял перемычку А0. Теперь подключим второй дисплей параллельно первому, и посмотрим, что выдаст программа:
Имеем два адреса: 0х26 (дисплей 1602) и 0х27 (дисплей 2004). Теперь о том, как работать с дисплеем. Микроконтроллер посылает байт адреса, а все устройства, подключенные к шине, сверяют его со своим. Если он совпадает, то модуль начинает общение с микроконтроллером. В первую очередь нужно настроить дисплей: откуда будет идти отсчет символов и в какую сторону, как будет вести себя курсор и т.п. После этого уже можно будет передавать дисплею информацию для вывода. Особенность в том, что мы можем использовать только 4 бита для передачи информации, т.е. данные необходимо разбивать на две части. Данные хранятся в старших битах (4-7), а младшие биты используются для указания того, будет ли включена подсветка (3 бит), приходят ли данные для вывода или же настройки работы дисплея (вывод RS, 0 бит), и 2 бит, по изменению которого происходит считывание, т.е чтобы отправить 1 байт данных необходимо отправить 4 байта – 1й байт будет содержать 4 бита информации, 2й бит в состояние 1, 2й байт это повторение 1-го, только уже 2й бит в состояние 0. 3й и 4й байт аналогично, только там содержится вторая половина данных. Звучит немного непонятно, покажу на примере:
void I2C_send(uint8_t data, uint8_t flags)
{
HAL_StatusTypeDef res;
// бесконечный цикл
for(;
{
// проверяем, готово ли устройство по адресу lcd_addr для связи
res = HAL_I2C_IsDeviceReady(&hi2c1, LCD_ADDR, 1, HAL_MAX_DELAY);
// если да, то выходим из бесконечного цикла
if(res == HAL_OK) break;
}
// операция И с 1111 0000 приводит к обнулению бит с 0 по 3, остаются биты с 4 по 7
uint8_t up = data & 0xF0;
// то же самое, но data сдвигается на 4 бита влево
uint8_t lo = (data / 4-7 биты содержат информацию, биты 0-3 настраивают работу дисплея
data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
// дублирование сигнала, на выводе Е в этот раз 0
data_arr[1] = up|flags|BACKLIGHT;
data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
data_arr[3] = lo|flags|BACKLIGHT;
HAL_I2C_Master_Transmit(&hi2c1, LCD_ADDR, data_arr, sizeof(data_arr), HAL_MAX_DELAY);
HAL_Delay(LCD_DELAY_MS);
}
Разберем все по порядку. В начале идут переменные, хранящие в себе адрес дисплея, и биты настроек, которые необходимо отправлять каждый раз вместе с данными. В функции отправки мы в первую очередь проверяем, есть ли по записанному адресу модуль. В случае получения сообщения HAL_OK начинаем формировать байты для отправки. В начале байт, который мы будем отправлять, необходимо разделить на две части, оба из них записать в старшие биты. Допустим, мы хотим, чтобы дисплей отобразил символ ‘s’, в двоичной системе это 1110011 (
Но не спешите загружать программу, ведь в начале необходимо задать настройки дисплею. На
I2C_send(0b00110000,0); // 8ми битный интерфейс
I2C_send(0b00000010,0); // установка курсора в начале строки
I2C_send(0b00001100,0); // нормальный режим работы, выкл курсор
I2C_send(0b00000001,0); // очистка дисплея
Эти команды поместим перед началом бесконечного цикла, чтобы настройки отправлялись единожды перед началом работы (как void setup у ардуинки). Функция I2C_send помимо байта требует указать, будут отправляться настройки дисплея или же данные. Если второй аргумент функции 0, то настройки, а если 1, то данные.
И последний штрих – нужна функция, которая будет отправлять сроку посимвольно. Тут все довольно просто:
void LCD_SendString(char *str)
{
// *char по сути является строкой
// пока строчка не закончится
while(*str)
{
// передача первого символа строки
I2C_send((uint8_t)(*str), 1);
// сдвиг строки налево на 1 символ
str++;
}
}
Собрав все эти функции воедино можно написать:
LCD_SendString(" Hello");
I2C_send(0b11000000,0); // перевод строки
LCD_SendString(" Habr");
Отлично, с дисплеем 1602 разобрались, теперь 2004. Разница между ними минимальная, даже этот код будет отлично работать. Все отличие сводится к организации адресов ячеек на дисплее. В обоих дисплеях память содержит 80 ячеек, в дисплее 1602 первые 16 ячеек отвечают за первую строчку, а за вторую строчку отвечают ячейки с 40 по 56. Остальные ячейки памяти на дисплей не выводятся, поэтому, если отправить на дисплей 17 символов, последний не перенесется на вторую строчку, а будет записан в ячейку памяти, не имеющую выхода на дисплей. Чуть более наглядно, память устроена так:
Для перевода строки я пользовался командой I2C_send(0b11000000,0);, она просто переходит к 40 ячейке. В дисплее 2004 все поинтереснее.
Первая строка — ячейки с 1 по 20
Вторая строка — ячейки с 40 по 60
Третья строка — ячейки с 21 по 40
Четвертая строка — ячейки с 60 по 80,
т.е. если отправить команду
LCD_SendString("___________________1___________________2___________________3___________________4");
Получим следующее:
Для организации переходов между строками необходимо переводить на нужную ячейку памяти курсор вручную, либо можно программно дополнить функцию. Я пока остановился на ручном варианте:
I2C_send(0b10000000,0); // переход на 1 строку
LCD_SendString(" Hello Habr");
I2C_send(0b11000000,0); // переход на 2 строку
LCD_SendString(" STM32 + LCD 1602");
I2C_send(0b10010100,0); // переход на 3 строку
LCD_SendString(" +LCD 2004A");
I2C_send(0b11010100,0); // переход на 4 строку
LCD_SendString(" library HAL");
Результат:
На этом пожалуй все с этими дисплеями, полезные ссылки, благодаря которым я смог во всем этом разобраться:
1) Код во многом посмотрел вот
2) Таблицы для конфигурации дисплея смотрел
3) Порядок действий смотрел
P.S. не забывайте настроить яркость дисплея заранее.
В этой статье я хотел бы рассказать о своем опыте подключения LCD дисплеев к микроконтроллеру STM32 с использованием библиотеки HAL по I2C шине.

Подключать буду дисплей 1602 и 2004. Они оба имеют припаянный I2C адаптер на основе чипа PCF8574T. Отладочной платой выступит Nucleo676ZI, а средой разработки – STM32CubeIDE 1.3.0.
Про принцип работы I2C шины подробно рассказывать не буду, советую заглянуть
You must be registered for see links
и
You must be registered for see links
.Создаем проект, выбираем отладочную плату:

Указываем, что будем использовать I2C1. Также я подключу UART5 для общения с платой, это нужно для получения информации от платы об адресе дисплея.


В этом же окне можно посмотреть номера ножек, к которым подключается дисплей, в моем случае получилось так:

Для начала подключим всего один дисплей, я начну с 1602. Также я подключу известный бывалым ардуинщикам адаптер USB-UART CH340 для получения данных с платы.

Обратите внимание, адаптер подключается RX к TX и TX к RX, перемычка на адаптере стоит на 3.3В

Рассмотрим подробнее работу с микросхемой PCF8574T и дисплеем. Ниже приведена принципиальная схема модуля с дисплеем:

Микросхема PCF8574T по функционалу схожа с регистром сдвига 74hc595 – она получает по I2C интерфейсу байт и присваивает своим выводам (P0-P7) значения соответствующего бита. Рассмотрим какие выводы микросхемы соединены с дисплеем и за что отвечают:
• Вывод Р0 микросхемы соединен с выводом RS дисплея, отвечающего за то, принимает дисплей данные (1) или инструкции по работе дисплея (0);
• Вывод Р1 соединен с R\W, если 1 – запись данных в дисплей, 0 – считывание;
• Вывод Р2 соединен с CS – вывод, по изменению состояния которого идет считывание;
• Вывод Р3 – управление подсветкой;
• Выводы Р4 — Р7 служат для передачи данных дисплею.
К одной I2C шине может быть подключено несколько устройств одновременно. Для того, чтобы можно было обращаться к конкретному устройству, каждое из них имеет свой адрес, для начала выясним его. Если контакты А1, А2 и А3 на плате адаптера не запаяны, то адрес будет скорее всего 0х27, но лучше проверить. Для этого напишем небольшую функцию, которая покажет адреса всех устройств, которые подключены к I2C шине:
void I2C_Scan ()
{
// создание переменной, содержащей статус
HAL_StatusTypeDef res;
// сообщение о начале процедуры
char info[] = "Scanning I2C bus...\r\n";
// отправка сообщения по UART
HAL_UART_Transmit(&huart5, (uint8_t*)info, strlen(info), HAL_MAX_DELAY);
/* &huart5 - адрес используемого UART
* (uint8_t*)info - указатель на значение для отправки
* strlen(info) - длина отправляемого сообщения
* HAL_MAX_DELAY - задержка
*/
// перебор всех возможных адресов
for(uint16_t i = 0; i < 128; i++)
{
// проверяем, готово ли устройство по адресу i для связи
res = HAL_I2C_IsDeviceReady(&hi2c1, i / если да, то
if(res == HAL_OK)
{
char msg[64];
// запись адреса i, на который откликнулись, в строку в виде
// 16тиричного значения:
snprintf(msg, sizeof(msg), "0x%02X", i);
// отправка номера откликнувшегося адреса
HAL_UART_Transmit(&huart5, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
// переход на новую строчку
HAL_UART_Transmit(&huart5, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}
else HAL_UART_Transmit(&huart5, (uint8_t*)".", 1, HAL_MAX_DELAY);
}
HAL_UART_Transmit(&huart5, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY);
}
Данная функция опрашивает все адреса от 0 до 127 и если с этого адреса поступил ответ, она отправляет номер этого адреса в 16-тиричной форме в UART.
Для общения с платой я использую программу Termite. По умолчанию скорость UART у микроконтроллера устанавливается в значении 115200, необходимо установить такую же в термите. Вызываем функцию в основном теле программы, прошиваем плату и коннектимся в термите к нашему микроконтроллеру:

Точками отображаются все адреса, с которых ответ не был получен. Адрес у моего дисплея 0х26, так как я запаял перемычку А0. Теперь подключим второй дисплей параллельно первому, и посмотрим, что выдаст программа:

Имеем два адреса: 0х26 (дисплей 1602) и 0х27 (дисплей 2004). Теперь о том, как работать с дисплеем. Микроконтроллер посылает байт адреса, а все устройства, подключенные к шине, сверяют его со своим. Если он совпадает, то модуль начинает общение с микроконтроллером. В первую очередь нужно настроить дисплей: откуда будет идти отсчет символов и в какую сторону, как будет вести себя курсор и т.п. После этого уже можно будет передавать дисплею информацию для вывода. Особенность в том, что мы можем использовать только 4 бита для передачи информации, т.е. данные необходимо разбивать на две части. Данные хранятся в старших битах (4-7), а младшие биты используются для указания того, будет ли включена подсветка (3 бит), приходят ли данные для вывода или же настройки работы дисплея (вывод RS, 0 бит), и 2 бит, по изменению которого происходит считывание, т.е чтобы отправить 1 байт данных необходимо отправить 4 байта – 1й байт будет содержать 4 бита информации, 2й бит в состояние 1, 2й байт это повторение 1-го, только уже 2й бит в состояние 0. 3й и 4й байт аналогично, только там содержится вторая половина данных. Звучит немного непонятно, покажу на примере:
void I2C_send(uint8_t data, uint8_t flags)
{
HAL_StatusTypeDef res;
// бесконечный цикл
for(;
// проверяем, готово ли устройство по адресу lcd_addr для связи
res = HAL_I2C_IsDeviceReady(&hi2c1, LCD_ADDR, 1, HAL_MAX_DELAY);
// если да, то выходим из бесконечного цикла
if(res == HAL_OK) break;
}
// операция И с 1111 0000 приводит к обнулению бит с 0 по 3, остаются биты с 4 по 7
uint8_t up = data & 0xF0;
// то же самое, но data сдвигается на 4 бита влево
uint8_t lo = (data / 4-7 биты содержат информацию, биты 0-3 настраивают работу дисплея
data_arr[0] = up|flags|BACKLIGHT|PIN_EN;
// дублирование сигнала, на выводе Е в этот раз 0
data_arr[1] = up|flags|BACKLIGHT;
data_arr[2] = lo|flags|BACKLIGHT|PIN_EN;
data_arr[3] = lo|flags|BACKLIGHT;
HAL_I2C_Master_Transmit(&hi2c1, LCD_ADDR, data_arr, sizeof(data_arr), HAL_MAX_DELAY);
HAL_Delay(LCD_DELAY_MS);
}
Разберем все по порядку. В начале идут переменные, хранящие в себе адрес дисплея, и биты настроек, которые необходимо отправлять каждый раз вместе с данными. В функции отправки мы в первую очередь проверяем, есть ли по записанному адресу модуль. В случае получения сообщения HAL_OK начинаем формировать байты для отправки. В начале байт, который мы будем отправлять, необходимо разделить на две части, оба из них записать в старшие биты. Допустим, мы хотим, чтобы дисплей отобразил символ ‘s’, в двоичной системе это 1110011 (
You must be registered for see links
). С помощью логической операции & мы записываем в переменную up = 01110000, т.е. записываем только старшие биты. Младшие биты в начале сдвигаются влево на 4 символа, а потом записываются в переменную lo = 00110000. Дальше мы формируем массив из 4 байт, которые содержат информацию о символе, который необходимо вывести. Теперь к существующим байтам приписываем биты конфигурации (0-3 биты). После этого отправляем байт адреса и 4 байта информации на дисплей с помощью функции HAL_I2C_Master_Transmit();Но не спешите загружать программу, ведь в начале необходимо задать настройки дисплею. На
You must be registered for see links
есть прекрасная переведенная таблица с командами для настройки дисплея. Сверив ее с документацией, я пришел к следующим оптимальным для себя настройкам:I2C_send(0b00110000,0); // 8ми битный интерфейс
I2C_send(0b00000010,0); // установка курсора в начале строки
I2C_send(0b00001100,0); // нормальный режим работы, выкл курсор
I2C_send(0b00000001,0); // очистка дисплея
Эти команды поместим перед началом бесконечного цикла, чтобы настройки отправлялись единожды перед началом работы (как void setup у ардуинки). Функция I2C_send помимо байта требует указать, будут отправляться настройки дисплея или же данные. Если второй аргумент функции 0, то настройки, а если 1, то данные.
И последний штрих – нужна функция, которая будет отправлять сроку посимвольно. Тут все довольно просто:
void LCD_SendString(char *str)
{
// *char по сути является строкой
// пока строчка не закончится
while(*str)
{
// передача первого символа строки
I2C_send((uint8_t)(*str), 1);
// сдвиг строки налево на 1 символ
str++;
}
}
Собрав все эти функции воедино можно написать:
LCD_SendString(" Hello");
I2C_send(0b11000000,0); // перевод строки
LCD_SendString(" Habr");

Отлично, с дисплеем 1602 разобрались, теперь 2004. Разница между ними минимальная, даже этот код будет отлично работать. Все отличие сводится к организации адресов ячеек на дисплее. В обоих дисплеях память содержит 80 ячеек, в дисплее 1602 первые 16 ячеек отвечают за первую строчку, а за вторую строчку отвечают ячейки с 40 по 56. Остальные ячейки памяти на дисплей не выводятся, поэтому, если отправить на дисплей 17 символов, последний не перенесется на вторую строчку, а будет записан в ячейку памяти, не имеющую выхода на дисплей. Чуть более наглядно, память устроена так:

Для перевода строки я пользовался командой I2C_send(0b11000000,0);, она просто переходит к 40 ячейке. В дисплее 2004 все поинтереснее.
Первая строка — ячейки с 1 по 20
Вторая строка — ячейки с 40 по 60
Третья строка — ячейки с 21 по 40
Четвертая строка — ячейки с 60 по 80,
т.е. если отправить команду
LCD_SendString("___________________1___________________2___________________3___________________4");
Получим следующее:

Для организации переходов между строками необходимо переводить на нужную ячейку памяти курсор вручную, либо можно программно дополнить функцию. Я пока остановился на ручном варианте:
I2C_send(0b10000000,0); // переход на 1 строку
LCD_SendString(" Hello Habr");
I2C_send(0b11000000,0); // переход на 2 строку
LCD_SendString(" STM32 + LCD 1602");
I2C_send(0b10010100,0); // переход на 3 строку
LCD_SendString(" +LCD 2004A");
I2C_send(0b11010100,0); // переход на 4 строку
LCD_SendString(" library HAL");
Результат:

На этом пожалуй все с этими дисплеями, полезные ссылки, благодаря которым я смог во всем этом разобраться:
1) Код во многом посмотрел вот
You must be registered for see links
2) Таблицы для конфигурации дисплея смотрел
You must be registered for see links
3) Порядок действий смотрел
You must be registered for see links
You must be registered for see links
P.S. не забывайте настроить яркость дисплея заранее.