资讯动态

FPGA图像缩放与移动的坐标变换:从Verilog到SignalTap实测

发布时间:2026/10/14 6:03:39 来源:尧图企业网站定制
这个系列做到第三篇前面已经把图像能显示出来和图像能通过SDRAM缓存再读出来这两件事聊透了。这一篇换个角度当缩放、移动同时出现在一条显示通路上它们不是两个独立功能而是一个坐标变换问题。我在实际做的时候踩了不少坑最典型的就是单看缩放没问题、单看移动也没问题一旦两个参数同时改画面边缘就抖、切参数瞬间还会撕裂。这篇就把我从数学映射到Verilog实现、再到SignalTap实测的完整过程写出来适合已经跑通VGA显示通路、正在做缩放功能、并且被动态切参和边界抖动折磨的读者。1. 坐标变换引擎缩放和移动为什么必须一起算1.1 显示通路的基本盘读侧与写侧解耦先回顾一下通路的整体结构。写侧负责把图像数据写进缓存读侧负责按显示时序把数据读出来送给屏幕。绝大多数情况下写侧是固定节拍的比如一行一行地从采集端或者测试图发生器收数据读侧则是跟着VGA时序走的hsync、vsync一拉像素时钟一拍一个地址往外读。缩放和移动做在读侧原因很直接写侧是原图进读侧才是最终呈现给用户看什么。只要读侧地址逻辑足够灵活写侧完全可以不感知缩放和移动的存在继续按原始分辨率往缓存里写。这样两边解耦之后后续换摄像头、换分辨率都不需要动缩放引擎。但解耦也带来一个问题读侧处理的是屏幕坐标到缓存坐标的映射一旦这个映射变得复杂读地址就不能再用简单的行列计数器递增了。1.2 正向映射的坑与逆映射的好处很多人第一次做缩放脑子里的第一反应是把源图像按比例放大得到一张新图再送到屏幕这就是正向映射。如果只是软件逐像素处理一张静态图正向映射完全没问题但放在实时显示通路上正向映射是个灾难原因是输出像素和源像素不是一一对应放大时源图像里一个像素要映射到输出图像的好几个位置你得决定中间那些空缺填什么缩小时源图像里好几个像素可能挤到输出图像的同一个位置你得决定谁覆盖谁。这两个问题在硬件实时流水线里都要用额外的缓冲和判断逻辑去解决非常别扭。换一个思路从输出像素反推这个像素应该看源图像的哪个位置一切就顺了。显示时序每来一个像素时钟我们就知道当前屏幕坐标(dst_x, dst_y)通过逆映射函数算出源坐标(src_x, src_y)然后去缓存里读这个位置附近的数据。每个输出像素恰好对应一次读请求不存在空洞和覆盖问题。逆映射是这个系列第三篇的核心起点后面所有代码都围绕它展开。缩放和移动的数学表达式统一写成src_x (dst_x - center_x) / scale center_x offset_x src_y (dst_y - center_y) / scale center_y offset_y这里scale是缩放系数center_x和center_y是缩放锚点offset_x和offset_y是平移量。注意这个形式里缩放以锚点为中心移动在缩放之后叠加这样镜头拉近拉远和画面平移两个操作就解耦了。1.3 移动不是简单加减缩放下的平移量换算移动听起来最简单不就是给坐标加个偏移量吗真做起来才发现关键在于偏移量的单位是什么。屏幕上的一个像素对应源图像里多远取决于当前的缩放系数。scale1.0时屏幕移动1个像素源图像坐标也动1scale2.0时屏幕移动1个像素源图像坐标只动0.5。如果直接把用户输入的屏幕像素偏移当成源图像坐标偏移加进去你会看到同一个平移按键在1倍画面下移动速度正常放到2倍画面下移动速度明显变慢体验很割裂。我在设计里把用户的平移量定义为屏幕像素单位在硬件里换算成源坐标偏移时需要除以scale。但这又有新问题除法在FPGA里开销大而且scale还是在线可变的。与其每次现算不如把偏移量/scale这一步预先算好再以定点数形式下发到读侧逻辑。后面讲寄存器设计时会详细说这个换算这里先记住结论移动量必须结合缩放系数换算不能直接加。2. 参数寄存器的定点化设计与跨时钟同步2.1 缩放因子与锚点的定点表示缩放系数是浮点数但FPGA里做浮点运算很奢侈尤其是这种每个像素都要参与的运算。工程上用定点数就够了。我的做法是把scale用Q8格式表示也就是放大256倍存整数例如scale0.75存成192scale1.5存成384。每次输出像素更新时源坐标累加器加的是1/scale但1/scale不是整数所以我避免直接做除法改成预计算一个步长。显示一行内锚点不变化src_x的递推可以写成src_x_next src_x (1 / scale) * 2561/scale再乘以256仍然可能不是整数所以步长本身也做定点近似。精度取到1/256像素对VGA级别的显示完全够用肉眼基本看不出量化误差。我把这个预计算放在参数更新模块里乘除法只在参数变化瞬间各算一次而不是每个像素算一次。锚点我用两个正整数寄存器表示一般就设成显示区域中心比如640x480下是(320, 240)。不建议把锚点和scale塞进同一个寄存器再把运算拉长调试起来会很难受。2.2 参数更新的帧同步锁存机制这是整个第三篇里最值得说的一环。如果你直接把按键变化后的新参数接到坐标变换逻辑上立刻就能看到画面撕裂上半屏还是旧的缩放倍数下半屏突然跳到新的缩放倍数中间一条明显的分界线。我当时看到这个现象第一反应是缓存读出的数据错了排查了半天才发现纯粹是参数更新时机的问题。原因是VGA扫描是逐行进行的参数在扫描到第300行时突变第301行开始用新坐标映射整个画面就变成新旧两截。解决思路是帧同步锁存参数从按键模块进来后不直接进入坐标变换逻辑而是先存在一组影子寄存器里坐标变换逻辑使用另一组工作寄存器每当vsync场消隐到来把影子寄存器的内容一次性锁存到工作寄存器。这样参数变化只会发生在两帧之间的消隐期新的一整帧要么全部用旧参数要么全部用新参数不会出现帧内跳跃。实现上是一段很简单的代码always (posedge clk or negedge rst_n) begin if (~rst_n) begin scale_work 16d256; // 默认1.0 offset_x_work 16d0; end else if (vsync_negedge) begin scale_work scale_shadow; offset_x_work offset_x_shadow; end end注意这里的锁存条件我用的是vsync下降沿也就是一帧扫描结束进入场消隐的时刻。有些显示控制器vsync是低有效有些是高有效具体取哪个沿要对着时序图确认常见错误就是把极性搞反导致锁存时机刚好落在有效显示区内。2.3 用In-System Sources and Probes在板上直接改参数参数验证如果每次都重新编译烧录效率太低。Quartus里有个现成的调试IP叫In-System Sources and Probes可以在线读写FPGA内部寄存器不需要重新编译。我在工程里挂了两个探针一个写scale一个写offset跑起来之后对着屏幕一边改参数一边看效果整个调试体验完全不一样。用法很简单在Platform Designer或者老版本的Qsys里例化In-System Sources and Probes IP把source端口接在影子寄存器上综合烧录后通过Quartus的Tools菜单打开In-System Sources and Probes Editor就能在线输入十六进制值。source的位宽要提前设计好比如scale那组给16位offset那组给两个16位拼接。这里有个小提醒在线改的是影子寄存器改完要等下一帧锁存才能生效。你看到的画面变化会比输入值晚一拍这是正常的不是卡死了。3. 核心Verilog实现的三个关键段3.1 源坐标累加器的写法坐标变换虽然有两个维度但VGA扫描天然是先从左到右再从上到下所以我可以把src_x做成每像素累加src_y做成每行累加。假设一行有效像素是H_ACTIVE我的读侧核心逻辑大致长这样// 每像素时钟x方向步进 always (posedge pix_clk or negedge rst_n) begin if (~rst_n) begin src_x_acc 16d0; end else if (line_start) begin src_x_acc src_x_start; // 行首装入经过锚点换算的起始坐标 end else if (pixel_valid) begin src_x_acc src_x_acc x_step_q; // x_step_q 1/scale的定点近似 end end // 每行结束y方向步进 always (posedge pix_clk or negedge rst_n) begin if (~rst_n) begin src_y_acc 16d0; end else if (frame_start) begin src_y_acc src_y_start; end else if (line_end) begin src_y_acc src_y_acc y_step_q; end end行首的src_x_start和帧首的src_y_start就是在锚点基础上叠加平移量的结果src_x_start center_x offset_x_src src_y_start center_y offset_y_srcoffset_x_src是2.2节说的平移量/scale的定点结果。这样写的好处是每个像素时钟只需要一次加法完全不涉及乘法时序很稳。3.2 插值开销控制最近邻与双线性的取舍坐标变换算出来的src_x和src_y是定点数小数部分不为0意味着源图像的采样点落在像素之间。这时候就要决定取哪个像素的颜色。最省资源的是最近邻直接把小数位截掉读整数坐标对应的像素。缩放倍数不是整数时比如0.75倍最近邻会让画面产生轻微的像素跳动感边缘会有锯齿但静态画面下其实可以接受。双线性插值质量好很多代价是每个输出像素需要读四个相邻像素、做三次乘加。我最开始在720p时序下直接上双线性结果时序报告一堆红色。后来优化成两个措施只在放大倍数超过1.5倍时才启用双线性缩小场景下最近邻的视觉差异不明显复用两个行缓冲把四像素读取变成两行流水读取避免同一周期并发读四个SDRAM地址。实际效果是显示时钟从125MHz降一点就全线满足时序画质在放大档位下明显变细腻。我建议如果嫌麻烦可以先只做最近邻把整套坐标变换和参数同步跑通再回头加插值。插值是锦上添花坐标变换才是骨架。3.3 边界处理与填充色的选择当画面移动超过源图像边界时坐标变换引擎会在有效显示区内算出超出范围的src坐标比如src_x变成-100或者大于源图宽度。读缓存肯定读不到这种地址所以必须在坐标变换模块里做钳位判断。我试过两种方案。第一种是钳位到边界像素画面到边缘时最后一行像素会被拉伸观感像贴边。第二种是输出填充色比如黑色或者灰色观感像画面移出屏幕边界。两种都有道理我最后选择的是缩放时用钳位因为放大画面边缘出现黑边会很突兀移动时用填充色因为用户移动图片时能看到边界更自然。实现上就是在读取SDRAM前先判断wire out_of_bounds_x (src_x_int 0) || (src_x_int IMG_WIDTH); wire out_of_bounds_y (src_y_int 0) || (src_y_int IMG_HEIGHT); wire out_of_bounds out_of_bounds_x || out_of_bounds_y; // 越界时输出填充色而不是走SDRAM读通路这里有个容易忽略的点越界标志必须在请求发出前生效否则SDRAM已经收到非法地址可能读回脏数据。我加了一级流水寄存让地址有效信号和越界标志保持严格对齐宁可晚一个周期出数据也不能让非法地址真的发出去。4. 上板调试SignalTap实测与常见问题4.1 改参数瞬间画面撕裂的排查链路我前面提过帧同步锁存但我不是一开始就做对的这里把排查过程完整写一遍希望能帮人少走弯路。当时的现象是按下放大按键画面中间某一行突然出现跳变上半屏旧参数、下半屏新参数。我第一反应是SDRAM读地址跨时钟域有问题于是在SignalTap里挂了读地址和读数据有效信号发现读侧数据根本没有断流问题不在缓存通路。然后又怀疑坐标变换模块里有寄存器没复位干净挂src_x_acc看了很久也没看出毛病——它确实在每个像素时钟都在累加。最后实在没法子把锁存的vsync信号和参数工作寄存器一起抓到SignalTap里才看到参数工作寄存器在显示有效期内某个任意时刻发生了跳变。到这一步答案已经很清楚参数没有等到vsync再更新。修复方式就是2.2节那段帧同步锁存代码。修完再用SignalTap验证可以看到vsync下降沿之后、下一帧有效显示开始之前scale_work才变化。这样观察一次波形比看十遍代码都管用。排错经验遇到显示异常先别急着怀疑SDRAM和像素通路优先抓哪个信号在行有效期间发生了变化。显示问题大部分是同步或者时序问题数据本身往往是好的。4.2 非整数缩放下的行缓冲与带宽观察做0.75倍缩放时源图像的1.333个像素映射到1个屏幕像素意味着屏幕每3个像素要读4个源像素读频率是变化的不是均匀的。在SignalTap里观察SDRAM读请求信号你会发现请求密度一帧内有波动这会让缓存仲裁的压力忽高忽低。如果只是缩放1.0或者2.0这种整数倍带宽是均匀的SDRAM基本上没什么压力非整数倍才是考验。我实测在1280x72060Hz、0.75倍缩放场景下SDRAM读带宽峰值明显上涨但不至于溢出。如果分辨率再往上走就要考虑把行缓冲做成两行甚至三行给突发读留出余量。4.3 一张投入实测的功能验证表最后放一张我在板子上实际验证过的功能表每条都是真实跑过的场景。数值基于640x480的输入图像、1280x720的显示时序测试场景操作方式预期结果实测情况1.0倍静态显示按键切换回1.0倍图像完整无裁剪通过边缘无锯齿2.0倍放大在线设scale512画面中心放大四周溢出中心锚点稳定边界钳位正常0.75倍缩小在线设scale192全图显示留黑边通过缩小画质轻微细节损失移动至左边界连续减小offset_x画面左移出屏幕左边界出现黑色填充无拖影移动至下边界连续增大offset_y画面下移出屏幕通过底部填充色正常缩放时移动2.0倍下左右移动平移速度与1.0倍时一致换算后速度一致无跳跃动态切参2.0倍直接切0.75倍整帧切换不撕裂帧同步锁存生效无明显分界这张表后来直接成了我的回归用例每次改完代码都要跑一遍。尤其是缩放时移动这一项最容易暴露平移量换算没除以scale的问题。这个功能做完之后我最大的体会是图像缩放和移动这类需求在软件里写一个双重循环就够了但在FPGA上真正的工作量全在参数同步、坐标量化、边界判断这些周边设施上。坐标变换本身反而不是难点。后面如果再往下做可以考虑把缩放中心做成可控锚点或者把插值升级成三次卷积但骨架还是这套逆映射思路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑