ZYNQ驱动LVGL 9.5.0输出RGB屏的完整移植指南
2026/8/31 21:52:58 网站建设 项目流程

ZYNQ 驱动 LVGL 9.5.0 并输出到 1024x600 RGB 屏,是一套非常典型的嵌入式 GUI 工程组合。LVGL 负责绘制界面,ZYNQ 的 PL 端生成 LCD 时序,PS 端通过帧缓冲(fbdev)传递像素,三个环节只要有一个细节没对齐,最终都会表现为黑屏、花屏、触摸漂移或帧率异常。这篇文章会以一条完整链路把关键环节讲清楚:从 ZYNQ PL 端显示链路配置,到 Linux 下的 framebuffer 设备树,再到 LVGL 9.5.0 在新版本的移植适配,最后给出验证、排错和性能优化建议。

适合的读者是已经有一定 ZYNQ 或嵌入式 Linux 基础的开发者。如果你只是刚把 Vivado 跑通,建议先补齐 AXInterconnect、VDMA、PetaLinux 设备树的资源再动手。如果你对 LVGL 的旧版本(7.x、8.x)比较熟,这篇文章也会重点强调 9.x 系列的 API 变化,避免把旧写法直接搬进来导致编译失败。

1. 先理解 ZYNQ 下 LVGL 9.5.0 的显示链路

1.1 ZYNQ 显示方案的基本组成

ZYNQ 与传统 MCU 平台带屏幕的方式有很大区别。MCU 平台上,LVGL 通常直接调用 SPI、并行总线或者 DMA 接口去写屏。但在 ZYNQ 上,典型方案是:

  • PL 端生成 LCD 需要的时钟、同步信号、数据信号。常见做法是用 Xilinx 的 Video Timing Controller(VTC)和 AXI SmartConnect 构建像素流。
  • PS 端运行 Linux,驱动提供/dev/fb0这个帧缓冲设备。应用层可以使用mmapwrite把图像数据写入帧缓冲。
  • LVGL 作为一个用户态库,不直接操作寄存器,而是通过 framebuffer 的write回调把绘制结果送出去。
  • 如果使用 VDMA,帧缓冲在 DDR 中,VDMA 负责定时把 DDR 中的像素读到 PL 端并输出到 LCD。

这套链路可以抽象为:

LVGL 绘制 → 帧缓冲内存 → VDMA 读取 → PL 时序输出 → LCD 面板

理解这条链路很重要。排查问题时你要先确定是哪一段出了问题:是 LVGL 没绘制,还是绘制了没写进帧缓冲,还是帧缓冲地址不对,还是 PL 时序参数和屏幕规格不匹配。

1.2 LVGL 9.5.0 版本变化带来的移植差异

LVGL 9.x 相比 8.x 做了大量重构,很多老教程里的写法不能直接用。

几个影响比较大的变化:

  • lv_disp_drv_t被替换为lv_display_t,注册显示设备的函数名和参数都变了。
  • 以前用lv_disp_buf_tlv_disp_drv_register,9.x 中统一改用lv_display_createlv_display_set_flush_cblv_display_set_buffers
  • 驱动层不再通过lv_disp_drv_t里的driver_data字段传自定义结构,而是通过lv_display_set_user_datalv_display_get_user_data
  • 输入设备从lv_indev_drv_t切换为lv_indev_createlv_indev_set_typelv_indev_set_read_cb
  • 默认内存管理方式也改了。9.x 可以通过LV_USE_STDLIB_MALLOCLV_USE_STDLIB_FREE等宏替换内存分配函数,很多桌面模拟器直接把标准库的 malloc 配置为默认分配器。

实际移植时,需要先确认自己拿到的是 9.5.0 的哪个分支,再对照源码里的lvgl.hlv_display.hlv_indev.h查看具体声明。不要凭记忆写 API,9.5.0 的接口处于持续演进中,可能在你下载时又有微调。

1.3 为什么是 1024x600 与 RGB 屏

1024x600 是很常见的 7 寸或者 10.1 寸 RGB 屏分辨率。RGB 屏意味着不需要 MCU 并口协议,而是按行同步、场同步、像素时钟的方式持续送数据。对 ZYNQ 来说,这种方式最适合发挥 PL 端并行数据输出的优势。

1024x600 的像素数据量不大,但实时性要求高。以 60Hz 刷新率计算:

1024 × 600 × 4 字节(ARGB8888) ≈ 2.46 MB 每帧

VDMA 从 DDR 中读取 2.46 MB 数据只需要几毫秒,瓶颈不在带宽,而在时序参数和缓冲区分配是否合理。

2. 用 Vivado 构建 PL 端显示链路

2.1 Vivado 工程里的关键 IP

在 PL 端搭建 RGB LCD 输出的最小工程,一般需要这些 IP:

IP 名称作用关键配置
ZYNQ7 Processing SystemPS 配置使能 DDR、UART、SD,设置 AXI HP 接口
AXI SmartConnect连接 PS 与 VDMA地址宽度、数据宽度与 PS 端对齐
AXI VDMA从 DDR 搬像素帧缓冲数量、行数、列数、颜色格式
AXI Video Timing Controller生成行场同步信号根据屏幕时序配置 H/V 参数
AXI4-Stream to Video Out把像素流转换为 AXI4-Stream 与 VTC 信号配置接口格式

其中 VDMA 的配置要和 LVGL 的颜色格式保持一致。如果 LVGL 使用 ARGB8888,VDMA 的 framebuffer 格式也要选择对应的 32bit 格式,否则会出现颜色错乱或透明通道异常。

2.2 地址映射与帧缓冲基址

在地址映射里,通常给 VDMA 分配一段 DDR 地址。比如把 VDMA 的 MM2S 基址设置为0x40000000,长度 16MB。这段地址会通过设备树传入 Linux,驱动把0x40000000映射为/dev/fb0的物理首地址。

需要注意,这个地址必须是 DDR 的范围。PS 端 DDR 默认从0x001000000x00000000开始,具体取决于你的内存控制器配置。VDMA 访问 DDR 必须走 HP 口,直接在 Address Editor 里把 VDMA 映射到 PS DDR 范围之外的地址是错的。

2.3 检查 PL 端输出时序参数

LCD 屏幕的时序一般由面板数据手册决定。下面是一个典型 1024x600、像素时钟约 51.2 MHz 的例子,实际数据请以你的屏幕手册为准:

参数说明
H Active1024有效行像素
H Front Porch160行前肩
H Sync Pulse20行同步脉冲
H Back Porch140行后肩
V Active600有效场行数
V Front Porch20场前肩
V Sync Pulse10场同步脉冲
V Back Porch20场后肩

VTC 的配置就是把这几个值直接填进去。这里的数字只是示例,不同屏幕差异很大。如果时序不对,画面可能偏斜、闪烁,甚至直接无信号。

3. Linux 侧设备树与 framebuffer 驱动

3.1 设备树中 framebuffer 节点的作用

当 PL 端通过 AXI VDMA 驱动显示时,Linux 侧的 framebuffer 驱动通常由 Xilinx 提供的 DRM 驱动或者传统的 frame buffer 驱动实现。使用 PetaLinux 时,一般的做法是在设备树中加入 VDMA、VTC 和 framebuffer 节点。

一个简化的设备树节点示例:

amba_pl: amba_pl { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; ranges; vdma0: vdma@40000000 { compatible = "xlnx,axi-vdma-1.00.a"; reg = <0x40000000 0x10000>; #dma-cells = <1>; dma-channel = <&vdma0 0>; }; vtc0: vtc@40010000 { compatible = "xlnx,video-timing-controller-1.0"; reg = <0x40010000 0x1000>; xlnx,generator; xlnx,has-axi4s-out; }; fb0: framebuffer@40000000 { compatible = "xlnx,fb-vdma"; reg = <0x40000000 0x10000>; dma-coherent; dmas = <&vdma0 0>; dma-names = "vdma0"; xlnx,vtc = <&vtc0>; xlnx,bpp = <32>; xlnx,width = <1024>; xlnx,height = <600>; }; };

这里把 VDMA 的基址设置为0x40000000,framebuffer 节点指向同一个 DMA 通道。xlnx,bpp表示每个像素的字节数,这里设为 32,对应 ARGB8888。xlnx,widthxlnx,height要与 LVGL 初始化时的宽高一致,也要与 VDMA 的行列配置一致。

实际使用中,设备树的具体 compatible 值要和你生成的内核驱动匹配。不同 BSP 或 PetaLinux 版本采用的 compatible 可能不同,这里只是说明思路。

3.2 确认/dev/fb0是否存在

硬件工程和内核启动之后,终端里执行:

ls -l /dev/fb* fbset -i

fbset -i会显示当前 framebuffer 的信息,包括分辨率、颜色深度、虚拟分辨率等。如果你的屏幕分辨率是 1024x600、bpp 是 32,那么你应该看到:

mode "1024x600" geometry 1024 600 1024 600 32 timings 0 0 0 0 0 0 0 endmode

如果这一步失败,后面 LVGL 的移植没有必要进行,因为 LVGL 只是用户态绘制,最终还是要靠 framebuffer 输出。

4. 编译 LVGL 9.5.0 基础工程

4.1 获取源码与目录结构

LVGL 9.5.0 的源码可以从 GitHub 仓库直接下载。工程目录建议按下面的结构组织:

lvgl9_demo/ ├── lvgl/ │ ├── src/ │ ├── lvgl.h │ └── lv_conf.h ├── porting/ │ ├── lv_port_disp.c │ ├── lv_port_disp.h │ ├── lv_port_indev.c │ └── lv_port_indev.h ├── main.c ├── CMakeLists.txt └── Makefile

这里有两点要注意:

  1. lv_conf.h放在源码根目录,通过LV_CONF_INCLUDE_SIMPLELV_CONF_PATH让模块能找到它。
  2. 9.x 默认不再自动包含lv_compatibility.h,如果你有旧代码想用 8.x 的兼容写法,需要在lv_conf.h里打开LV_USE_COMPAT相关宏。但迁移到 9.x 之后建议直接使用新 API,而不是长期依赖兼容层。

4.2 配置lv_conf.h

核心配置项集中在lv_conf.h,下面列出直接相关的选项:

#define LV_COLOR_DEPTH 32 #define LV_USE_DRAW_SW 1 #define LV_USE_STDLIB_MALLOC lv_malloc #define LV_USE_STDLIB_FREE lv_free #define LV_TIMER_TICK_PERIOD_MS 1 #define LV_DEF_REFR_PERIOD 33

LV_COLOR_DEPTH必须和 framebuffer、VDMA 的颜色格式一致。使用 32 位色深时,LVGL 内部使用lv_color32_t,每个像素占 4 字节。LV_DEF_REFR_PERIOD是默认刷新周期,单位是毫秒。值越小刷新越频繁,CPU 占用越高。对于 1024x600 的屏幕,建议先保持 33ms 即约 30fps,跑起来后再根据实际帧率调整。

4.3 显示驱动适配:lv_port_disp.c

在 LVGL 9.x 中,显示驱动注册的流程大约是:

static lv_display_t *disp; void lv_port_disp_init(void) { disp = lv_display_create(1024, 600); if (!disp) { printf("lv_display_create failed\n"); return; } lv_display_set_flush_cb(disp, disp_flush_cb); static lv_color_t buf1[1024 * 100]; static lv_color_t buf2[1024 * 100]; lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL); }

这里使用 1024x100 两个部分缓冲。对于 1024x600 全屏,单帧缓冲要 2.46 MB,双缓冲更大。如果 DDR 内存充裕,可以直接分配一整个全屏缓冲并设置全屏渲染模式,这样绘制效率更高,但内存占用会明显增加。

disp_flush_cb的作用是把 LVGL 绘制好的区域复制到 framebuffer,并告诉 LVGL“绘制结果已经提交”。在 framebuffer 方案下,最简单的 flush 回调如下:

static void disp_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { int32_t w = lv_area_get_width(area); int32_t h = lv_area_get_height(area); uint32_t offset = ((uint32_t)area->y1 * FB_WIDTH + area->x1) * 4; memcpy((void *)(fb_ptr + offset), px_map, w * h * 4); lv_display_flush_ready(disp); }

这里假设fb_ptr是已经通过mmap映射好的/dev/fb0地址,FB_WIDTH是屏幕宽度。offset计算时要注意fb_ptr是字节地址,px_mapuint8_t *类型,每像素 4 字节。

实际开发中,更高效的做法是把/dev/fb0当作一个连续内存区域,在 flush 回调里只做一次memcpy。如果屏幕支持局部刷新能力,还可以使用FBIO_WAITFORVSYNC等待垂直同步后再刷,但大部分 RGB 屏不需要做这个等待,因为 LVGL 自身已经按刷新周期调度。

4.4 输入设备适配:触摸屏与 LVGL 9.x

如果只是先用屏幕显示,可以暂时不接输入设备,直接在屏幕上运行一个循环动画。但要形成完整 UI,触摸输入通常是必须的。

触摸屏在嵌入式 Linux 下常见的接入方式:

  • 通过 tslib 读取/dev/input/eventX,在用户态做滤波和坐标变换。
  • 直接读取/dev/input/eventX,自己解析input_event结构。

在 LVGL 9.x 中,注册输入设备的方式如下:

static lv_indev_t *indev; void lv_port_indev_init(void) { indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, touch_read_cb); }

touch_read_cb里需要把触摸屏上报的原始坐标转换为 LVGL 的坐标,并填入lv_indev_data_t

static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) { if (ts_available) { >data->point.x = FB_WIDTH - 1 - raw_x;>void *lvgl_thread(void *arg) { while (1) { lv_timer_handler(); usleep(5000); } return NULL; }

注意lv_timer_handler()的调用周期不能快于 LVGL 内部的最小定时器周期,否则空转浪费 CPU。lv_tick_inc()用于向 LVGL 提供时间基准,在 Linux 上可以通过gettimeofday实现,也可以使用LV_USE_TICK_CUSTOM配置系统 tick。

5. 编译、运行与验证

5.1 交叉编译环境准备

ZYNQ 的 Linux 用户态程序通常使用 ARM Cortex-A9 的交叉编译工具链。常见组合:

工具选择
交叉编译器arm-linux-gnueabihf-gcc 或 PetaLinux 提供的 SDK 工具链
CMake 版本3.10 以上
LVGL 配置lv_conf.h中关闭不需要的组件,裁剪内存

一个简单的 Makefile 示例:

CC = arm-linux-gnueabihf-gcc CFLAGS = -O2 -Wall -I./lvgl -I./porting -DLV_CONF_PATH=\"./lv_conf.h\" LDFLAGS = -lm -lpthread SRCS = main.c \ lvgl/src/core/lv_obj.c \ lvgl/src/core/lv_display.c \ ... porting/lv_port_disp.c \ porting/lv_port_indev.c TARGET = zynq_lvgl_demo all: $(TARGET) $(TARGET): $(SRCS) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

实际编译时直接编译 LVGL 的所有源码文件更方便。LVGL 9.x 的源码组织比较清晰,可以这样收集:

find lvgl/src -name "*.c" -o -name "*.h"

然后用循环把.c文件加入编译列表。

5.2 在 ZYNQ 板卡上运行

把生成的zynq_lvgl_demo可执行文件拷到板卡上,用 root 权限运行:

chmod +x zynq_lvgl_demo ./zynq_lvgl_demo

屏幕正常时会出现 LVGL 默认的 demo 画面,比如显示一个仪表盘或按钮。

如果画面只有一半、花屏、或者整体偏移,优先检查 framebuffer 和 LVGL 的分辨率、颜色深度是否一致。可以用/dev/fb0的纯色测试先做验证:

dd if=/dev/zero of=/dev/fb0 bs=1024 count=600

这条命令会把 framebuffer 全部清零,如果屏幕显示纯黑,说明 PL 链路和 framebuffer 驱动正常。再写入 0xFFFFFFFF,屏幕应该变为纯白。这个手段能直接区分是 LVGL 的问题还是显示链路的问题。

5.3 验证帧率和内存占用

在板卡上统计帧率最简单的方法是在lv_timer_handler()调用处做计数:

uint32_t frame_cnt = 0; uint32_t last_sec = 0; while (1) { lv_timer_handler(); frame_cnt++; now = time(NULL); if (now != last_sec) { printf("fps: %u\n", frame_cnt); frame_cnt = 0; last_sec = now; } usleep(5000); }

内存占用可以通过top/proc/<pid>/status查看。如果 LVGL 使用了部分缓冲方案,内存占用会明显小于全屏缓冲,但绘制效率可能下降。尤其注意不要使用超出 DDR 大小的大数组,否则会出现运行一段时间后系统被 OOM 打断,或绘图内容异常跳动。

6. 常见问题排查

6.1 黑屏或白屏

可能原因按优先级排列:

问题现象常见原因检查方式处理建议
黑屏VDMA 没有启动或地址错误dmesg | grep dma,查看 VDMA 寄存器状态检查设备树地址,确认帧缓冲地址在 DDR 范围内
黑屏framebuffer 无法打开ls -l /dev/fb0检查驱动加载状态,确认 VDMA、DRM 模块是否加载
白屏LVGL 刷新回调没有调用flush_cb中加打印检查 LVGL 定时器是否运行,lv_timer_handler是否被周期调用
白屏framebuffer 被写纯白,但 LVGL 没有初始化检查初始化顺序确保lv_init()在创建显示设备之前调用

6.2 花屏、偏移或闪烁

花屏的根因通常是像素格式不一致。比如 LVGL 使用 16bit 色深,而 framebuffer 设置为 32bit 显示,或者反过来。出现花屏时不要修改算法,先统一颜色深度。

偏移通常和行时序中的 H Front Porch / H Sync Pulse / H Back Porch 有关。把这些参数对照屏幕手册重新确认。特别要注意,部分屏幕的时序值要求单位是像素时钟,有些要求以“扫描行时间”为单位,填错会让画面整体右移或左移。

闪烁现象常见于 VDMA 只有一个帧缓冲且没有乒乓机制。VDMA 在读上一帧时,LVGL 在写下一帧,如果写地址覆盖到 VDMA 尚未读完的区域,就会出现撕裂闪烁。通常的做法是让 VDMA 开启两个或者三个帧缓冲。

6.3 触摸坐标不准或反轴

先进入调试模式打印原始坐标:

printf("raw x=%d y=%d\n", raw_x, raw_y);

然后在屏幕上显示一个点或一个按钮,点击屏幕的不同角落,记录坐标值。如果实际点按下时打印的y方向和屏幕显示相反,说明触摸和显示的原点相反。此时在touch_read_cb中做坐标映射即可。

另一个容易被忽略的是,多个输入设备时事件串台。比如触摸屏被识别为event0,键盘或鼠标也被识别为event1,LVGL 的输入驱动读取了错误的事件节点。运行cat /proc/bus/input/devices可以查看每个输入设备的名称和对应的 event 编号。

6.4 帧率低或 CPU 占用高

1024x600 全屏刷新对 CPU 有一定压力。如果屏幕变化很频繁,建议优先优化渲染区域:

  • 使用LV_DISPLAY_RENDER_MODE_PARTIAL让 LVGL 只刷新变化区域。
  • 不要在全屏背景上频繁播放 gif 或大动画,LVGL 的软件渲染性能有限。
  • 开启LV_USE_DRAW_SW中的多线程渲染,但要确认在同一套线程模型下没有竞态。
  • 如果板卡带 GPU,检查 LVGL 9.x 是否已经提供硬件加速驱动,否则仍走软件渲染。

LV_DEF_REFR_PERIOD也可以适当调大。从 33ms 改成 40ms 能降低约 20% 的刷新压力,但视觉上可能不够流畅。

7. 生产环境下的工程建议

7.1 多缓冲与垂直同步

在资源允许的情况下,最好使用多个帧缓冲。LVGL 侧可以配置两块部分缓冲,VDMA 侧配置两个或三个帧缓冲。这样 LVGL 写一块,VDMA 读另一块,互不干扰,能明显减少撕裂。

垂直同步适合使用/dev/fb0FBIO_WAITFORVSYNCioctl。在 flush 回调中,可以先执行:

ioctl(fb_fd, FBIO_WAITFORVSYNC, 0);

再执行memcpy。这样可以让绘制的数据在一个完整的垂直同步间隙内提交,减少屏幕中部切换导致的撕裂。

7.2 日志与监控

从项目工程化角度,至少记录以下日志:

  • LVGL 初始化是否成功。
  • framebuffer 打开和 mmap 是否成功。
  • 每个 flush 回调的执行耗时,统计最大值。
  • 触摸事件点是否异常偏离。

可以写一个环形缓冲区保存最近 1000 次 flush 的执行时间,出现卡顿时导出分析。实际项目中,画面卡死往往不是 LVGL 卡死,而是 flush 回调里memcpy卡在了 DMA 带宽或内存总线竞争上。

7.3 内存管理建议

LVGL 9.x 默认支持标准库 malloc,但嵌入式环境建议在lv_conf.h中自定义分配器:

#define LV_USE_STDLIB_MALLOC my_custom_malloc #define LV_USE_STDLIB_FREE my_custom_free

自定义分配器可以统计当前内存峰值,也可以把 LVGL 的内存限制在一个固定池内。对于 1024x600 的界面,UI 对象和样式使用内存一般在几十 KB 到几百 KB,具体的峰值可以通过lv_mem_monitor()查看。

7.4 升级和版本兼容

LVGL 版本更新很快,9.5.0 和未来版本之间仍然可能有 API 变化。建议锁定一个具体版本,并把lv_conf.h放到自己的工程版本控制中。升级 LVGL 时,优先查看 release note 中的 Breaking Changes,再逐项检查自己用到的 API 是否被替换。

如果项目会在多个屏幕分辨率之间切换,建议把分辨率定义放在独立的lcd_hw.h中,而不是散落在lv_port_disp.c、flush 回调、设备树三处。设备树、VDMA、LVGL、flush 回调四处都必须保持一致,任何一处单独改动都会造成问题。

8. 扩展:从 framebuffer 到 DRM/KMS

如果你的目标系统支持 DRM/KMS,可以考虑脱离 framebuffer 方案,让 LVGL 通过 DRM 渲染。DRM 相比 framebuffer 的优势是更贴近现代 Linux 图形栈,支持多屏、硬件图层、ModeSetting 和垂直同步回调。

在 LVGL 9.x 中,lv_display_set_flush_cb的 flush 回调可以对接 DRM 的 dumb buffer 或 GEM buffer。要把 LVGL 绘制结果提交到 DRM,需要处理drmModeAddFBdrmModeSetCrtcdrmModePageFlip这些底层调用,代码量比 framebuffer 方案大不少,但为后续接入 GPU 提前打了基础。

对于大多数以显示仪表、控制面板、菜单页面为主的 ZYNQ 项目,framebuffer 方案足够稳定,优先把时序、帧缓冲和 LVGL 三个环节跑稳,再去追求 DRM 的现代特性。如果一个页面切换时偶发花屏,最先排查的永远是帧缓冲数量、像素格式和时序参数,而不是 LVGL 版本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询