易灵思FPGA双线性插值缩放:DDR3帧缓存与Verilog实现
简介:这份资源面向具备Verilog与数字视频处理基础的中高级FPGA工程师及科研人员,提供一套基于易灵思TI60F225平台、纯Verilog实现的双线性插值视频缩放方案,支持HDMI输入输出任意比例缩放,可解决低成本平台上高效缩放与商业IP替代问题。压缩包共5个文件,约289KB,包含docx设计说明、pdf技术文档、html与md源码解析及txt补充材料,覆盖算法定点化、DDR3帧缓存裸接口控制、双口RAM环形行缓存、跨时钟域处理与参数化移植等关键模块。已有277人学习。读者可从中获取完整工程思路与代码参考,理解以定点数替代浮点运算的资源优化策略、直接操作物理层接口提升吞吐的方法,以及环形缓冲区与DDR控制逻辑的协同设计,便于在资源受限FPGA上复现并移植到其他平台。
1. 易灵思 FPGA 上跑双线性插值缩放:为什么 DDR3 才是那个隐藏主角
很多人第一次在 FPGA 上做视频缩放,思路都很直接:摄像头进来,写个双线性插值模块,算完送显示器,收工。真上手才发现,1080p 一帧 1920×1080×2 字节接近 4MB,FPGA 片内 BRAM 撑死几百 KB 到一两 MB,一帧都存不下,更别说乒乓缓存。这时候 DDR3 就不是可选项,而是整个系统能不能跑起来的命门。这篇笔记讲的是在易灵思(Efinix)FPGA 平台上,用 Verilog 写一个双线性插值视频缩放系统,并用 DDR3 做帧缓存,把任意分辨率输入缩放到目标分辨率输出。适合已经会写 Verilog 状态机、跑过 UART 或简单图像通路,但还没把「视频流 + 外部存储 + 插值算法」串成一条完整链路的工程师。核心矛盾只有一个:插值算法本身不难,难的是怎么让 DDR3 的读写带宽和延迟,喂得动实时视频流。
2. 双线性插值与 DDR3 帧缓存:先把账算清楚再动手
2.1 双线性插值到底在算什么
双线性插值的本质是二维平面上的加权平均。目标图上一个像素 (dx, dy),映射回源图坐标 (sx, sy),这个坐标通常不是整数,落在四个相邻源像素围成的矩形里。设 sx 的整数部分为 x0,小数部分为 fx;sy 的整数部分为 y0,小数部分为 fy。那么目标像素值由四个源像素加权得到:
P = (1-fx)(1-fy)·P(x0,y0) + fx(1-fy)·P(x0+1,y0) + (1-fx)fy·P(x0,y0+1) + fx·fy·P(x0+1,y0+1)四个权重加起来恒等于 1,这是它比最近邻插值平滑、又比双三次插值省资源的根本原因。硬件实现时,fx 和 fy 用定点小数表示,常见做法是保留 8 位或 10 位小数位,权重乘法用 DSP 硬核完成。缩放比例决定了坐标映射的步进:放大时步进小于 1,多个目标像素会命中同一组源像素;缩小时步进大于 1,源图上会有像素被跳过。这个步进值通常由输入输出分辨率之比算出,用累加器逐像素推进,避免每帧重复做除法。
选双线性而不是最近邻,是因为最近邻在缩放后边缘锯齿明显,做视频观感差;不选双三次,是因为它需要 4×4 共 16 个源像素和更多乘法器,在易灵思这类中低密度器件上资源吃紧。双线性是画质和资源的平衡点,也是绝大多数 FPGA 视频缩放项目的默认选择。
2.2 为什么必须上 DDR3,片内 BRAM 为什么不够
算一笔账。1080p 单帧 YUV422 或 RGB565 数据量约 2MB 到 4MB。要做双线性插值,至少需要缓存两行源像素(行缓冲),如果缩放比例导致同一行被多次读取,还需要缓存整帧或至少一个条带。易灵思 Trion 系列(比如常见的 T20、T120)片内 BRAM 总量在几百 Kbit 到几 Mbit 量级,换算成字节远小于一帧。就算用乒乓缓存两帧,也放不下。
所以架构上必须把 DDR3 当作帧缓冲区:输入视频流写入 DDR3 的一块区域,缩放模块从 DDR3 读出需要的源像素行,插值后输出。DDR3 在这里承担两个角色——帧缓存和行数据供给。它的带宽和延迟直接决定系统能不能实时。1080p60 的像素时钟约 148.5MHz,每像素 2 字节,写入带宽需求约 300MB/s,读出还要再算一份,加上插值可能重复读取,总带宽需求轻松超过 600MB/s。DDR3 在 16 位位宽、400MHz 时钟下理论带宽 1.6GB/s,看起来够,但实际有效带宽受刷新、行切换、读写切换影响,通常只能用到 60% 到 70%。这就是为什么 DDR3 控制器的效率和访问模式,比插值算法本身更影响成败。
2.3 易灵思平台上的 DDR3 硬核与控制方式
易灵思 FPGA 的 DDR3 接口通常通过硬核内存控制器(Hard Memory Controller)实现,配合 Efinity 工具链里的 DDR 控制器 IP。和 Xilinx 的 MIG、Intel 的 UniPHY 类似,你需要配置引脚分配、时序参数、位宽和突发长度。关键配置项包括:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 数据位宽 | 16 bit | 单颗 DDR3 常见位宽,影响带宽 |
| 突发长度 | 8 | BL8,一次突发传 8 个数据 |
| CAS 延迟 | 依颗粒而定 | 配置错误会导致读数据错位 |
| 时钟频率 | 400MHz(800Mbps) | 需与颗粒速率匹配 |
| 读写切换 | 控制器自动管理 | 频繁切换会掉带宽 |
我一般会先用 Efinity 的 DDR 示例工程把读写测试跑通,确认控制器能正确写入和读回数据,再往上叠视频通路。跳过这一步直接上视频,出了问题你分不清是 DDR 没配对还是插值逻辑写错了,这是血泪经验。
2.4 最小可跑通的读写通路
下面是一段简化的 DDR3 读写测试逻辑,用于验证控制器通路。实际项目中读写地址和突发控制会更复杂,但结构一致。
// DDR3 读写测试:写入递增数据,再读回比对 module ddr3_rw_test ( input wire clk, input wire rst_n, // 用户接口(简化) output reg [27:0] wr_addr, output reg [15:0] wr_data, output reg wr_en, output reg [27:0] rd_addr, input wire [15:0] rd_data, output reg rd_en, output reg test_pass ); reg [3:0] state; reg [15:0] cnt; localparam IDLE = 0, WRITE = 1, WAIT = 2, READ = 3, CHECK = 4; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; wr_addr <= 0; wr_en <= 0; rd_en <= 0; cnt <= 0; test_pass <= 0; end else begin case (state) IDLE: begin cnt <= 0; state <= WRITE; end WRITE: begin wr_en <= 1; wr_data <= cnt; // 写入递增数据 wr_addr <= {14'd0, cnt, 2'b00}; // 地址按 4 字节对齐 if (cnt == 16'd255) state <= WAIT; else cnt <= cnt + 1; end WAIT: begin wr_en <= 0; state <= READ; // 等待写完成 end READ: begin rd_en <= 1; rd_addr <= {14'd0, cnt, 2'b00}; if (cnt == 16'd255) state <= CHECK; else cnt <= cnt + 1; end CHECK: begin rd_en <= 0; // 实际比对需考虑读延迟,此处简化 test_pass <= 1; state <= IDLE; end endcase end end endmodule这段代码的逻辑是:先向 DDR3 连续写入 256 个递增数据,等待写完成后逐个读回。wr_addr的拼接方式保证地址按 4 字节对齐,因为 DDR3 突发传输以 8 个 16 位数据为单位,地址低几位有对齐要求。WAIT状态是必须的,DDR3 写入到实际落盘有延迟,立刻读可能读到旧数据。参数上,cnt位宽决定测试深度,实际项目里地址位宽要按 DDR3 容量算,比如 256MB 需要 28 位地址。这段代码跑通,说明控制器配置和引脚分配没问题,可以往上加视频逻辑了。
3. 视频缩放通路的 Verilog 实现:从像素流到插值输出
3.1 整体数据流与模块划分
一个能跑的缩放系统,数据流是这样的:视频输入(比如 HDMI 或 MIPI 解码后的像素流)先写入 DDR3 的输入帧缓冲区;缩放模块从 DDR3 读出源像素行,做双线性插值;插值结果写入 DDR3 的输出帧缓冲区;显示模块从输出缓冲区读出,送到显示接口。中间用 FIFO 做跨时钟域和带宽缓冲。
模块划分建议:
video_in_capture:接收输入像素流,产生写地址和写数据ddr3_frame_wr:把输入像素写入 DDR3 输入缓冲区scaler_core:核心插值模块,从 DDR3 读源行,算插值,输出ddr3_frame_rd:从 DDR3 输出缓冲区读像素送显示pll_clk:产生像素时钟、DDR3 时钟、控制器时钟
这里最容易翻车的是时钟域。像素时钟和 DDR3 控制器时钟通常不同频,所有跨域信号必须过异步 FIFO,不能直接打拍。我见过有人图省事直接跨域,结果画面随机撕裂,查了一周才发现是亚稳态。
3.2 行缓冲与插值计算的状态机
双线性插值需要同时访问两行源像素。做法是:从 DDR3 读出一行源像素存入行缓冲 BRAM,再读下一行,两行都就绪后开始逐像素插值。状态机大致是:
// 插值核心状态机(简化) localparam S_IDLE = 0, S_RD_ROW0 = 1, // 读第一行 S_RD_ROW1 = 2, // 读第二行 S_INTERP = 3, // 插值计算 S_NEXT = 4; // 推进到下一组行 always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_IDLE; else case (state) S_IDLE: if (frame_start) state <= S_RD_ROW0; S_RD_ROW0: if (row0_done) state <= S_RD_ROW1; S_RD_ROW1: if (row1_done) state <= S_INTERP; S_INTERP: if (line_done) state <= S_NEXT; S_NEXT: state <= S_RD_ROW0; // 行推进,注意边界 endcase endS_RD_ROW0和S_RD_ROW1分别从 DDR3 读两行源像素,存入两个行缓冲。S_INTERP状态下,每个时钟周期算一个目标像素,四个权重由小数部分 fx、fy 生成。S_NEXT负责行地址推进,缩小时可能跳过若干源行,放大时同一行可能被重复使用,这里要按缩放比例算好行步进。
参数上,行缓冲深度等于源图宽度,位宽等于像素位宽。如果源图宽度超过 BRAM 单块容量,需要多块拼接或用更宽的 BRAM 配置。易灵思的 BRAM 支持多种宽度深度组合,配置时注意别超。
3.3 权重计算与定点小数处理
权重来自坐标映射的小数部分。假设水平缩放比是scale_x,目标像素 x 对应的源坐标是x * scale_x。用定点数表示scale_x,比如 16 位整数加 16 位小数,累加器每周期加一次,取整数部分做地址,小数部分做权重。
// 水平方向坐标累加与权重提取 reg [31:0] x_acc; // 16.16 定点累加器 wire [15:0] src_x_int = x_acc[31:16]; // 整数部分 wire [15:0] src_x_frac = x_acc[15:0]; // 小数部分,即 fx always @(posedge clk or negedge rst_n) begin if (!rst_n) x_acc <= 0; else if (pixel_valid) x_acc <= x_acc + scale_x_fixed; endscale_x_fixed是输入宽度除以输出宽度的定点表示。比如 1920 缩到 1280,比例 1.5,定点表示为32'h0001_8000。小数位宽决定插值精度,8 位小数够用,10 位更细腻,但乘法器位宽增加。权重乘法用 DSP 硬核,易灵思的 DSP 支持 18×18 或更宽,四个乘法并行做,一个周期出结果。
注意边界处理:当src_x_int等于源图最右列时,src_x_int+1会越界,需要钳位到最右列,否则读到下一行数据,画面右边会出现一条杂色。这个坑很隐蔽,因为只在最右一列出现,容易被忽略。
3.4 DDR3 读写带宽的分配与仲裁
视频通路里 DDR3 要同时服务写入(输入帧)和读出(缩放读源、显示读输出)。如果不管仲裁,读写请求撞在一起,控制器频繁切换方向,带宽直接掉一半。常见做法是给读写各开一块 FIFO,用仲裁器按优先级调度:显示读优先级最高,因为断流会黑屏;缩放读次之;输入写最低,因为写可以缓一缓。
突发长度尽量用满 BL8,一次传 8 个 16 位数据,减少命令开销。地址要连续,避免跨行。如果缩放读需要跳行,尽量把同一行的多次读合并成连续突发。这些细节在仿真里看不出问题,上板跑 1080p 才暴露,所以带宽余量至少留 30%。
4. 避坑与排查:那些上板才暴露的问题
4.1 画面撕裂或随机闪烁
现象:显示画面出现水平撕裂,或者随机闪烁,仿真完全正常。
原因:跨时钟域信号没做同步。像素时钟域和 DDR3 控制器时钟域之间的地址、使能信号直接打拍,产生亚稳态。
解决:所有跨域信号过异步 FIFO,或者用双触发器同步加握手。数据总线必须用 FIFO,不能简单打两拍。检查方法是用示波器看跨域信号的建立保持时间,或者干脆在代码里强制所有跨域走 FIFO 原语。
4.2 缩放后图像整体偏移或缩放比例不对
现象:输出图像比预期偏左或偏上,或者缩放比例差一点。
原因:坐标映射的初始相位没算对。累加器初值决定了第一个目标像素对应哪个源像素,初值设错会导致整体偏移。另外定点比例值算错,比如把 1.5 写成 1.499,累积误差会让图像慢慢偏。
解决:初值设为scale/2,让目标像素中心对齐源像素中心。比例值用高精度定点,算完后在仿真里打印前几个目标像素的源坐标,和手算对比。这个验证步骤不能省。
4.3 DDR3 读回数据错位或全零
现象:读回的数据和写入对不上,或者全是零。
原因:CAS 延迟配置错误,或者读命令发出后没等够延迟就采样。DDR3 读数据相对读命令有固定延迟,控制器 IP 通常会输出一个 valid 信号,必须用这个信号采样,不能自己数周期。
解决:严格按控制器 IP 的读时序采样,用rd_data_valid而不是固定延迟。如果 IP 没给 valid,查手册确认延迟周期数,在代码里精确计数。另外检查引脚分配,DQS 和 DQ 的对应关系错了也会读错。
4.4 高分辨率下带宽不够导致画面卡顿
现象:720p 正常,1080p 开始丢帧或卡顿。
原因:DDR3 有效带宽不足。读写频繁切换、突发没打满、地址不连续都会吃带宽。
解决:用带宽计算工具先估需求,再留 30% 余量。优化访问模式:合并连续读、用满突发、减少读写切换。如果还不够,考虑提高 DDR3 时钟或加宽位宽。易灵思部分器件支持 32 位 DDR3,带宽翻倍,但引脚占用增加,要权衡。
4.5 行缓冲边界处理导致画面边缘杂色
现象:图像最右列或最下行出现一条杂色。
原因:插值时src_x_int+1或src_y_int+1越界,读到了相邻行或无效数据。
解决:在地址生成时加钳位逻辑,越界时强制取最右列或最下行。这个逻辑要放在地址计算的最末端,确保所有路径都经过钳位。
5. 进阶技巧:用仿真波形反推 DDR3 访问效率
做到能跑之后,下一步是优化。我一般不看代码猜瓶颈,而是抓仿真波形算实际带宽。方法是在 DDR3 控制器用户接口上打信号,记录一段时间内的有效读写数据量和总周期数,算出有效带宽占比。如果低于 60%,说明仲裁或访问模式有问题。
具体做法:在仿真里跑 10 帧,统计wr_en和rd_en有效的周期数,乘以每周期传输字节数,除以总时间。对比理论带宽,差距大就查突发是否打满、地址是否连续、读写切换是否频繁。这个数字比任何直觉都准。
另一个技巧是给缩放模块加一个可配置的缩放比例寄存器,通过 UART 或 SPI 在线改,不用重新综合就能测不同分辨率。调试阶段能省大量时间。我现在的习惯是:任何视频项目,第一版一定留一个在线配置接口,哪怕只是几个寄存器,后面调参时会感谢自己。
最后说个习惯:DDR3 的引脚分配和时序约束,一定要在项目初期就锁定,不要等到逻辑写完再调。引脚一变,时序全乱,后悔药没处买。希望帮到你。
本文还有配套的精品资源,点击获取