☰
ZYNQ FPGA电子相册实战:PS-PL协同实现SD卡JPEG解码与HDMI显示
2026/10/27 1:41:22 网站建设 项目流程

简介:本资源面向FPGA与嵌入式开发学习者,提供一套基于ZYNQ SoC的电子相册完整工程,帮助理解ARM与FPGA协同工作的软硬件设计方法。项目通过按键交互控制图片轮换,在HDMI显示器上动态输出图像,涉及PS端ARM程序编写、PL端HDMI控制器与VDMA配置,是掌握SoC系统级设计的实践案例。压缩包共约2000个文件,大小77.55MB,以Verilog头文件、C源码、Vivado工程配置、XDC约束、Tcl脚本及比特流文件为主,覆盖从硬件逻辑到软件驱动的完整开发链路。已有161人学习下载。读者可从中获取可综合的HDMI显示与VDMA传输参考实现、按键中断处理与图片索引管理思路,以及工程目录组织与综合实现流程,适合具备一定Verilog和C语言基础、希望深入ZYNQ多媒体接口开发的工程师与高年级学生参考。

1. ZYNQ FPGA 做电子相册:为什么它比单片机方案更值得折腾

手里有一块 ZYNQ 开发板,跑过流水灯、点过 RGB 屏,接下来想做一个能拿得出手、又能把 PS 和 PL 都用上的小项目——电子相册是个很合适的选择。它不像纯 FPGA 图像处理那样一上来就要啃时序和带宽,也不像纯单片机方案那样只能做低分辨率、低帧率的图片轮播。ZYNQ 的独特之处在于 ARM 核(PS)和 FPGA 逻辑(PL)在同一颗芯片里,PS 负责文件系统、图片解码、界面调度这些“软件活”,PL 负责像素搬运、缩放、叠加这些“硬活”,两边通过 AXI 总线握手。这个标题背后真正要解决的问题是:怎么把 SD 卡里的 JPEG 图片读出来、解码成 RGB 像素流、再稳定地送到 HDMI 或 LCD 屏上循环播放,同时保证切换不撕裂、不卡顿。适合已经跑通 ZYNQ 最小系统、想找一个综合性强但难度可控的项目来练手的人,也适合想理解 PS-PL 协同开发范式的 FPGA 入门者。

2. 先想清楚数据怎么走:ZYNQ 电子相册的软硬件分工

2.1 为什么不让 PL 单独干完所有事

纯 FPGA 做电子相册,理论上可行,但实际会非常别扭。JPEG 解码本身是变长编码,用硬件写一个 JPEG 解码器不是不行,但调试周期长、占用逻辑资源多,而且一旦要换图片格式(比如加 PNG),整个解码链路都得重做。ZYNQ 的 PS 端有 ARM Cortex-A9 或 A53,跑 Linux 或裸机,用软件解码 JPEG 是成熟方案,libjpeg 这类库直接调用,开发效率高得多。PL 端则专注于它擅长的事:把解码后的 RGB 数据从内存里搬出来,按显示时序打成像素流,驱动 HDMI 或 RGB LCD。这样分工,PS 负责“内容”,PL 负责“通道”,各干各的,边界清晰。

常见做法是:PS 端从 SD 卡读取 JPEG 文件,用软件解码成 RGB888 或 RGB565 格式,写入 DDR 中预先分配好的帧缓冲区;PL 端通过 AXI HP 接口以 DMA 方式读取帧缓冲,再按视频时序输出。这里的关键是帧缓冲的管理——至少需要两个缓冲区,一个正在显示,一个正在写入下一张,切换时交换指针,避免撕裂。

2.2 显示通路的三种选型与对比

方案接口分辨率上限逻辑资源占用开发难度适用场景
RGB LCD 直驱并行 RGB1024x600低低板载 LCD 屏,入门首选
HDMI 输出TMDS1080p中中需要外接显示器
AXI Stream 到 VDMAAXI4-Stream取决于 IP中高中需要缩放、叠加等处理

如果开发板自带 RGB LCD 接口,建议先从直驱方案入手,时序简单,不需要额外芯片。HDMI 方案需要 TMDS 编码,Xilinx 有现成的 HDMI IP 或可以用原语手写,但调试起来麻烦一些。VDMA 方案适合需要做图像缩放、多图层叠加的场景,但引入的 IP 多,新手容易在 AXI 互联上卡住。

我一般会建议:第一版用 RGB LCD 直驱,把 PS 解码、帧缓冲、PL 时序输出这条链路跑通;第二版再换成 HDMI 或加 VDMA 做缩放。这样每一步都有可见结果,不会一开始就陷在 AXI 互联的玄学里。

2.3 帧缓冲的分配与地址对齐

在 PS 端分配帧缓冲时,有几个参数必须注意。首先是地址对齐:AXI HP 接口对突发传输有对齐要求,帧缓冲的起始地址最好按 64 字节对齐,否则 DMA 效率会下降。其次是缓存一致性:如果 PS 端开了数据缓存,写入帧缓冲后需要执行 cache flush 操作,否则 PL 读到的可能是旧数据。裸机环境下可以用Xil_DCacheFlushRange,Linux 下用dma_alloc_coherent分配一致性内存。

// 裸机环境下分配帧缓冲并刷 cache #define FRAME_WIDTH 800 #define FRAME_HEIGHT 480 #define BYTES_PER_PIXEL 3 // RGB888 u8 *frame_buffer[2]; frame_buffer[0] = (u8 *)MEM_BASE_ADDR; frame_buffer[1] = (u8 *)(MEM_BASE_ADDR + FRAME_WIDTH * FRAME_HEIGHT * BYTES_PER_PIXEL); // 解码完成后刷 cache,确保 PL 能读到最新数据 Xil_DCacheFlushRange((INTPTR)frame_buffer[write_idx], FRAME_WIDTH * FRAME_HEIGHT * BYTES_PER_PIXEL);

这段代码里,MEM_BASE_ADDR需要根据实际 DDR 地址映射来定,通常在 0x10000000 以上避开程序区。Xil_DCacheFlushRange的参数是起始地址和字节长度,长度要覆盖整帧。如果忘了刷 cache,现象是屏幕显示花屏或旧图,但 PS 端读回来的数据是对的——这种“黑匣子”问题排查起来很费时间,血泪经验是先把 cache 操作加上再调其他。

3. 从 SD 卡到屏幕:PS 端解码与 PL 端时序的落地步骤

3.1 PS 端 JPEG 解码与帧缓冲写入

PS 端的任务很明确:遍历 SD 卡里的图片文件,逐个解码,写入帧缓冲,然后通知 PL 切换。裸机环境下可以用 Xilinx 的 xilffs 库读文件,用 libjpeg 或 tinyjpeg 解码。tinyjpeg 更轻量,适合资源紧张的裸机工程。

#include "tinyjpeg.h" #include "ff.h" FATFS fs; FIL fil; FRESULT fr; UINT br; u8 jpeg_buf[200*1024]; // 假设单张 JPEG 不超过 200KB fr = f_mount(&fs, "0:/", 1); fr = f_open(&fil, "0:/photo1.jpg", FA_READ); fr = f_read(&fil, jpeg_buf, sizeof(jpeg_buf), &br); f_close(&fil); // 解码到帧缓冲 struct jpeg_decdata *decdata; tinyjpeg_decode(decdata, jpeg_buf, frame_buffer[write_idx]);

jpeg_buf的大小要根据实际图片调整,太小会读不全,太大浪费内存。tinyjpeg_decode的输出格式需要和 PL 端约定好,通常是 RGB888 或 RGB565。如果图片尺寸和屏幕分辨率不一致,需要在解码后做缩放,或者让 PL 端做缩放。裸机下做缩放比较麻烦,建议前期先用和屏幕同尺寸的图片,后面再考虑用 PL 做缩放。

3.2 PL 端视频时序生成与帧缓冲读取

PL 端需要产生两组信号:一组是显示时序(行同步、场同步、数据有效),另一组是帧缓冲读取地址。最简单的方式是用一个状态机生成时序,同时用 AXI HP 接口读数据。但更规范的做法是用 Xilinx 的 AXI VDMA IP,它自动处理帧缓冲切换和 AXI 突发传输。

如果手写时序,核心是一个行计数器和一个场计数器:

// 简化的 800x480 RGB LCD 时序生成 parameter H_ACTIVE = 800, H_FP = 40, H_SYNC = 48, H_BP = 88; parameter V_ACTIVE = 480, V_FP = 13, V_SYNC = 3, V_BP = 32; always @(posedge pixel_clk) begin if (h_cnt < H_ACTIVE + H_FP + H_SYNC + H_BP - 1) h_cnt <= h_cnt + 1; else begin h_cnt <= 0; if (v_cnt < V_ACTIVE + V_FP + V_SYNC + V_BP - 1) v_cnt <= v_cnt + 1; else v_cnt <= 0; end end assign de = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE); assign hsync = (h_cnt >= H_ACTIVE + H_FP) && (h_cnt < H_ACTIVE + H_FP + H_SYNC); assign vsync = (v_cnt >= V_ACTIVE + V_FP) && (v_cnt < V_ACTIVE + V_FP + V_SYNC);

这段代码里,pixel_clk是像素时钟,800x480@60Hz 通常用 33.3MHz 左右。de是数据有效信号,只在有效区域为高。hsync和vsync的极性要根据屏幕手册确认,有的屏是低有效。地址生成可以用h_cnt和v_cnt组合成偏移量,加上帧缓冲基地址,送到 AXI 读接口。

3.3 帧切换的同步机制

帧切换是电子相册最容易翻车的地方。如果 PS 端在 PL 正在读帧缓冲 A 的时候直接改写帧缓冲 A,屏幕就会撕裂。正确做法是双缓冲加握手:PS 写缓冲区 B,写完后通过 GPIO 或 AXI 寄存器通知 PL;PL 在当前帧结束后切换到缓冲区 B,同时通知 PS 可以写缓冲区 A 了。

// PS 端等待 PL 空闲信号 while (XGpio_DiscreteRead(&gpio, 1) & 0x01) { // 等待 PL 完成当前帧 } // 切换写缓冲索引 write_idx = 1 - write_idx; // 解码下一张到 write_idx 对应的缓冲 // ... // 通知 PL 新帧就绪 XGpio_DiscreteWrite(&gpio, 1, 0x01);

PL 端在每帧结束时采样这个信号,如果为高则切换读地址并清零。这个握手逻辑用 GPIO 就够了,不需要复杂的 AXI 寄存器。注意 GPIO 的跨时钟域问题:PS 端的 GPIO 信号相对于 PL 的像素时钟是异步的,需要打两拍同步。

4. 避坑与排查:ZYNQ 电子相册调试中容易翻车的五个点

4.1 屏幕花屏但 PS 端读回数据正常

现象:PL 端显示花屏、噪点或旧图,但 PS 端从帧缓冲读回的数据是正确的。

原因:PS 端开了数据缓存,写入帧缓冲后没有刷 cache,PL 通过 AXI 读到的还是 DDR 里的旧数据。

解决:每次写完帧缓冲后调用Xil_DCacheFlushRange,或者把帧缓冲分配在非缓存区域。Linux 下用dma_alloc_coherent分配一致性内存。

4.2 图片切换时屏幕撕裂

现象:切换图片的瞬间,屏幕上半部分是旧图,下半部分是新图。

原因:PS 端在 PL 正在读当前帧缓冲时直接改写了同一块内存。

解决:用双缓冲加握手,PS 只写非当前显示缓冲区,写完后等 PL 发切换信号。PL 端在帧消隐期切换读地址。

4.3 AXI HP 接口读数据带宽不够

现象:高分辨率图片显示时出现闪烁或掉帧,PL 端读数据断流。

原因:AXI HP 接口的突发长度设置太小,或者帧缓冲地址没有对齐,导致传输效率低。

解决:把 AXI 突发长度设到 64 或 128,帧缓冲起始地址按 64 字节对齐。如果还是不够,检查 DDR 带宽是否被 PS 端其他操作占用。

4.4 JPEG 解码后颜色偏色

现象:屏幕显示的图片颜色明显偏红或偏蓝。

原因:JPEG 解码输出的颜色空间是 YCbCr,没有转换成 RGB,或者 RGB 分量顺序搞错了。

解决:确认 tinyjpeg 的输出格式,如果是 YCbCr 需要手动转 RGB。RGB 顺序要和 PL 端约定一致,常见的是 RGB888 或 BGR888。

4.5 屏幕时序参数不对导致无显示

现象:屏幕背光亮但没有图像,或者图像偏移、抖动。

原因:行场同步的极性、前后沿参数和屏幕手册不匹配。

解决:对照屏幕数据手册逐项核对H_FP、H_SYNC、H_BP、V_FP、V_SYNC、V_BP和同步极性。用示波器看hsync和vsync波形是最直接的排查手段。

5. 进阶技巧:用 PL 做缩放和叠加,让电子相册更实用

5.1 为什么要在 PL 做缩放

PS 端软件缩放一张 1920x1080 的图片到 800x480,耗时可能超过 100ms,切换时会有明显卡顿。PL 做缩放是流水线操作,每个像素时钟处理一个像素,延迟只有几个时钟周期。常见做法是用双线性插值,在 PL 里用行缓冲和乘法器实现。

// 双线性插值简化示意 // 根据缩放比例计算源坐标 wire [15:0] src_x = (dst_x * src_width) / dst_width; wire [15:0] src_y = (dst_y * src_height) / dst_height; // 取四个邻近像素,按小数部分加权 // 需要两个行缓冲存储相邻两行

这段逻辑的关键是行缓冲的管理:缩放比例决定每行需要缓存多少像素。如果缩放比例是固定的,行缓冲大小可以预先算好。如果比例可变,行缓冲需要按最大源宽度分配。

5.2 用 VDMA 做多图层叠加

如果想让电子相册同时显示图片和文字(比如文件名、日期),可以用 VDMA 的多通道功能,把文字图层作为第二个通道,在 PL 里做 Alpha 混合。VDMA 的配置比手写 AXI 读复杂,但 Xilinx 提供了完整的 IP 和示例工程,照着改参数就行。

配置 VDMA 时注意几个参数:Number of Frame Buffers至少设 3,给 PS 端留足写入时间;Memory Map Data Width设成 64 或 128 位,匹配 AXI HP 接口位宽;Stream Data Width设成 24 或 32,匹配像素格式。

5.3 验证方法:用测试图案先跑通通路

在接真实图片之前,先让 PL 生成彩条或渐变图案,确认时序和显示通路没问题。这样可以把问题隔离在 PS 端或 PL 端,不用同时怀疑两边。我一般会先写一个简单的彩条生成模块,替换掉帧缓冲读取,屏幕能正常显示彩条了,再接 VDMA 或 AXI 读。

// 彩条生成:根据 h_cnt 输出不同颜色 always @(posedge pixel_clk) begin if (h_cnt < H_ACTIVE/8) rgb <= 24'hFF0000; else if (h_cnt < H_ACTIVE*2/8) rgb <= 24'h00FF00; else if (h_cnt < H_ACTIVE*3/8) rgb <= 24'h0000FF; // ... 其余彩条 end

这个模块只有几十行逻辑,但能验证像素时钟、同步信号、数据有效信号是否都正确。彩条显示正常了,再换成从帧缓冲读数据,如果出问题,那一定是帧缓冲或 AXI 读的问题,排查范围就小多了。

做这个项目最大的教训是:不要一上来就追求高分辨率、多格式、炫酷切换效果。先把 800x480、单张图片、RGB 直驱这条最小通路跑通,再逐步加功能。每加一个功能,先用测试图案验证通路,再接真实数据。这样每一步都有可见结果,不会在某个玄学问题上卡太久。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询