Дружим STM32 с LCD дисплеем 1604 по I2C шине (библиотека HAL)
Привет, Хабр!
В этой статье я хотел бы рассказать о своем опыте подключения LCD дисплеев к микроконтроллеру STM32 с использованием библиотеки HAL по I2C шине.
Подключать буду дисплей 1602 и 2004. Они оба имеют припаянный I2C адаптер на основе чипа PCF8574T. Отладочной платой выступит Nucleo767ZI, а средой разработки — 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 << 1, 1, HAL_MAX_DELAY);
// если да, то
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) & 0xF0;
uint8_t data_arr[4];
// 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 (калькулятор). С помощью логической операции & мы записываем в переменную up = 01110000, т.е. записываем только старшие биты. Младшие биты в начале сдвигаются влево на 4 символа, а потом записываются в переменную lo = 00110000. Дальше мы формируем массив из 4 байт, которые содержат информацию о символе, который необходимо вывести. Теперь к существующим байтам приписываем биты конфигурации (0–3 биты). После этого отправляем байт адреса и 4 байта информации на дисплей с помощью функции HAL_I2C_Master_Transmit ();
Но не спешите загружать программу, ведь в начале необходимо задать настройки дисплею. На сайте есть прекрасная переведенная таблица с командами для настройки дисплея. Сверив ее с документацией, я пришел к следующим оптимальным для себя настройкам:
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) Порядок действий смотрел тут
Программа и datasheet
P.S. не забывайте настроить яркость дисплея заранее.