Текстовые ЖК-дисплеи с I2C-адапетором являются крайне популярными устройствами среди любителей электроники. Да и в массово производимой технике, от кофе-машин до 3D-принтеров , такие дисплеи встречаются нередко. На AliExpress устройство можно купить за 100 рублей (1.75$) — дешевле вы найдете разве что этот же экранчик без I2C или семисегментные индикаторы. Давайте же разберемся, как подружить такой дисплей с микроконтроллером STM32.
I2C-адаптер представляет собой простенькую плату на базе чипа PCF8574. Этот чип является I/O-расширителем с I2C-интерфейсом. Вот его распиновка (иллюстрация взята из даташита [PDF] ):
VCC и GND — это, понятно, питание. PCF8574 рассчитан на работу от 2.5 В до 6 В. SDA и SCL — это I2C-шина. Чип имеет адрес 0b0100zyx, где биты x, y и z определяются напряжением на пинах A0, A1 и A2. При низком напряжении соответствующий бит равен нулю, а при высоком равен единице. Таким образом, чип может иметь адрес от 0 x 20 до 0 x 27. Существует также модификация чипа PCF8574A, имеющая адрес 0b0111zyx, или от 0 x 38 до 0 x 3F. На адаптерах к ЖК-дисплеям для смены адреса обычно предусмотрено место для впаивания перемычек. Пины с P0 по P7 — это те пины, на которые мы будем писать, или с которых мы будем считывать напряжение по I2C. Наконец, пин INT может быть использован, как источник прерывания при изменении напряжения на пинах P0-P7.
Протокол общения с самим экранчиком ранее был рассмотрен в заметке Работаем с LCD на базе HD44780 без библиотек . Что же касается I2C в контексте STM32, с ним мы познакомились в рамках поста Микроконтроллеры STM32: работа с внешним EEPROM . Другими словами, теперь мы обладаем всеми необходимыми знаниями. Осталось только взять шаблон проекта из поста про EEPROM и послать экранчику все те же команды, что в посте про HD44780, только в этот раз — по I2C.
Для начала определим I2C-адрес нашего LCD. В адаптерах обычно используется чип PCF8574T или PCF8574AT (буква T указывает на то, что это SMD-версия чипа), все пины A0-A2 которого подтянуты к плюсу через резисторы на 1 кОм. Другими словами, адрес должен быть либо 0 x 27 в случае PCF8574T, либо 0 x 3F в случае PCF8574AT. Но чтобы узнать наверняка, проще всего выполнить такую процедуру:
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 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, 5×7 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 ) ;
}
Проверяем, что все работает:
Где у плат Nucleo находятся какие пины легко гуглится. Лично я для своей Nucleo-F411RE подсмотрел здесь .
В боевом коде вам, вероятно, захочется более строго проверять коды возврата, чем в приведенном коде это делал я. Заметьте также, что по I2C можно управлять не только самим экранчиком, но и его подсветкой. Приведенный код постоянно держит ее включенной, но в каким-то проектах может быть не лишено смысла включать и выключать ее в зависимости от каких-то условий.
Это все, о чем я хотел сегодня рассказать. Полная версия исходников, как обычно, лежит на GitHub . Вопросы и дополнения всячески приветствуются.
Дополнение: Продолжение ищите в посте Отображение произвольных символов на ЖКИ 1602 . Вас также могут заинтересовать статьи об OLED-экранчиках на базе SSD1306 и LED-дисплеях на MAX7219 .