简介:面向FPGA开发者的图像处理系列第5份资料,聚焦基础功能中的双线性插值原理与FPGA实现。文档从算法公式出发,结合四邻域像素加权计算目标像素的典型场景,说明了双线性插值在图像缩放、旋转、平移等操作中的意义,并重点讲解在Sysgen环境中通过Slice模块截去低位代替除法、将插值系数设计为2的指数等工程化技巧,避免FPGA内部出现复杂除法。压缩包内为1个PDF文件,约71KB,内容精炼而充实,配有示意图与步骤分解,适合硬件工程师和图像算法入门者按需查阅与快速上手。已有1342人学习下载,读者可参照图文推导理清系数计算、邻域坐标定位与位宽对齐等关键细节,可直接用于图像预处理模块的方案设计与功能验证,并为后续更高阶算法提供基础支撑。
1. 用双线性插值打开 FPGA 图像处理的第一道门
做视频采集或显示驱动的 FPGA 工程师,早晚会撞上图像缩放:摄像头输出 1280x720,显示屏是 1920x1080,点对点显示不了。直接丢行列会闪,把源像素复制成四个锯齿又太重。双线性插值是画质与资源之间最常见的折中,只读目标像素周围四个源像素做加权平均,就能把缩放结果拉回可接受水平。这篇按 FPGA 图像处理基础功能的视角讲清双线性插值原理与落点:先解释四邻域加权为什么有效,再给出能在 Vivado/Quartus 直接综合的坐标与加权求和的 Verilog 代码,最后把行缓存、流水线和仿真对拍的做法讲透。适合正在做 ISP、视频缩放或显示控制器,有 Verilog 基础但没手写过插值核的开发者。
2. 双线性插值原理:四邻域加权平均是图像缩放的最小可用模型
2.1 反向坐标映射:硬件缩放的第一步不是插值
图像缩放本质是重采样。放大一张 640x480 的图到 960x720,直接做法是让每个输出像素去源图像里找自己的“前世”。输出像素坐标 (dst_x, dst_y) 映射回源坐标 (src_x, src_y),映射公式是:
src_x = dst_x × (SRC_W / DST_W) src_y = dst_y × (SRC_H / DST_H)这个“反向映射”决定了插值算法能不能落地。如果反过来做正向映射,把源像素直接复制到输出位置,缩放倍数大于 2 时目标图上会出现大量空洞,还得靠后处理填补。FPGA 做实时视频缩放,数据是逐行推进的流,反向映射天然适合流水线:每个输出时钟周期,只要算出一个源坐标,再从缓存里抓四个像素即可。硬件里这个映射不是每周期做一次除法,而是用定点累加器按步长推进,后面第 3 章会给出具体写法。
这里容易出现第一个错误:映射用的是 SRC_W / DST_W 还是 (SRC_W-1) / (DST_W-1)。前者会让人物在缩放后向中心收缩,后者能保证图片左右边缘贴齐。工程上我默认用 (SRC_W-1)/(DST_W-1),原因是缩放前后像素中心要对齐,源图像最后一个像素必须能映射到目标图像最后一个像素。
2.2 从最近邻到双线性:为什么只取四个点就够
最近邻插值在 FPGA 上实现最便宜,取坐标四舍五入后直接读一个像素。它的缺陷是放大时出现明显块状效应,缩小高频区域会产生闪烁感。双线性插值把目标像素映射到源坐标后,取这个坐标周围最近的 2x2 像素窗口,按距离加权求和。
假设源坐标为 (x0+dx, y0+dy),其中整数部分 x0、y0 从 floor 得到,小数部分 dx、dy 都在 [0,1) 区间。四个相邻像素记为 p00、p10、p01、p11,取值分别为 (x0,y0)、(x0+1,y0)、(x0,y0+1)、(x0+1,y0+1)。插值公式为:
out = (1-dx)(1-dy)·p00 + dx(1-dy)·p10 + (1-dx)dy·p01 + dx·dy·p11四个权重之和恒等于 1,这意味着输出不会发生整体亮度偏移。这个约束在硬件定点化时很有价值,后面会用它省乘法器。
双线性插值本质上是个低通滤波器,对高频细节有抑制作用。放大照片时边缘会偏软,缩小文字行时会丢失对比度。但实时视频流里人眼对软边缘的容忍度远高于马赛克和闪烁,所以它是 ISP 管线里性价比最高的选择。
2.3 可分离性:把二维插值拆成两次一维插值
上述四邻域公式可以改写为两次一维插值。先对当前行的两个像素做水平插值:
tmp0 = (1-dx)·p00 + dx·p10 tmp1 = (1-dx)·p01 + dx·p11再对两行结果做垂直插值:
out = (1-dy)·tmp0 + dy·tmp1二维双线性插值是可分离的,这个数学性质在实际工程里用来调整流水线节奏。当权重位宽较大时,拆分结构还方便插入寄存器打拍:水平插值一级流水,垂直插值一级流水,关键路径长度能明显缩短。如果缩放比例固定,两次一维插值还可以共用同一套权重计算模块。
| 算法 | MUX/DSP消耗 | 画质 | FPGA典型延迟 | 适用场景 |
|---|---|---|---|---|
| 最近邻 | 0 个 DSP | 边缘锯齿明显,高频闪烁 | 1 周期 | 缩略图、OSD、对延迟极敏感的通道 |
| 双线性 | 4~8 个 DSP48 | 边缘柔和,亮度连续 | 3~5 周期 | 视频缩放、ISP、显示控制 |
| 双三次 | 16~32 个 DSP48 | 细节较好,过冲振荡 | 8~16 周期 | 静态图像离线处理,实时性难保证 |
双三次在实时 4K@60 场景下资源代价过高,而且人眼在运动画面上并不总能感受到它的提升。工程实践中,绝大多数 FPGA 视频通路最终都收敛到双线性,再叠加锐化滤波补偿细节损失。
2.4 边界策略:越界像素的三种处理方式
当源坐标落在图像右边缘或下边缘时,x0+1 会越过列号范围。常见做法有三种:把像素值钳位在边界复制、回绕到对侧边缘、用最近的有效像素替代。硬件里最常选第一种,因为边界复制不需要额外存储器,只把 x1 限制为 min(x0+1, W-1) 即可。
具体到 Verilog,边界的处理是在读像素之前完成的。整数坐标经过饱和裁剪后再送给行缓存读地址,而不是等读到越界地址再纠错。实现时用一个比较器加选择器:
wire [11:0] x0_clip = (x0 >= SRC_W - 1) ? SRC_W - 2 : x0; wire [11:0] x1_clip = x0_clip + 1;x0_clip 限制到 SRC_W-2,保证 x1 最大不超过 SRC_W-1。注意不要只裁剪 x1 不裁剪 x0,否则窗口整体偏移会造成图像右边缘收缩。亮度与色度分开缩放的项目里,色度平面边界常会额外多采一个像素做 Chroma Reuse,这一步会放到后面通道设计时单独讨论。
3. 把插值公式落成 Verilog:坐标定点化与加权求和
3.1 为什么 FPGA 上不直接写浮点,定点数才是常态
浮点数的双线性公式在 C 语言里很直观,放到 RTL 里就变味了。FPGA 做浮点乘加要么调用 Floating-Point IP,要么让综合器把浮点表达式推断成 DSP 阵列,两种情况资源都会翻倍,延迟增加 3~5 个周期。另外浮点加法器不满足交换律,位级仿真结果和 MATLAB 模型存在漂移,对拍时很难收敛。
业界约定俗成的是定点小数:坐标用 Q16.16,权重用 Q8.8。QP 格式的含义是整数部分 P 位,小数部分 16-P 位。坐标步长只需要保证两方面的精度:单行累积误差不能超过 0.5 个像素,连续 4096 列累加后不能出现漂移。16 位小数位宽下,步长量化误差是 1/65536 像素,即使 4K 宽度 3840 列累积,最大误差也只有 6% 像素,肉眼完全不可见。权重用 8 位小数是因为权重本身只在 [0,1] 区间,256 级分辨率已经足够表达亮度过渡的连续性。
3.2 坐标计算模块:累加器代替除法器
固定缩放比场景下,反向映射的除法在编译期就能算成常量,运行时只做加法。下面这段 Verilog 是生成源坐标的核心模块:
module coord_step #( parameter SRC_W = 640, parameter DST_W = 1280, parameter FBITS = 16, // 定点小数位宽 parameter CORD_W = 28 // 整数部分 12bit + 小数 16bit )( input wire clk, input wire rst_n, input wire en, // 每时钟有效表示一个输出像素 input wire line_last, // 行结束信号 output wire [CORD_W-1:0] src_pos // 定点源坐标 ); // 编译期常量除法,不消耗任何逻辑 localparam integer STEP_X = ((SRC_W - 1) << FBITS) / (DST_W - 1); reg [CORD_W-1:0] pos_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) pos_r <= 'd0; else if (en) begin if (line_last) pos_r <= 'd0; else pos_r <= pos_r + STEP_X; end end assign src_pos = pos_r; endmodule代码里 STEP_X 是在 Elaboration 阶段算出的常量,综合后只是一个加法器的常数输入,不会生成除法器。每次 en 有效,坐标累加一次;一行结束后重置为 0,避免行间误差累积。这里行内连续累加的前提是输出像素的使能信号均匀,如果插值链路中插入了 FIFO 反压,en 信号不能简单用 valid 代替。
CORD_W位宽计算方法:整数部分需要容纳 SRC_W 的最大值 640 对应 10 位,留 2 位冗余做中途临时值,取 12 位,加 16 位小数共 28 位。换分辨率时整数位宽按clog2(SRC_W)+2计算,不要把整数位设成固定 16 位,否则极端缩放比下坐标溢出后插值窗口会串行。
x 坐标和 y 坐标可以用同一个模块实例化两份。y 方向的 STEP_Y 按行推进,每来一行line_last时累加一次。注意 x 方向每行都重置为 0,y 方向整个帧都不重置,只在帧同步信号到来时清 0。
3.3 加权求和:四路乘累加加一点截断技巧
拿到源坐标后,取整数部分和小数部分。整数位进行缓存读地址,小数位作为权重进入乘法器。下面这段代码覆盖了双线性公式的核心计算:
module bilinear_core #( parameter DATA_W = 8, parameter WF_W = 8 // 权重小数位宽 )( input wire clk, input wire rst_n, input wire en, // 输入使能 input wire [DATA_W-1:0] p00, p01, p10, p11, input wire [WF_W-1:0] dx, dy, // 定点小数权重 output reg [DATA_W-1:0] dout, output reg dout_vld ); // 1 用常量表示权重总数,便于后续归一化 localparam integer WF = (1 << WF_W); // 256 // 2 计算 2x2 窗口权重,全部定点乘法 wire [WF_W*2-1:0] w00 = (WF - dx) * (WF - dy); wire [WF_W*2-1:0] w01 = dx * (WF - dy); wire [WF_W*2-1:0] w10 = (WF - dx) * dy; wire [WF_W*2-1:0] w11 = dx * dy; // 3 像素与权重相乘后累加,乘积按 32bit 处理 wire [31:0] mul00 = p00 * w00; wire [31:0] mul01 = p01 * w01; wire [31:0] mul10 = p10 * w10; wire [31:0] mul11 = p11 * w11; reg [31:0] sum_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sum_r <= 'd0; dout <= 'd0; dout_vld <= 1'b0; end else if (en) begin sum_r <= mul00 + mul01 + mul10 + mul11; dout <= sum_r[(WF_W*2) +: DATA_W]; // 从第 16 位开始取 8 位 dout_vld <= en; end end endmodule权重 w00 到 w11 是 16 位定点数,最大值是 65535 正好对应整数 1。四个权重和必然等于 65536,这个性质让输出不会整体偏亮或偏暗。像素值 8 位乘以权重 16 位得到 24 位乘积,四次乘积累加扩展到 32 位是为了防止进位溢出。输出截断从 bit16 开始取 8 位,等价于除以 65536 后四舍五入到最接近整数。不要直接取高 8 位,那样对小像素值会有 -1 的固定偏差,连续视频流里看起来是暗部发灰。
注意到这里 dout 的输出比起 sum 多打了一拍,所以 dout_vld 也要对齐到同一拍。忘记对齐使能信号是自写代码里最常见的时序错误,仿真时数据看起来错半拍,上板后表现为图像右移一两个像素。
3.4 接口与参数设计:一份可复用的插值子模块
把坐标模块和核心计算模块封装起来之前,需要先定好接口。下面是实用端口定义:
| 信号 | 方向 | 位宽 | 说明 |
|---|---|---|---|
| clk / rst_n | input | 1 | 时钟,建议 150MHz 以上 |
| s_axis_tvalid | input | 1 | 输入像素有效 |
| s_axis_tready | output | 1 | 反压信号 |
| s_axis_tdata | input | 8/16/24 | 灰度或 RGB 像素 |
| s_axis_tuser | input | 1 | 帧同步 |
| tlast | input | 1 | 行同步 |
| m_axis_tvalid | output | 1 | 输出数据有效 |
| width / height | input | 16 | 运行时分辨率,可动态切换 |
参数用SRC_W/DST_W/FBITS/WF_W四个即可覆盖绝大多数项目。分辨率固定时把参数写死能省资源,但做多分辨率适配一定要把宽高信号做成寄存器,主机端通过 AXI-Lite 配置。缩放的边界情况也在这里处理:当 DST_W == SRC_W 且 DST_H == SRC_H 时,坐标模块输出 0 步长,插值核变成纯通路,这时可以直接把旁路开关打开,省掉全部乘法器功耗。
4. 行缓存与流水线:让缩放过程不打断视频数据流
4.1 为什么必须有行缓存:一次输出需要读两行两列
输入视频流是逐行来的,但双线性插值一次输出要同时访问 (x0,y0)、(x0+1,y0)、(x0,y0+1)、(x0+1,y0+1) 四个像素。这四个像素分布在相邻两行里,如果没有缓存,等第二行数据到达时第一行早就过去了。行缓存的任务是把当前行延迟一拍,让插值器随时能从“上一行”和“当前行”各取相邻两个像素。实现上最常用双口 RAM:写侧接收新像素,读侧滞后一拍输出上一行数据。
设计行缓存时一个反复出现的错误是缓存深度只看行宽本身。行缓存深度至少等于 SRC_W,但读侧要支持同时访问 y0 和 y0+1 两行的同列像素,所以实际是两个深度为 SRC_W 的银行。对 1080p 灰度图,2 行 x 1920 像素 x 8bit 共 30Kbit,约等于 1 个块 RAM (BRAM36K);RGB888 则是 90Kbit,需要 3 个 BRAM36K。
4.2 双行缓存读写控制:写侧顺序写,读侧跟行延迟
下面代码展示双行缓存的控制逻辑。这里用寄存器数组便于理解,综合工具会自动把它映射成块 RAM:
module line_buffer #( parameter DATA_W = 8, parameter LINE_W = 1920 )( input wire clk, input wire rst_n, input wire wr_en, input wire [DATA_W-1:0] din, input wire [$clog2(LINE_W)-1:0] wr_addr, input wire [$clog2(LINE_W)-1:0] rd_addr, output reg [DATA_W-1:0] dout_prev, // 上一行像素 output wire [DATA_W-1:0] dout_curr // 当前行像素 ); reg [DATA_W-1:0] mem_bank [0:LINE_W-1]; always @(posedge clk) begin if (wr_en) mem_bank[wr_addr] <= din; dout_prev <= mem_bank[rd_addr]; end assign dout_curr = din; // 当前行直接从输入取 endmodule写法上把当前行数据直接接到 din,上一行数据延迟一拍读出来。这样插值器每个时钟周期都能拿到同一列的上下两个像素。rd_addr 通常比 wr_addr 少 1,保证读出来的上一行像素和当前输入像素在空间上是对齐的。注意 mem_bank 读使能不要额外加条件,否则读出数据会多延迟一拍,打乱插值窗口对齐关系。
行缓存写满一帧后 RAM 里的数据就是残帧,下一个帧同步到来时不能只清零读写指针,还要把 mem_bank 的存储内容保持原样。双缓存或乒乓结构是更稳妥的做法:两个 bank 交替服务奇偶行,插值器永远从“正在写的 bank”和“刚写完的 bank”读数据。代价是多 1 倍 BRAM,换取的是不需要在行切换时等 RAM 清空。
4.3 流水线拆解:分几级、每级做什么
双线性插值的完整数据通分段位:
| 流水级 | 处理内容 | 操作类型 | 延迟 |
|---|---|---|---|
| P0 | 计算定点源坐标 src_pos | 加法器累加 | 1clk |
| P1 | 从行缓存读 p00/p10/p01/p11 | BRAM 读延迟 | 1~2clk |
| P2 | 权重 w00~w11 计算 | 4 个乘法器 | 1clk |
| P3 | 像素 x 权重乘累加 | 4 个 DSP + 加法树 | 1~2clk |
| P4 | 位宽截断与输出对齐 | 寄存器打拍 | 1clk |
总共 5~7 个时钟周期延迟,对视频链路来说完全可以忽略。关键是每级之间的寄存器要打拍对齐,使能信号 en 要跟着数据一起走。如果把 tready 反压恢复后数据重新使能,但使能信号没有穿过 BRAM 读延迟对齐,就会出现输出花屏。
P2 和 P3 在资源充足时可以合并成一级,4 个权重乘法加 4 个像素乘法共 8 个 DSP48E1,1080p@60 下频率仍能跑到 200MHz。如果 DSP 数量吃紧,把权重用 LUT 实现(WF_W=8 时 w00 只是两张 256x16 的查找表),DSP 可以压到 4 个。反过来要省 LUT 就保留 DSP 权重,两种方案综合结果差 100 个 LUT,影响不大。
4.4 AXI-Stream 时序适配:握手信号不能省
视频缩放核通常嵌在 AXI-Stream 链路里,输入输出都有 tvalid/tready 握手。插值器内部需要连续两个行缓存数据才能出第一个有效像素,所以输入侧 tready 不能一直拉高。简化处理是在输入侧做一个深度 64 的 FIFO:FIFO 已有两个像素时把 tready 拉高,插值器从 FIFO 取数,保证行缓存写侧始终连续。
输出侧问题在于双线性插值的输出速率不稳定。当缩放比不是整数时,每个输出像素消耗的源像素数量呈周期变化,输出 tvalid 会周期性地拉低几个周期。接收端如果是 HDMI TX 这类要求连续流的接口,必须插一个输出 FIFO 吸收抖动。FIFO 深度按最大抖动量估算,一般 256 足够。
两个容易踩的手握时序细节:其一,tready 拉低前必须保持 tvalid 不变,否则上游可能丢像素;其二,tlast 要与对应行的最后一个像素同步,不能滞后一拍,否则接收端行计数错乱。仿真时用 AXI VIP 或手写 monitor 脚本检查这两条规则,比盲目抓波形效率高得多。
4.5 资源估算:一句话决定用不用这个方案
| 目标分辨率 | 位宽 | 块 RAM | DSP48E1 | LUT 估值 | 最大频率参考 |
|---|---|---|---|---|---|
| 1280x720 灰度 | 8bit | 1 个 BRAM36K | 4 | 350~450 | 250MHz |
| 1920x1080 灰度 | 8bit | 1 个 BRAM36K | 4 | 400~500 | 200MHz |
| 1920x1080 RGB | 24bit | 3 个 BRAM36K | 12 | 1200~1500 | 200MHz |
| 3840x2160 RGB | 24bit | 7 个 BRAM36K | 12 | 1500~1800 | 150MHz |
RGB 三个通道各自独立做插值时,权重模块可以共享,但行缓存和乘法器必须复制三份。如果项目对 BRAM 极敏感,可以先把 RGB 转成 YUV422,对 Y 全分辨率插值,UV 各半分辨率插值再合并,BRAM 占用能降 40%。代价是色度细节垂直方向略软,多数显示场景可接受。
5. 仿真验证与上板排错:从 Testbench 到 ILA 抓波形的完整闭环
5.1 用 $readmemh 喂图,先验证单像素级计算
写 Testbench 的第一步是验证插值核算术是否正确。手工构造一个 2x2 窗口,给定 dx/dy,手算出期望值。以 p00=100、p01=200、p10=150、p11=250,dx=0.25、dy=0.5 为例:
期望输出 = 0.375×100 + 0.125×200 + 0.375×150 + 0.125×250 = 150Testbench 代码核心片段:
module tb_bilinear; reg clk = 0; reg en = 0; reg [7:0] p00 = 100, p01 = 200, p10 = 150, p11 = 250; reg [7:0] dx = 64, dy = 128; // 对应 0.25 / 0.5 wire [7:0] dout; wire dout_vld; bilinear_core dut ( .clk(clk), .rst_n(1'b1), .en(en), .p00(p00), .p01(p01), .p10(p10), .p11(p11), .dx(dx), .dy(dy), .dout(dout), .dout_vld(dout_vld) ); always #5 clk = ~clk; initial begin repeat (10) @(posedge clk); en = 1; repeat (2) @(posedge clk); en = 0; repeat (5) @(posedge clk); $display("dout=%0d expect=150", dout); if (dout == 150) $display("PASS"); else $error("FAIL"); $finish; end endmodule这段验证的是纯组合逻辑和输出打拍,能立刻发现位宽截断或权重公式写反的问题。dout 输出晚于 en 两拍,因此用repeat(2)拉低 en 后等 5 拍再看结果。工程里我会把 256 组随机 dx/dy 放进循环生成 Testbench,跑完统计 err 是否都在 1 LSB 内。
5.2 与 Python 参考模型对拍:允许 1 个 LSB 误差
单像素手算只能验证核心公式,整帧验证要靠参考模型。Python 里写一个同样使用定点算法的模型,喂同一张测试图,输出和 RTL 仿真相减:
import numpy as np from PIL import Image def bilinear_fp(src, dst_w, dst_h, fbits=16, wbits=8): h, w = src.shape out = np.zeros((dst_h, dst_w), dtype=np.uint8) for j in range(dst_h): for i in range(dst_w): sx = (i * (w - 1) * (1 << fbits)) // (dst_w - 1) sy = (j * (h - 1) * (1 << fbits)) // (dst_h - 1) x0 = min(sx >> fbits, w - 2) y0 = min(sy >> fbits, h - 2) dx = (sx >> (fbits - wbits)) & ((1 << wbits) - 1) dy = (sy >> (fbits - wbits)) & ((1 << wbits) - 1) w00 = (256 - dx) * (256 - dy) w01 = dx * (256 - dy) w10 = (256 - dx) * dy w11 = dx * dy val = (src[y0, x0] * w00 + src[y0, x0+1] * w01 + src[y0+1, x0] * w10 + src[y0+1, x0+1] * w11) >> 16 out[j, i] = val return out src = np.arange(64 * 64, dtype=np.uint8).reshape(64, 64) ref = bilinear_fp(src, 128, 128)Python 模型里必须保持和 Verilog 相同的位宽、相同的截断时机,连舍入方式都要一致。权重用乘法器算完,乘积累加后右移 16 位,这在 Python 用>> 16模拟。如果两边结果差超过 1,先把坐标定点部分拉出来单测,大概率是 SRC_W-1 和 DST_W-1 那两条减 1 到底是减在哪个变量上没对齐。
5.3 整帧仿真提效:用灰度渐变图做空间连续性检查
全 64x64 逐点循环的 Python 模型能验证正确性,但速度慢。回归仿真是个更好的折衷方案:生成一张 1920x1 的横向渐变图,作为单行数据送入 Testbench,观察输出行像素值是否单调递增。双线性插值对线性渐变图理论上输出仍是线性,任何非线性跳变都说明坐标累加或权重截断有 bug。仿真跑一行 1920 个时钟只要几微秒,比 50 帧仿真快三个数量级。
用 Modelsim 或 Vivado Simulator 跑完,把结果导出成 CSV,与参考模型同一行数据做差分统计。重点看两个指标:最大绝对误差不超过 1,误差位置是否固定在某些坐标。误差固定出现在行尾说明 STEP_X 位宽不足;误差分散出现则要检查权重截断方式。
5.4 上板排错:先抓信号,再调阈值
仿真过了不等于上板没问题,板级系统里的行缓存、FIFO 反压、帧同步都会引入仿真里没有的情况。ILA 调试时的优先级:
- 先抓输入侧 tready/tvalid 是否因为 FIFO 满而周期拉低,这决定坐标累加节奏是否被打乱
- 再抓一行中的 tlast 是否与最后一个像素对齐,这是行缓存切行是否正确的直接依据
- 最后抓输出侧 tvalid 与 dout 的对齐关系,dout 不能比 tvalid 早或晚出现
花屏常见原因集中在两个地方:行缓存读地址没跟写地址错一拍,以及帧首行插值窗口里混入了上一帧数据。前者把 dout_prev 打拍与 din 对齐能解决;后者在帧同步后延迟两个时钟再使能行缓存写入,确保插值器前两行不读垃圾数据。
6. 进阶技巧:资源压缩和动态缩放的落地细节
DSP 数量敏感的项目,可以让 w11 不显式计算,利用四个权重和为 65536 这一点直接做减法:
w11 = 65536 - w00 - w01 - w10这样权重计算从 4 个乘法器降到 3 个,代价是增加两级减法链。减法和加法级数交换后对时序影响很小,17x17 乘法器本来就是 DSP48 的单个原生操作,省一个 DSP 对整片资源布局意义不小。注意 w11 一定要钳位到非负值,定点舍入导致的极端情况下它可能算出 -1。
动态分辨率切换项目里,把 STEP_X/STEP_Y 做成寄存器由主机下发,避免重新综合。定点位宽取 20 位小数,整数部分 12 位,这样既能覆盖 4K 宽度,切换分辨率时也不需要改 RTL。主机计算步长的公式用(src-1)/(dst-1)<<20,传下来后 RTL 只做累加和截断。
RGB 三通道实现里有一个容易被忽略的优化:三分量共用同一套 dx/dy 权重和坐标。真正的计算差异只有行缓存数据和乘法加法,权重模块只需要一个副本,再配以每周期三路并行读像素,整核只消耗 3 个 DSP 权重乘法器加 9 个像素乘法器就把 1080p RGB 缩放做完了,比直接复制三路完整模块省一半资源。把这段逻辑整理成参数化 IP 后,配 AXI-Lite 配置步长和开关,就可以直接复用到多路视频拼接、画中画、多屏显示项目里。
本文还有配套的精品资源,点击获取