Использование TFT-дисплеев на базе ILI9341 с тачскрином

25 июня 2018

Некоторое время назад мы познакомились с цветными TFT-дисплеями на базе контроллера ST7735. Дисплеи эти весьма неплохи, но не лишены недостатков. В частности, максимальная диагональ таких дисплеев составляет 1.8 дюйма, а разрешение ограничено 128x160 пикселями, что может подходить не для всех проектов. Поэтому сегодня речь пойдет о TFT-дисплеях на базе ILI9341, имеющих среди прочих преимуществ больший размер и большее разрешение.

Такие дисплеи, как правило, продаются в виде модулей с SPI-интерфейсом, имеющих разъем для SD-карт (полноразмерных, не MicroSD). На eBay можно найти модули с диагональю 2.2", 2.4" и 2.8". Независимо от диагонали, дисплеи на базе ILI9341 всегда имеют разрешение 240x320. Большинство модулей имеют резистивную сенсорную панель, но также встречаются модули и без нее. Наличие или отсутствие тачскрина несложно определить чисто визуально. Цена одного модуля составляет 7-9$.

ILI9341: как отличить модуль с тачскрином от модуля без тачскрина

Fun fact! На eBay также доступны модули на базе ILI9341 с диагональю 3.2". Но в рамках данной статьи они не рассматриваются. Эти модули, как правило, не разведены для работы по SPI, и я не думаю, что буду использовать их в своих проектах. Дело в том, что цена на такие модули довольно высока (16$ или больше), а разница между 2.8" и 3.2" не настолько значительна.

Протокол, по которому работают данные модули, идентичен протоколу ST7735. Отличается только последовательность команд, которую необходимо выполнить для инициализации дисплея. Я лично подглядел эту последовательность в репозитории martnak/STM32-ILI9341. Кстати, на вид это довольно адекватная библиотека для STM32 на базе HAL. Что, впрочем, не помешало мне сделать собственную библиотеку с интерфейсом, аналогичным интерфейсу моей библиотеки для ST7735. Для Arduino же можно рекомендовать библиотеку Adafruit_ILI9341, а также связку UTFT и URTouch. Библиотека от Adafruit работает заметно быстрее UTFT. Но в отличие от UTFT, она написана специально для модулей производства Adafruit и, похоже, несовместима с тачскринами китайских модулей.

Важно! ILI9341 не понимает 5-и вольтовую логику от слова совсем. Если вы собираетесь использовать его из 5-и вольтовой Arduino, используйте ковертер уровня, например на базе TXS0108E (даташит [PDF]). Иначе ничего не будет работать. STM32 использует 3.3 вольтовую логику, поэтому для этих микроконтроллеров ковертер уровня не нужен.

Контроллер тачскрина на имеющихся у меня модулях имеет маркировку HR2046. Он также работает по SPI, притом протокол весьма незамысловат. Есть две команды — read X (0xD0) и read Y (0x90). В ответ на каждую команду контроллер посылает 16 бит (число от 0 до 65535), соответствующих координате X или Y точки, которую нажал пользователь. Нажат ли сейчас тачскрин можно определить по состоянию дополнительного пина IRQ. Если он имеет низкое напряжение, значит тачскрин нажат, иначе пользователь его не касается.

Весь код работы с сенсорной панелью выглядит так:

#define READ_X 0xD0
#define READ_Y 0x90

static void ILI9341_TouchSelect() {
    HAL_GPIO_WritePin(ILI9341_TOUCH_CS_GPIO_Port,
        ILI9341_TOUCH_CS_Pin, GPIO_PIN_RESET);
}

void ILI9341_TouchUnselect() {
    HAL_GPIO_WritePin(ILI9341_TOUCH_CS_GPIO_Port,
        ILI9341_TOUCH_CS_Pin, GPIO_PIN_SET);
}

bool ILI9341_TouchPressed() {
    return HAL_GPIO_ReadPin(ILI9341_TOUCH_IRQ_GPIO_Port,
               ILI9341_TOUCH_IRQ_Pin) == GPIO_PIN_RESET;
}

bool ILI9341_TouchGetCoordinates(uint16_t* x, uint16_t* y) {
    static const uint8_t cmd_read_x[] = { READ_X };
    static const uint8_t cmd_read_y[] = { READ_Y };
    static const uint8_t zeroes_tx[] = { 0x00, 0x00 };

    ILI9341_TouchSelect();

    uint32_t avg_x = 0;
    uint32_t avg_y = 0;
    uint8_t nsamples = 0;
    for(uint8_t i = 0; i < 16; i++) {
        if(!ILI9341_TouchPressed())
            break;

        nsamples++;

        HAL_SPI_Transmit(&ILI9341_TOUCH_SPI_PORT,
            (uint8_t*)cmd_read_y, sizeof(cmd_read_y), HAL_MAX_DELAY);
        uint8_t y_raw[2];
        HAL_SPI_TransmitReceive(&ILI9341_TOUCH_SPI_PORT,
            (uint8_t*)zeroes_tx, y_raw, sizeof(y_raw), HAL_MAX_DELAY);

        HAL_SPI_Transmit(&ILI9341_TOUCH_SPI_PORT,
            (uint8_t*)cmd_read_x, sizeof(cmd_read_x), HAL_MAX_DELAY);
        uint8_t x_raw[2];
        HAL_SPI_TransmitReceive(&ILI9341_TOUCH_SPI_PORT,
            (uint8_t*)zeroes_tx, x_raw, sizeof(x_raw), HAL_MAX_DELAY);

        avg_x += (((uint16_t)x_raw[0]) << 8) | ((uint16_t)x_raw[1]);
        avg_y += (((uint16_t)y_raw[0]) << 8) | ((uint16_t)y_raw[1]);
    }

    ILI9341_TouchUnselect();

    if(nsamples < 16)
        return false;

    uint32_t raw_x = (avg_x / 16);
    if(raw_x < ILI9341_TOUCH_MIN_RAW_X)
        raw_x = ILI9341_TOUCH_MIN_RAW_X;
    if(raw_x > ILI9341_TOUCH_MAX_RAW_X)
        raw_x = ILI9341_TOUCH_MAX_RAW_X;

    uint32_t raw_y = (avg_y / 16);
    if(raw_y < ILI9341_TOUCH_MIN_RAW_X)
        raw_y = ILI9341_TOUCH_MIN_RAW_Y;
    if(raw_y > ILI9341_TOUCH_MAX_RAW_Y)
        raw_y = ILI9341_TOUCH_MAX_RAW_Y;

    // Uncomment this line to calibrate touchscreen:
    // UART_Printf("raw_x = %d, raw_y = %d\r\n", x, y);

    *x = (raw_x - ILI9341_TOUCH_MIN_RAW_X) * ILI9341_TOUCH_SCALE_X /
         (ILI9341_TOUCH_MAX_RAW_X - ILI9341_TOUCH_MIN_RAW_X);
    *y = (raw_y - ILI9341_TOUCH_MIN_RAW_Y) * ILI9341_TOUCH_SCALE_Y /
         (ILI9341_TOUCH_MAX_RAW_Y - ILI9341_TOUCH_MIN_RAW_Y);

    return true;
}

При использовании тачскрина следует учитывать, что для его надежной работы частота SPI-шины не должна превышать 1 МГц, иначе координаты начинают приходить с существенными искажениями. Но с дисплеем по тому же SPI вы наверняка захотите работать на большей частоте. Одно из решений заключается в том, чтобы использовать отдельные SPI-шины. Если же этого хочется избежать, частоту шины можно менять динамически:

void loop() {
    if(HAL_SPI_DeInit(&hspi1) != HAL_OK) {
        UART_Printf("HAL_SPI_DeInit failed!\r\n");
        return;
    }  

    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;

    if(HAL_SPI_Init(&hspi1) != HAL_OK) {
        UART_Printf("HAL_SPI_Init failed!\r\n");
        return;
    }

    /* ... Пропущено: тут проходит тест дисплея,
           полностью аналогичный тесту ST7735 ... */


    if(HAL_SPI_DeInit(&hspi1) != HAL_OK) {
        UART_Printf("HAL_SPI_DeInit failed!\r\n");
        return;
    }

    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128;

    if(HAL_SPI_Init(&hspi1) != HAL_OK) {
        UART_Printf("HAL_SPI_Init failed!\r\n");
        return;
    }

    int npoints = 0;
    while(npoints < 10000) {
        uint16_t x, y;

        if(ILI9341_TouchGetCoordinates(&x, &y)) {
            ILI9341_DrawPixel(x, 320-y, ILI9341_WHITE);
            npoints++;
        }
    }
}

Приведенный отрывок кода превращает дисплей в рисовалку:

Рисовалка на базе ILI9341

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

Кстати, вы можете заметить, что на приведенном фото я использовал тот же шилд, что был использован для ST7735. Это возможно благодаря тому, что модули на базе ST7735 и ILI9341 имеют совместимое расположение пинов. Понадобилось только добавить на шилд дополнительные гнезда для тачскрина. Правда, из-за нехватки места на плате, гнезда мне пришлось повесить в воздухе, зафиксировав их термоклеем.

Полную версию исходников к этой заметке вы найдете на GitHub. Наиболее же полную информацию об ILI9341 можно найти в его даташите [PDF].

А доводилось ли вам использовать ILI9341 и если да, то в каких проектах? Интересно также, что вам нравится больше — использовать стилус с тачскрином, или же обычные кнопки, потенциометры и роторные энкодеры?

Метки: , .

Подпишись через RSS, Google+, Facebook, ВКонтакте или Twitter!

Понравился пост? Поделись с другими: