← На главную

Микроконтроллеры STM32: работа с экранчиком 1602 по I2C

Текстовые ЖК-дисплеи с I2C-адапетором являются крайне популярными устройствами среди любителей электроники. Да и в массово производимой технике, от кофе-машин до 3D-принтеров, такие дисплеи встречаются нередко. На AliExpress устройство можно купить за 100 рублей (1.75$) – дешевле вы найдете разве что этот же экранчик без I2C или семисегментные индикаторы. Давайте же разберемся, как подружить такой дисплей с микроконтроллером STM32.

I2C-адаптер представляет собой простенькую плату на базе чипа PCF8574. Этот чип является I/O-расширителем с I2C-интерфейсом. Вот его распиновка (иллюстрация взята из даташита [PDF]):

Распиновка PCF8574

VCC и GND – это, понятно, питание. PCF8574 рассчитан на работу от 2.5 В до 6 В. SDA и SCL – это I2C-шина. Чип имеет адрес 0b0100zyx, где биты x, y и z определяются напряжением на пинах A0, A1 и A2. При низком напряжении соответствующий бит равен нулю, а при высоком равен единице. Таким образом, чип может иметь адрес от 0x20 до 0x27. Существует также модификация чипа PCF8574A, имеющая адрес 0b0111zyx, или от 0x38 до 0x3F. На адаптерах к ЖК-дисплеям для смены адреса обычно предусмотрено место для впаивания перемычек. Пины с P0 по P7 – это те пины, на которые мы будем писать, или с которых мы будем считывать напряжение по I2C. Наконец, пин INT может быть использован, как источник прерывания при изменении напряжения на пинах P0-P7.

Протокол общения с самим экранчиком ранее был рассмотрен в заметке Работаем с LCD на базе HD44780 без библиотек. Что же касается I2C в контексте STM32, с ним мы познакомились в рамках поста Микроконтроллеры STM32: работа с внешним EEPROM. Другими словами, теперь мы обладаем всеми необходимыми знаниями. Осталось только взять шаблон проекта из поста про EEPROM и послать экранчику все те же команды, что в посте про HD44780, только в этот раз – по I2C.

Для начала определим I2C-адрес нашего LCD. В адаптерах обычно используется чип PCF8574T или PCF8574AT (буква T указывает на то, что это SMD-версия чипа), все пины A0-A2 которого подтянуты к плюсу через резисторы на 1 кОм. Другими словами, адрес должен быть либо 0x27 в случае PCF8574T, либо 0x3F в случае PCF8574AT. Но чтобы узнать наверняка, проще всего выполнить такую процедуру:

#include <string.h> void I2C_Scan() { char info[] = "Scanning I2C bus...\r\n"; HAL_UART_Transmit(&huart2, (uint8_t*)info, strlen(info), HAL_MAX_DELAY); HAL_StatusTypeDef res; for(uint16_t i = 0; i < 128; i++) { res = HAL_I2C_IsDeviceReady(&hi2c1, i << 1, 1, 10); if(res == HAL_OK) { char msg[64]; snprintf(msg, sizeof(msg), "0x%02X", i); HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); } else { HAL_UART_Transmit(&huart2, (uint8_t*)".", 1, HAL_MAX_DELAY); } } HAL_UART_Transmit(&huart2, (uint8_t*)"\r\n", 2, HAL_MAX_DELAY); }

Не забываем, что HAL работает с I2C-адресами, сдвинутыми на 1 бит влево:

#define LCD_ADDR (0x27 << 1)

Вот теперь можно и вывести какой-то текст:

#define PIN_RS (1 << 0) #define PIN_EN (1 << 2) #define BACKLIGHT (1 << 3) #define LCD_DELAY_MS 5 HAL_StatusTypeDef LCD_SendInternal(uint8_t lcd_addr, uint8_t data, uint8_t flags) { HAL_StatusTypeDef res; for(;;) { res = HAL_I2C_IsDeviceReady(&hi2c1, lcd_addr, 1, HAL_MAX_DELAY); if(res == HAL_OK) break; } uint8_t up = data & 0xF0; uint8_t lo = (data << 4) & 0xF0; uint8_t data_arr[4]; data_arr[0] = up|flags|BACKLIGHT|PIN_EN; data_arr[1] = up|flags|BACKLIGHT; data_arr[2] = lo|flags|BACKLIGHT|PIN_EN; data_arr[3] = lo|flags|BACKLIGHT; res = HAL_I2C_Master_Transmit(&hi2c1, lcd_addr, data_arr, sizeof(data_arr), HAL_MAX_DELAY); HAL_Delay(LCD_DELAY_MS); return res; } void LCD_SendCommand(uint8_t lcd_addr, uint8_t cmd) { LCD_SendInternal(lcd_addr, cmd, 0); } void LCD_SendData(uint8_t lcd_addr, uint8_t data) { LCD_SendInternal(lcd_addr, data, PIN_RS); } void LCD_Init(uint8_t lcd_addr) { // 4-bit mode, 2 lines, 5x7 format LCD_SendCommand(lcd_addr, 0b00110000); // display & cursor home (keep this!) LCD_SendCommand(lcd_addr, 0b00000010); // display on, right shift, underline off, blink off LCD_SendCommand(lcd_addr, 0b00001100); // clear display (optional here) LCD_SendCommand(lcd_addr, 0b00000001); } void LCD_SendString(uint8_t lcd_addr, char *str) { while(*str) { LCD_SendData(lcd_addr, (uint8_t)(*str)); str++; } } void init() { LCD_Init(LCD_ADDR); // set address to 0x00 LCD_SendCommand(LCD_ADDR, 0b10000000); LCD_SendString(LCD_ADDR, " Using 1602 LCD"); // set address to 0x40 LCD_SendCommand(LCD_ADDR, 0b11000000); LCD_SendString(LCD_ADDR, " over I2C bus"); } void loop() { HAL_Delay(100); }

Проверяем, что все работает:

STM32: работа с экранчиком 1602 по I2C

Где у плат Nucleo находятся какие пины легко гуглится. Лично я для своей Nucleo-F411RE подсмотрел здесь.

В боевом коде вам, вероятно, захочется более строго проверять коды возврата, чем в приведенном коде это делал я. Заметьте также, что по I2C можно управлять не только самим экранчиком, но и его подсветкой. Приведенный код постоянно держит ее включенной, но в каким-то проектах может быть не лишено смысла включать и выключать ее в зависимости от каких-то условий.

Это все, о чем я хотел сегодня рассказать. Полная версия исходников, как обычно, лежит на GitHub. Вопросы и дополнения всячески приветствуются.

Дополнение: Продолжение ищите в посте Отображение произвольных символов на ЖКИ 1602. Вас также могут заинтересовать статьи об OLED-экранчиках на базе SSD1306 и LED-дисплеях на MAX7219.