Top.Mail.Ru

Работа с TFT дисплеями на примере контроллера ILI9341.

Продолжу теорию работы с различным железом. Сейчас рассмотрим TFT-дисплеи, для конкретики рассмотрим дисплей на контроллере ILI9341.

Как устроен дисплей.

Основой дисплея (чаще всего) являются:

  1. TFT-матрица;
  2. Контроллер дисплея;
  3. Интерфейс;
  4. Преобразователи уровня;
  5. Питание.

TFT-матрица.

Прежде чем рассматривать дисплей определимся с цветами. Практически все матрицы - RGB, бывают BGR или что-то иное, но это только порядок следования цветов в слове, которое подаётся на выводы матрицы. Есть два типа матриц...

Первый тип. Матрица имеет разрядность 8-8-8. То есть на матрицу подаётся 8 бит красного, 8 бит зелёного и 8 бит синего, синхросигналы и некоторые управляющие сигналы. Следовательно слово состоит из 24-х бит. Этот тип матрицы имеет уже встроенный контроллер и используется для прямого подключения к микроконтроллеру, который умеет с ними работать. У STM32Lx такие кристаллы обычно в корпусах, имеющих от 48 выводов, в них матрица управляется 6-ти битным интерфейсом и от 100 выводов у STM32Fx с интерфейсом от 16-ти бит и более. Дисплеи такого типа ставятся в основном в смартфонах и планшетах.

Второй тип. Это матрицы, у которых есть выводы строк и столбцов, и задача контроллера - сканировать эти строки/столбцы и поддерживать свечение точек в соответствие с картой памяти внутри контроллера. Именно ILI9341 и дисплеи на его основе относятся к этому типу.

Да-да, я не ошибся, сказав, что слово - 24 бита, так как существуют два понятия "слова". Есть "программное слово", им пользуются программисты, оно может быть от 16-ти до 32-х бит и даже больше - всё зависит от разрядности компьютера или микроконтроллера. И есть "машинное слово", им оперируют железячники, чаще всего его разрядность определяется шириной шины данных. За свою жизнь я встречал микроконтроллеры с разрядностью шины равной 2-м битам, их можно было каскадировать и получать любую разрядность шины данных кратную двум.

В случае следования цветов RGB на контроллер может подаваться три формата цвета:

  1. RGB 4-4-4. На каждую точку 12 бит данных - получаем 4K оттенков;
  2. RGB 5-6-5. На каждую точку 16 бит данных - получаем 65K оттенков;
  3. RGB 6-6-6. На каждую точку 18 бит данных -получаем 262K оттенков.

Правда есть ещё форматы, один из них вообще отпад - RGB 1-1-1. В этом случае в одном байте можно передать цвета для двух точек, соответственно мы имеем 8 цветов на точку. Цифры означают, сколько бит отводится на один цвет. Конвертацию формата цвета, который мы отправляем контроллеру, в формат RGB888 он производит сам.

Матрицы имеют или 3 вертикальных светящихся элемента или 3 горизонтальных, вместе они образуют квадратную точку. Если вывести линию, где каждая точка имеет свой цвет, выглядеть она будет (если присмотреться поближе) немного неровной.

Контроллер дисплея.

Контроллер дисплея занимается получением информации от управляющего МК, хранением её в своей памяти и сканированием TFT-матрицы с засвечиванием необходимых пикселей. Также есть контроллеры, например RA8875, которые кроме всего прочего умеют сами рисовать геометрические фигуры и выводить текст.

Интерфейс.

Интерфейсы дисплеев весьма разнообразны - I2C на мелких дисплеях, SPI, параллельный 8-ми битный и 16-ти битный, самый быстрый - параллельный на 16 бит. Кроме этого есть сигналы:

  • DC - данные/команда. При помощи этого вывода мы говорим контроллеру, что собираемся писать, данные или команду, и что собираемся читать, регистр или данные. Обычно под командой подразумевается регистр контроллера дисплея. Если в регистр что-то писать, то перезапись этого регистра является для контроллера как бы командой, а данные могут быть от одного до нескольких байт, в которых содержится информация для данной команды. Если в качестве команды мы обратимся к регистру адреса внутренней памяти, то последующие данные будут интерпретироваться как яркость и цветность пикселя. При чтении регистра мы можем узнать или режим который нас интересует или цвет точки при обращении к регистру памяти. Но данная возможность есть не у всех дисплеев.
  • RST - сброс контроллера дисплея.
  • CS - выбор кристалла контроллера дисплея, бывает нужно, когда у вас несколько дисплеев. У STM32Fx можно подключить до четырёх по интерфейсу FSMC.

У параллельных интерфейсов добавляются ещё пара сигналов:

  • RD - чтение из контроллера.
  • WR - запись в контроллер.

Правда они и на SPI могут присутствовать, зависит от того, имеет ли контроллер в протоколе маркер операции чтения/записи или нет. Существует два типа передачи данных:

  • Команда. В этом случае DC переключается в "0", и контроллеру передаётся адрес регистра, обычно он 8-ми битный. Затем DC переводится в "1" и посылается от 1 до кучи байт данных, в них указывается режим, за который отвечает данный регистр.
  • Данные. Обычно это данные, которыми заполняется буфер экрана. Начальный адрес имеет длину 16 бит, цвет точки на экране также 16 бит. Если точки идут друг за другом, инкремент счётчика происходит автоматически, достаточно ему вовремя отдавать данные о цвете точки. Однако у ILI9341 немного по-другому - там нет прямого доступа к адресу в памяти, мы это рассмотрим.

Более подробно о протоколах обмена можно узнать из даташита.

Питание и преобразование уровней.

Питание и логические входы контроллера в 90% случаев - 3.3 В. TFT-матрица имеет несколько видов питания, но нам они до лампочки, так как необходимые напряжения вырабатывает или сам контроллер или дополнительный блок питания.

Для того, чтобы у ардуинщика было счастье, используется понижающий стабилизатор с 5 В до 3.3 В и преобразователи логического уровня 5 В <-> 3.3 В. На тех дисплеях, где нет чтения, может использоваться резистивный делитель или токоограничивающие резисторы, пример: дисплей 1.8" на контроллере ST7789. У него питание 5 В, а интерфейс на 3.3 В, можно питать и от 3.3 В, но яркость подсветки будет мала.

И теперь от общего описания можно перейти конкретно к программированию.

Изначально нам нужно во все управляющие регистры прошить константы, так как TFT-матрицы имеют разный размер и производители дисплеев тоже разные. Эти константы характерны для каждой матрицы, но драйвера под многие дисплеи уже написаны и значения можно брать оттуда. Регистров там туча, около 250, и описать их полностью на русском языке, никто ещё не заморочился. Вот пример инициализации 3.2" дисплея на ILI9341 имеющего матрицу 320х240 точек:

WR_REG(0x01);
WR_DATA(0x00);

WR_REG(0xCB);  // Управление питанием матрицы - A
WR_DATA(0x39);
WR_DATA(0x2C);
WR_DATA(0x00);
WR_DATA(0x34);
WR_DATA(0x02);

WR_REG(0xCF);  // Управление питанием матрицы - B
WR_DATA(0x00);
WR_DATA(0xC1);
WR_DATA(0X30);

WR_REG(0xE8);  // ВременнЫе значения контроллера A
WR_DATA(0x85);
WR_DATA(0x00);
WR_DATA(0x78);

WR_REG(0xEA);  // ВременнЫе значения контроллера B
WR_DATA(0x00);
WR_DATA(0x00);

WR_REG(0xED);  // Последовательность команд управления питанием
WR_DATA(0x64);
WR_DATA(0x03);
WR_DATA(0X12);
WR_DATA(0X81);

WR_REG(0xF7);  // Pump Ratio
WR_DATA(0x20);

WR_REG(0xC0);  // Управление питанием
WR_DATA(0x23); // VRH[5:0]

WR_REG(0xC1);  // Управление питанием
WR_DATA(0x10); // SAP[2:0];BT[3:0]

WR_REG(0xC5);  // VCM контроль 1
WR_DATA(0x3E); //3F
WR_DATA(0x28); //3C

WR_REG(0xC7);  // VCM контроль 2
WR_DATA(0X86);

WR_REG(0x36);  // Тип доступа к памяти
WR_DATA(0x48); // Ориентация дисплея - вертикальная

WR_REG(0x3A);  // Формат пикселя
WR_DATA(0x55); // 16 бит входной, 16 бит выходной

WR_REG(0xB1);  // Частота кадров, стандартный цвет RGB
WR_DATA(0x00);
WR_DATA(0x18);

WR_REG(0xB6);  // Display Function Control
WR_DATA(0x08);
WR_DATA(0x82);
WR_DATA(0x27);

WR_REG(0xF2);  // Отключаем гамма-коррекцию
WR_DATA(0x00);

WR_REG(0x26);  // Выбор кривой гамма-коррекции
WR_DATA(0x01);

WR_REG(0xE0);  // Гамма коррекция позитивного изображения
WR_DATA(0x0F);
WR_DATA(0x31);
WR_DATA(0x2B);
WR_DATA(0x0C);
WR_DATA(0x0E);
WR_DATA(0x08);
WR_DATA(0x4E);
WR_DATA(0XF1);
WR_DATA(0x37);
WR_DATA(0x07);
WR_DATA(0x10);
WR_DATA(0x03);
WR_DATA(0x0E);
WR_DATA(0x09);
WR_DATA(0x00);

WR_REG(0XE1);  // Гамма коррекция негативного изображения
WR_DATA(0x00);
WR_DATA(0x0E);
WR_DATA(0x14);
WR_DATA(0x03);
WR_DATA(0x11);
WR_DATA(0x07);
WR_DATA(0x31);
WR_DATA(0xC1);
WR_DATA(0x48);
WR_DATA(0x08);
WR_DATA(0x0F);
WR_DATA(0x0C);
WR_DATA(0x31);
WR_DATA(0x36);
WR_DATA(0x0F);

WR_REG(0x11);  // Выход из сна

delay(120);

WR_REG (CMD_DISPLAY_ON); // Включение дисплея

Где WR_REG() - запись регистра, WR_DATA() - запись данных в него.

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

Сейчас же рассмотрим кое-что поинтереснее - как на дисплее отображается информация. Каждая из 76800 точек имеет свой адрес или последовательный доступ через специальный регистр, куда в режиме RGB565 нужно записать 16-ти битное число, в котором закодирован цвет. Матрица разбита на 240 столбцов с адресами 0х0000 - 0х00EF и 320 строк с адресами 0x0000 - 0x013F, нумерация идёт с левого верхнего угла:

Чтобы зажечь точку по какой-то координате, нужно указать или её адрес или координаты окна вывода, у разных контроллеров по-разному. Конкретно у ILI9341 задаются координаты окна вывода, выглядеть это будет так:

WR_REG(CMD_SET_X);  // Запись команды в регистр координат X
WR_DATA(x >> 8);    // Запись старшего байта координаты X
WR_DATA(x & 0xFF);  // Запись младшего байта координаты X
WR_DATA(x >> 8);
WR_DATA(x & 0xFF);

WR_REG(CMD_SET_Y);  // Запись команды в регистр координат Y
WR_DATA(y >> 8);    // Запись старшего байта координаты Y
WR_DATA(y & 0xFF);  // Запись младшего байта координаты Y
WR_DATA(y >> 8);
WR_DATA(y & 0xFF);

Где CMD_SET_X = 0x2A - регистр адреса столбца (координата X), а CMD_SET_Y = 0x2B - регистр адреса строки (координата Y), причём передаётся два значения: 16 бит адреса начального столбца и 16 бит адреса конечного столбца - координата X. Также 16 бит начальной строки и 16 бит конечной строки - координата Y. А так как у нас засвечивается 1 точка, начало координат совпадает с концом координат, что в строках, что в столбцах. Если теперь выполнить следующие команды, то изменим цвет одного пикселя на красный:

#define RED    0b1111100000000000

WR_REG(0x2C);         // Команда записи в память
WR_DATA(RED >> 8);    // Первым пишем старший байт цвета
WR_DATA(RED & 0xFF);  // Вторым пишем младший байт цвета

Такое справедливо для интерфейсов I2C, SPI и 8-ми битного параллельного интерфейса, правда у SPI и I2C, если вы программируете STM32, есть небольшая хитрость. Если данные для контроллера идут младшим битом вперёд, можно перевести их в 16-ти битный режим, для команды незначащие биты просто проигнорируются, а для данных выведутся на экран (если что, регистры этого контроллера 18-ти битные, поэтому цвет можно выводить в режиме RGB666). Но для этого случая придётся передавать информацию побайтно, по три байта на точку.

Таким образом, если координата точки у нас X = 3, Y = 2, после команды "записать в память" двух байтов цвета у нас включится одна точка:

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

Вывод вертикальных линий, горизонтальных линий и закрашенных прямоугольников делается очень просто. Для вертикальных линий указываем координату по оси Х два раза одну и ту же. Для оси Y координату верхней точки и нижней, а потом пишем нужный нам цвет столько раз, какова разница между координатой верха и низа плюс 1.

Допустим X = 1, Yнач = 0, Yкон = 3, длина L = (Yкон - Yнач) + 1 = (3 - 0) + 1. Единица прибавляется, так как нумерация адресов идёт с нуля, и чисто математически мы получим длину линии равную трём, когда она у нас 4 точки:

#define RED    0b1111100000000000
#define X      0x01
#define Y1     0
#define Y2     3

WR_REG(CMD_SET_X);      // Запись команды в регистр
WR_DATA(X >> 8);        // Запись координаты X старший разряд
WR_DATA(X & 0xFF);      // Запись координаты X младший разряд
WR_DATA(X >> 8);
WR_DATA(X & 0xFF);

WR_REG(CMD_SET_Y);
WR_DATA(Y1 >> 8);       // Координата Y начала линии
WR_DATA(Y1 & 0xFF);     // Координата Y конца линии
WR_DATA(Y2 >> 8);
WR_DATA(Y2 & 0xFF);

WR_REG(0x2C);           // Команда записи в память

for(uint8_t i = 0; i < 4; i++)
{
  WR_DATA(RED >> 8);    // Первым пишем старший байт цвета
  WR_DATA(RED & 0xFF);  // Вторым пишем младший байт цвета
}

Для горизонтальных линий нам нужны две координаты X и одна Y, код будет тот же самый, что и раньше, только у нас координат X теперь две, а Y одна. Для отрисовки закрашенного прямоугольника нужно две координаты X и две координаты Y, количество точек будет уже ((Xкон - Xнач) + 1) * ((Yкон - Yнач) + 1). Остальные фигуры рисуются сложнее, но пока на этом всё, продолжение следует.

Даташит на ILI9341.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x