Модуль, который мы рассмотрим сегодня, сочетает в себе свойства двух линеек подобных устройств. С одной стороны, размеры его дисплея всё ещё позволяют использовать протокол SPI без существенного ущерба для картинки на экране. В этом он похож на модули с небольшим дисплеем (к примеру, на базе контроллера ILI9163 или рассмотренного в предыдущей части статьи ST7735). Кстати, из предлагаемых на AliExpress и eBay модулей на базе ILI9341 с протоколом SPI – значительное большинство.
С другой стороны, не редки реализации этого модуля на протоколе 8080, как 8-ми так и 16-битном, что роднит его со второй группой, в которую входит и модуль на базе контроллера ILI9481, о котором мы поговорим в следующей части статьи.
Помимо этого, принцип работы и система команд ILI9341 и контроллеров из вышеуказанных групп настолько схожи (иногда – вплоть до совпадения кодов команд), что, разобравшись, как работает этот модуль, вы без особых усилий сможете запустить и многие другие.
Содержание статьи / Table Of Contents
↑ Характеристики модуля на ILI9341
• Размеры модуля: 52мм х 78мм х 12мм.• Диагональ 2.8 или 3.2 дюйма. Тут есть некоторая путаница, свойственная китайским производителям. На моём модуле написано, что диагональ – 2.8, а измерения показывают 3.2 дюйма. Но, для того, чтобы приведённый в архиве код работал, важна не диагональ, а количество пикселей, а оно для обоих модулей, указанных в ссылках выше, одинаково.
• Разрешение: 240х320 пикселей.
• Цветность: 65 тысяч цветов в формате RGB 5-6-5.
• Напряжение питания: 3.3В – 5В.
• Протокол обмена данными: SPI, 8- или 16-битный параллельный интерфейс 8080.
• Сенсорная панель.
• Встроенный разъём для SSD-карты.
↑ Назначение выводов модуля на ILI9341
Для модуля с протоколом SPI:• SCK, MOSI – линии протокола SPI. Альтернативные названия – DO/DI, SCL/SDA. Вывод MISO отсутствует.
• CS – выбор чипа. Активный уровень – низкий.
• DC – выбор типа записываемого в модуль слова: данные («1») или команда («0»). Возможны и иные обозначения этого вывода – A0, RS.
• RESET – аппаратный сброс. В коде, прилагаемом к статье, предусмотрен выбор между аппаратным и программным сбросом, так что на этом выводе можно сэкономить, подтянув его к питанию. Однако, следует иметь в виду, что для некоторых регистров контроллера значения по аппаратному и программному сбросу не совпадают.
• VCC, GND – линии питания контроллера. Диапазон напряжения – от 3.3 В до 5 В.
• LED (или BL) – питание дисплея. Максимально допустимое значение подаваемого на этот вывод напряжения – 3.3В.
Для модуля с 8 – или 16-битным протоколом 8080:
• D0-D7 (8-битный протокол) или D0-D15 (16-битный протокол) – шина данных.
• RST – аппаратный сброс. Как и в предыдущем случае можно соединить этот вывод с питанием и использовать программный сброс.
• CS – выбор чипа. Активный уровень – низкий.
• DC – выбор типа записываемого в модуль слова – данные или команда. Альтернативные названия – A0, RS.
• WR – управление записью в ILI9341.
• RD – управление чтением из ILI9341.
• VDD (или VCC, 5V), GND – линии питания контроллера.
• LED (или BL, 3V3) – питание дисплея.
↑ Протокол обмена данными SPI
Особенности этого протокола, как программного, так и аппаратного, были подробно рассмотрены в статье «Беспроводной канал связи 2,4 ГГц на базе трансивера nRF24L01+ от Nordic Semiconductor», поэтому приведу содержимое файлов, обеспечивающих работу SPI, отметив лишь, что:1. Мы будем использовать программный вариант протокола без линии MISO, в связи с отсутствием в модуле одноименного вывода.
2. Для выбора программного сброса нужно закомментировать строку #define RESET_HW в файле spi.h, не забыв подтянуть к питанию сам вывод RESET модуля.
3. Порт B определён в файле spi.h дважды (как CONTROL_PORT и ILI9341_PORT), а в файле spi.с названия функций SPI_init() и SPI_byte() изменены на ili9341_pins_init() и write_byte(), соответственно. Сделано это с целью адаптации библиотеки под протокол 8080. Более подробно об этом – чуть позже.
Файл spi.h
Файл spi.c
↑ 8-битный параллельный протокол обмена данными 8080
Этот протокол даёт нам 8-кратное, по сравнению со SPI, увеличение скорости обмена. Кроме того, модули в этой реализации уже позволяют читать содержимое своих регистров. Соответственно вырастут и аппаратные издержки: понадобится уже почти полных два порта МК (один – для управляющих выводов, второй – для шины данных). В приведённом в архиве примере для этого выбраны порты C и D.#define CONTROL_PORT PORTC
#define CONTROL_DDR DDRC
#define RD_pin PC0
#define WR_pin PC1
#define DC_pin PC2
#define CS_pin PC3
// Для выбора программного сброса закомментируйте следующую строку,
// а вывод RESET модуля подтяните к питанию
#define RESET_HW
#ifdef RESET_HW
#define RESET_HW_pin PC4
#define RESET_HW_DELAY 10
#endif
#define LCD_PORT PORTD
#define LCD_DDR DDRD
#define LCD_PIN PIND
#define LCD_D0_pin PD0
#define LCD_D1_pin PD1
#define LCD_D2_pin PD2
#define LCD_D3_pin PD3
#define LCD_D4_pin PD4
#define LCD_D5_pin PD5
#define LCD_D6_pin PD6
#define LCD_D7_pin PD7
Сэкономим время, которое тратится на вызов обычных функций и возврат из них и оформим процедуры записи/чтения байта как макрофункции, тем самым ещё немного увеличив скорость обмена.
Запись байта в ILI9341 производится следующим образом:
1. Выставляем на шину данных записываемый байт.
2. Обеспечиваем переход «низкий - высокий» на выводе WR.
#define write_byte(_byte) \
{ \
LCD_PORT = _byte; \
cbi(CONTROL_PORT, WR_pin); \
sbi(CONTROL_PORT, WR_pin); \
}
Для чтения байта из ILI9341 необходимо:
1. Обеспечить на выводе RD переход «высокий - низкий», дающий понять ILI9341, что пора выставлять байт на шину данных.
2. Выждать около 10 мкс.
3. Принять из регистра PIN МК в заранее объявленную переменную считанный байт.
4. Вернуть вывод RD в высокое состояние.
#define read_byte(_byte) \
{ \
cbi(CONTROL_PORT, RD_pin); \
_delay_us(10); \
_byte = LCD_PIN; \
sbi(CONTROL_PORT, RD_pin); \
}
Поскольку порт данных будет использоваться и для чтения, и для записи, пропишем макрофункции смены направления его работы:
#define WRITE_DIR 0xFF
#define READ_DIR 0x00
#define set_write_dir() \
{ \
LCD_DDR = WRITE_DIR; \
}
#define set_read_dir() \
{ \
LCD_DDR = READ_DIR; \
}
Собственно, это - и есть почти полное содержание файла parallel_8.h:
Файл parallel_8.с содержит одну единственную функцию – инициализации портов и выводов, участвующих в протоколе обмена.
↑ 16-битный параллельный протокол обмена данными 8080
Подключение модуля, работающего на этом протоколе, требует уже три порта МК: один - для управляющих выводов и два – для шины данных. При этом, все 16 битов шины данных используются только при обращении к ОЗУ (GRAM), входящего в состав ILI9341.Во всех остальных случаях (запись команды, запись/чтение параметров) обмен между МК и модулем осуществляется, как и в 8-битном протоколе, с помощью 8 битов - через младший байт шины данных. Если перевести всё это на человеческий язык, то получится следующее: на настройку модулей с 16-битным протоколом уходит столько же времени, что и с 8-битным, но, картинка на экран выводится в два раза быстрее.
Поскольку отличия между 8- и 16-битным протоколами, как выяснилось, небольшие, возьмём за основу файлы 8-битного протокола и внесём необходимые дополнения. Чтобы свести их число к минимуму примем порт D за младший байт шины данных, а в качестве старшего добавим порт B.
#define CONTROL_PORT PORTC
#define CONTROL_DDR DDRC
#define RD_pin PC0
#define WR_pin PC1
#define DC_pin PC2
#define CS_pin PC3
// Для выбора программного сброса закомментируйте следующую строку,
// а вывод RESET модуля подтяните к питанию
#define RESET_HW
#ifdef RESET_HW
#define RESET_HW_pin PC4
#define RESET_HW_DELAY 10
#endif
#define LCD_PORT PORTD // теперь это – порт младшего байта шины данных
#define LCD_DDR DDRD
#define LCD_PIN PIND
#define LCD_D0_pin PD0
#define LCD_D1_pin PD1
#define LCD_D2_pin PD2
#define LCD_D3_pin PD3
#define LCD_D4_pin PD4
#define LCD_D5_pin PD5
#define LCD_D6_pin PD6
#define LCD_D7_pin PD7
#define LCD_HIGH_PORT PORTB // а это – порт старшего байта шины данных
#define LCD_HIGH_DDR DDRB
#define LCD_HIGH_PIN PINB
#define LCD_D8_pin PB0
#define LCD_D9_pin PB1
#define LCD_D10_pin PB2
#define LCD_D11_pin PB3
#define LCD_D12_pin PB4
#define LCD_D13_pin PB5
#define LCD_D14_pin PB6
#define LCD_D15_pin PB7
Тогда, макрофункции записи/чтения одного байта останутся без изменения,
#define write_byte(_byte) \
{ \
LCD_PORT = _byte; \
cbi(CONTROL_PORT, WR_pin); \
sbi(CONTROL_PORT, WR_pin); \
}
#define read_byte(_byte) \
{ \
cbi(CONTROL_PORT, RD_pin); \
_delay_us(10); \
_byte = LCD_PIN; \
sbi(CONTROL_PORT, RD_pin); \
}
а для работы с двухбайтным словом добавим следующую макрофункцию:
#define write_two_bytes(two_bytes) \
{ \
LCD_PORT = two_bytes; \
LCD_HIGH_PORT = two_bytes >> 8; \
cbi(CONTROL_PORT, WR_pin); \
sbi(CONTROL_PORT, WR_pin); \
}
Осталось добавить в макрофункции set_write_dir() и set_read_dir() строки, обеспечивающие смену направления работы старшего байта шины данных
#define set_write_dir() \
{ \
LCD_DDR = WRITE_DIR; \
LCD_HIGH_DDR = WRITE_DIR; \
}
#define set_read_dir() \
{ \
LCD_DDR = READ_DIR; \
LCD_HIGH_DDR = READ_DIR; \
}
и получим файл parallel_16.h.
Для получения parallel_16.с меняем в файле parallel_8.с только число в названиях – с «8» на «16».
В заключение этой главы хочу обратить ваше внимание, что при инициализации управляющие выводы для всех протоколов выставляются в высокое состояние, а выводы шины данных для обеих протоколов 8080 настраиваются как выходы. Это в последующем позволит получить самый простой и быстрый код.
↑ Типы обращений к контроллеру ILI9341
Теперь, когда мы можем записывать в модуль и читать из него байты, нужно ещё заставить ILI9341 понять, что именно мы от него хотим: совершить какое-либо действие, изменить/прочитать содержимое регистра или вывести изображение на дисплей. Для этого существуют вывод DC и 5 типов обращений к контроллеру.1. Запись команды.
2. Запись параметра.
3. Чтение параметра.
4. Запись данных в ОЗУ. По сути, это – вывод изображение на дисплей.
5. Чтение данных из ОЗУ.
Отбросим сразу пункт 5: реальные случаи, требующие знания текущих значений содержимого ячеек ОЗУ, выходят далеко за рамки рассмотрения данной статьи. Поскольку в модулях с протоколом SPI нет вывода MISO, для них отпадает и пункт 3.
Заглянем в даташит, выясним, какие действия должны сопровождать каждое обращение и оформим это в виде, опять же, макрофункций.
Запись команды для всех трёх протоколов одинакова:
а) Установить вывод DC в низкое состояние.
б) Записать байт – 8-битный код команды.
Благодаря тому, что в файле spi.h мы дали второе имя порту В (CONTROL_PORT), а также унифицировали для всех протоколов название функции записи байта, макрофункция записи команды будет выглядеть одинаково для всех трёх случаев:
#define ili9341_write_command(command) \
{ \
cbi(CONTROL_PORT, DC_pin); \
write_byte(command); \
}
Всё сказанное выше справедливо и для записи параметра с одним отличием – обратное состояние вывода DC:
а) Установить вывод DC в высокое состояние.
б) Записать байт - 8-битный код параметра.
#define ili9341_write_parametr(parametr) \
{ \
sbi(CONTROL_PORT, DC_pin); \
write_byte(parametr); \
}
Третий тип обращения – чтение параметра – не доступен для протокола SPI, но, идентичен для 8- и 16- битного протоколов, поскольку, как вы помните, в этом случае обмен – однобайтный.
а) Установить вывод DC в высокое состояние.
б) Считать байт – 8-битный код параметра.
Вспомним, что при инициализации мы настроили шину данных на вывод данных, и при входе в макрофункцию перенастроим её на ввод данных, а при выходе из макрофункции – вернём всё на место:
#define ili9341_read_parametr(parametr) \
{ \
set_read_dir(); \
sbi(CONTROL_PORT, DC_pin); \
read_byte(parametr); \
set_write_dir(); \
}
Последний тип обращения – запись данных в ОЗУ – отличается от записи параметра тем, что в ILI9341 передаётся двухбайтное слово. Поэтому для протоколов SPI и 8-битный 8080 это будет выглядеть так:
а) Установить вывод DC в высокое состояние.
б) Записать старший байт 16-битного слова.
в) Записать младший байт 16-битного слова.
#define ili9341_write_data(data) \
{ \
sbi(CONTROL_PORT, DC_pin); \
write_byte(data >> 8); \
write_byte(data); \
}
а для 16-битного 8080 так:
а) Установить вывод DC в высокое состояние.
б) Записать двухбайтное слово.
#define ili9341_write_data(data) \
{ \
sbi(CONTROL_PORT, DC_pin); \
write_two_bytes(data); \
}
Приведу примеры использования макрофункций с реальными командами контроллера ILI9341 четырёх разных видов:
1. Команды без параметров, которые заставляют выполнять контроллер какое-либо действие. Например, команда с кодом 0x29 – включение дисплея.
ili9341_write_command(0x29);
2. Команды записи в регистры контроллера. Имеют один параметр и более. К примеру, команда установки адреса столбца – код 0x2A, четыре параметра.
ili9341_write_command(0x2A);
ili9341_write_parametr(parameter_1);
ili9341_write_parametr(parameter_2);
ili9341_write_parametr(parameter_3);
ili9341_write_parametr(parameter_4);
3. Команды чтения регистров ILI9341. Количество параметров – 1 и более. Например, команда чтения регистра формата пикселя – код 0x0C, два параметра.
uint8_t parameter_1, parameter_2;
ili9341_write_command(0x0C);
ili9341_read_parametr(parameter_1);
ili9341_ read_parametr(parameter_2);
4. Команда записи в ОЗУ - код 0x2C.
uint16_t color = 0xFFE0 // YELLOW
ili9341_write_command(0x2C);
ili9341_write_data(color);
Объединив написанные нами макрофункции получим следующий файл архива – ili9341.h
для протокола SPI
8-битного протокола
и 16-битного протокола
В заключение скажу, что использование вышеприведённых макрофункций в таком виде, как есть, не приведёт к видимому результату. То есть, ваш МК будет добросовестно передёргивать управляющие выводы, пытаться отправлять и получать данные, но впустую, так как вывод CS – в высоком состоянии, а, следовательно, ILI9341 – не доступен. В предыдущих частях статьи, посвященных модулям на базе контроллеров SSD1306 и ST7735, аналогичные функции начинались и заканчивались строками активации и деактивации вывода CS:
cbi(ST_PORT, CS_pin);
sbi(ST_PORT, CS_pin);
В случае с модулем на базе ILI9341 эти строки перенесены непосредственно в функции, которые объявлены в самом конце каждого из файлов ili9341.h и будут рассмотрены позже. Причина такого переноса прежняя – экономия времени и повышение скорости обмена.
↑ Система команд контроллера ILI9341
Из 81 команд контроллера для полноценной работы модуля нам понадобится всего 7.Sleep OUT (код – 0x11) – выводит ILI9341 из состояния сна, в который он погружается после сброса/подачи питания на модуль. В этом состоянии отключены практически все узлы ILI9341 - DC/DC конвертер, внутренний осциллятор и др. Однако, узел протокола обмена (SPI, 8080) контроллера и его регистры доступны. Поэтому, в функции инициализации модуля команду Sleep OUT можно, с целью энергосбережения, прописывать в последнюю очередь, после осуществления всех настроек. В даташите вслед за этой командой рекомендуется пауза длительностью до 120 миллисекунд.
Display ON (код – 0x29) – подключение дисплея к ОЗУ. При подаче напряжение на вывод LED модуля дисплей начинает светиться белым цветом. Даже, если вывести предыдущей командой контроллер из спячки и записать в ОЗУ данные, изображение на дисплей выведено не будет, так как по сбросу/подаче питания происходит отключение дисплея от ОЗУ. Так же, как и предыдущую, эту команду следует использовать в заключение инициализации и сопровождать задержкой в 120мс.
Memory Write (код – 0x2C) – запись в ОЗУ, а по сути – вывод изображения на экран. Если контроллер выведен из состояния сна и дисплей подключен к ОЗУ, т.е. исполнены две предыдущие команды, то отправив последовательность
ili9341_write_command(0x2C);
ili9341_write_data(color);
вы получите точку на дисплее. N-кратный повтор второй строки обеспечит вывод на экран соответствующего количества точек
ili9341_write_command(0x2C);
ili9341_write_data(color); // 1-я точка
ili9341_write_data(color); // 2-я точка
.
.
.
ili9341_write_data(color); // N-я точка
В каком месте дисплея и в каком порядке появятся точки, зависит от команд, которые будут рассмотрены ниже, а вот их цвет определяется значением 16-битного параметра «color» следующим образом.
Каждый пиксель дисплея состоит из трёх субпикселей красного (R), зелёного (G) и синего (B) цветов. Когда все три субпикселя погашены мы имеем на дисплее пиксель чёрного цвета, одновременно зажжённые все три субпикселя окрашивают пиксель в белый цвет, ну а если горит только один субпиксель, то пиксель, очевидно, примет соответствующий цвет. Попарная комбинация субпикселей завершит палитру из 8 основных цветов.
Как получить остальные цвета и оттенки из заявленных 65 тысяч?
Каждый из трёх субпикселей питается отдельным ШИМ-сигналом от внутреннего генератора, входящего в состав ILI9341. Меняя частоту сигнала от 0 до максимальной, мы получим соответствующий оттенок цвета (от чёрного до максимально насыщенного). 16-битное число, которое мы отправляем в качестве параметра команды Memory Write, и задаёт частоту ШИМ-сигнала:
• Старшие 5 бит – для субпикселя красного цвета,
• Средние 6 бит – для субпикселя зелёного цвета,
• Младшие 5 бит – для субпикселя синего цвета.
Если мы отправим в ОЗУ число 0хF800 (1111100000000000 в двоичном исчислении),
ili9341_write_command(0x2C);
ili9341_write_data(0хF800);
то получим максимальную частоту для красного субпикселя и нулевую – для двух остальных. Как результат – пиксель красного цвета на экране.
Запись числа 0хFFFF (1111111111111111 в двоичном исчислении) задаст максимальную частоту ШИМ-сигнала для всех трёх субпикселей и окрасит пиксель на дисплее в белый цвет, а 0х0000 – отключает генерацию и задаёт чёрный цвет пикселя.
Возможное количество вариантов числа (2 в степени 16) и даёт искомые 65536 цветов и оттенков.
Приведу в виде макроопределений числовые значения основных цветов.
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
Column Address Set или COLADDRSET (код – 0x2A).
Page Address Set или PAGEADDRSET (код – 0x2B).
Эти команды настолько тесно связаны, что лучше их рассматривать вместе. ОЗУ контроллера ILI9341 состоит из 240x320 16-битных регистров. Адрес каждого регистра определяется двумя значениями – номер столбца (Column Address) и номер строки (Page Address). Помимо этого, в состав контроллера входят два счётчика (счётчик номера столбца и счётчик номера строки), текущее значение которых и определяет в какой именно регистр будут записаны данные.
При каждом исполнении команды Memory Write происходит следующее:
1. Контроллер извлекает из счётчиков адрес регистра и записывает в него значение цвета пикселя.
2. Значение счётчика номера столбца увеличивается на 1 – инкрементируется.
3. Если счётчик номера столбца достиг конечного значения, он сбрасывается в начальное значение, а счетчик номера строки увеличивается на 1.
4. По достижению своего конечного значения счетчик номера строки также сбрасывается в своё начальное значение.
Так вот, четыре параметра команд Column Address Set и Page Address Set и определяют эти самые начальные и конечные значения соответствующего счётчика: 1-й и 2-й параметры – два байта начального значения, 3-й и 4-й параметры – два байта конечного значения.
Поскольку ОЗУ напрямую связано с дисплеем, рассматриваемые нами команды, по сути, определяют координаты Х и Y пикселя, который будет закрашен выбранным цветом при очередном исполнении команды Memory Write.
К параметрам команд Column Address Set и Page Address Set предъявляется следующее требование – конечное значение должно быть больше или равно начальному значению.
Рассмотрим пару примеров использования трёх команд для работы с ОЗУ.
В первом примере сделаем следующее:
1. Зададим одинаковые начальное и конечное значения для счётчика номера столбцов – 3 (двухбайтное шестнадцатиричное представление – 0x0003, старший байт – 0х00, младший байт – 0х03):
ili9341_write_command(0x2A);
ili9341_write_parametr(0x00); // старший байт начального значения
ili9341_write_parametr(0x03); // младший байт начального значения
ili9341_write_parametr(0x00); // старший байт конечного значения
ili9341_write_parametr(0x03); // младший байт конечного значения
2. То же самое, но с числом 5, сделаем для счётчика номера строки:
ili9341_write_command(0x2B);
ili9341_write_parametr(0x00); // старший байт начального значения
ili9341_write_parametr(0x05); // младший байт начального значения
ili9341_write_parametr(0x00); // старший байт конечного значения
ili9341_write_parametr(0x05); // младший байт конечного значения
3. Пять раз запишем в ОЗУ значение красного цвета.
ili9341_write_command(0x2C);
ili9341_write_data(RED);
ili9341_write_data(RED);
ili9341_write_data(RED);
ili9341_write_data(RED);
ili9341_write_data(RED);
По причине того, что начальные и конечные значения счётчиков равны, каждая запись в ОЗУ будет вызывать их сброс. В итоге, на дисплее мы пять раз закрасим красным цветом одну и ту же точку с координатами X=3, Y=5.
Теперь повторим тот же пример, только увеличив на 1 конечные значения счётчиков и проследим по комментариям, как будут меняться координаты закрашиваемой точки.
ili9341_write_command(0x2A);
ili9341_write_parametr(0x00);
ili9341_write_parametr(0x03);
ili9341_write_parametr(0x00);
ili9341_write_parametr(0x04); // здесь уже 4, а не 3
ili9341_write_command(0x2B);
ili9341_write_parametr(0x00);
ili9341_write_parametr(0x05);
ili9341_write_parametr(0x00);
ili9341_write_parametr(0x06); // здесь уже 6, а не 5
ili9341_write_command(0x2C);
ili9341_write_data(RED); // X=3, Y=5
ili9341_write_data(RED); // X=4, Y=5
ili9341_write_data(RED); // X=3, Y=6
ili9341_write_data(RED); // X=4, Y=6
ili9341_write_data(RED); // X=3, Y=5
Как видите, на дисплее мы получим квадрат из четырёх красных точек, а пятая – окажется на месте первой.
Pixel Format Set или COLMOD (код – 0x3A) – установка формата пикселя. Эта команда определяет длину параметра команды Memory Write – 16-бит (значение параметра команды COLMOD – 0x55) или 18-бит (значение параметра команды COLMOD – 0x66). Во втором случае на каждый суб-пиксель будет приходиться по 6 битов значения цвета и именно этот вариант устанавливается по сбросу/подаче питания. Поэтому, необходимо поменять формат на 16-битный (как и в случае с ST7735 из предыдущей части статьи), поскольку мы планируем в дальнейшем использовать единую графическую библиотеку для всех модулей.
Memory Access Control или MADCTL (код – 0x36) – определение порядка вывода данных из ОЗУ на дисплей. С точки зрения изображения на экране эта команда определяет местоположение начала системы координат и направление её осей. Параметр команды содержит 6 значимых битов, из которых мы будем использовать 4 – MX, MY, MV и BGR.
Представим, что мы задали нужные пределы счёта счётчикам столбца (от 0 до 240) и строки (от 0 до 320), после чего 240 х 320 = 76800 раз записали в память ILI9341 значение 0xFFE0, чтобы закрасить дисплей жёлтым цветом, а затем ещё вывели надпись «Datagor» чёрного цвета. На рисунке ниже вы можете наблюдать, что получится на дисплее в зависимости от значений вышеуказанных битов. Во всех вариантах красная точка отмечает откуда начинается вывод данных из ОЗУ на дисплей (начало координат), синяя – куда отсчитываются столбцы (направление оси X), а зелёная – строки (направление оси Y).
Выясним, что и почему происходит в каждом случае.
1. Все биты равны нулю. Это значение устанавливается по сбросу/подаче питания. Дисплей окрасился в голубоватый, а не жёлтый, цвет, хотя надпись «Datagor» – чёрная, как и планировалось. Однако, с надписью тоже не всё в порядке – она выведена в зеркальном отражении. Начало координат – в углу B.
Причина путаницы с цветами следующая. В описании команды Memory Write мы выяснили взаимосвязь между битами параметра этой команды и частотой ШИМ-сигнала субпикселей:
• Старшие 5 бит – частота сигнала для субпикселя красного цвета,
• Средние 6 бит – частота сигнала для субпикселя зелёного цвета,
• Младшие 5 бит – частота сигнала для субпикселя синего цвета.
Однако, такой порядок действует, если бит BGR параметра команды MADCTL равен 1. Если же BGR = 0, то порядок – зеркальный:
• Старшие 5 бит – частота сигнала для субпикселя синего цвета,
• Средние 6 бит – частота сигнала для субпикселя зелёного цвета,
• Младшие 5 бит – частота сигнала для субпикселя красного цвета.
В этом случае неизменными остаются только те числовые значения цветов, которые симметричны в двоичном представлении – чёрный (0000000000000000), белый (1111111111111111) и зелёный (0000011111100000). Остальные цвета меняются на зеркально-противоположные: красный (1111100000000000) на синий (0000000000011111), синий на красный и т.д. Именно поэтому мы получили голубоватый экран вместо жёлтого, а надпись, как и хотели, черную.
Что касается ориентации надписи «Datagor», то дело вот в чём. В память ILI9341 мы записали байты, составляющие каждую отдельную букву, слева-направо, как это обычно принято, а из ОЗУ на дисплей при нулевых значениях битов MX, MY и MV данные выводятся, как видно по синей точке на рисунке, наоборот – справа-налево.
2. BGR = 1. Установив бит BGR в 1, мы получили требуемый – жёлтый – цвет дисплея.
3. MX = 1. Как видите, в этом случае начало координат перемещается в угол A, а направление оси X становится привычным – справа-налево, в связи с чем надпись «Datagor» принимает нормальный вид. Иными словами, этот бит обеспечивает отражение по горизонтали. Мы получаем первую из четырёх рабочих ориентаций модуля – вертикальную, с колодкой выводов внизу.
4. MY = 1. Отражение по вертикали. Начало координат – в углу С. Это – вторая рабочая ориентация модуля – вертикальная, с колодкой выводов вверху.
5. MV = 1. Начало координат вернулось в первоначальное положение – угол B. При этом ось Х приобрела нормальное направление, а столбцы поменялись местами со строками. Последний факт привёл к тому, что дисплей закрасился не полностью. Причина этого заключается в том, что столбцов стало 320, а мы в самом начале задали конечное значение счётчика столбцов 240, поэтому 241-й пиксель, как и положено, был выведен с новой строки.
Со строками – всё наоборот: мы задали предел счёта 320, а реальное количество строк уменьшилось до 240. По сути, имеет место следующая ситуация:
Просто те данные в ОЗУ, которые соответствуют вышедшему за пределы дисплея прямоугольнику, игнорируются. Чтобы исправить ситуацию, необходимо изменить надлежащим образом конечные значения счётчиков командами Column Address Set и Page Address Set. Тогда мы получим третью рабочую ориентацию модуля – горизонтальную, с колодкой выводов справа.
6. MX = 1, MY = 1, MV = 1. Эта комбинация битов приводит к таким же последствиям, что и в предыдущем случае, с одним отличием – начало координат занимает последний из неиспользованных углов, D. Меняем значения счётчиков столбцов и строк и в нашем распоряжении оказывается четвёртая рабочая ориентация модуля – горизонтальная, с колодкой выводов слева.
В прошлой части статьи, посвященной модулю на базе ST7735, выбор ориентации модуля и соответствующие изменения конечных значений счётчиков осуществлялись специальной функцией st7735_set_rotation() с помощью глобальных переменных _width и _height. В этот раз мы будем делать это через макроопределения и условную компиляцию.
#define ROTATION 2 // здесь определяем ориентацию модуля – от 1 до 4
#if ROTATION == 1
#define MADCTL_PARAMETR MADCTL_MX | MADCTL_BGR
#define ILI9341_TFTWIDTH 240
#define ILI9341_TFTHEIGHT 320
#elif ROTATION == 2
#define MADCTL_PARAMETR MADCTL_MV | MADCTL_BGR
#define ILI9341_TFTWIDTH 320
#define ILI9341_TFTHEIGHT 240
#elif ROTATION == 3
#define MADCTL_PARAMETR MADCTL_MY | MADCTL_BGR
#define ILI9341_TFTWIDTH 240
#define ILI9341_TFTHEIGHT 320
#elif ROTATION == 4
#define MADCTL_PARAMETR MADCTL_MX | MADCTL_MY| MADCTL_MV | MADCTL_BGR
#define ILI9341_TFTWIDTH 320
#define ILI9341_TFTHEIGHT 240
#endif
Такой поход имеет свои плюсы - экономия флэш-памяти и ОЗУ. Минус тоже есть – ориентация модуля выбирается один раз и больше не меняется. Но, часто ли вам встречались проекты, требующие по ходу исполнения смены ориентации дисплея? Как бы там ни было, выбор – за вами.
К семи обязательным командам добавим одну полу-обязательную и одну совсем не обязательную.
Software Reset (код – 0x01). Команда обеспечивает программный сброс и нужна только в случае, если вы откажетесь от аппаратного.
Read ID4 (код – 0xD3). Чтение регистра ID4, где в шестнадцатиричном формате хранится номер контроллера - 0x9341. Эта команда совсем уж не обязательна. Но, во-первых, зря что ли мы старались, прописывая макрофункции чтения? Давайте хоть что-нибудь прочитаем. А, во-вторых, используя эту команду, вы сможете убедиться, что продавец не всучил вам модуль с как-то другим контроллером.
Объединим всё изложенное в этой главе в архивный файл ili9341_reg.h, который будет единым для всех трёх протоколов.
↑ Основные функции для работы с модулем
Все вспомогательные файлы готовы, можем приступить к написанию основных функций.Начнём с функций сброса – программного и аппаратного.
void ili9341_reset_sw(void)
{
ili9341_write_command(ILI9341_SOFTRESET);
_delay_ms(120);
}
void ili9341_reset_hw(void)
{
#ifdef RESET_HW
cbi(CONTROL_PORT, RESET_HW_pin);
_delay_us(RESET_HW_DELAY);
sbi(CONTROL_PORT, RESET_HW_pin);
_delay_ms(120);
#endif
}
Как видите, в функции программного сброса всё еще не затрагивается вывод CS. Дело в том, что обычно сброс производится один раз – внутри процедуры инициализации модуля. Именно там мы и будем активировать указанный вывод.
Директивы препроцессора #ifdef и #endif включены во вторую функцию по следующей причине. Если вы решите выбрать программный сброс и закомментируете строку #define RESET_HW, то вывод сброса останется не определённым. Тогда при компиляции, дойдя до функции ili9341_reset_hw(), компилятор, при отсутствии указанных директив, выдаст ошибку о неизвестном символе RESET_HW_pin.
Теперь функция инициализации модуля:
1. Инициализируем порты и выводы МК, участвующие в протоколе обмена.
2. Активируем вывод CS, установив его в 0 .
3. Сбрасываем, аппаратно или программно, ILI9341.
4. Записываем в модуль 4 команды – COLMOD, MADCTL, Sleep OUT и Display ON – с параметрами и задержками, озвученными в предыдущей главе.
5. Поднимаем в 1 вывод CS.
void ili9341_init(void)
{
ili9341_pins_init();
cbi(CONTROL_PORT, CS_pin);
#ifndef RESET_HW
ili9341_reset_sw();
#else
ili9341_reset_hw();
#endif
ili9341_write_command(ILI9341_COLMOD);
ili9341_write_parametr(COLMOD_PARAMETR);
ili9341_write_command(ILI9341_MADCTL);
ili9341_write_parametr(MADCTL_PARAMETR);
ili9341_write_command(ILI9341_SLEEPOUT);
_delay_ms(120);
ili9341_write_command(ILI9341_DISPLAYON);
_delay_ms(120);
sbi(CONTROL_PORT, CS_pin);
}
Следующая - функция полной закраски дисплея.
1. Устанавливаем вывод CS в 0.
2. Записываем в счётчики столбца и строки значения, определённые в файле ili9341_reg.h для выбранной ориентации модуля.
3. 240 х 320 = 76 800 раз записываем в ОЗУ значение выбранного цвета.
4. Возвращаем вывод CS в 1.
void ili9341_fill_screen(uint16_t color)
{
cbi(CONTROL_PORT, CS_pin);
ili9341_write_command(ILI9341_COLADDRSET);
ili9341_write_parametr(0x00); ili9341_write_parametr(0x00);
ili9341_write_parametr((ILI9341_TFTWIDTH - 1) >> 8);
ili9341_write_parametr(ILI9341_TFTWIDTH - 1);
ili9341_write_command(ILI9341_PAGEADDRSET);
ili9341_write_parametr(0x00); ili9341_write_parametr(0x00);
ili9341_write_parametr((ILI9341_TFTHEIGHT - 1) >> 8);
ili9341_write_parametr(ILI9341_TFTHEIGHT - 1);
ili9341_write_command(ILI9341_MEMORYWRITE);
for(uint32_t counter = 0;
counter < ((uint32_t)ILI9341_TFTWIDTH * (uint32_t)ILI9341_TFTHEIGHT);
counter++)
{
ili9341_write_data(color);
}
sbi(CONTROL_PORT, CS_pin);
}
Осталась функция прорисовки точки.
1. Устанавливаем вывод CS в 0.
2. Через запись в счётчики столбца и строки устанавливаем выбранные координаты точки.
3. Отправляем в ILI9341 команду Memory Write, с выбранным цветом точки в качестве параметра команды.
4. Возвращаем вывод CS в 1.
void draw_pixel(int16_t x, int16_t y, uint16_t color)
{
cbi(CONTROL_PORT, CS_pin);
ili9341_write_command(ILI9341_COLADDRSET);
ili9341_write_parametr(x >> 8); ili9341_write_parametr(x);
ili9341_write_parametr(x >> 8); ili9341_write_parametr(x);
ili9341_write_command(ILI9341_PAGEADDRSET);
ili9341_write_parametr(y >> 8); ili9341_write_parametr(y);
ili9341_write_parametr(y >> 8); ili9341_write_parametr(y);
ili9341_write_command(ILI9341_MEMORYWRITE);
ili9341_write_data(color);
sbi(CONTROL_PORT, CS_pin);
}
Прописав в шапке включение вспомогательных файлов
#include "ili9341_reg.h"
#include "ili9341.h"
мы получим файл ili9341.с для протокола SPI
В аналогичный файл для обоих протоколов 8080 лишь добавиться ещё одна функция – чтения идентификационного номера контроллера ILI9341.
1. Устанавливаем вывод CS в 0.
2. Записываем команду Read ID4.
3. Считываем четыре байта из контроллера в буферную переменную. Первый и второй байты – пустые, поэтому игнорируем их, а третий и четвёртый байты размещаем в заранее объявленную 16-битную переменную id.
4. Устанавливаем вывод CS в 1.
5. Возвращаем из функции значение переменной id.
uint16_t ili9341_readID(void)
{
uint8_t parametr;
uint16_t id;
cbi(CONTROL_PORT, CS_pin);
ili9341_write_command(ILI9341_READ_ID4);
ili9341_read_parametr(parametr);
ili9341_read_parametr(parametr);
ili9341_read_parametr(parametr);
id = parametr;
id <<= 8;
ili9341_read_parametr(parametr);
id |= parametr;
sbi(CONTROL_PORT, CS_pin);
return id;
}
Окончательно файл ili9341.с для протокола 8- и 16-битного протоколов выглядит следующим образом.
Последнее, что нам осталось – главная функция, которую мы разместим в файле с одноимённым названием main.c. Обязательным в этом файле будет только подключение необходимых библиотек и исполнение функции ili9341_init().
Вот так выглядит шаблон файла main.c для протокола SPI.
#include <avr/io.h>
#include <util/delay.h>
#include "spi.c"
#include "ili9341.c"
int main(void)
{
ili9341_init();
while(1)
{
}
}
В случае с протоколом 8- или 16-битного протокола меняется лишь строка
#include "spi.c"
на
#include "parallel_8.c"
или
#include "parallel_16.c"
Наполнение содержанием функции main() – дело вашей фантазии. В архиве представлен пример закрашивания дисплея красным цветом и прорисовки четырёх точек жёлтого цвета.
↑ Файлы
🎁ili9341_parallel_8_kod.7z 1.77 Kb ⇣ 192🎁ili9341_parallel_16_kod.7z 1.83 Kb ⇣ 155
🎁ili9341_spi_kod.7z 1.56 Kb ⇣ 189
Спасибо за внимание! До новых встреч!
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.