LVGL(轻量级图形库)是一款开源的图形用户界面(GUI)库,旨在为嵌入式设备提供高效、低资源消耗的图形显示解决方案。它广泛应用于低功耗、资源受限的嵌入式系统中,支持多种硬件平台,并提供丰富的图形界面组件和动画效果。
LVGL 是完全开源的,这有几个显著的优势。首先,它让你可以完全掌控该库,因为你不仅可以查看、修改、编译和调试底层源代码,还可以完全获取它。一旦下载,它就属于你了。这种不依赖单一供应商的独立性具有巨大的价值。此外,开源还鼓励协作与知识共享,全球的开发者都在为软件的改进做出贡献,使其变得更加可靠并功能更加丰富,以解决现实生活中的各种问题。
LVGL 采用 MIT 许可证发布,允许用户自由使用、修改和分发软件,而无需承担复杂的限制或条件。这为开发者和企业提供了灵活性,可以将该软件集成到自己的项目中,甚至用于商业目的,同时仍需保留对原作者的署名。
AXI VDMA(简称 VDMA),是 Xilinx 提供的软核 IP,用于将 AXI Stream 格式的数据流转换为 Memory Map 格式的数据流或将 Memory Map 格式的数据流转换为 AXI Stream 格式的数据流,也就是说 VDMA 旨在提供从 AXI4 域到 AXI4-Stream 域的视频读/写传输功能,从而实现系统内存(主要指 DDR3)和基于 AXI4-Stream 的目标视频 IP 之间的高速数据移动。其功能和 AXI DMA( DMA)有些类似,主要是为 PS 端存储器和 AXI4-Stream 类目标外设存储器之间提供高带宽的数据搬运。VDMA 是针对视频图像应用而做的升级版的 DMA,和 DMA 相比,VDMA 增加了帧缓存(Frame Buffer)的缓冲机制和动态同步锁相(GenLock)等功能。VDMA 集成了视频专用功能,如帧同步和 2D DMA 传输等,非常适合基于 ZYNQ 架构上的图像和视频处理,缩短了开发者的开发周期。
这里使用的液晶屏的分辨率是1024*600,接口为RGB888,触摸芯片为GT911。液晶屏的具体参数如下图:
在本设计中,图像显示流程的核心任务是实现 LVGL 图形库在 RGB LCD 上的驱动与显示功能。系统整体架构如图所示,主要由 处理系统(PS) 与 可编程逻辑(PL) 协同完成。
具体过程如下:
处理器系统(PS)通过PS侧的 DMA 控制器 将 LVGL 渲染生成的图像数据传输并写入 DDR3 存储器。
位于 PL 端的 AXI VDMA 控制器 从 DDR 中读取这些图像帧缓冲数据,并以 AXI-Stream 的形式传输至 Video Out 接口模块。
视频时序控制器(Video Timing Controller) 提供精确的时序信号,以确保图像数据与 LCD 时序要求严格同步。同时,通过时钟配置模块,实现对显示分辨率、刷新率等参数的动态配置。
图像数据最终以 RGB888信号形式输出至 LCD 显示屏。
总体框架:
在之前创建的vivado工程基础上进行,此前工程已经包含了HDMI、PL_NET等功能,其相关配置可以参考之前的配置步骤,本次我们主要是需要补充或修改一些配置:
补充zynq PS的相关配置:
此处的FCLK_CLK0给LVGL使用,FCLK_CLK1 给HDMI使用,FCLK_CLK2给PL_NET1使用。
此处HP0为HDMI使用,HP1为LVGL使用。
通过EMIO添加7个GPIO引脚,用于 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,其他的参数按下图所示填写即可。后面可以通过PS端的vitis代码修改这些配置。
相应的参数,其描述可以参考:Video Beginner Series 16: Understanding Video Timing with the VTC IP
根据下图进行修改参数,修改方式将在下文进行叙述。
增加clk_wiz模块:搜索并添加 clk_wiz模块,并双击打开配置页:
按照下图所示进行配置:需要使能AXI接口,这样可以通过vitis控制输出频率。
这时我们已经得到了所需要的各个模块,接下来的工作就是连接各个模块。
左键 video out 模块的vid_io_out 端口的“加号”,展开端口的内容,并选中vid_active_video,vid_data,vid_hsync,vid_vsync这几个信号,并右键make external 将信号引出
分别将这些信号重命名为lcd_de,lcd_data,lcd_hsync,lcd_vsync。 (选中信号,在左侧窗格的name 中修改)
将VTC(Video Timing Controller)模块的clk与clk_out1像素时钟链接,因为这个clk和像素时钟需要高度同步,同理Axi4-Stream to video_out模块的clk也应该和这个像素时钟连接。
增加video timing 模块的GEN_CLKEN连线如下图所示
点 Run connection automation 来自动连接剩下的走线
在弹出的设置对话框里勾选所有内容之后系统将自动帮我们连接好剩下的信号线以及添加需要的模块。
添加一个constant模块来添加一个常量1,用于连接我们各个模块的ce以及aclken 使能信号
最终的连接图如下:高亮的IP为本工程新增的IP
保存工程,然后点击source→Design Source ,右键我们创建的BLOCK工程,点击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]}]
对工程进行编译和综合,生成bitstream,File→Export→Export hardware,导出.xsa文件
选择刚才导出的.xsa文件,并将操作系统改为freertos操作系统:
在此选择空文件
在src下创建clk_wiz文件夹:用来设置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
该芯片是一颗电容触摸屏驱动 IC,支持 100Hz 触点扫描频率,支持 5 点触摸,支持 18*10 个检测通道。GT911 与 FPGA 的连接是通过 4 根线:SDA、SCL、RST 和 INT。其中:SDA 和 SCL 是 I2C 通信用的,RST 是复位脚(低电平有效),INT 是中断信号。GT911采用标准的I2C通信,最大通信速率为400KHz。GT911 的I2C器件地址可以是0X14或者0X5D,当复位结束后的 5ms 内,如果 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 //将VDMA固定在第一个帧缓冲上 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固定在第一个帧缓冲上。这样VDMA只会从第一个帧缓冲上读取数据;
当LVGL界面有数据要更新时,我们通过DMA的方式把数据搬移到VDMA的帧缓冲所在的内存地址上。如果当前VDMA固定在第一个帧缓冲上,则DMA会将数据搬移到第二个帧缓冲的内存地址上;如果当前VDMA固定在第二个帧缓冲上,则DMA会将数据搬移到第一个帧缓冲的内存地址上。这个逻辑的实现具体见移植LVGL文件部分;
当DMA搬移结束时,在对应的回调函数中,更新帧索引,并通过XAxiVdma_StartParking函数将VDMA固定在新的帧缓冲;
xxxxxxxxxx
//DMA完成回调函数static 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 } } // 在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通道能搬移的长度是有限制的(在dma的bsp代码中: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 越大,传输效率越高,但硬件支持上有上限。LCD在不同的分辨率下,一次需要搬移的数据长度也就不一样。比如在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 通道完成标志,默认全部通道为空闲(done) 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); }}
在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下,并rename为lv_conf.h
对lv_conf.h文件的内容按照下图所示进行修改代码:
删除src/lvgl/lvgl/examples下除porting外的其他所有文件和文件夹,删除porting文件下的osal文件夹
在平台工程中的freertos配置中,将tick_rate配置为1000,即1ms一次tick;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//添加头文件
//添加定义GT911_TouchPoint tp_points[GT911_MAX_TOUCHES];
//修改 touchpad_initstatic void touchpad_init(void){ /*Your code comes here*/ // add by mind GT911_Init();}
//修改 touchpad_is_pressedstatic bool touchpad_is_pressed(void){ /*Your code comes here*/ // add by mind return GT911_ScanTouch(tp_points, GT911_MAX_TOUCHES) > 0;}
//修改 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;
//修改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);
}
//修改disp_init()static void disp_init(void){ /*You code here*/ // add by mind xdma_init(DMA_DEVICE_ID, DMA_TOTAL_LEN); // 初始化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) { // 当前通过VDMA显示的是第一帧数据(frame_index为0)时,则通过XDMA往第二帧的内存空间上 xdma_start((uint32_t)(frame_buffer_addr + DMA_TOTAL_LEN), (uint32_t)px_map); } else { // 当前通过VDMA显示的是第二帧数据(frame_index为1)时,则通过XDMA往第一帧的内存空间上 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口和电脑,进行Build,debug,观察LCD显示。