Работаем с цветным TFT дисплеем ILI9341

Наконец добрались руки до дисплея. Купил его еще летом, а вот нормально заняться им вышло только сейчас.
Дисплеев на сегодняшний день огромное количество, мой выбор пал на решение «лоу кост». Такой дисплей у китайских друзей стоит в 5-6$.

В общем то ничего необычного. Разрешение всего 320х240, но зато он цветной и с последовательным интерфейсом, что нещадно бъет по скорости обновления, но сильно экономит ноги контролера (и нервы при соединении на макетке).
У данного дисплея есть контролер для обработки команд и обмена с микроконтроллером — ILI9341.

Из него можно узнать набор команд и последовательность данных.
У дисплея есть ноги для обмена по SPI, нога аппаратного сброса и нога, которая помогает ему понять когда принятые данные — команда, а когда — цвет пикселя.
Для всего этого «дергонога» я решил заготовить макросы:

#define TFT_RESET_PIN GPIO_Pin_2
#define TFT_DC_PIN    GPIO_Pin_3
#define TFT_CS_PIN    GPIO_Pin_4

#define TFT_DC_SET    GPIO_SetBits(GPIOA, TFT_DC_PIN);
#define TFT_DC_RESET  GPIO_ResetBits(GPIOA, TFT_DC_PIN);

#define TFT_RST_SET   GPIO_SetBits(GPIOA, TFT_RESET_PIN);
#define TFT_RST_RESET GPIO_ResetBits(GPIOA, TFT_RESET_PIN);

#define TFT_CS_SET    GPIO_SetBits(GPIOA, TFT_CS_PIN);
#define TFT_CS_RESET   GPIO_ResetBits(GPIOA, TFT_CS_PIN);

Этап настройки SPI я пожалуй пропущу. Тут все стандартно, берем настройку из любого примера для вашего камня в режиме 1 (CPOL = 0, CPHA = 0).
Когда это готово, делаем функции посылки команды и данных:

// шлем команду
void LCD_SendCommand(u8 com) {
  TFT_DC_RESET;
  TFT_CS_RESET;
  spi1_send(com);
  TFT_CS_SET;
}

// шлем данные
void LCD_SendData(u8 data) {
  TFT_DC_SET;
  TFT_CS_RESET;
  spi1_send(data);
  TFT_CS_SET;
}

Тут тоже все просто. Для посылки команды нужно чтобы линия DC была в 0, а для данных — в 1. С линией CS поступаем как всегда. Можно в принципе возложить эту ответственность на железо, если ваш контроллер такое умеет.

Инициализация дисплея

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

void LCD_Init(){
  // настраиваем ноги
  LCD_InitGPIO();
  
  TFT_CS_SET;
  spi1_init(); 
  
  // сброс дисплея
  TFT_RST_SET;
  LCD_SendCommand(ILI9341_RESET);
  Delay(100);
  
  /// настраиваем дисплей
  LCD_SendCommand(ILI9341_POWERA);
  LCD_SendData(0x39);
  LCD_SendData(0x2C);
  LCD_SendData(0x00);
  LCD_SendData(0x34);
  LCD_SendData(0x02);
  LCD_SendCommand(ILI9341_POWERB);
  LCD_SendData(0x00);
  LCD_SendData(0xC1);
  LCD_SendData(0x30);
  LCD_SendCommand(ILI9341_DTCA);
  LCD_SendData(0x85);
  LCD_SendData(0x00);
  LCD_SendData(0x78);
  LCD_SendCommand(ILI9341_DTCB);
  LCD_SendData(0x00);
  LCD_SendData(0x00);
  LCD_SendCommand(ILI9341_POWER_SEQ);
  LCD_SendData(0x64);
  LCD_SendData(0x03);
  LCD_SendData(0x12);
  LCD_SendData(0x81);
  LCD_SendCommand(ILI9341_PRC);
  LCD_SendData(0x20);
  LCD_SendCommand(ILI9341_POWER1);
  LCD_SendData(0x23);
  LCD_SendCommand(ILI9341_POWER2);
  LCD_SendData(0x10);
  LCD_SendCommand(ILI9341_VCOM1);
  LCD_SendData(0x3E);
  LCD_SendData(0x28);
  LCD_SendCommand(ILI9341_VCOM2);
  LCD_SendData(0x86);
  LCD_SendCommand(ILI9341_MAC);
  LCD_SendData(0x48);
  LCD_SendCommand(ILI9341_PIXEL_FORMAT);
  LCD_SendData(0x55);
  LCD_SendCommand(ILI9341_FRC);
  LCD_SendData(0x00);
  LCD_SendData(0x18);
  LCD_SendCommand(ILI9341_DFC);
  LCD_SendData(0x08);
  LCD_SendData(0x82);
  LCD_SendData(0x27);
  LCD_SendCommand(ILI9341_3GAMMA_EN);
  LCD_SendData(0x00);
  LCD_SendCommand(ILI9341_COLUMN_ADDR);
  LCD_SendData(0x00);
  LCD_SendData(0x00);
  LCD_SendData(0x00);
  LCD_SendData(0xEF);
  LCD_SendCommand(ILI9341_PAGE_ADDR);
  LCD_SendData(0x00);
  LCD_SendData(0x00);
  LCD_SendData(0x01);
  LCD_SendData(0x3F);
  LCD_SendCommand(ILI9341_GAMMA);
  LCD_SendData(0x01);
  LCD_SendCommand(ILI9341_PGAMMA);
  LCD_SendData(0x0F);
  LCD_SendData(0x31);
  LCD_SendData(0x2B);
  LCD_SendData(0x0C);
  LCD_SendData(0x0E);
  LCD_SendData(0x08);
  LCD_SendData(0x4E);
  LCD_SendData(0xF1);
  LCD_SendData(0x37);
  LCD_SendData(0x07);
  LCD_SendData(0x10);
  LCD_SendData(0x03);
  LCD_SendData(0x0E);
  LCD_SendData(0x09);
  LCD_SendData(0x00);
  LCD_SendCommand(ILI9341_NGAMMA);
  LCD_SendData(0x00);
  LCD_SendData(0x0E);
  LCD_SendData(0x14);
  LCD_SendData(0x03);
  LCD_SendData(0x11);
  LCD_SendData(0x07);
  LCD_SendData(0x31);
  LCD_SendData(0xC1);
  LCD_SendData(0x48);
  LCD_SendData(0x08);
  LCD_SendData(0x0F);
  LCD_SendData(0x0C);
  LCD_SendData(0x31);
  LCD_SendData(0x36);
  LCD_SendData(0x0F);
  LCD_SendCommand(ILI9341_SLEEP_OUT);
  
  Delay(100);
  LCD_SendCommand(ILI9341_DISPLAY_ON);
  LCD_SendCommand(ILI9341_GRAM);
}

Рисуем

Чтобы что-то нарисовать нужно поставить курсор в нужное место:

void LCD_SetCursorPosition(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
        LCD_SendCommand(ILI9341_COLUMN_ADDR);
        LCD_SendData(x1 >> 8);
        LCD_SendData(x1 & 0xFF);
        LCD_SendData(x2 >> 8);
        LCD_SendData(x2 & 0xFF);

        LCD_SendCommand(ILI9341_PAGE_ADDR);
        LCD_SendData(y1 >> 8);
        LCD_SendData(y1 & 0xFF);
        LCD_SendData(y2 >> 8);
        LCD_SendData(y2 & 0xFF);
}

Курсор поставили, можно рисовать:

void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
    LCD_SetCursorPosition(x, y, x, y);
    LCD_SendCommand(ILI9341_GRAM);
    LCD_SendData(color >> 8);
    LCD_SendData(color & 0xFF);
}

Ради интереса можно сохранить картинку в GIMP в формат C файла. И подтянуть его в проект. Там вы увидите массив. Его нужно просто вывести в цикле на экран.

void LCD_Image(unsigned char const *img) {
        uint32_t n;
        
        LCD_SetCursorPosition(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);

        LCD_SendCommand(ILI9341_GRAM);

        for (n = 0; n < LCD_PIXEL_COUNT; n++) {
                u8 color = *img;
                img++;
                LCD_SendData(*img);
                LCD_SendData(color);
                img++;
        }
}

Результат:

Шрифты

Вывести текст на экрна тоже очень просто. Нужно раздобыть массив со шрифтом. Сделать это можно при помощи утилиты BitFontCreator (она крутая, но денег немереных стоит) или же народной приблуды FontsGenerator.exe, которую я нашел на easyelectronics.
В массиве аккуратно уложены битовые маски шрифта. Например имеем 16 пикселей высотой и 8 шириной, тогда нам нужно последовательно выводить по пикселю и при этом следить каким цветом нужно сделать текущий пиксель.

uint32_t LCD_Putchar(uint32_t x, uint32_t y, char c) {
        uint32_t i, j;
        unsigned short Data;
        
        uint32_t offset = (c-32)*TimesNewRoman.height;
        uint16_t width = TimesNewRoman.width;
        
        for (i = 0; i < TimesNewRoman.height; i++) {

            Data = TimesNewRoman.data_table[offset+i];    
            
            
            for (j = 0; j < width; j++) {
                if ((Data << j) & 0x8000) {
                    LCD_DrawPixel(x + j, (y + i), 0xFFFF);  //white
                } else {
                    LCD_DrawPixel(x + j, (y + i), 0x0000);  //black
                }
            }
        }
        
        return x+width;
}

Для описанного примера в массиве будет 16 двухбайтных чисел. В каждом из чисел информация о том, закрашен данный пиксель или нет.
BitFontCreator генерит таблици с шрифтами различной ширины. Т.е. каждая буква имеет различную ширину, это повышает читаемость и делает текст красивее для глаза, но принуждает нас каждый раз смотреть ширину в массиве.

Строку можно выводить обычным методом при помощи указателей:

void LCD_DrawString(uint32_t x, uint32_t y, char *str)
{
    while(*str) {
        x = LCD_Putchar(x,y,*str++);
    }
}

Получается вот так:

Пример можно посмотреть тут: https://www.dropbox.com/s/3q0tvjz2f6j399p/tft_lcd_spi.rar

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий

  1. sva

    Какая частота полной заливки экрана в секунду у вас получилась?

    Ответить
  2. lamazavr

    Не замерял. Но медленно. Порядка двух секунд идет полная перерисовка. Но с частотой не играл.

    Ответить
    1. lamazavr

      Я думаю тормоза могут быть вызваны тем, что вы наверное делаете заливку с помощью функции LCD_DrawPixel.

      Т.е. каждый пиксел в цикле рисуете с ее помощью. И тут получается Нереально много команд, 10 шт только на установку курсора для каждого пикселя. на самом деле если нужно выводить картинку (или большие цифры в виде картинок) или залитый прямоугольник, то его можно выводить построчно, например слева направо, команды

      LCD_SetCursorPosition(x, y, x+(размер картинки по Х), y+1) и LCD_SendCommand(ILI9341_GRAM) даете в начале каждой строки, а потом в цикле рисуете строку двумя командами:

      LCD_SendData(color >> 8);

      LCD_SendData(color & 0xFF);

      у дисплея автоинкремент.

      переменную color меняете цветом пикселя из картинки. Скорость увеличивается в разы.

      вот например переделанная заливка прямоугольника из этой библиотеки как у вас, процесса заливки не заметно теперь вообще:

      void LCD_DrawFilledRectangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color)

      { uint16_t i = 0;

      uint16_t j = 0;

      ILI9341_x = x0;

      ILI9341_y = y0;

      for(i=0;i<(y1-y0);i++){

      ILI9341_SetCursorPosition(ILI9341_x, ILI9341_y + i, x1, ILI9341_y + i+1);

      ILI9341_SendCommand(ILI9341_GRAM);

      for(j=0;j<(x1-x0);j++)

      {

      ILI9341_SendData(color >> 8);

      ILI9341_SendData(color & 0xFF);

      }

      }

      }

      Я ещё себе сделал шрифт 64х100, для электронного спидометра на этом экране, переписал все функции вывода картинок и шрифтов таким же образом, и все теперь летает, вывод цифр очень быстрый.) перерисовки не видно. Но у меня STM32F103, работает на 72 Мгц, SPI — 36 Мгц. Это в дебагере показывает так, сколько на самом деле — х.з. Разогнал его до 128 МГц, обновление цифр стало еще быстрее и плавное — хрен заметишь.)

      Если интересно могу выслать исходники.

      Ответить
      1. lamazavr

        Цикл все равно медленный (на мой вкус).

        Тут https://blablacode.ru/node/513 настроил DMA. Вот только картинку конвертировать нужно иначе.

        Ответить
  3. sva

    Имеет смысл поиграть. Я когда-то брал готовую библиотеку, на частоте SPI 18 МГц экран выдавал 6-7 кадров в секунду. Это уже вполне приемлемо для интерфейса какого-нибудь девайса. В целом, экран вполне годный для изделий, если не гонять на нем видео)

    Ответить
  4. lamazavr

    Согласен. Только вот пока задачу под него не подобрал.

    Ответить
  5. Гость

    Судя из этого

    https://www.youtube.com/watch?feature=player_detailpage&v=fPTaWHccljo

    можно добиться и видео 😉

    Ответить
  6. Иван

    Большая просьба подробнее описать как преобразовать формат jpg в Си (.c и .h). Я использовал программу GIMP 2. Но при выводе изображения на экране получается абракадабра

    Ответить
  7. lamazavr

    Описывать там особо нечего.

    Думаю вам просто нужно перевернуть картинку. Скорее всего вы пытаетесь вывести вертикальное изображение горизонтально.

    Ответить
  8. Анатолий

    Спасибо за код импортировал на AVR заработало почти сразу. Почти потому что кто то написал что нужен режим 1 spi оказалось для AVR нужен режим 3 SPI. При заливке экрана прога зависает. Еще бы n 16 битная а LCD_PIXEL_COUNT=320*240=76800

    void LCD_Fill(uint16_t color) {

    unsigned int n, i, j;

    i = color >> 8;

    j = color & 0xFF;

    LCD_SetCursorPosition(0, 0, LCD_WIDTH — 1, LCD_HEIGHT — 1);
    LCD_SendCommand(ILI9341_GRAM);
    for (n = 0; n < LCD_PIXEL_COUNT; n++) {

    LCD_SendData(i);

    LCD_SendData(j);

    }

    }

    Ответить
  9. radiomanoff

    Здравствуйте.

    Вопрос такого плана. Откуда взялись в инициализации — ILI9341_POWERA (0xCB) и ILI9341_POWERB (0xCF) ,в datasheet на ILI9341 я их в упор невижу.

    Ответить
    1. lamazavr

      Power Control A

      Страница 195. Даташит приложен в статье.

      Ответить
  10. Евгений

    Есть возможность управления яркостью подстветки в таких модулях? У моего подстветка заведена на 5В, отдельного вывода нет. Спасибо за хорошую статью.

    Ответить
    1. lamazavr

      кроме ШИМ ничего в голову не приходит

      Ответить
  11. Iptash

    Что за команда GRAM?

    Ответить
  12. lamazavr

    ILI9341_GRAM — это команда Memory Write

    Ответить
  13. Егор

    А какой программой можно массив с шрифтом сделать? Подскажите, что бы как у вас uint16_t был?

    Ответить
    1. lamazavr

      их целая куча, гуглите по ttf to c. к сожалению большинство или платные или не умеют делать шрифты с разной шириной букв

      Ответить
  14. Андрей

    Скажите чем открывается ваш пример? Заранее спасибо.

    Ответить
    1. lamazavr

      IAR embedded workbench for ARM

      Ответить
  15. cgw

    Статья отстой ничего не понятно что куда написано для тех кто и так уже все знает ,только зачем для них писать неясно. А новички голову ломают недоумевая что куда, а статья говорит ну вы же и так все знаете.

    Ответить
  16. Дмитрий

    Здравствуйте. Подскажите пожалуйста, как выводить переменную информацию, например данные с АЦП?

    Ответить
  17. Сергей123123

    Статья ужас для новичков, нету схемы подключения даже, мало всего расписано! Но в основном из-за схемы, какие провода куда. толку от кода?!

    Ответить
  18. Кирилл

    Медленный вывод получается потому, что сама функция вывода — неправильная. За один раз выводится только один байт данных (а это полпикселя), и в каждом байте дергается CS. А это — неправильно. С таким подходом даже на максимальных частотах интерфейса и ЦП у вас перерисовка дисплея будет со скоростью всего лишь пару кадров в секунду, а то и меньше!

    Поэтому, учитесь работать эффективно! Даже если не используете DMA, только на одних лишь прерываниях модуля SPI — и то можно добиться существенной скорости. Лучше всего конечно DMA — аппаратная передача данных блоком некоторого размера.

    Но если хотите действительно быстрых FPS — используйте параллельный интерфейс, 8-ми или лучше даже 16-битный.

    Ответить
  19. Oleg

    Здравствуйте.

    Подскажите пожалуйста, почему для прорисовки пикселя используется такая функция с четырьмя координатами:

    LCD_SetCursorPosition(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
    Тоесть, местоположение одного пикселя должно описываться только двумя координатами, а не четырьмя. Что реально делает эта функция

    Ответить
    1. lamazavr

      Указывается диапазон пикселей которые можно писать.

      Команда описана на с.110 приложенного к статье даташита.

      Ответить
  20. Oleg

    Спасибо !

    Ответить
  21. cgw

    Кто не будь подключил его на Atmega8/16/32/64…

    Вот 1 человек подключил.

    http://www.vabolis.lt/2013/12/07/avr49-kitas-grafinis-lcd-ili9341/

    https://www.youtube.com/watch?v=1-yzlwWFBjc
    Я сделал так же на 12Mhz кварц. ту же распиновку, залил его *.hex и без результата, 5раз прочел даташит, пробовал на нескольких дисплеях ничего не получается. Все правильно а ничего не происходит на экране.
    Люди поделитесь кодом на atmega плиз, сделайте кто не будь нормальную статью, у кого получилось.

    больше в сети ни одного примера на Atmega нету.

    Ответить
  22. Саша

    Применил для stm32f103c8t6!!!

    Ответить
  23. Дима

    объясните как работает функция, пожалуйста

    void LCD_Image(unsigned char const *img) {

    uint32_t n;
    LCD_SetCursorPosition(0, 0, LCD_WIDTH — 1, LCD_HEIGHT — 1);
    LCD_SendCommand(ILI9341_GRAM);
    for (n = 0; n < LCD_PIXEL_COUNT; n++) {

    u8 color = *img;

    img++;

    LCD_SendData(*img);

    LCD_SendData(color);

    img++;

    }

    }

    Ответить
    1. lamazavr

      Сначала посылается команда установки курсора.

      Затем в цикле данные изображения из массива img выводятся в цикле.

      LCD_PIXEL_COUNT=320*240=76800 — количество пикселей на экране. оно же количество пикселей изображения.

      Изображение при помощи GIMP я конвертировал в C файл и добавил в программу.

      Ответить
  24. Андрей

    Спасибо, искал инфу для старта с этим дисплеем эта статья — то что нужно!

    Ответить
  25. -=Женек=-

    Может кому пригодится мой опыт.

    Китайцы для дисплеев ili9341 с диагональю 2.8 дюйма делают какую-то особую плату, точнее ставят на нее какой-то особый преобразватель питания. У меня дисплей глючил, отказывался запускаться, если не подключен JTAG (да, не запускался именно дисплей), зависал когда отключаешь JTAG и не только — мог зависнуть в любое удобное время.

    Возле преобразователя на плате разведены контактные площадки под 0603 резистор с нулевым сопротивлением, если их соединить пайкой, питание идет непосредственно от источника.

    Я запаял нахрен и все проблемы исчезли.

    Ответить
  26. Nick

    Потому что питать модуль надо от 5 вольт!

    Там стоит преобразователь — он делает из 5 вольт 3.3 вольта.

    Если соединить выводы — закорачиваем вход и выход преобразователя и напряжение идет напрямую. Сделано для питания модуля от 3 вольт.
    Кстати, у меня получалось выжать 12 fps при 16 MHz SPI.

    Ответить
  27. Дмитрий

    Есть у кого кряк или ключ к BitFontCreator, или кто знает еще какую нибудь программу для генерации моноширинных шрифтов, в том числе и русских, в формате 16ти битного массива?

    Ответить
  28. Андрей

    Уже все стали забывать. Старые телевизоры, с ЭЛТ, выводили изображение на экран черезстрочно. При общей частоте кадровой развертки 50гц. реально было 25. И ведь никто не замечал смены кадра. Мозг так устроен, что додумывает недостающие пробелы.

    Ответить
  29. Дмитрий с Москвы

    Странные у вас цифры. Я гоняю подобные дисплеи на ~40 кадров в секунду с лёгкой графикой. Контроллер stm32f103c8t6 в разгоре до 120 мгц. Без разгона или со сложной графикой где-то 24-25 кадров. На фоне успеваю графику спрайтовую обсчитать, кнопки всякие вообще без проблем. Считаем строку — посылаем в дма, считаем следующую пока шлётся и так далее. Эти дисплеи отличные в плане производительности.

    Ответить