尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ZYNQ驱动LVGL 9.5.0输出RGB屏的完整移植指南

发布时间:2026/8/31 21:53:14

资讯中心
01
ARTICLE

ZYNQ驱动LVGL 9.5.0输出RGB屏的完整移植指南

ZYNQ驱动LVGL 9.5.0输出RGB屏的完整移植指南
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 ControllerVTC和 AXI SmartConnect 构建像素流。PS 端运行 Linux驱动提供/dev/fb0这个帧缓冲设备。应用层可以使用mmap或write把图像数据写入帧缓冲。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_t和lv_disp_drv_register9.x 中统一改用lv_display_create、lv_display_set_flush_cb、lv_display_set_buffers。驱动层不再通过lv_disp_drv_t里的driver_data字段传自定义结构而是通过lv_display_set_user_data和lv_display_get_user_data。输入设备从lv_indev_drv_t切换为lv_indev_create、lv_indev_set_type、lv_indev_set_read_cb。默认内存管理方式也改了。9.x 可以通过LV_USE_STDLIB_MALLOC、LV_USE_STDLIB_FREE等宏替换内存分配函数很多桌面模拟器直接把标准库的 malloc 配置为默认分配器。实际移植时需要先确认自己拿到的是 9.5.0 的哪个分支再对照源码里的lvgl.h、lv_display.h、lv_indev.h查看具体声明。不要凭记忆写 API9.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 输出的最小工程一般需要这些 IPIP 名称作用关键配置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 使用 ARGB8888VDMA 的 framebuffer 格式也要选择对应的 32bit 格式否则会出现颜色错乱或透明通道异常。2.2 地址映射与帧缓冲基址在地址映射里通常给 VDMA 分配一段 DDR 地址。比如把 VDMA 的 MM2S 基址设置为0x40000000长度 16MB。这段地址会通过设备树传入 Linux驱动把0x40000000映射为/dev/fb0的物理首地址。需要注意这个地址必须是 DDR 的范围。PS 端 DDR 默认从0x00100000或0x00000000开始具体取决于你的内存控制器配置。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: vdma40000000 { compatible xlnx,axi-vdma-1.00.a; reg 0x40000000 0x10000; #dma-cells 1; dma-channel vdma0 0; }; vtc0: vtc40010000 { compatible xlnx,video-timing-controller-1.0; reg 0x40010000 0x1000; xlnx,generator; xlnx,has-axi4s-out; }; fb0: framebuffer40000000 { 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 的基址设置为0x40000000framebuffer 节点指向同一个 DMA 通道。xlnx,bpp表示每个像素的字节数这里设为 32对应 ARGB8888。xlnx,width和xlnx,height要与 LVGL 初始化时的宽高一致也要与 VDMA 的行列配置一致。实际使用中设备树的具体 compatible 值要和你生成的内核驱动匹配。不同 BSP 或 PetaLinux 版本采用的 compatible 可能不同这里只是说明思路。3.2 确认/dev/fb0是否存在硬件工程和内核启动之后终端里执行ls -l /dev/fb* fbset -ifbset -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这里有两点要注意lv_conf.h放在源码根目录通过LV_CONF_INCLUDE_SIMPLE或LV_CONF_PATH让模块能找到它。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 33LV_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_map是uint8_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 bs1024 count600这条命令会把 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键盘或鼠标也被识别为event1LVGL 的输入驱动读取了错误的事件节点。运行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/fb0的FBIO_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需要处理drmModeAddFB、drmModeSetCrtc、drmModePageFlip这些底层调用代码量比 framebuffer 方案大不少但为后续接入 GPU 提前打了基础。对于大多数以显示仪表、控制面板、菜单页面为主的 ZYNQ 项目framebuffer 方案足够稳定优先把时序、帧缓冲和 LVGL 三个环节跑稳再去追求 DRM 的现代特性。如果一个页面切换时偶发花屏最先排查的永远是帧缓冲数量、像素格式和时序参数而不是 LVGL 版本。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。