» » » Визуализация для микроконтроллера. Часть 3. TFT дисплей 2.8" (240х320) на ILI9341

 
 
 

Визуализация для микроконтроллера. Часть 3. TFT дисплей 2.8" (240х320) на ILI9341

Разместил erbol 15 ноября 2016. Просмотров: 2744

8 Битва за урожай закончена, можно продолжить повествование.
Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341



Модуль, который мы рассмотрим сегодня, сочетает в себе свойства двух линеек подобных устройств. С одной стороны, размеры его дисплея всё ещё позволяют использовать протокол SPI без существенного ущерба для картинки на экране. В этом он похож на модули с небольшим дисплеем (к примеру, на базе контроллера ILI9163 или рассмотренного в предыдущей части статьи ST7735). Кстати, из предлагаемых на AliExpress и eBay модулей на базе ILI9341 с протоколом SPI – значительное большинство.

С другой стороны, не редки реализации этого модуля на протоколе 8080, как 8-ми так и 16-битном, что роднит его со второй группой, в которую входит и модуль на базе контроллера ILI9481, о котором мы поговорим в следующей части статьи.
Помимо этого, принцип работы и система команд ILI9341 и контроллеров из вышеуказанных групп настолько схожи (иногда – вплоть до совпадения кодов команд), что, разобравшись, как работает этот модуль, вы без особых усилий сможете запустить и многие другие.

Характеристики модуля на 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 – выбор типа записываемого в модуль слова – данные (низкий уровень на выводе) или команда (высокий уровень). Возможны и иные обозначения этого вывода – 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 основных цветов.
Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

Как получить остальные цвета и оттенки из заявленных 65 тысяч?
Каждый из трёх субпикселей питается отдельным ШИМ-сигналом от внутреннего генератора, входящего в состав ILI9341. Меняя частоту сигнала от 0 до максимальной, мы получим соответствующий оттенок цвета (от чёрного до максимально насыщенного). 16-битное число, которое мы отправляем в качестве параметра команды Memory Write, и задаёт частоту ШИМ-сигнала:
• Старшие 5 бит – для субпикселя красного цвета,
• Средние 6 бит – для субпикселя зелёного цвета,
• Младшие 5 бит – для субпикселя синего цвета.

Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

Если мы отправим в ОЗУ число 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).

Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

Выясним, что и почему происходит в каждом случае.
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 данные выводятся, как видно по синей точке на рисунке, наоборот – справа-налево.

Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

2. BGR = 1. Установив бит BGR в 1, мы получили требуемый – жёлтый – цвет дисплея.

3. MX = 1. Как видите, в этом случае начало координат перемещается в угол A, а направление оси X становится привычным – справа-налево, в связи с чем надпись «Datagor» принимает нормальный вид. Иными словами, этот бит обеспечивает отражение по горизонтали. Мы получаем первую из четырёх рабочих ориентаций модуля – вертикальную, с колодкой выводов внизу.

4. MY = 1. Отражение по вертикали. Начало координат – в углу С. Это – вторая рабочая ориентация модуля – вертикальная, с колодкой выводов вверху.

5. MV = 1. Начало координат вернулось в первоначальное положение – угол B. При этом ось Х приобрела нормальное направление, а столбцы поменялись местами со строками. Последний факт привёл к тому, что дисплей закрасился не полностью. Причина этого заключается в том, что столбцов стало 320, а мы в самом начале задали конечное значение счётчика столбцов 240, поэтому 241-й пиксель, как и положено, был выведен с новой строки.

Со строками – всё наоборот: мы задали предел счёта 320, а реальное количество строк уменьшилось до 240. По сути, имеет место следующая ситуация:
Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

Просто те данные в ОЗУ, которые соответствуют вышедшему за пределы дисплея прямоугольнику, игнорируются. Чтобы исправить ситуацию, необходимо изменить надлежащим образом конечные значения счётчиков командами Column Address Set и Page Address Set. Тогда мы получим третью рабочую ориентацию модуля – горизонтальную, с колодкой выводов справа.
Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341


6. MX = 1, MY = 1, MV = 1. Эта комбинация битов приводит к таким же последствиям, что и в предыдущем случае, с одним отличием – начало координат занимает последний из неиспользованных углов, D. Меняем значения счётчиков столбцов и строк и в нашем распоряжении оказывается четвёртая рабочая ориентация модуля – горизонтальная, с колодкой выводов слева.
Визуализация для микроконтроллера. Часть 3.  TFT дисплей 2.8" (240х320) на ILI9341

В прошлой части статьи, посвященной модулю на базе 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() – дело вашей фантазии. В архиве представлен пример закрашивания дисплея красным цветом и прорисовки четырёх точек жёлтого цвета.

Файлы

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

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

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


Спасибо за внимание! До новых встреч!

Об авторе

Ербол (erbol)
Актобе, Казахстан
Мне 50 лет. Родился и вырос в г. Актюбинск (ныне - Актобе), Казахстан. Женат, воспитываю четверых прекрасных детей.

С программированием и электроникой имею может и не отдаленные, но весьма эпизодические отношения: в далекие и бурные 90-е годы аж прошлого века посчастливилось участвовать в автоматизации экспериментальной установки с использованием микропроцессорного комплекта КР580.

А пару лет назад мои сорванцы вдруг заинтересовались микроконтроллерами. Пришлось из дальних уголков памяти вытаскивать свои старые познания и стряхивать с них пыль.
 

Понравилось? Палец вверх!

  • всего лайков: 36

Поделись с друзьями!


Связанные материалы:


Программная реализация протокола SPI на AVR в CodeVisionAVR

Всем коллегам и согражданам привет! Увлёкся я изучением протоколов. Про реализацию ...

Визуализация для микроконтроллера. Часть 2. TFT дисплей 1.8" (128х160) на ST7735

Следующий из рассматриваемых нами модулей обладает полноцветным дисплеем под управлением...

Визуализация для микроконтроллера. Часть 1. OLED дисплей 0.96" (128х64) на SSD1306

Добрый день, друзья! Эта статья открывает цикл, посвящённый средствам визуального отображения...

Беспроводной канал связи 2,4 ГГц на базе трансивера nRF24L01+ от Nordic

Доброго вам дня, уважаемые граждане и гости Датагор.ру - этого замечательного сообщества...

Вторая жизнь лампового радиоприемника Philips 592LN (Голландия, 1947). Часть 5

В этой части статьи речь пойдет:  — о предварительном усилителе и его питании,...

Вторая жизнь лампового радиоприемника Philips 592LN (Голландия, 1947). Часть 4.

Привет, датагорцы! В этой части моего повествования речь пойдёт о модуле BlueTooth...

Вторая жизнь лампового радиоприемника Philips 592LN (Голландия, 1947). Часть 3

В этой части разберемся с инсталляцией китайского ФМ-радиомодуля в древний Philips 592LN , с...

Рулим китайским FM-радиомодулем на TEA5767. Датагорская библиотека на C

Приобрел я на пробу радио-модуль на базе микросхемы TEA5767. Модуль представляет...

Двухполярный блок питания из готовых китайских модулей dc-dc step down LM2596

Сегодня стали доступны готовые модули импульсных стабилизаторов напряжения на микросхеме LM2596. ...

Доработка модуля китайского вольтметра

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

Часы с GPS-синхронизацией времени и винтажными индикаторами ИН-12. Наш вариант Nixie

Не думал, что спустя много лет я вернусь к часам на газоразрядных индикаторах. В конце 70-х я...

Что внутри усилителя Technics SU-V620 и его гибридного модуля RSM3306

Перед вами кракий фотоотчет одного вскрытия... Принес мне как-то один человек усилитель Technics...
<
  • Подписчик
15 ноября 2016 12:35

Игорь / StalKer-NightMan

  • Регистрация: 15.03.2012
  • Публикаций: 1
  • Комментариев: 84
 
  • +1
Спасибо, Ербол, за очередную фундаментальную статью, даже исследование. Все систематизировано, разложено по полочкам. Буду теперь штудировать материал.

<
  • Бонус
15 ноября 2016 13:01

Ербол / erbol

  • Регистрация: 11.12.2014
  • Публикаций: 7
  • Комментариев: 58
 
  • 0
Цитата: StalKer-NightMan
Спасибо, Ербол, за очередную фундаментальную статью, даже исследование...


Спасибо, Игорь! handshake Очень надеюсь, что статья будет полезна Вам и другим читателям! smile

<
  • Гражданин
15 ноября 2016 17:04

Сергей / Sergiy_83

  • Регистрация: 16.10.2012
  • Публикаций: 1
  • Комментариев: 23
 
  • 0
Спасибо Ербол за труд. Вы как всегда красавчик.
Так как вызовов функций данных (параметров) гораздо больше чем функции команд в процессе работы, DC лучше "поднять" в конце функции команды. Это тоже сэкономит кучу времени.
Если дисплей в проекте один, CS можно пояльником на землю намертво.

<
  • Бонус
15 ноября 2016 17:43

Ербол / erbol

  • Регистрация: 11.12.2014
  • Публикаций: 7
  • Комментариев: 58
 
  • +1
Спасибо,Сергей! smile Полностью согласен с Вами: в зависимости от конкретного проекта код и схему можно оптимизировать. Вот только мои попытки рассмотреть в рамках статьи все возможные варианты обычно приводят к плачевному результату: статья превращается в монстра с кучей ветвлений и ни один нормальный человек в трезвом уме не возьмётся её читать, а уж новичок - ... pardon

<
  • Гражданин
17 ноября 2016 01:52

Радик / galrad

  • Регистрация: 23.08.2011
  • Публикаций: 12
  • Комментариев: 84
 
  • 0
Великолепная статья, Ербол! Написано очень обстоятельно, как в диссертации, СПАСИБО ЗА ПРОВЕДЕННУЮ РАБОТУ! Сам знаю, как пишутся такие статьи, поэтому очень признателен за Ваше терпение и желание довести до начинающих пользователей материал в доступной форме!

<
  • Бонус
17 ноября 2016 09:30

Ербол / erbol

  • Регистрация: 11.12.2014
  • Публикаций: 7
  • Комментариев: 58
 
  • +1
Спасибо, Радик! handshake Выходит, не зря я корпел в своё время над диссертацией: как минимум, научился доступно выражать свои мысли smile

<
  • Главный редактор
4 декабря 2016 15:47

Игорь Петрович Котов / Datagor

  • Регистрация: 25.02.2011
  • Публикаций: 261
  • Комментариев: 1599
 
  • 0
Ербол, СПАСИБО! Как читаю очередную вашу статью, так сразу хочется купить все эти экранчики, модули, контроллеры и пробовать и пробовать.
Сплошное разорение drinks
Давай ещё!!!

<
  • Бонус
4 декабря 2016 18:16

Ербол / erbol

  • Регистрация: 11.12.2014
  • Публикаций: 7
  • Комментариев: 58
 
  • 0
Спасибо, Игорь! drinks
Всегда хочется, чтобы твои работы вызывали мысль "Блин, это же - не сложно, надо повторить!", а не "Мама родная, куда я попал?!"
Продолжение - в работе smile

Информация
Вы не можете участвовать в комментировании. Вероятные причины:
— Администратор остановил комментирование этой статьи.
— Вы не авторизовались на сайте. Войдите с паролем.
— Вы не зарегистрированы у нас. Зарегистрируйтесь.
— Вы зарегистрированы, но имеете низкий уровень доступа. Получите полный доступ.