1. 项目概述
在嵌入式系统开发中,图形用户界面(GUI)的实现一直是个挑战。LVGL(轻量级图形库)作为一款开源的GUI库,因其丰富的组件和动画效果而广受欢迎。本文将详细介绍如何在Xilinx Zynq SoC平台上,使用Vitis 2023.2工具链移植LVGL 9.4版本到RGB显示屏。
Zynq平台的特殊性在于其结合了ARM处理器(PS)和FPGA(PL)的优势,这为GUI实现带来了更多可能性,但也增加了系统设计的复杂度。我们将重点解决三个核心问题:DDR缓存一致性、AXI VDMA持续占用和缓冲区切换延迟。
2. 硬件准备与系统架构
2.1 硬件组件清单
要完成这个项目,你需要准备以下硬件:
- Zynq开发板(显示屏部分至少需要28个可用IO,触控部分至少4个IO)
- 50pin标准RGB显示屏
- 开发板与显示屏的转接板
这些组件组合起来的效果如下图所示:

2.2 系统架构设计
RGB显示屏通常不带显存,因此我们需要在FPGA的DDR中开辟一块区域作为显存。系统工作流程如下:
- ARM处理器将要显示的数据写入显存
- PL端使用AXI VDMA和AXI4-Stream to Video Out模块读取显存
- 数据最终显示到LCD上
这种架构虽然合理,但会带来几个关键问题,我们将在下一节详细分析。
3. 核心问题分析与解决方案
3.1 DDR缓存一致性问题
LVGL在绘制过程中需要一个缓冲区进行图形运算。由于Cache的存在,数据不会立即刷新到DDR,如果直接读取会导致新旧数据混杂。解决方案是手动刷新数据Cache:
c复制void Xil_DCacheFlushRange(INTPTR adr, u32 len); // 区域刷新
void Xil_DCacheFlush(void); // 全局刷新
建议优先使用区域刷新,只刷新必要的内存范围,以提高效率。
3.2 AXI VDMA持续占用问题
假设显示屏刷新率为60Hz,VDMA会持续以60Hz频率读取内存。这意味着VDMA和LVGL不能同时读写同一块内存。简单的双缓冲方案看似可行,但实际上还存在第三个问题。
3.3 AXI VDMA缓冲区切换延迟
当LVGL请求VDMA切换缓冲区时,最坏情况下VDMA需要完成当前帧的读取才会开始新缓冲区的读取。在此期间,前一帧的内容可能已被LVGL修改。针对这个问题,我们提出三种解决方案:
-
三缓冲方案:三重缓冲提升了渲染与数据传输的并行性。当一个缓冲区完成渲染而另一个正在进行DMA传输时,第三个缓冲区可立即开始下一帧渲染。即使遇到最坏情况(VDMA刚开始读取旧帧),LVGL仍有另外两个缓冲区可用。
-
VDMA中断同步:开启VDMA中断功能,将LVGL刷新与DMA就绪同步,确保每次切换都已完成上一帧读取。
-
四缓冲乒乓操作(来自Hello FPGA论坛的方案):建立四个缓冲区,两个用于显存,两个用于LVGL,通过ARM核心的DMA进行内存复制。这种方法在LVGL 8版本可行,但在9版本中由于API变化不再推荐。
本文选择三缓冲方案作为实现方案,因其在性能和实现复杂度上取得了良好平衡。
4. PL硬件设计实现
4.1 创建Vivado工程
首先在Vivado中创建Block Design工程,添加Zynq处理器IP核。关键配置如下:
-
开启两个AXI接口(一主一从)

-
启用必要的外设:UART、I2C、TTC0、TTC1(可选)、EMIO

-
配置时钟:为AXI互联分配专用PLL时钟

4.2 添加视频控制IP
添加以下三个关键IP核:
- AXI Video Direct Memory Access (VDMA)
- Video Timing Controller
- AXI4-Stream to Video Out

4.3 IP核配置
-
AXI VDMA配置:
- 启用读通道
- 设置帧缓冲数量为3(对应三缓冲方案)
- 配置合适的存储器映射

-
Video Timing Controller配置:
- 根据显示屏规格设置时序参数
- 配置适当的像素时钟

-
AXI4-Stream to Video Out配置:
- 设置正确的色彩空间和像素格式
- 配置与VDMA的接口参数

4.4 系统连接
按照以下步骤连接各模块:
-
连接主要数据通路

-
连接复位与时钟
- 注意区分AXI互联时钟和RGB显示屏同步时钟两个时钟域

- 注意区分AXI互联时钟和RGB显示屏同步时钟两个时钟域
-
完成总体连接

4.5 引脚约束与工程导出
为所有使用的IO添加引脚约束,例如:
tcl复制set_property IOSTANDARD LVCMOS33 [get_ports LCD_CLK]
set_property PACKAGE_PIN AA22 [get_ports LCD_CLK]
完成约束后,综合工程并导出.xsa文件,供Vitis软件开发使用。
5. PS软件设计
5.1 Vitis工程设置
-
创建工作区

-
创建Platform工程
- 导入Vivado生成的.xsa文件
- 选择FreeRTOS作为操作系统

-
修改FreeRTOS tick rate为1000,使系统以ms为单位运行

5.2 创建应用程序
-
新建Application工程
- 选择之前创建的Platform
- 选择手动构建模式以提高开发效率

-
备份关键文件
- 备份CMakeLists.txt和链接脚本
- 这些文件在后续修改中可能被覆盖
5.3 修改链接脚本
关键修改点:
- 增加堆栈大小:
c复制_STACK_SIZE = DEFINED(_STACK_SIZE) ? _STACK_SIZE : 0x80000;
_HEAP_SIZE = DEFINED(_HEAP_SIZE) ? _HEAP_SIZE : 0x80000;
- 划分DDR区域:
c复制MEMORY {
ps7_ddr_0 : ORIGIN = 0x00100000, LENGTH = 0x0C800000 /* 200 MB */
ps7_ddr_user : ORIGIN = 0x0C900000, LENGTH = 0x13200000
}
- 添加LVGL专用缓冲区段:
c复制.lv_buf (NOLOAD) : {
. = ALIGN(64);
__lv_buf_start = .;
KEEP(*(.lv_buf))
KEEP(*(.lv_buf.*))
__lv_buf_end = .;
. = ALIGN(64);
} > ps7_ddr_user
这样可以在代码中通过特定属性声明缓冲区:
c复制static uint8_t frame_buffer0[SCREEN_WIDTH*SCREEN_HEIGHT*3] __attribute__ ((section(".lv_buf"), aligned(64)));
5.4 配置CMake构建系统
- 启用C++支持:
cmake复制enable_language(C CXX ASM)
set(CMAKE_CXX_STANDARD 17)
- 添加LVGL构建配置:
cmake复制set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lvgl/lv_conf.h)
set(LV_CONF_BUILD_DISABLE_EXAMPLES ON)
add_subdirectory(lvgl)
- 启用NEON指令集优化:
cmake复制set(USER_COMPILE_OTHER_FLAGS "-mfpu=neon")
5.5 LVGL配置
- 下载LVGL源码:
bash复制git clone https://github.com/lvgl/lvgl.git
-
修改lv_conf.h:
- 启用配置文件:
c复制#if 1 /* Set this to "1" to enable content */- 设置颜色深度:
c复制#define LV_COLOR_DEPTH 24- 配置内存大小:
c复制#define LV_MEM_SIZE (128 * 1024U)- 启用FreeRTOS支持:
c复制#define LV_USE_OS LV_OS_FREERTOS- 启用NEON优化:
c复制#define LV_USE_DRAW_SW_ASM LV_DRAW_SW_ASM_NEON
6. 关键代码实现
6.1 FreeRTOS中断控制器配置
在FreeRTOS环境下,SCUGIC中断控制器会在vTaskStartScheduler()中配置。为避免冲突,我们需要特殊处理:
c复制void setup_fpga_irq(uint32_t intr_id, irq_trigger_t trig, uint8_t priority,
Xil_InterruptHandler handler, void *callback_ref) {
// 获取GIC配置
const XScuGic_Config *cfg = XScuGic_LookupConfig(XPAR_XSCUGIC_0_BASEADDR);
if (cfg == NULL) return;
// 配置中断触发类型
const UINTPTR icfgr_addr = dist + GIC_DIST_ICFGR_OFFSET(intr_id);
u32 icfgr = Xil_In32(icfgr_addr);
icfgr &= ~(0x3U << icfgr_shift);
icfgr |= ((trig == IRQ_EDGE ? 0x2U : 0x0U) << icfgr_shift);
Xil_Out32(icfgr_addr, icfgr);
// 设置优先级和目标CPU
u32 prio_reg = Xil_In32(prio_addr);
prio_reg = (prio_reg & ~(0xFFU << prio_shift)) | (((u32)priority & 0xFFU) << prio_shift);
Xil_Out32(prio_addr, prio_reg);
// 注册中断处理程序
XScuGic_RegisterHandler(cpu, intr_id, handler, callback_ref);
XScuGic_EnableIntr(dist, intr_id);
}
6.2 VDMA驱动实现
VDMA驱动主要包括初始化和帧切换功能:
c复制// 初始化VDMA
void init_pl_vdma0(uint8_t *buf0, uint8_t *buf1, uint8_t *buf2) {
XAxiVdma_Config *pl_vdma_0_config;
XAxiVdma_DmaSetup read_cfg;
// 查找并初始化VDMA配置
pl_vdma_0_config = XAxiVdma_LookupConfig(VDMA_DEVICE_ID);
XAxiVdma_CfgInitialize(&pl_vdma_0, pl_vdma_0_config, pl_vdma_0_config->BaseAddress);
// 配置读取参数
memset(&read_cfg, 0, sizeof(read_cfg));
read_cfg.VertSizeInput = SCREEN_HEIGHT;
read_cfg.HoriSizeInput = SCREEN_WIDTH * 3;
read_cfg.Stride = SCREEN_WIDTH * 3;
read_cfg.EnableCircularBuf = 1;
read_cfg.FrameStoreStartAddr[0] = (UINTPTR)buf0;
read_cfg.FrameStoreStartAddr[1] = (UINTPTR)buf1;
read_cfg.FrameStoreStartAddr[2] = (UINTPTR)buf2;
// 配置并启动VDMA
XAxiVdma_DmaConfig(&pl_vdma_0, XAXIVDMA_READ, &read_cfg);
XAxiVdma_SetFrmStore(&pl_vdma_0, 3, XAXIVDMA_READ);
XAxiVdma_DmaStart(&pl_vdma_0, XAXIVDMA_READ);
XAxiVdma_StartParking(&pl_vdma_0, 0, XAXIVDMA_READ);
}
// 切换帧缓冲区
void vdma_switch_frame(uint32_t index) {
if (index > 2) return;
XAxiVdma_StartParking(&pl_vdma_0, index, XAXIVDMA_READ);
}
6.3 LVGL初始化与接口实现
- 初始化LVGL:
c复制void lvgl_init() {
lv_init();
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = SCREEN_WIDTH;
disp_drv.ver_res = SCREEN_HEIGHT;
disp_drv.flush_cb = my_flush_cb;
lv_disp_drv_register(&disp_drv);
}
- 实现显示刷新回调:
c复制static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
// 刷新Cache确保数据一致性
Xil_DCacheFlushRange((INTPTR)color_p, lv_area_get_size(area) * sizeof(lv_color_t));
// 切换VDMA缓冲区
current_buf = (current_buf + 1) % 3;
vdma_switch_frame(current_buf);
// 通知LVGL刷新完成
lv_disp_flush_ready(disp_drv);
}
7. 性能优化与问题排查
7.1 性能优化技巧
-
NEON指令加速:
- 在CMake中启用NEON支持
- 在LVGL配置中开启NEON优化
- 实测显示moving wallpaper测试从29fps提升到45fps
-
内存访问优化:
- 确保缓冲区64字节对齐
- 使用专用DDR区域减少内存碎片
-
渲染优化:
- 合理设置LVGL刷新周期
- 使用局部刷新而非全屏刷新
7.2 常见问题排查
-
显示异常:
- 检查色彩通道顺序(RGB/BGR)
- 验证时序参数是否正确
- 确保Cache刷新范围正确
-
VDMA卡死:
- 检查缓冲区地址是否有效
- 验证中断配置是否正确
- 确保时钟和复位信号稳定
-
LVGL渲染慢:
- 检查是否启用了NEON优化
- 验证内存配置是否足够
- 调整LVGL任务优先级
8. 项目总结与扩展
通过本文介绍的方法,我们成功在Zynq平台上实现了LVGL到RGB显示屏的移植。三缓冲方案有效解决了VDMA与LVGL的同步问题,NEON优化显著提升了渲染性能。
后续可能的扩展方向包括:
- 添加触摸屏支持
- 实现硬件加速的图形操作
- 集成更复杂的GUI应用
在实际开发中,建议先验证VDMA和显示屏的基本功能,再逐步集成LVGL。遇到问题时,可以从时序、缓存一致性和内存配置三个维度进行排查。
