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

21 марта 2018

Текстовые ЖК-дисплеи с 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.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.