LVGL (Light and Versatile Graphics Library) — это графическая библиотека с открытым исходным кодом для создания пользовательских интерфейсов (GUI), предназначенная для предоставления эффективных решений для графического отображения с низким потреблением ресурсов на встраиваемых устройствах. Она широко используется в маломощных встраиваемых системах с ограниченными ресурсами, поддерживает множество аппаратных платформ и предлагает богатый набор компонентов графического интерфейса и анимационных эффектов.
LVGL является полностью открытым исходным кодом, что дает несколько значительных преимуществ. Во-первых, это дает вам полный контроль над библиотекой, поскольку вы можете не только просматривать, изменять, компилировать и отлаживать исходный код, но и получать его целиком. После загрузки он принадлежит вам. Эта независимость от одного поставщика чрезвычайно ценна. Кроме того, открытый исходный код способствует сотрудничеству и обмену знаниями, поскольку разработчики со всего мира вносят свой вклад в улучшение программного обеспечения, делая его более надежным и многофункциональным для решения широкого круга реальных проблем.
LVGL выпускается под лицензией MIT, которая позволяет пользователям свободно использовать, изменять и распространять программное обеспечение без каких-либо сложных ограничений или условий. Это обеспечивает гибкость для разработчиков и компаний, позволяя им интегрировать программное обеспечение в свои проекты, в том числе в коммерческих целях, при условии сохранения указания на первоначального автора.
AXI VDMA (сокращенно VDMA) — это программное IP-ядро, предоставляемое Xilinx, которое используется для преобразования потоков данных из формата AXI Stream в формат Memory Map или из формата Memory Map в формат AXI Stream. Другими словами, VDMA предназначен для обеспечения функциональности передачи видеоданных (чтение/запись) из домена AXI4 в домен AXI4-Stream, что позволяет осуществлять высокоскоростное перемещение данных между системной памятью (в основном DDR3) и целевыми видео-IP на базе AXI4-Stream. Его функциональность в некоторой степени схожа с AXI DMA (DMA), в основном обеспечивая высокоскоростную передачу данных между памятью на стороне PS и целевыми периферийными устройствами типа AXI4-Stream. VDMA — это усовершенствованная версия DMA, специально разработанная для приложений, связанных с видео и изображениями. По сравнению с DMA, в VDMA добавлены такие функции, как механизм кадрового буфера (Frame Buffer) и GenLock (динамическая синхронизация). VDMA интегрирует специфичные для видео функции, такие как синхронизация кадров и 2D DMA-передачи, что делает его очень подходящим для обработки изображений и видео на архитектуре ZYNQ и сокращает цикл разработки для программистов.
Используемый здесь ЖК-экран имеет разрешение 1024*600, интерфейс RGB888 и сенсорный чип GT911. Конкретные параметры ЖК-экрана показаны на рисунке ниже:
В данном проекте ключевой задачей процесса отображения изображения является реализация функций управления и отображения графической библиотеки LVGL на RGB LCD. Общая архитектура системы, как показано на рисунке, в основном достигается за счет совместной работы Processing System (PS) и Programmable Logic (PL).
Конкретный процесс выглядит следующим образом:
Processing System (PS) использует DMA контроллер на стороне PS для передачи данных изображения, отрендеренного LVGL, и записи их в память DDR3.
AXI VDMA контроллер на стороне PL считывает эти данные буфера кадра из DDR и передает их в формате AXI-Stream в модуль Video Out interface.
Контроллер синхронизации видео (Video Timing Controller) предоставляет точные синхросигналы для обеспечения строгой синхронизации данных изображения с требованиями синхронизации LCD. Одновременно модуль конфигурации тактовых сигналов позволяет динамически настраивать такие параметры, как разрешение дисплея и частота обновления.
Данные изображения в конечном итоге выводятся на LCD экран в формате сигналов RGB888.
Общая структура:
Это будет основано на ранее созданном проекте Vivado, который уже включает такие функции, как HDMI и PL_NET. Для их конфигурации вы можете обратиться к предыдущим шагам настройки. Здесь мы в основном добавим или изменим некоторые конфигурации:
Дополним конфигурации, связанные с Zynq PS:
Здесь FCLK_CLK0 используется для LVGL, FCLK_CLK1 — для HDMI, а FCLK_CLK2 — для PL_NET1.
Здесь HP0 используется для HDMI, а HP1 — для LVGL.
Добавим 7 выводов GPIO через EMIO для управляющих сигналов LCD экрана и сенсорной панели.
Добавим VDMA. Найдите и добавьте модуль VDMA, затем дважды щелкните, чтобы открыть страницу его конфигурации.
В этом проекте используется только функция чтения VDMA, поэтому функция Enable Write Channel здесь отключена. Буфер кадров установлен на 2. Поскольку отображаемое содержимое — это RGB888 (24-битный цвет), ширина данных потока (Stream Data Width) изменена на 24. Глубина буфера строк (Line Buffer Depth) изменена на 2048, а размер пакета чтения (Read Burst Size) изменен на 64.
Добавим модуль Video Out. Цель этого модуля — преобразовать поток данных AXI4-Stream в стандартный видеовыход, соответствующий RGB LCD экрану.
Измените режим тактирования (Clock Mode) на Independent. Для режима синхронизации (Timing Mode): поскольку мы будем использовать модуль Video Timing Controller для предоставления видеосинхронизации позже, выберите здесь режим Slave:
Добавим модуль Video Timing Controller: Найдите и добавьте модуль Video Timing Controller, затем дважды щелкните, чтобы открыть страницу его конфигурации.
Страница первая:
Страница вторая:
На второй странице мы устанавливаем разрешение. Здесь мы сначала установим его на 1024x600. Выберите CUSTOM и заполните остальные параметры, как показано на рисунке ниже. Эти конфигурации можно будет изменить позже через код Vitis на стороне PS.
Описание соответствующих параметров можно найти по ссылке: Серия для начинающих по видео 16: Понимание видеосинхронизации с помощью IP VTC
Измените параметры в соответствии с рисунком ниже. Способ изменения будет описан позже.
Добавим модуль clk_wiz: Найдите и добавьте модуль clk_wiz, затем дважды щелкните, чтобы открыть страницу его конфигурации:
Настройте, как показано на рисунке ниже: Необходимо включить интерфейс AXI, чтобы выходная частота могла управляться через Vitis.
На этом этапе у нас есть все необходимые модули. Следующий шаг — их соединение.
Щелкните левой кнопкой мыши на знак «плюс» на порту vid_io_out модуля Video Out, чтобы развернуть его содержимое. Выберите сигналы vid_active_video, vid_data, vid_hsync и vid_vsync, затем щелкните правой кнопкой мыши и выберите «Make External», чтобы вывести сигналы.
Переименуйте эти сигналы в lcd_de, lcd_data, lcd_hsync и lcd_vsync соответственно. (Выберите сигнал и измените его имя в поле «Name» на левой панели).
Подключите clk модуля VTC (Video Timing Controller) к пиксельному тактовому сигналу clk_out1. Поскольку этот clk и пиксельный тактовый сигнал должны быть высокосинхронизированы, clk модуля AXI4-Stream to Video Out также должен быть подключен к этому пиксельному тактовому сигналу.
Добавьте соединение для GEN_CLKEN модуля Video Timing, как показано на рисунке ниже.
Нажмите «Run Connection Automation», чтобы автоматически соединить оставшиеся линии.
В появившемся диалоговом окне настроек отметьте все пункты, и система автоматически соединит оставшиеся сигнальные линии и добавит необходимые нам модули.
Добавьте модуль Constant для предоставления константы '1', которая будет использоваться для подключения сигналов разрешения ce и aclken наших различных модулей.
Окончательная схема соединений выглядит следующим образом: Выделенные IP-блоки — это новые, добавленные в этом проекте.
Сохраните проект, затем нажмите Source → Design Sources. Щелкните правой кнопкой мыши на созданный нами блочный дизайн и нажмите «Create HDL Wrapper».
Настройте назначение выводов в соответствии со схемой:
Готовый файл .xdc:
xset_property PACKAGE_PIN V18 [get_ports lcd_de]set_property PACKAGE_PIN W18 [get_ports lcd_hsync]set_property PACKAGE_PIN W19 [get_ports lcd_vsync]set_property IOSTANDARD LVCMOS33 [get_ports lcd_de]set_property IOSTANDARD LVCMOS33 [get_ports lcd_hsync]set_property IOSTANDARD LVCMOS33 [get_ports lcd_vsync]
set_property -dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS33} [get_ports {lcd_data[0]}]set_property -dict {PACKAGE_PIN U17 IOSTANDARD LVCMOS33} [get_ports {lcd_data[1]}]set_property -dict {PACKAGE_PIN T15 IOSTANDARD LVCMOS33} [get_ports {lcd_data[2]}]set_property -dict {PACKAGE_PIN T14 IOSTANDARD LVCMOS33} [get_ports {lcd_data[3]}]set_property -dict {PACKAGE_PIN Y17 IOSTANDARD LVCMOS33} [get_ports {lcd_data[4]}]set_property -dict {PACKAGE_PIN Y18 IOSTANDARD LVCMOS33} [get_ports {lcd_data[5]}]set_property -dict {PACKAGE_PIN Y16 IOSTANDARD LVCMOS33} [get_ports {lcd_data[6]}]set_property -dict {PACKAGE_PIN Y14 IOSTANDARD LVCMOS33} [get_ports {lcd_data[7]}]set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS33} [get_ports {lcd_data[8]}]set_property -dict {PACKAGE_PIN P19 IOSTANDARD LVCMOS33} [get_ports {lcd_data[9]}]set_property -dict {PACKAGE_PIN W15 IOSTANDARD LVCMOS33} [get_ports {lcd_data[10]}]set_property -dict {PACKAGE_PIN V15 IOSTANDARD LVCMOS33} [get_ports {lcd_data[11]}]set_property -dict {PACKAGE_PIN R17 IOSTANDARD LVCMOS33} [get_ports {lcd_data[12]}]set_property -dict {PACKAGE_PIN R16 IOSTANDARD LVCMOS33} [get_ports {lcd_data[13]}]set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {lcd_data[14]}]set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {lcd_data[15]}]set_property -dict {PACKAGE_PIN N17 IOSTANDARD LVCMOS33} [get_ports {lcd_data[16]}]set_property -dict {PACKAGE_PIN P18 IOSTANDARD LVCMOS33} [get_ports {lcd_data[17]}]set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports {lcd_data[18]}]set_property -dict {PACKAGE_PIN T17 IOSTANDARD LVCMOS33} [get_ports {lcd_data[19]}]set_property -dict {PACKAGE_PIN P15 IOSTANDARD LVCMOS33} [get_ports {lcd_data[20]}]set_property -dict {PACKAGE_PIN P16 IOSTANDARD LVCMOS33} [get_ports {lcd_data[21]}]set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports {lcd_data[22]}]set_property -dict {PACKAGE_PIN U19 IOSTANDARD LVCMOS33} [get_ports {lcd_data[23]}]
set_property IOSTANDARD LVCMOS33 [get_ports {lcd_reset[0]}]set_property PACKAGE_PIN P20 [get_ports {lcd_reset[0]}]set_property IOSTANDARD LVCMOS33 [get_ports lcd_clk]set_property PACKAGE_PIN W14 [get_ports lcd_clk]set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_0_tri_io[6]}]set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_0_tri_io[5]}]set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_0_tri_io[4]}]# lcd_blset_property PACKAGE_PIN V17 [get_ports {GPIO_0_0_tri_io[4]}]# lcd_tp_resetset_property PACKAGE_PIN V13 [get_ports {GPIO_0_0_tri_io[5]}]# lcd_tp_intset_property PACKAGE_PIN N20 [get_ports {GPIO_0_0_tri_io[6]}]
Скомпилируйте и синтезируйте проект, сгенерируйте битстрим, затем перейдите в File → Export → Export Hardware, чтобы экспортировать файл .xsa.
Выберите только что экспортированный файл .xsa и измените операционную систему на freertos:
Выберите здесь пустой файл
Создайте папку clk_wiz в каталоге src: используется для установки выходной тактовой частоты IP-ядра.
Добавьте файл .c
xxxxxxxxxx
// MHz
//Status Register//Clock Configuration Register 0//Clock Configuration Register 2//Clock Configuration Register 23
bool clk_wiz_cfg(uint32_t clk_device_id, double freq_MHz){ double div_factor = 0; uint32_t div_factor_int = 0, div_factor_frac = 0; uint32_t clk_divide = 0; uint32_t status = 0;
// Initialize XCLK_Wiz XClk_Wiz_Config *clk_cfg_ptr; clk_cfg_ptr = XClk_Wiz_LookupConfig(clk_device_id); XClk_Wiz_CfgInitialize(&clk_wiz_inst, clk_cfg_ptr, clk_cfg_ptr->BaseAddr);
if(freq_MHz <= 0) return false;
// Configure input clock frequency XClk_Wiz_WriteReg(clk_cfg_ptr->BaseAddr, CLK_CFG0_OFFSET, 0x00000a01); //10x input clock frequency // Calculate output frequency div_factor = CLK_WIZ_IN_FREQ * 10 / freq_MHz; div_factor_int = (uint32_t)div_factor; div_factor_frac = (uint32_t)((div_factor - div_factor_int) * 1000); clk_divide = div_factor_int | (div_factor_frac << 8);
XClk_Wiz_WriteReg(clk_cfg_ptr->BaseAddr, CLK_CFG2_OFFSET, clk_divide);
// bit0: SADDR_LOAD, bit1: SEN XClk_Wiz_WriteReg(clk_cfg_ptr->BaseAddr, CLK_CFG23_OFFSET, 0x00000003);
// Wait for the clock IP to lock while(1) { status = XClk_Wiz_ReadReg(clk_cfg_ptr->BaseAddr,CLK_SR_OFFSET); if(status & 0x00000001) //Bit0 Locked status { return true; // Clock is locked, return success } vTaskDelay(1); // Delay to avoid busy waiting }
return false;}Источник кода: https://github.com/Digilent/Zybo-hdmi-out/tree/master/sdk/displaydemo/src/display_ctrl
В файле vga_modes.h необходимо изменить следующие параметры в соответствии с разрешением вашего экрана:
Настройте в соответствии с приведенной выше таблицей таймингов:
xxxxxxxxxxstatic const VideoMode VMODE_1024x600 = { .label = "1024x600@60Hz", .width = 1024,/*Width of the active video frame*/ .height = 600,/*Height of the active video frame*/ //1024+160 .hps = 1184,/*Start time of Horizontal sync pulse, in pixel clocks (thd + thfp)*/ //1024+160+20 .hpe = 1204,/*End time of Horizontal sync pulse, in pixel clocks (thd + thfp + thpw)*/ //1024+160+20+140 .hmax = 1344,/*Total number of pixel clocks per line (thd + thfp + thpw + thb = th) */ .hpol = 0, /*hsync pulse polarity*/ //600+12 .vps = 612,/*Start time of Vertical sync pulse, in lines (tvd + tvfp)*/ //600+12+3 .vpe = 615,/*End time of Vertical sync pulse, in lines (tvd + tvfp + tvpw)*/ //600+12+3+20 .vmax = 635,/*Total number of lines per frame (tvd + tvfp + tvpw + tvb) */ .vpol = 0,/*vsync pulse polarity*/ .freq = 50.0/*Pixel Clock frequency (fclk)*/};Создайте файлы touch.c и touch.h, используя сенсорный экран GT911:
О GT911
Этот чип представляет собой драйвер емкостного сенсорного экрана, который поддерживает частоту сканирования контактов 100 Гц, 5-точечное касание и 18*10 каналов обнаружения. GT911 подключается к ПЛИС через 4 провода: SDA, SCL, RST и INT. Среди них SDA и SCL предназначены для связи по I2C, RST — это вывод сброса (активный низкий уровень), а INT — сигнал прерывания. GT911 использует стандартную связь I2C с максимальной скоростью 400 кГц. Адрес устройства I2C для GT911 может быть 0X14 или 0X5D. В течение 5 мс после сброса, если на выводе INT высокий уровень, используется адрес 0X14; в противном случае используется 0X5D. В этом проекте мы будем использовать 0xBA в качестве адреса устройства.
Код выглядит следующим образом:
xxxxxxxxxx//定义寄存器地址// 7-bit address
static XIicPs i2c;static XGpioPs Gpio;
static bool GT911_ReadReg(uint16_t reg, uint8_t *buf, uint16_t len){ uint8_t reg_buf[2]; int status;
// Set the high and low bytes of the register address reg_buf[0] = (uint8_t)(reg >> 8); reg_buf[1] = (uint8_t)(reg & 0xFF);
// send the read cmd status = XIicPs_MasterSendPolled(&i2c, reg_buf, 2, GT911_SLAVE_ADDR); if (status != XST_SUCCESS) { return false; }
// Wait for the I2C bus to be idle while (XIicPs_BusIsBusy(&i2c));
// Receive data from the I2C bus status = XIicPs_MasterRecvPolled(&i2c, buf, len, GT911_SLAVE_ADDR); if (status != XST_SUCCESS) { return false; }
// Wait for the I2C bus to be idle again while (XIicPs_BusIsBusy(&i2c));
return true;}
static bool GT911_WriteReg(uint16_t reg, uint8_t *buf, uint16_t len){ uint8_t reg_buf[256] = {0}; int status;
// Set the high and low bytes of the register address reg_buf[0] = (uint8_t)(reg >> 8); reg_buf[1] = (uint8_t)(reg & 0xFF);
// Copy the data from the buffer into reg_buf memcpy(®_buf[2], buf, len);
// Send the write command status = XIicPs_MasterSendPolled(&i2c, reg_buf, len + 2, GT911_SLAVE_ADDR); if (status != XST_SUCCESS) { return false; }
// Wait for the I2C bus to be idle while (XIicPs_BusIsBusy(&i2c));
return true;}
bool GT911_Init(void){ uint8_t id[4] = {0}; XIicPs_Config *i2cConfig = NULL; XGpioPs_Config *ConfigPtr = NULL;
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID); XGpioPs_CfgInitialize(&Gpio, ConfigPtr,ConfigPtr->BaseAddr);
XGpioPs_SetDirectionPin(&Gpio, LCD_BLK, 1); XGpioPs_SetOutputEnablePin(&Gpio, LCD_BLK, 1); XGpioPs_WritePin(&Gpio, LCD_BLK, 1);
XGpioPs_SetDirectionPin(&Gpio, TP_RESET, 1); // Set TP_RESET as output XGpioPs_SetDirectionPin(&Gpio, TP_INT, 1); // Set TP_INT as output XGpioPs_SetOutputEnablePin(&Gpio, TP_RESET, 1); XGpioPs_SetOutputEnablePin(&Gpio, TP_INT, 1);
//SET 0XBA/0XBB XGpioPs_WritePin(&Gpio, TP_RESET, 0); // Set TP_RESET low XGpioPs_WritePin(&Gpio, TP_INT, 0); // Set TP_INT low: SET addr to 0XBA/0XBB vTaskDelay(10); // Delay at least 100us XGpioPs_WritePin(&Gpio, TP_RESET, 1); vTaskDelay(20); // Delay at least 5ms XGpioPs_SetDirectionPin(&Gpio, TP_INT, 0);//input_floating XGpioPs_SetOutputEnablePin(&Gpio, TP_INT, 0); vTaskDelay(10);
i2cConfig = XIicPs_LookupConfig(GT911_I2C_DEV_ID); XIicPs_CfgInitialize(&i2c, i2cConfig, i2cConfig->BaseAddress); XIicPs_SetSClk(&i2c, 100000);
//READ ID GT911_ReadReg(GT911_PRODUCT_ID_ADDR, id, 4); id[3]='\0'; xil_printf("CTP ID:%s\r\n", id);
if(strcmp((char*)id,"911")==0){ return 0; }
return 1;}
uint8_t GT911_ScanTouch(GT911_TouchPoint *points, uint8_t max_points){ uint8_t status = 0; uint8_t touch_num = 0; uint8_t clear = 0;
if (!GT911_ReadReg(GT911_STATUS_ADDR, &status, 1)) goto _exit;
touch_num = status & 0x0F; if (touch_num == 0 || touch_num > max_points) goto _exit;
uint8_t buf[8 * GT911_MAX_TOUCHES] = {0}; if (!GT911_ReadReg(GT911_POINT_DATA_ADDR, buf, touch_num * 8)) goto _exit;
for (uint8_t i = 0; i < touch_num; ++i) { uint8_t *p = &buf[i * 8]; points[i].id = p[0]; points[i].x = p[1] | (p[2] << 8); points[i].y = p[3] | (p[4] << 8); points[i].size = p[5] | (p[6] << 8); }
_exit: // clear the status register GT911_WriteReg(GT911_STATUS_ADDR, &clear, 1);
return touch_num;}Создайте vdma.c и vdma.h:
Для конкретной реализации кода вы можете обратиться к примеру, предоставленному Xilinx.
Код VDMA:
xxxxxxxxxxtypedef struct vdma_handle{ /* The device ID of the VDMA */ unsigned int device_id; /* The state variable to keep track if the initialization is done*/ unsigned int init_done; /** The XAxiVdma driver instance data. */ XAxiVdma* InstancePtr; /* The XAxiVdma_DmaSetup structure contains all the necessary information to * start a frame write or read. */ XAxiVdma_DmaSetup ReadCfg; XAxiVdma_DmaSetup WriteCfg; /* Horizontal size of frame */ unsigned int hsize; /* Vertical size of frame */ unsigned int vsize; /* Buffer address from where read and write will be done by VDMA */ unsigned int buffer_address; /* Flag to tell VDMA to interrupt on frame completion*/ unsigned int enable_frm_cnt_intr; /* The counter to tell VDMA on how many frames the interrupt should happen*/ unsigned int number_of_frame_count;}vdma_handle;
int run_vdma_frame_buffer(XAxiVdma* InstancePtr, int DeviceId, int hsize, int vsize, int buf_base_addr, int number_frame_count, int enable_frm_cnt_intr, vdma_work_mode mode){ int Status,i; XAxiVdma_Config *Config; XAxiVdma_FrameCounter FrameCfgPtr;
/* This is one time initialization of state machine context. * In first call it will be done for all VDMA instances in the system. */ if(context_init==0) { for(i=0; i < XPAR_XAXIVDMA_NUM_INSTANCES; i++) { vdma_context[i].InstancePtr = NULL; vdma_context[i].device_id = -1; vdma_context[i].hsize = 0; vdma_context[i].vsize = 0; vdma_context[i].init_done = 0; vdma_context[i].buffer_address = 0; vdma_context[i].enable_frm_cnt_intr = 0; vdma_context[i].number_of_frame_count = 0;
} context_init = 1; }
/* The below initialization will happen for each VDMA. The API argument * will be stored in internal data structure */
/* The information of the XAxiVdma_Config comes from hardware build. * The user IP should pass this information to the AXI DMA core. */ Config = XAxiVdma_LookupConfig(DeviceId); if (!Config) { xil_printf("No video DMA found for ID %d\r\n",DeviceId ); return XST_FAILURE; }
if(vdma_context[DeviceId].init_done ==0) { vdma_context[DeviceId].InstancePtr = InstancePtr;
/* Initialize DMA engine */ Status = XAxiVdma_CfgInitialize(vdma_context[DeviceId].InstancePtr, Config, Config->BaseAddress); if (Status != XST_SUCCESS) { xil_printf("Configuration Initialization failed %d\r\n", Status); return XST_FAILURE; }
vdma_context[DeviceId].init_done = 1; }
vdma_context[DeviceId].device_id = DeviceId; vdma_context[DeviceId].vsize = vsize;
vdma_context[DeviceId].buffer_address = buf_base_addr; vdma_context[DeviceId].enable_frm_cnt_intr = enable_frm_cnt_intr; vdma_context[DeviceId].number_of_frame_count = number_frame_count;
/* Setup the write channel */ if (mode == WRITE_ONLY || mode == READ_WRITE) { vdma_context[DeviceId].hsize = hsize * (Config->S2MmStreamWidth>>3); Status = WriteSetup(&vdma_context[DeviceId]); if (Status != XST_SUCCESS) { xil_printf("Write channel setup failed %d\r\n", Status); if(Status == XST_VDMA_MISMATCH_ERROR) xil_printf("DMA Mismatch Error\r\n"); return XST_FAILURE; } }
/* Setup the read channel */ if (mode == READ_ONLY || mode == READ_WRITE) { vdma_context[DeviceId].hsize = hsize * (Config->Mm2SStreamWidth>>3); Status = ReadSetup(&vdma_context[DeviceId]); if (Status != XST_SUCCESS) { xil_printf("Read channel setup failed %d\r\n", Status); if(Status == XST_VDMA_MISMATCH_ERROR) xil_printf("DMA Mismatch Error\r\n"); return XST_FAILURE; } }
/* The frame counter interrupt is enabled, setting VDMA for same */ if(vdma_context[DeviceId].enable_frm_cnt_intr) { FrameCfgPtr.ReadDelayTimerCount = 1; FrameCfgPtr.ReadFrameCount = number_frame_count; FrameCfgPtr.WriteDelayTimerCount = 1; FrameCfgPtr.WriteFrameCount = number_frame_count;
XAxiVdma_SetFrameCounter(vdma_context[DeviceId].InstancePtr,&FrameCfgPtr); /* Enable DMA read and write channel interrupts. The configuration for interrupt * controller will be done by application */ XAxiVdma_IntrEnable(vdma_context[DeviceId].InstancePtr, XAXIVDMA_IXR_ERROR_MASK | XAXIVDMA_IXR_FRMCNT_MASK,XAXIVDMA_WRITE); XAxiVdma_IntrEnable(vdma_context[DeviceId].InstancePtr, XAXIVDMA_IXR_ERROR_MASK | XAXIVDMA_IXR_FRMCNT_MASK,XAXIVDMA_READ); } else { /* Enable DMA read and write channel interrupts. The configuration for interrupt * controller will be done by application */ XAxiVdma_IntrEnable(vdma_context[DeviceId].InstancePtr, XAXIVDMA_IXR_ERROR_MASK,XAXIVDMA_WRITE); XAxiVdma_IntrEnable(vdma_context[DeviceId].InstancePtr, XAXIVDMA_IXR_ERROR_MASK ,XAXIVDMA_READ); }
/* Start the DMA engine to transfer */ Status = StartTransfer(vdma_context[DeviceId].InstancePtr, mode); if (Status != XST_SUCCESS) { if(Status == XST_VDMA_MISMATCH_ERROR) xil_printf("DMA Mismatch Error\r\n"); return XST_FAILURE; }
// start parking mode on a certain frame // Park the VDMA on the first frame buffer XAxiVdma_StartParking(vdma_context[DeviceId].InstancePtr, frame_index, XAXIVDMA_READ);
return XST_SUCCESS;}
static int StartTransfer(XAxiVdma *InstancePtr, vdma_work_mode mode){ int Status = XST_SUCCESS; /* Start the write channel of VDMA */ if (mode == WRITE_ONLY || mode == READ_WRITE) { Status = XAxiVdma_DmaStart(InstancePtr, XAXIVDMA_WRITE); if (Status != XST_SUCCESS) { xil_printf("Start Write transfer failed %d\r\n", Status);
return XST_FAILURE; } } /* Start the Read channel of VDMA */ if (mode == READ_ONLY || mode == READ_WRITE) { Status = XAxiVdma_DmaStart(InstancePtr, XAXIVDMA_READ); if (Status != XST_SUCCESS) { xil_printf("Start read transfer failed %d\r\n", Status);
return XST_FAILURE; } }
return XST_SUCCESS;}Аналогично VDMA, код может быть основан на примере, предоставленном Xilinx.
Измените код на основе шаблона:
В проекте Vivado VDMA настроен с двумя кадровыми буферами. Во время инициализации VDMA блокируется на первом кадровом буфере с помощью функции XAxiVdma_StartParking. Это гарантирует, что VDMA будет считывать данные только из первого кадрового буфера.
Когда интерфейсу LVGL необходимо обновить данные, мы используем DMA для перемещения данных в адрес памяти кадрового буфера VDMA. Если VDMA в данный момент заблокирован на первом кадровом буфере, DMA переместит данные в адрес памяти второго кадрового буфера. И наоборот, если VDMA заблокирован на втором кадровом буфере, DMA переместит данные в адрес памяти первого кадрового буфера. Реализация этой логики подробно описана в разделе Porting LVGL Files.
Когда передача DMA завершена, индекс кадра обновляется в соответствующей функции обратного вызова, и VDMA блокируется на новом кадровом буфере с помощью функции XAxiVdma_StartParking.
xxxxxxxxxx
// Функция обратного вызова по завершении DMAstatic void xdma_done_handler(unsigned int Channel, XDmaPs_Cmd *xdma_cmd, void *CallbackRef){ xdma_done[Channel] = true;
for (int i = 0; i < MAX_DMA_CHANNELS; i++) { if (xdma_done[i] == false) { return; // ждем завершения всех каналов } } // Когда передача DMA завершена, обновляем индекс кадрового буфера VDMA frame_index = (frame_index + 1) % 2; // Блокируем VDMA на этом кадровом буфере XAxiVdma_StartParking(&vdma, frame_index, XAXIVDMA_READ);// Мы определяем только два кадровых буфера; это для предотвращения одновременных операций чтения и записи в один и тот же кадр}
static bool setup_dma_interrupt(XDmaPs *DmaPtr, uint32_t chan){ XScuGic_Connect(&gic_inst, dma_done_intr_ids[chan], (Xil_InterruptHandler)xdma_done_isr[chan], (void *)DmaPtr);
XScuGic_Enable(&gic_inst, dma_done_intr_ids[chan]);
return XST_SUCCESS;}Важно отметить, что существует ограничение на длину данных, которые может передать один канал DMA (это можно найти в коде BSP DMA: xdmaps.c).
xxxxxxxxxx// На основе исходного кода xdmaps.c/* * the loop count register is 8-bit wide, so if we need * a larger loop, we need to have nested loops */ if (LoopCount > 256) { LoopCount1 = LoopCount / 256; if (LoopCount1 > 256) { xil_printf("DMA operation cannot fit in a 2-level " "loop for channel %d, please reduce the " "DMA length or increase the burst size or " "length", Channel); return 0; } LoopResidue = LoopCount % 256; // Общий размер передачи = DMA_BURST_SIZE × DMA_BURST_LEN × LoopCount// Чем больше настроенные burst size и burst len, тем выше эффективность передачи, но существует ограничение аппаратной поддержки.Объем данных, который необходимо передать за одну операцию, зависит от разрешения ЖК-дисплея. Например, при разрешении 1024*600 в формате RGB888 одна передача DMA требует 1024 * 600 * 3 = 1843200 байт.
При высоких разрешениях длина передаваемых данных может превышать максимальную емкость одного канала DMA. В этом случае можно либо увеличить burst size и burst len, либо разделить передачу данных на несколько каналов DMA.
Код секции DMA:
xxxxxxxxxxbool xdma_init(uint32_t dma_dev_id, int dma_length){ int32_t Status = XST_FAILURE; dma_len_per_chan = dma_length; // Поиск конфигурации DMA XDmaPs_Config *xmda_cfg = XDmaPs_LookupConfig(dma_dev_id); // Инициализация экземпляра контроллера DMA Status = XDmaPs_CfgInitialize(&dma_inst, xmda_cfg, xmda_cfg->BaseAddress); if (Status != XST_SUCCESS) return XST_FAILURE;
if (dma_length > MAX_DMA_LENGTH * MAX_DMA_CHANNELS || dma_length < 0) { xil_printf("Invalid DMA length: %d\n", dma_length); return XST_FAILURE; }
// Инициализация флагов завершения каналов DMA; по умолчанию все каналы свободны (завершены) for (int i = 0; i < MAX_DMA_CHANNELS; i++) { xdma_done[i] = true; }
// Если запрашиваемая пользователем длина данных превышает максимальную длину одного канала, разделяем ее (делим на 2) до тех пор, пока она не сможет поддерживаться одним каналом while (dma_len_per_chan > MAX_DMA_LENGTH) { dma_len_per_chan = dma_len_per_chan / 2; }
for (int i = 0; i < MAX_DMA_CHANNELS; i++) { chans_used++; setup_dma_interrupt(&dma_inst, i);
XDmaPs_SetDoneHandler(&dma_inst, i, xdma_done_handler, NULL); memset(&(xdma_cmd[i]), 0, sizeof(XDmaPs_Cmd));
xdma_cmd[i].ChanCtrl.SrcBurstSize = DMA_BURST_SIZE; xdma_cmd[i].ChanCtrl.SrcBurstLen = DMA_BURST_LEN; xdma_cmd[i].ChanCtrl.SrcInc = 1; xdma_cmd[i].ChanCtrl.DstBurstSize = DMA_BURST_SIZE; xdma_cmd[i].ChanCtrl.DstBurstLen = DMA_BURST_LEN; xdma_cmd[i].ChanCtrl.DstInc = 1;
xdma_cmd[i].BD.SrcAddr = (u32) NULL; xdma_cmd[i].BD.DstAddr = (u32) NULL; xdma_cmd[i].BD.Length = dma_len_per_chan;
// Если количество выделенных каналов * длина на канал >= общей длины, прекращаем выделение if (dma_len_per_chan * chans_used >= dma_length) { break; } }
return XST_SUCCESS;}
void xdma_start(uint32_t dst_addr, uint32_t src_addr){ for (int chan = 0; chan < chans_used; chan++) { xdma_cmd[chan].BD.DstAddr = dst_addr + (chan * dma_len_per_chan); xdma_cmd[chan].BD.SrcAddr = src_addr + (chan * dma_len_per_chan); xdma_done[chan] = false; }
// Запуск всех настроенных каналов DMA for (int chan = 0; chan < chans_used; chan++) { XDmaPs_Start(&dma_inst, chan, &(xdma_cmd[chan]), 0); }}
Ссылка для скачивания исходного кода LVGL_V9.2.2
Создайте две папки в каталоге src: lvgl/lvgl и lvgl/lvgl_app,
Скопируйте lvgl-9.2.2/src и lvgl-9.2.2/examples в src/lvgl/lvgl;
Скопируйте lvgl-9.2.2/lv_version.h, lvgl-9.2.2/lv_version.h.in, lvgl-9.2.2/lvgl.h в src/lvgl/lvgl
Скопируйте lvgl-9.2.2/lv_conf_template.h в src/lvgl и переименуйте его в lv_conf.h
Измените содержимое файла lv_conf.h в соответствии со следующими изображениями:
Удалите все файлы и папки, кроме porting, в каталоге src/lvgl/lvgl/examples, а также удалите папку osal в файле porting
В конфигурации FreeRTOS проекта платформы установите tick_rate равным 1000, что означает один тик каждые 1 мс; установите use_tick_hook в значение true
Добавьте следующее в main.c:
xxxxxxxxxx
void vApplicationTickHook(){ lv_tick_inc(1); // lvgl heartbeat: 1ms}
Измените следующие четыре файла:
xxxxxxxxxxsrc/lvgl/examples/lv_port_indev_template.csrc/lvgl/examples/lv_port_indev_template.hsrc/lvgl/examples/lv_port_disp_template.csrc/lvgl/examples/lv_port_disp_template.hИзмените #if 0 на #if 1 во всех четырех файлах,
Измените файл lv_port_indev_template.c следующим образом:
xxxxxxxxxx//Add header files
//Add definitionGT911_TouchPoint tp_points[GT911_MAX_TOUCHES];
//Modify touchpad_initstatic void touchpad_init(void){ /*Your code comes here*/ // add by mind GT911_Init();}
//Modify touchpad_is_pressedstatic bool touchpad_is_pressed(void){ /*Your code comes here*/ // add by mind return GT911_ScanTouch(tp_points, GT911_MAX_TOUCHES) > 0;}
//Modify touchpad_get_xystatic void touchpad_get_xy(int32_t * x, int32_t * y){ /*Your code comes here*/ // add by mind *x = tp_points[0].x; *y = tp_points[0].y;}
В файле lv_port_disp_template.c измените ширину и высоту экрана и добавьте следующее (остальное оставьте без изменений):
xxxxxxxxxx
// in bytes
extern uint32_t const frame_buffer_addr;uint8_t frame_index=0;extern VideoMode vd_mode;
//Modify lv_port_disp_init()void lv_port_disp_init(void){ /*------------------------- * Initialize your display * -----------------------*/ disp_init();
LV_ASSERT(vd_mode.width == MY_DISP_HOR_RES); LV_ASSERT(vd_mode.height == MY_DISP_VER_RES);
/*------------------------------------ * Create a display and set a flush_cb * -----------------------------------*/ lv_display_t * disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES); lv_display_set_flush_cb(disp, disp_flush);
/* Example 1 * One buffer for partial rendering*/ // LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_1_1[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL]; /*A buffer for 10 rows*/ // lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
/* Example 2 * Two buffers for partial rendering * In flush_cb DMA or similar hardware should be used to update the display in the background.*/ // LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_2_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];
// LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_2_2[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL]; // lv_display_set_buffers(disp, buf_2_1, buf_2_2, sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
/* Example 3 * Two buffers screen sized buffer for double buffering. * Both LV_DISPLAY_RENDER_MODE_DIRECT and LV_DISPLAY_RENDER_MODE_FULL works, see their comments*/ LV_ATTRIBUTE_MEM_ALIGN static uint8_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL];
LV_ATTRIBUTE_MEM_ALIGN static uint8_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL]; lv_display_set_buffers(disp, buf_3_1, buf_3_2, sizeof(buf_3_1), LV_DISPLAY_RENDER_MODE_DIRECT);
}
//Modify disp_init()static void disp_init(void){ /*You code here*/ // add by mind xdma_init(DMA_DEVICE_ID, DMA_TOTAL_LEN); // Initialize DMA}
static void disp_flush(lv_display_t * disp_drv, const lv_area_t * area, uint8_t * px_map){ if(disp_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
uint16_t x; uint16_t y; uint32_t color_index = 0; uint8_t * lcd_base_addr = (uint8_t *)frame_buffer_addr;
// add by mind if (frame_index == 0) { // When the current VDMA displays the first frame of data (frame_index is 0), use XDMA to write to the second frame's memory space xdma_start((uint32_t)(frame_buffer_addr + DMA_TOTAL_LEN), (uint32_t)px_map); } else { // When the current VDMA displays the second frame of data (frame_index is 1), use XDMA to write to the first frame's memory space xdma_start((uint32_t)frame_buffer_addr, (uint32_t)px_map); } }
/*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_display_flush_ready(disp_drv);}xxxxxxxxxx/* Copyright (C) 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. Copyright (c) 2012 - 2020 Xilinx, Inc. All Rights Reserved. SPDX-License-Identifier: MIT
http://www.FreeRTOS.org http://aws.amazon.com/freertos
1 tab == 4 spaces!*/
/* FreeRTOS includes. *//* Xilinx includes. */
// LVGL
// clk IP ID// VDMA ID// VTC ID
/*-----------------------------------------------------------*/
static void prvLvglTask( void *pvParameters );/*-----------------------------------------------------------*/
/* The queue used by the Tx and Rx tasks, as described at the top of thisfile. */static TaskHandle_t xLvglTask;
XScuGic gic_inst;XAxiVdma vdma;DisplayCtrl disp_ctrl;VideoMode vd_mode = VMODE_1024x600;uint32_t const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR + 0x2000000);
static bool SetupInterruptSystem(){ XScuGic_Config *gic_config = NULL; int32_t Status = XST_FAILURE;
Xil_ExceptionInit(); gic_config = XScuGic_LookupConfig(INTC_DEVICE_ID); Status = XScuGic_CfgInitialize(&gic_inst, gic_config, gic_config->CpuBaseAddress); if (Status != XST_SUCCESS) return XST_FAILURE;
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &gic_inst);
Xil_ExceptionEnable();
return XST_SUCCESS;}
int main( void ){ xil_printf( "Hello from Freertos example main\r\n" );
SetupInterruptSystem();
xTaskCreate( prvLvglTask, /* The function that implements the task. */ ( const char * ) "Lvgl", /* Text name for the task, provided to assist debugging only. */ configMINIMAL_STACK_SIZE*50, /* The stack allocated to the task. */ NULL, /* The task parameter is not used, so set to NULL. */ tskIDLE_PRIORITY+1, /* The task runs at the idle priority. */ &xLvglTask );
/* Start the tasks and timer running. */ vTaskStartScheduler();
/* If all is well, the scheduler will now be running, and the following line will never be reached. If the following line does execute, then there was insufficient FreeRTOS heap memory available for the idle and/or timer tasks to be created. See the memory management section on the FreeRTOS web site for more details. */ for( ;; );}
/*-----------------------------------------------------------*/static void prvLvglTask( void *pvParameters ){ run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height, frame_buffer_addr,0, 0, READ_ONLY);
clk_wiz_cfg(CLK_WIZ_ID, vd_mode.freq); DisplayInitialize(&disp_ctrl, DISP_VTC_ID); DisplaySetMode(&disp_ctrl, &vd_mode); DisplayStart(&disp_ctrl);
lv_init(); lv_port_disp_init(); lv_port_indev_init(); lv_demo_music(); // lv_demo_widgets(); // lv_demo_benchmark();
while (1) { lv_task_handler(); vTaskDelay(10); }}
void vApplicationTickHook(){ lv_tick_inc(1); // lvgl heartbeat: 1ms}Все созданные в итоге файлы выглядят следующим образом:
Подключите сенсорный экран и отладочную плату ZYNQ,
Используйте TYPE-C для подключения порта JTAG отладочной платы к компьютеру, выполните сборку и отладку, и наблюдайте за ЖК-дисплеем.