Всем доброго времени суток. В этой статье мы разберём подключение TFT дисплея ER-TFT101-1 (10 дюймов, RA8876 драйвер) к плате STM32F429L Discovery по 16-битному параллельному интерфейсу 8080 используя модуль FMC (flexible memory controller).
О дисплейной сборке
ER-TFT101-1 от компании EastRising представляет собой сборку из 10 дюймовой TFT матрицы с разрешением 1024х600 и платы с драйвером RA8876. На плате с драйвером разведено всё нужное питание, стоит SD-RAM память на 16 мегабайт (шина 16 бит, максимальная частота 166 мГц, максимальный объём 64 мб), есть стандартный слот под microSD карту. Присутствуют пустые посадочные места под EEPROM с внешними шрифтами и под flash память для изображений с выведенными разъёмами для программирования оных. Так-же на сборке опционально может стоять резистивная или емкостная тач-панель.
На плате стоит топовый драйвер RAiO RA8876 с максимальной частотой работы 120 мГц, который при большом желании сам может работать в качестве управляющего микроконтроллера. Можно написать небольшую программу (всего 12 инструкций) и положить её во внешнюю флэш-память. При запуске дисплея в первую очередь начнёт выполняться эта программа, по сути дублируя все возможности управления через внешний интерфейс.
RA8876 не имеет собственного ОЗУ и поэтому использует внешнюю SD-RAM память. Может считывать с помощью DMA изображения из флэш-памяти и загружать в свой фрейм-буффер и имеет очень гибкую конфигурацию оного. Драйвер подключен к самой матрице по стандартному RGB интерфейсу шириной 18 бит. Используется всего 6 бит на красный канал, 6 бит на зелёный и 6 бит синий канал. Младшие два бита у каждого канала не используются, что даёт в теории 262144 цветов.
DMA модуль у RA8876 очень сильно напоминает DMA2D от STM – может копировать прямоугольные участки памяти из одного места в другое, с преобразованием цвета, прозрачностью и другими фишками.
Так-же у RA8876 есть встроенные английские и китайские шрифты (8x16,12x24,16x32 пикселей) с гибкой настройкой их отображения (поворот, масштаб и т.д.) и отдельный матричный интерфейс (5 х 5) для аппаратных кнопок (для автономного и не только использования) с кучей настроек, таких как, длинное и короткое нажатие, пробуждение дисплея от нажатия на кнопку и нажатие нескольких кнопок одновременно.
Присутствует функция картинка в картинке (без поддержки прозрачности) для отображения всяких всплывающих окон и менюшек.
Драйвер сам может рисовать графические примитивы, такие как, квадрат, круг, кривая, овал, треугольник, закруглённый квадрат, с заливкой и без. К слову, что в RA8875, что в RA8876 есть небольшой баг заливки треугольника, причём у каждого драйвера свой. Но RAiO на это наплевать с высокой колокольни… пробовал как-то писать им письмо, так они даже не ответили. Отрисовка таких примитивов позволяет создавать красивую графику даже медленным микроконтроллером.
С внешним миром RA8876 общается через интерфейсы 8080/6800 8/16 бит, 3/4 проводной SPI и I2C. Причём сама микросхема драйвера может выступать в качестве мастера SPI и I2C. Ещё в RA8876 имеет два PWM выхода, которые можно использовать для гибкого управления подсветкой экрана. Максимальная частота SPI CLK заявлена на уровне 66 мГц при частоте самого драйвера в 120 мГц, что в теории даёт 6 кадров в секунду полного обновления экрана (при 1024 х 600 х 16 бит). Это подключение было опробовано мной и показало, что имеет право на жизнь, если мы не будет выводить видео на экран.
В нашем же случае мы подключим дисплей по протоколу 8080 шириной 16 бит к STM32F429ZIT6 через модуль FMC (flexible memory controller), что позволит получить больше скорости заполнения экрана и меньше нагрузку на микроконтроллер.
Конфигурация пинов 8080 и FMC
Схему подключения по 8080 подсмотрим в даташите на дисплей:
Нужные пины подключения к STM32 подсматриваем в CubeMX. Нас интересует банк #1 (NOR Flash/PSRAM/SRAM/ROM/LDC 1).
По поводу XnWAIT в даташите можно прочитать следующее:
The continuous data write speed determines the display update speed. The cycle-to-cycle interval must be larger than 5 of system clock period if user without adopt XnWait to insert wait state. Over the specification may cause the data lose or function fail if xnwait mechanism does not use.
Дословно, между рабочими циклами протокола 8080 должна быть вставлена задержка в 5 системных клоков RA8876, если пользователь не использует механизм XnWAIT, чтобы ожидать освобождения RA8876. Мы же как-раз будем использовать этот пин, т.к. на практике я пробовал вставлять задержку в пять циклов, и оно не работало.
Вместо полноценной шины адреса у блока FMC мы используем всего один пин A16.
- Пины данных (D0 – D15) конфигурируем, как альтернативную функцию #12, типа пуш-пул, максимальной скорости и без каких-либо подтяжек.
- Пины XnWAIT, XnWR, XnRD, XA0 и XnCS конфигурируем, как альтернативную функцию #12, типа пуш-пул с подтяжкой к плюсу (PULL UP).
- XnRST конфигурируем, как обычный GPIO без подтяжки (она есть на самой плате).
- XnINTR конфигурируем, как GPIO на вход с подтяжкой к плюсу.
Ещё я подключил подсветку на 100% без управление оной по PWM. Для этого пин #14 на разъёме дисплейной сборки подключил к VDD.
Кода конфигурации пинов не привожу, т.к. использую свои собственные билиотеки конфигурации, да и сама конфигурация GPIO уже сто раз была разжёвана на хабре и на других источниках.
Библиотека инициализации находится тут.
Настройки FMC
За настройку банков FMC модуля (NOR Flash/PSRAM/SRAM/ROM/LDC 1) отвечают три регистра для каждого банка. Это FMC_BCRx, FMC_BTRx и FMC_BWTRx. В дефайнах МК STM32F429 регистры FMC_BCRx и FMC_BTRx объединены в один общий массив с названием FMC_BTCR с восемью элементами, где нулевой элемент это FMC_BCR1, первый элемент это FMC_BTR1, второй элемент это FMC_BCR2 и так далее. FMC_BWTRx объединены в массив FMC_BWTR с семью элементами, хотя должно быть четыре. Не спрашивайте меня почему…
FMC_BCRx содержит основные настройки, FMC_BTRx содержит общие тайминги, а FMC_BWTRx содержит отдельные тайминги для чтения, если устройство того требует.
Временная диаграмма и тайминги для взаимодействия STM32F429 и RA8876.
Для простоты конфигурации занесём тайминги протокола 8080 в константы. Сами тайминги подбирал опытным путём, уменьшая понемногу значения, т.к. таблица таймингов с даташита больше похожа на сферического коня в вакууме.
unsigned long ADDSET = 0;
unsigned long ADDHLD = 1;
unsigned long DATAST = 5;
unsigned long BUSTURN = 0;
unsigned long CLKDIV = 1;
unsigned long DATLAT = 0;
unsigned long ACCMOD = 0;
RCC->AHB3ENR |= RCC_AHB3ENR_FMCEN; // Активируем FMC
FMC_Bank1->BTCR[0] = 0; // Сбрасываем регистр настроек
FMC_Bank1->BTCR[0] |= FMC_BCR1_MWID_0; // Устанавливаем ширину шины 16 бит
FMC_Bank1->BTCR[0] |= FMC_BCR1_WREN; // Разрешаем операцию записи
FMC_Bank1->BTCR[0] |= FMC_BCR1_WAITEN; // Разрешаем использовать XnWAIT
FMC_Bank1->BTCR[0] |= FMC_BCR1_ASYNCWAIT; // XnWAIT в асинхронном режиме
FMC_Bank1->BTCR[0] |= FMC_BCR1_WAITCFG; // Настройка XnWAIT
FMC_Bank1->BTCR[1] = 0; // Сбрасываем регистр таймингов
FMC_Bank1->BTCR[1] |= (ADDSET << 0) | (ADDHLD << 4) | (DATAST << 8) | (BUSTURN << 16) | (CLKDIV << 20) | (DATLAT << 24) | (ACCMOD << 28); // Устанавливаем тайминги
FMC_Bank1->BTCR[0] |= 1; // Активируем первый банк
Значение регистра FMC_BTCRx после сброса равно 0x0FFF FFFF, т.е. выставлены максимальные тайминги. Если у вас новый дисплей или память, просто снижаем тайминги и пробуем запускать.
Инициализация дисплея
Работа с дисплеем сводится к чтению или записи определённых областей памяти. Всю остальную работу FMC берёт на себя. Для упрощения работы определим два дефайна:
#define LCD_DATA 0x60020000
#define LCD_REG 0x60000000
И теперь опишем низкоуровневые функции:
void LCD_CmdWrite (unsigned char cmd)
{
*(unsigned short *)(LCD_REG) = cmd;
};
void LCD_DataWrite (unsigned short data)
{
*(unsigned short *)(LCD_DATA)= data;
};
unsigned char LCD_StatusRead(void)
{
unsigned short data = *(unsigned short *)(LCD_REG);
return data;
};
unsigned char LCD_DataRead(void)
{
unsigned short data = * (unsigned short *)(LCD_DATA);
return (unsigned char)data;
};
void LCD_RegisterWrite(unsigned char cmd, unsigned char data)
{
*(unsigned short *)(LCD_REG) = cmd;
*(unsigned short *)(LCD_DATA) = data;
};
unsigned char LCD_RegisterRead (unsigned char cmd)
{
volatile unsigned char data = 0;
LCD_CmdWrite (cmd);
data = LCD_DataRead ();
return data;
};
Далее сама функция инициализации дисплея. Код взят от поставщика дисплея и немного переработан под свои нужны. Подфункции занимают огромный объём и в этой статье я их приводить не буду.
void RA8876_Init(void)
{
RA8876_PLL_Init (); // Настройка тактирования RA8876 на 120 мГц
RA8876_SDRAM_Init (); // Настройка SDRAM на 166 мГц
TFT_24bit (); // Перевод дисплея в 24 битный режим ???
Host_Bus_16bit (); // Ширина шины 16 бит
RGB_16b_16bpp (); // Активация режима 16 бит
MemWrite_Left_Right_Top_Down (); // Заполнение дисплея слева направо и сверху вниз.
Graphic_Mode (); // Активация графического режима
Memory_Select_SDRAM (); // Выбор памяти SDRAM
/* Настройки RGB интерфейса между дисплеем и RA8876 */
HSCAN_L_to_R ();
VSCAN_T_to_B ();
PDATA_Set_RGB ();
PCLK_Falling ();
DE_High_Active ();
HSYNC_High_Active ();
VSYNC_High_Active ();
LCD_HorizontalWidth_VerticalHeight (1024, 600);
LCD_Horizontal_Non_Display (160);
LCD_HSYNC_Start_Position (160);
LCD_HSYNC_Pulse_Width (70);
LCD_Vertical_Non_Display (23);
LCD_VSYNC_Start_Position (12);
LCD_VSYNC_Pulse_Width (10);
// Конфигурация фреймбуффера
Frame_Buffer_Start_Address (PAGE0_START_ADDR);
Frame_Buffer_Width (1024);
Frame_Buffer_Start_XY (0, 0);
Frame_Buffer_Color_Mode_16bpp ();
// Конфигурация холста
Canvas_Window_Start_Address (PAGE0_START_ADDR);
Canvas_Window_Width (1024);
Canvas_Window_Start_XY (0, 0);
Canvas_Window_WH (1024, 600);
Canvas_Memory_XY_Mode ();
Canvas_Window_Color_Mode_16bpp ();
}
О фреймбуффере и активной области чуть подробнее. Для этих двух настроек определим следующие дефайны:
#define PAGE0_START_ADDR 0
#define PAGE1_START_ADDR (1024 * 600 * 2 * 1)
#define PAGE2_START_ADDR (1024 * 600 * 2 * 2)
#define PAGE3_START_ADDR (1024 * 600 * 2 * 3)
#define PAGE4_START_ADDR (1024 * 600 * 2 * 4)
#define PAGE5_START_ADDR (1024 * 600 * 2 * 5)
#define PAGE6_START_ADDR (1024 * 600 * 2 * 6)
#define PAGE7_START_ADDR (1024 * 600 * 2 * 7)
#define PAGE8_START_ADDR (1024 * 600 * 2 * 8)
#define PAGE9_START_ADDR (1024 * 600 * 2 * 9)
#define PAGE10_START_ADDR (1024 * 600 * 2 * 10)
#define PAGE11_START_ADDR (1024 * 600 * 2 * 11)
#define PAGE12_START_ADDR (1024 * 600 * 2 * 12)
Каждая страница (PAGEx_START_ADDR) это начальный адрес в памяти SDRAM. В 16 мегабайтах памяти мы можем разместить 13 полноценных слоёв размером 1228800 байт (1024 * 600 * 2).
Функция Frame_Buffer_Start_Address устанавливает начальную область памяти для фреймбуффера (то, что в данный момент выводится на экран).
Функция Canvas_Window_Start_Address устанавливает начальную область памяти для холста. Причём холст может быть больше фреймбуффера, благодаря чему можно сделать скроллинг изображения на экране. Например, для игры-платформера можно сделать длинный холст размером 13312 х 600 пикселов и потом его скроллить горизонтально, сдвигая по нему фреймбуффер по горизонтали.
Если сравнивать вывод графики с LTDC от STM32, то здесь не так всё радужно. Единовременно драйвер сам может выводить на дисплей только один итоговый (буфферный) слой, а LTDC сразу два, смешивая их, не требуя вашего участия в этом процессе.
Отрисовка примитивов
Код от поставщика дисплея с уже готовыми функциями:
void Start_Line (void);
void Start_Triangle (void);
void Start_Triangle_Fill (void);
void Line_Start_XY (unsigned short WX, unsigned short HY);
void Line_End_XY (unsigned short WX, unsigned short HY);
void Triangle_Point1_XY (unsigned short WX, unsigned short HY);
void Triangle_Point2_XY (unsigned short WX, unsigned short HY);
void Triangle_Point3_XY (unsigned short WX, unsigned short HY);
void Square_Start_XY (unsigned short WX, unsigned short HY);
void Square_End_XY (unsigned short WX, unsigned short HY);
void Start_Circle_or_Ellipse (void);
void Start_Circle_or_Ellipse_Fill (void);
void Start_Left_Down_Curve (void);
void Start_Left_Up_Curve (void);
void Start_Right_Up_Curve (void);
void Start_Right_Down_Curve (void);
void Start_Left_Down_Curve_Fill (void);
void Start_Left_Up_Curve_Fill (void);
void Start_Right_Up_Curve_Fill (void);
void Start_Right_Down_Curve_Fill (void);
void Start_Square (void);
void Start_Square_Fill (void);
void Start_Circle_Square (void);
void Start_Circle_Square_Fill (void);
void Circle_Center_XY (unsigned short WX, unsigned short HY);
void Ellipse_Center_XY (unsigned short WX, unsigned short HY);
void Circle_Radius_R (unsigned short WX);
void Ellipse_Radius_RxRy (unsigned short WX, unsigned short HY);
void Circle_Square_Radius_RxRy (unsigned short WX, unsigned short HY);
Например, для отрисовки закрашенного треугольника мы выставляем три точки Triangle_Point1_XY, Triangle_Point2_XY, Triangle_Point2_XY и запускаем функцию Start_Triangle_Fill.
Работа с DMA
Для удобства я написал свою функцию со структурой в качестве передаваемого параметра:
struct GFX_BTE_options
{
unsigned long layer_s0_addr; // Стартовый адрес исходного слоя 0
unsigned long layer_s1_addr; // Стартовый адрес исходного слоя 1
unsigned long layer_d_addr; // Стартовый адрес выходного слоя
unsigned short layer_s0_width; // Ширина исходного слоя 0
unsigned short layer_s1_width; // Ширина исходного слоя 1
unsigned short layer_d_width; // Ширина выходного слоя
unsigned short layer_s0_start_x; // Начальная точка Х слоя 0
unsigned short layer_s0_start_y; // Начальная точка Y слоя 0
unsigned short layer_s1_start_x; // Начальная точка X слоя 1
unsigned short layer_s1_start_y; // Начальная точка Y слоя 1
unsigned short layer_d_start_x; // Начальная точка X выходного слоя
unsigned short layer_d_start_y; // Начальная точка Y выходного слоя
unsigned short window_size_x; // Размер окна по горизонтали
unsigned short window_size_y; // Размер окна по вертикали
unsigned char rop_code; // Код ROP
unsigned char operation_code; // Код операции DMA
};
И собственно сама функция для работы DMA:
void GFX_BTE_operation (struct GFX_BTE_options options)
{
BTE_S0_Color_16bpp ();
BTE_S0_Memory_Start_Address (options.layer_s0_addr);
BTE_S0_Image_Width (options.layer_s0_width);
BTE_S0_Window_Start_XY (options.layer_s0_start_x, options.layer_s0_start_y);
BTE_S1_Color_16bpp ();
BTE_S1_Memory_Start_Address (options.layer_s1_addr);
BTE_S1_Image_Width (options.layer_s1_width);
BTE_S1_Window_Start_XY (options.layer_s1_start_x, options.layer_s1_start_y);
BTE_Destination_Color_16bpp ();
BTE_Destination_Memory_Start_Address (options.layer_d_addr);
BTE_Destination_Image_Width (options.layer_d_width);
BTE_Destination_Window_Start_XY (options.layer_d_start_x, options.layer_d_start_y);
BTE_Window_Size (options.window_size_x, options.window_size_y);
BTE_ROP_Code (options.rop_code);
BTE_Operation_Code (options.operation_code);
BTE_Enable ();
Check_BTE_Busy ();
}
Описание операционных кодов (OPERATION CODE):
0000: Запись в память с ROP с помощью МК.
0001: Чтение памяти без ROP с помощью МК.
0010: Копирование блока памяти в прямом направлении с помощью ROP.
0011: Копирование блока памяти в обратном направлении с помощью ROP.
0100: Запись в память (с прозрачностью) без ROP с помощью МК.
0101: Копирование (перемещение) блока памяти (с прозрачностью) в прямом направлении без ROP.
0110: Заливка шаблоном с помощью ROP.
0111: Заливка шаблоном с хромакейем.
1000: Расширение цвета
1001: Расширение цвета с прозрачностью
1010: Перемещение блока памяти в прямом направлении с альфа-смешиванием
1011: Запись в память с альфа-смешиванием с помощью МК.
1100: Заливка области памяти с помощью сплошного цвета.
1101: Reserved
1110: Reserved
1111: Reserved
Описание растровых кодов (ROP CODE):
0000b: 0 (Чёрный цвет)
0001b: ~S0・~S1 or ~ (S0 + S1)
0010b: ~S0・S1
0011b: ~S0
0100b: S0・~S1
0101b: ~S1
0110b: S0^S1
0111b: ~S0+~S1 or ~ (S0・S1)
1000b: S0・S1
1001b: ~ (S0 ^ S1)
1010b: S1
1011b: ~S0+S1
1100b: S0
1101b: S0+~S1
1110b: S0+S1
1111b: 1 (Белый цвет)
S0 — нулевой слой, S1 — первый слой. Взаимодействие между ними происходит с помощью арифметических и битовых операций.
Вывод строки встроенным шрифтом
void GFX_Show_String_TMODE (short x, short y, char *ptr, unsigned short charColor, unsigned short bkColor)
{
Foreground_color_65k (charColor);
Background_color_65k (bkColor);
CGROM_Select_Internal_CGROM ();
Font_Select_12x24_24x24 ();
Text_Mode ();
Goto_Text_XY (x, y);
LCD_CmdWrite (0x04);
while (*ptr != '')
{
LCD_DataWrite (*ptr);
Check_Mem_WR_FIFO_not_Full ();
++ptr;
}
Check_2D_Busy ();
Graphic_Mode (); //back to graphic mode
}
Полную версию драйвера можно найти на гитхаб по ссылке
Всем спасибо за чтение!
Автор: rubinstein