尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA+DDR3+LCD图片显示完整方案与调试实录

FPGA+DDR3+LCD图片显示完整方案与调试实录 简介面向FPGA开发者的Xilinx FPGA DDR3 LCD图片显示工程是一份围绕DDR3 SDRAM控制器配置、时序约束与LCD显示驱动完整实现的参考设计。压缩包共1245个文件约14.45MB涵盖Verilog/VHDL源码v、vhd、UCF/XDC时序约束、ISE/Vivado工程脚本tcl、bat、IP核配置xco及BIT比特流文件等源码层次清楚便于直接打开工程仿真或上板验证。工程涉及DDR3 IP核配置、读写控制、跨时钟域同步、图像数据缓存与LCD并行接口时序等关键环节适合正在学习存储器接口或显示驱动的中高级FPGA学习者。资源中不仅包含完整RTL代码和约束文件还附有测试平台与项目报告文档可帮助理解从DDR3读取图像数据到LCD屏显示的整体数据通路。目前已有703人学习下载可作为相关课设或项目开发的直接参考。 接到这个项目标题的时候我第一反应是好熟悉的组合。Xilinx FPGA、DDR3、LCD图片显示这三样东西凑在一起基本就是电子系学生、嵌入式工程师、图像处理入门玩家绕不开的一套经典练手配置。当初我在这条路上折腾了整整两周从点亮屏幕到最终稳定显示一张彩色图片踩过的坑比想象中多但走通之后再看整个架构其实非常清晰。这篇文章我打算用最直接的方式把整套方案的架构设计、DDR3读写控制、LCD时序驱动、图片数据流处理以及调试过程中遇到的奇葩问题全部摊开来讲。既有原理层面的解释也有可以直接抄走的Verilog设计思路和参数配置。适合手里有一块Xilinx 7系列开发板、想玩DDR3和图像显示的同学也适合工作里突然被分配把图片显示到LCD上这种任务的工程师。1. 项目全貌与设计思路1.1 为什么是 DDR3而不是 SRAM 或者片上 Block RAM这个设计的第一步不是写代码而是想清楚图像数据到底应该放在哪里。以一块常见的 7 寸 LCD 屏为例分辨率 800x480RGB565 格式下每个像素占 16bit那么一帧完整图像的大小是 800 × 480 × 2 768,000 字节大约是 750KB。Xilinx 7 系列 FPGA 的片上 BRAM 总量有限以 Artix-7 35T 为例总共也就 1.8Mb约 225KB连一张完整的 800x480 图片都放不下更别说同时做帧缓存和读写缓冲。SRAM 虽然速度快、时序简单但大容量的 SRAM 芯片价格贵、封装大而且市面上单颗容量通常也就几 Mb 级别扩展性很差。DDR3 的优势在于容量大、单位成本低、且 Xilinx 在 7 系列里集成了非常成熟的 MIGMemory Interface GeneratorIP你不需要自己写复杂的 DDR3 物理层时序。一片常见的单片 16bit DDR3 颗粒或者一条 DDR3 内存条容量轻松做到 128MB/256MB存几百张图片都毫无压力。所以设计目标很明确DDR3 作为图像帧存储也就是一个大的临时缓冲池一路写入图片数据另一路由 LCD 控制器定时读出刷新屏幕。1.2 整体数据通路设计整套系统的数据流比想象中简单但每个环节都有自己的坑。我做的时候把系统分成了四个模块图片数据加载模块UART 下载、DDR3 写控制器、DDR3 读控制器、LCD 时序驱动模块。图片数据从 PC 端通过串口发送到 FPGAFPGA 内的状态机接收数据后按照预设的起始地址连续写入 DDR3。LCD 控制器这边每到了刷新一行的时刻就发送读请求到 DDR3 读控制器读出的数据经过 FIFO 缓冲后按照 LCD 时序拼成 RGB 信号输出到屏幕。这里有一个核心的设计原则DDR3 的访问必须是连续块状的绝对不能逐像素去读。DDR3 颗粒内部是按 Bank 和行列结构组织的每一次读操作都有固定的延迟如果每读一个像素就发起一次读命令效率会低到不可用。正确做法是一次性突发读出大量数据放入 FIFO然后 LCD 控制器自己从 FIFO 里慢慢取数。我后面会详细讲这个实现。2. DDR3 缓存控制器的实现细节2.1 MIG IP 配置要点Xilinx 的 MIG IP 是整套 DDR3 方案的基石它的配置界面看起来复杂实际上大部分参数都已经有合理默认值。我当时用 Artix-7 系列在 IP Catalog 里搜索 MIG然后按照下面几个关键点来配Memory Part选择开发板上真实使用的 DDR3 芯片型号。比如我用的是 MT41K256M16 HA-125这是镁光一颗常见的 4Gb、16bit 位宽的 DDR3-1600 颗粒。选错型号会导致 MIG 生成的物理时序参数不对板级直接不工作。时钟频率DDR3-1600 对应物理时钟 800MHz但很多人不知道 MIG 的输入时钟和用户接口时钟是分开的。我这边最终配置为 4:1 模式用户接口时钟跑 400MHzMIG 内部输入时钟经 PLL 倍频后给到 DDR3 物理层。数据位宽开发板上的 DDR3 通常有两种布局一种是单片 16bit一种是两条内存条组成 64bit。数据位宽越大一次突发能读到的数据越多带宽越高。16bit 位宽在 800x480 图片显示场景下已经足够因为 LCD 一帧数据的量级也就几百 KB完全跑不满 DDR3 的带宽。突发长度MIG 里默认都是 8针对 DDR3 标准不需要改。MIG 生成之后用户的接口是 AXI4 或者简单的 native 接口。用 AXI 接口的好处是可以直接接 AXI Interconnect后续如果要接 MicroBlaze 软核会比较方便。但纯 Verilog 逻辑控制的话native 接口更直观命令、写数据、读数据三组信号逻辑清晰我强烈建议新手先用 native 接口把流程摸透再去折腾 AXI。2.2 用户接口读写时序MIG 的 native 接口由四个通道组成地址命令通道app_addr/app_cmd/app_en、写数据通道app_wdf_data/app_wdf_wren/app_wdf_end、读数据通道app_rd_data/app_rd_data_valid、状态通道app_rdy/app_wdf_rdy。写操作的标准时序是当 app_rdy 和 app_wdf_rdy 同时拉高时在同一个时钟周期给出地址和写数据MIG 就会接收这条写命令。我最初写这块逻辑的时候犯过一个错误只等 app_rdy 拉高就发命令结果发现写数据经常不进去。后来查手册才意识到地址和写数据在 MIG 内部是两个独立的 FIFO必须保证两个 FIFO 都有空余也就是 app_rdy 和 app_wdf_rdy 同时有效才能安全地发起一次写入。读操作相对简单每次在 app_rdy 拉高时发起读命令然后等待若干周期在 app_rd_data_valid 拉高时取回数据。注意读延时的数量是不固定的取决于 DDR3 内部 Bank 命中和刷新操作所以不要用固定的延时去等数据一定以 app_rd_data_valid 信号为准。一个提高效率的小技巧是在写入图片数据时DDR3 的地址从 0x0000_0000 开始每次突发写入 64 字节16bit 位宽、突发长度 8 的模式下一次用户接口写命令对应 8 个 16bit 数据也就是 64 字节连续写下去直到一张图片全部写完。连续写的目的是为了让 MIG 内部尽量命中同一个 Bank 的同一行减少预充电和行激活开销。2.3 DDR3 读写带宽实测之前看过一些教程把 DDR3 的带宽算得非常理想化什么理论带宽 6.4GB/s之类。实际用起来根本不是这么回事。我这次用 16bit 位宽、DDR3-1600理论上用户接口带宽是 3.2GB/s但实际在图片写入场景下连续写吞吐大约只能到 1.8GB/s大概只有理论值的 55% 左右。原因很简单MIG 内部要处理刷新Refresh、ZQ 校准、读写 Turnaround 这些开销而且用户逻辑不可能做到每个时钟周期都在发送有效命令中间总有等待。但这对于图片显示来说完全够用。一张 750KB 的图片按 1.8GB/s 的写入速度理想情况 0.4ms 就能写完实际经过 UART 传输才是真正的时间瓶颈。UART 这块我有个惨痛教训。一开始用的是 115200 波特率一张 750KB 的图片要传 750,000 × 10 / 115200 ≈ 65 秒。第一次跑通的时候看着屏幕上缓慢跳动的下载进度条我整个人是崩溃的。后来把 UART 波特率换成 921600速度提升 8 倍一张图片只需要 8 秒左右。再往后我用 USB 转串口模块最高跑到了 2Mbps基本够用。如果要做更高效的传输SD 卡直接读取 BMP 文件是更好的方案但代价是要处理 FAT 文件系统工作量会明显增加。3. LCD 显示驱动与刷新机制3.1 RGB TFT 屏的时序参数推算市面上常见的 LCD 屏分两种接口MCU 接口8080/6800 并行和 RGB 接口TTL 并行。RGB 接口的 LCD 本质上就是一个需要持续不断刷新信号的显示器原理和老的 VGA 显示器一样必须按时序给出像素时钟、行同步、场同步和有效数据。RGB 屏的时序参数通常在数据手册里会给出但经常给得不够直观。以一块 800x480 的典型 7 寸屏为例行同步脉宽 Hsync Pulse Width 一般是 1~8 个像素时钟行后肩 HBP 约 40 个像素时钟行前肩 HFP 约 40 个像素时钟场同步 Vsync Pulse 约 1~3 行垂直前肩 VFP 约 13 行垂直后肩 VBP 约 29 行。像素时钟的计算方法一行的总像素数 有效像素 800 HFP 40 Hsync 40 HBP 40 ≈ 920具体和屏参有关一场的总行数 有效行 480 VFP 13 Vsync 3 VBP 29 ≈ 525帧率 60Hz那么像素时钟 ≈ 920 × 525 × 60 ≈ 28.98MHz。很多入门教程直接套 33.3MHz 或者 40MHz实际上屏也能亮但最好还是按照数据手册的参数来否则可能出现屏幕边缘有黑边、或者画面抖动的问题。我当时做的方式是写了几个参数常量放在代码顶部方便随时调整// LCD 800x480 时序参数单位像素时钟/行 localparam H_ACTIVE 800; localparam H_FRONT_PORCH 40; localparam H_SYNC_PULSE 48; localparam H_BACK_PORCH 40; localparam H_TOTAL H_ACTIVE H_FRONT_PORCH H_SYNC_PULSE H_BACK_PORCH; // 垂直方向参数 localparam V_ACTIVE 480; localparam V_FRONT_PORCH 13; localparam V_SYNC_PULSE 3; localparam V_BACK_PORCH 29; localparam V_TOTAL V_ACTIVE V_FRONT_PORCH V_SYNC_PULSE V_BACK_PORCH;一个非常关键的细节是DEData Enable信号。很多 RGB 屏支持 DE 模式此时行场同步信号只作为辅助复位参考真正判断像素是否有效的是 DE 信号。DE 拉高期间输出的像素数据才是有效数据否则必须输出 0。如果 DE 信号逻辑搞错最典型的症状是屏幕左右或者上下出现整块的纯色条带或者颜色淡一层输出黑电平的时间不对。3.2 从 DDR3 读数据到 LCD 的节奏控制显示屏是一个时刻都在消耗数据的设备。在 60Hz 刷新率、800x480 分辨率下每秒要输出 800 × 480 × 60 23,040,000 个像素RGB565 格式下就是 46MB/s。虽然这个速率对 DDR3 来说小菜一碟但如果设计不好LCD 控制器会频繁打断 DDR3 的连续读导致效率骤降。我的做法是在 LCD 控制器和 MIG 之间加两级缓冲。第一级是一段 4KB 的 Block RAM或者用 Xilinx FIFO IP第二级是 LCD 侧的同步 FIFO。具体流程是当行有效信号开始时提前从 DDR3 一次性读出该行的全部数据放入 FIFO然后 LCD 按照像素时钟从 FIFO 取出。整条行数据的预取发生在消隐时间HBP Hsync内这样既不占用的显示时间又能保证 FIFO 里始终有数据。关于行缓冲的大小有一个值得说的细节。800 个像素、RGB565、每像素 2 字节一行是 1600 字节。DDR3 一次突发 64 字节那么读一行需要 25 次突发。如果你把四个 Bank 的访问地址每次都错开理论上还可以通过 Bank 交替来隐藏行激活延迟但代码复杂度会上升不少。我试过最简单的顺序读方案配合 FIFO 预取实际上屏幕显示效果已经非常稳定没有出现因为 DDR3 读延迟导致的画面撕裂。3.3 RGB565 与 LCD 物理接口的色深转换DDR3 里存的是 16bit 的 RGB565 数据但很多 RGB 接口 LCD 屏的数据线是 24bit 的R/G/B 各 8bit。直接把 16bit 数据接到 LCD 上会缺色通常需要做一次简单的位宽扩展。最常见的做法是高位补齐低位。5bit 红色扩展到 8bit可以取 {R[4:0], R[4:2]}也就是把高 5 位复制到低 3 位这样能得到近似线性亮度。绿色是 6bit 到 8bit用 {G[5:0], G[5:4]}蓝色和红色一致。这个转换用组合逻辑就能完成不需要消耗额外的存储资源wire [4:0] r565 rgb565[15:11]; wire [5:0] g565 rgb565[10:5]; wire [4:0] b565 rgb565[4:0]; assign lcd_r {r565, r565[4:2]}; assign lcd_g {g565, g565[5:4]}; assign lcd_b {b565, b565[4:2]};还有一个容易踩的坑是 BGR 顺序问题。BMP 文件存储的像素顺序在 24bit 真彩色下是 BGR 而不是 RGB如果用 Python 脚本把 BMP 转成 RGB565 二进制时忽略了字节顺序就会看到图片里红色和蓝色的区域完全互换。我当时转换时用的脚本是逐字节读取所以要在转换逻辑里把 R 和 B 调换过来否则显示屏上的蓝天会变成橙色那种错乱会让你排查半天。4. 完整调试实录与常见问题排查4.1 我在调试中踩过的坑这块是整篇文章含金量最高的部分。我调这个项目的时候每个问题都花了不少冤枉时间记录下来希望能帮读者直接跳过。第一个坑是白屏什么都看不到。我一开始以为是 LCD 驱动时序有问题于是用 ILA 去抓像素时钟和 DE 信号发现一切都正常但屏幕就是亮不起来。后来用万用表一量发现 LCD 的复位引脚被 FPGA 连接到了一个没有初始化的 IO 上上电后一直处于不确定状态。RGB 屏的复位至少需要保持低电平 10ms 再拉高这个时序在很多屏的数据手册里都会写但很容易被忽略。解决办法是在代码里写一个简单的延时状态机上电后先拉低复位 20ms再拉高并等待 20ms然后才开始输出时序。第二个坑是花屏而且是像雪花一样的随机花屏。这个问题的根源是 DDR3 读数据的地址有问题。最开始我从 LCD 的行号换算 DDR3 地址时没有把每行有效像素 800和DDR3 中存储的每行字节数 1600对应起来结果行与行之间串位了。具体表现是图片大体轮廓能看出来但像素点错位严重一条一条的彩色条纹。解决办法是写了一个简单的规则地址 行号 × 1600 列号 × 2并且用 ILA 抓 LCD 起始行的首个像素地址做验证。第三个坑印象最深是图片显示出来了但颜色不对。红色的车变成了蓝色天空变成了橙色。当时第一反应是 RGB565 的位序搞反了检查了半天代码没发现问题。后来把数据从 DDR3 里读出来直接在 FPGA 内部用逻辑分析仪比对才发现是我用 Python 转换 BMP 时图片的 BGR 存储顺序没有转换到 RGB565 的正确顺序。这个问题在生产环境中非常常见任何从 BMP 或者摄像头采集的数据都要先确认色彩通道顺序再送进 DDR3。4.2 问题排查速查表根据这次项目及之前类似项目的经验整理了一张问题排查表遇到类似情况的可以直接照着查现象可能原因排查手段白屏/黑屏无任何输出LCD 复位时序不对、IO 未初始化万用表量复位电平和初始电平检查端口约束屏幕有背光但无图像像素时钟没起振、DE 信号恒低ILA 抓 pixel_clk 和 DE确认时序参数无误显示花屏且有规则条纹DDR3 地址换算错误、行缓冲读写不同步检查行地址计算模拟验证行首数据花屏且颜色错乱RGB565 转换时 B/R 通道对调读取 DDR3 数据打印对比源图像字节流图像卡顿/撕裂FIFO 深度不够、读 DDR3 效率过低增大 FIFO 深度尽量减少 DDR3 频繁命令切换屏幕闪烁背光 PWM 频率过低、LCD 刷新率没达到 60Hz提高 PWM 频率到 20kHz 以上核对像素时钟计算4.3 高效调试手段先纯色后图片这个技巧是导师教我的特别适合有图像显示需求的项目。首次调通 LCD 之后不要急着显示图片先在 FPGA 内部生成一个纯红色的图像信号直接送到 LCD确认颜色和位置正确后再生成彩条图案例如红绿蓝白四段然后从 DDR3 读图片对比。这一步看似多花了时间实际能省掉大量定位问题的功夫。因为如果纯色显示正常说明 LCD 时序和屏幕本身没有问题如果彩条显示正确说明像素时钟、DE 和数据位宽的映射没有问题最后一步如果图片异常那问题几乎一定出在 DDR3 数据通路或者图片格式转换上。这样定位问题一下子缩小到了剩下的 1/3。5. 性能优化与工程化建议5.1 带宽瓶颈与优化策略图片显示本身对 DDR3 带宽的要求很低但如果后续要从图片显示扩展到视频播放或者摄像头采集显示就需要花更多心思关注 DDR3 的带宽利用率。实测下来这个系统里 DDR3 的带宽消耗主要来自两个方面LCD 控制器持续不断的行预取读操作以及图片下载时的连续写操作。在图片下载过程中如果写操作和读操作同时发生DDR3 需要频繁切换读写方向这个切换会有额外的 turn-around 惩罚。一个简单有效的策略是把写优先级提得比读高并且在写入时让 LCD 的读操作暂时停下来。由于 LCD 的 FIFO 里通常还有 1~2 行的数据缓冲暂停几个微秒不会产生闪烁或撕裂但 DDR3 的写效率能明显提升。如果要做视频播放建议把 LCD 的刷新率和视频帧率对齐例如都用 60Hz同时采用双缓冲机制。解码一帧写入 DDR3 的 A 区域同时从 B 区域读取数据送显一帧完成后交换 A/B 指针这样能避免画面出现割裂感。这个双缓冲机制在图片显示项目里稍微改动一下就能实现因为地址转换只需要一个寄存器。5.2 工程上的几条经验想法做完这个项目我最大的一点心得是DDR3 的控制逻辑绝对不是看几篇文档就能完全掌握的一定要动手用 ILA 抓波形看 MIG 的 app_rdy 和 app_wdf_rdy 信号如何关联看读数据和写数据之间的关系。多观察几次你就能形成非常直观的时序感。另外在管脚约束上MIG 生成的 .xdc 文件里已经包含了 DDR3 物理管脚的时序约束不要手动修改。但 LCD 的信号约束必须自己写尤其要注意驱动能力的选择。如果不确定使用什么电平标准可以查看开发板的原理图一般 RGB LCD 信号都走 3.3V 电平在 Vivado 里把 IO 标准设为 LVCMOS33并加上合适的上拉。最后想分享一个特别好用的小技巧在调试 LCD 时序的时候把像素时钟频率降低到正常值的一半比如用 15MHz 代替 30MHz。这样肉眼能看到屏幕刷新率下降但每个像素的保持时间加长万一有信号毛刺或者建立时间不够的问题会更容易用逻辑分析仪抓到。等一切正常再把频率调回正常工作值做最后的稳定性验证。这个方法在后续多个项目里帮了我大忙尤其是新板子第一次点亮屏幕时绝对能省下半天时间。本文还有配套的精品资源点击获取
返回列表