LVGL (Light and Versatile Graphics Library) is an open-source graphical user interface (GUI) library designed to provide efficient, low-resource-consumption graphical display solutions for embedded devices. It is widely used in low-power, resource-constrained embedded systems, supports multiple hardware platforms, and offers a rich set of graphical interface components and animation effects.
LVGL is completely open-source, which offers several significant advantages. First, it gives you full control over the library, as you can not only view, modify, compile, and debug the underlying source code but also acquire it in its entirety. Once downloaded, it belongs to you. This independence from a single vendor is immensely valuable. Additionally, being open-source encourages collaboration and knowledge sharing, with developers worldwide contributing to the software's improvement, making it more reliable and feature-rich to solve a wide range of real-world problems.
LVGL is released under the MIT license, which allows users to freely use, modify, and distribute the software without being subject to complex restrictions or conditions. This provides flexibility for developers and businesses to integrate the software into their own projects, even for commercial purposes, as long as they retain the original author's attribution.
AXI VDMA (abbreviated as VDMA) is a soft-core IP provided by Xilinx, used to convert data streams from AXI Stream format to Memory Map format or from Memory Map format to AXI Stream format. In other words, VDMA is designed to provide video read/write transfer functionality from the AXI4 domain to the AXI4-Stream domain, enabling high-speed data movement between system memory (primarily DDR3) and AXI4-Stream-based target video IPs. Its functionality is somewhat similar to AXI DMA (DMA), primarily providing high-bandwidth data transfer between PS-side memory and AXI4-Stream type target peripheral memories. VDMA is an upgraded version of DMA tailored for video and image applications. Compared to DMA, VDMA adds features such as a Frame Buffer mechanism and GenLock (dynamic synchronization locking). VDMA integrates video-specific functions like frame synchronization and 2D DMA transfers, making it highly suitable for image and video processing on the ZYNQ architecture and shortening the development cycle for developers.
The LCD screen used here has a resolution of 1024*600, an RGB888 interface, and a GT911 touch chip. The specific parameters of the LCD screen are shown in the figure below:
In this design, the core task of the image display process is to implement the driving and display functions of the LVGL graphics library on an RGB LCD. The overall system architecture, as shown in the figure, is primarily accomplished through the collaboration of the Processing System (PS) and Programmable Logic (PL).
The specific process is as follows:
The Processing System (PS) uses the DMA controller on the PS side to transfer the image data rendered by LVGL and write it into DDR3 memory.
The AXI VDMA controller on the PL side reads this image frame buffer data from DDR and transmits it in AXI-Stream format to the Video Out interface module.
The Video Timing Controller provides precise timing signals to ensure that the image data is strictly synchronized with the LCD timing requirements. Simultaneously, the clock configuration module allows for dynamic configuration of parameters such as display resolution and refresh rate.
The image data is ultimately output to the LCD screen in RGB888 signal format.
Overall Framework:
This will be based on the previously created Vivado project, which already includes functionalities like HDMI and PL_NET. For their configurations, you can refer to the previous setup steps. Here, we will primarily add or modify some configurations:
Supplement the Zynq PS related configurations:
Here, FCLK_CLK0 is used for LVGL, FCLK_CLK1 is for HDMI, and FCLK_CLK2 is for PL_NET1.
Here, HP0 is used for HDMI, and HP1 is for LVGL.
Add 7 GPIO pins via EMIO for the control signals of the LCD screen and touch panel.
Add VDMA. Search for and add the VDMA module, then double-click to open its configuration page.
This project only uses the read function of the VDMA, so the Enable Write Channel feature is unchecked here. The frame buffer is set to 2. Since the content we are displaying is RGB888 (24-bit color), the Stream Data Width is changed to 24. The Line Buffer Depth is changed to 2048, and the Read Burst Size is modified to 64.
Add the Video Out module. The purpose of this module is to convert the AXI4-Stream data flow into a standard video format output, corresponding to the RGB LCD screen.
Change Clock Mode to Independent. For Timing Mode: since we will use the Video Timing Controller module to provide video timing later, select Slave mode here:
Add the Video Timing Controller module: Search for and add the Video Timing Controller module, then double-click to open its configuration page.
Page one:
Page two:
On the second page, we set the resolution. Here, we will first set it to 1024x600. Select CUSTOM, and fill in the other parameters as shown in the figure below. These configurations can be modified later through the Vitis code on the PS side.
For a description of the corresponding parameters, you can refer to: Video Beginner Series 16: Understanding Video Timing with the VTC IP
Modify the parameters according to the figure below. The modification method will be described later.
Add the clk_wiz module: Search for and add the clk_wiz module, then double-click to open its configuration page:
Configure as shown in the figure below: The AXI interface needs to be enabled so that the output frequency can be controlled via Vitis.
At this point, we have all the necessary modules. The next step is to connect them.
Left-click the 'plus' sign on the vid_io_out port of the Video Out module to expand its contents. Select the vid_active_video, vid_data, vid_hsync, and vid_vsync signals, then right-click and select 'Make External' to bring out the signals.
Rename these signals to lcd_de, lcd_data, lcd_hsync, and lcd_vsync respectively. (Select the signal and modify its name in the 'Name' field in the left pane).
Connect the clk of the VTC (Video Timing Controller) module to the clk_out1 pixel clock. Since this clk and the pixel clock need to be highly synchronized, the clk of the AXI4-Stream to Video Out module should also be connected to this pixel clock.
Add the connection for the GEN_CLKEN of the Video Timing module as shown in the figure below.
Click 'Run Connection Automation' to automatically connect the remaining traces.
In the pop-up settings dialog, check all the items, and the system will automatically connect the remaining signal lines and add the necessary modules for us.
Add a Constant module to provide a constant '1', which will be used to connect the ce and aclken enable signals of our various modules.
The final connection diagram is as follows: The highlighted IPs are the new ones added in this project.
Save the project, then click Source → Design Sources. Right-click the block design we created and click 'Create HDL Wrapper'.
Configure the pin assignments according to the schematic:
The completed .xdc file:
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]}]
Compile and synthesize the project, generate the bitstream, then go to File → Export → Export Hardware to export the .xsa file.
Select the .xsa file you just exported and change the operating system to freertos:
Select an empty file here
Create a clk_wiz folder under src: used to set the output clock frequency of the IP core.
Add the .c file
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;}Code source: https://github.com/Digilent/Zybo-hdmi-out/tree/master/sdk/displaydemo/src/display_ctrl
In vga_modes.h, you need to modify the following parameters according to your screen resolution:
Configure according to the timing table above:
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)*/};Create touch.c and touch.h, using the GT911 touch screen:
About GT911
This chip is a capacitive touch screen driver IC that supports a 100Hz contact scan rate, 5-point touch, and 18*10 detection channels. The GT911 connects to the FPGA via 4 wires: SDA, SCL, RST, and INT. Among them, SDA and SCL are for I2C communication, RST is the reset pin (active low), and INT is the interrupt signal. The GT911 uses standard I2C communication with a maximum rate of 400KHz. The I2C device address for the GT911 can be 0X14 or 0X5D. Within 5ms after a reset, if the INT pin is high, the address 0X14 is used; otherwise, 0X5D is used. For this project, we will use 0xBA as the device address.
The code is as follows:
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;}Create vdma.c and vdma.h:
For the specific code implementation, you can refer to the example provided by Xilinx.
VDMA Code:
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;}Similar to VDMA, the code can be based on the example provided by Xilinx.
Modify the code based on the template:
In the Vivado project, the VDMA is configured with two frame buffers. During initialization, the VDMA is locked to the first frame buffer using the XAxiVdma_StartParking function. This ensures that the VDMA only reads data from the first frame buffer.
When the LVGL interface has data to update, we use DMA to move the data to the memory address of the VDMA's frame buffer. If the VDMA is currently locked to the first frame buffer, the DMA will move the data to the memory address of the second frame buffer. Conversely, if the VDMA is locked to the second frame buffer, the DMA will move the data to the first frame buffer's memory address. The implementation of this logic is detailed in the Porting LVGL Files section.
When the DMA transfer is complete, the frame index is updated in the corresponding callback function, and the VDMA is locked to the new frame buffer using the XAxiVdma_StartParking function.
xxxxxxxxxx
// DMA completion callback functionstatic 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; // wait for all channels to complete } } // When the DMA transfer is complete, update the VDMA frame buffer index frame_index = (frame_index + 1) % 2; // Lock the VDMA to this frame buffer XAxiVdma_StartParking(&vdma, frame_index, XAXIVDMA_READ);// We only define two frame buffers; this is to prevent simultaneous read and write operations on the same frame}
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;}It is important to note that there is a limit to the length of data a single DMA channel can transfer (found in the DMA BSP code: xdmaps.c).
xxxxxxxxxx// Based on the source code 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; // Total transfer size = DMA_BURST_SIZE × DMA_BURST_LEN × LoopCount// The larger the configured burst size and burst len, the higher the transfer efficiency, but there is a hardware support limit.The amount of data that needs to be transferred in a single operation varies with the LCD resolution. For example, at a 1024*600 resolution with RGB888 format, a single DMA transfer requires 1024 * 600 * 3 = 1843200 bytes.
At high resolutions, the length of the data to be transferred may exceed the maximum capacity of a single DMA channel. In this case, you can either increase the burst size and burst len, or split the data transfer across multiple DMA channels.
DMA section code:
xxxxxxxxxxbool xdma_init(uint32_t dma_dev_id, int dma_length){ int32_t Status = XST_FAILURE; dma_len_per_chan = dma_length; // Look up DMA configuration XDmaPs_Config *xmda_cfg = XDmaPs_LookupConfig(dma_dev_id); // Initialize the DMA controller instance 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; }
// Initialize DMA channel completion flags; by default, all channels are idle (done) for (int i = 0; i < MAX_DMA_CHANNELS; i++) { xdma_done[i] = true; }
// If the user-requested data length is greater than the maximum length of a single channel, split it (divide by 2) until it can be supported within a single channel 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 the number of allocated channels * length per channel >= total length, stop allocating 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; }
// Start all configured DMA channels for (int chan = 0; chan < chans_used; chan++) { XDmaPs_Start(&dma_inst, chan, &(xdma_cmd[chan]), 0); }}
LVGL_V9.2.2 Source Code Download Link
Create two folders under src: lvgl/lvgl and lvgl/lvgl_app,
Copy lvgl-9.2.2/src and lvgl-9.2.2/examples to src/lvgl/lvgl;
Copy lvgl-9.2.2/lv_version.h, lvgl-9.2.2/lv_version.h.in, lvgl-9.2.2/lvgl.h to src/lvgl/lvgl
Copy lvgl-9.2.2/lv_conf_template.h to src/lvgl and rename it to lv_conf.h
Modify the content of the lv_conf.h file according to the following images:
Delete all files and folders except porting under src/lvgl/lvgl/examples, and delete the osal folder under the porting file
In the FreeRTOS configuration of the platform project, set tick_rate to 1000, meaning one tick every 1ms; set use_tick_hook to true
Add the following to main.c:
xxxxxxxxxx
void vApplicationTickHook(){ lv_tick_inc(1); // lvgl heartbeat: 1ms}
Modify the following four files:
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.hChange #if 0 to #if 1 in all four files,
Modify the lv_port_indev_template.c file as follows:
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;}
In the lv_port_disp_template.c file, modify the screen width and height and add the following (keep the rest unchanged):
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}All files created at the end are as follows:
Connect the touchscreen and ZYNQ development board,
Use TYPE-C to connect the development board's JTAG port to the computer, perform Build and debug, and observe the LCD display.