FPGA图像处理入门,选ZYNQ7020作为平台,把中值滤波、膨胀、腐蚀这三个最基础的算子亲手写一遍,是我见过性价比最高的一条路。区块里“ZYNQ7020”几乎是入门标配,PL端做像素流水线,PS端做配置和控制,既不用一开始就啃复杂的视频协议,又能把FPGA并行计算的特性摸清楚。这篇文章我就按实际跑过的一个项目来讲:基于ZYNQ7020的PL端图像中值滤波、膨胀腐蚀处理,从算法原理到Verilog实现,再到Vivado里的仿真和上板验证,全程用干活的口吻,不念PPT。
如果你现在手上有块ZYNQ7020开发板,或者正准备买,想看FPGA图像处理到底怎么入门,这篇应该能帮你少走很多弯路。读完后你会知道,所谓图像算法在FPGA里落地,核心不是背公式,而是理解数据怎么流动、窗口怎么组织、时序怎么对齐。
1. 项目到底做什么:先看清ZYNQ7020上的图像处理全貌
1.1 核心需求拆解:芯片选型、算法链路与实时性要求
这个项目表面上看只是写三个滤波算法,但真正做下来你会发现,算法本身反而是最简单的部分。完整链条是:摄像头采集图像,经过灰度转换,进入PL端做中值滤波和膨胀腐蚀,最后通过显示接口输出到屏幕。每个环节都卡着不少细节,如果不懂数据流的组织方式,代码写出来也跑不出正确效果。
选型上,ZYNQ7020具体型号一般是XC7Z020,PL端有85K逻辑单元、220个DSP、140块BRAM,这些资源对于做1080p分辨率的实时预处理是完全足够的。实时性是个关键指标,举个例子,1080p@30fps意味着每秒钟要处理约六千万个像素,如果所有处理都在ARM核上用C语言跑,即使做了优化也很难流畅;而FPGA的数据流架构天然适合这种逐像素的流水线处理,一个时钟处理一个像素,整个链路几乎没有停顿。这也是为什么很多工业相机、医疗图像设备的前端预处理都用FPGA。
项目里最容易被新手低估的,是“图像处理”和“视频时序”的区别。图像处理算法的本质是对二维数组做运算,但在FPGA里没有“数组”这种概念,数据是一个像素一个像素按行流进来的。所以做这个项目,要先接受一个思维转变:写代码时脑子里想的不是数组下标,而是数据在时钟节拍下怎么一步步流过寄存器、FIFO、行缓存。搞懂了这一点,中值滤波、膨胀腐蚀、高斯滤波、边缘检测都会变得很简单。
1.2 为什么说ZYNQ7020是入门利器:PS+PL分工
很多初学者会纠结:图像处理用DSP行不行,用ARM行不行,为什么非要ZYNQ?答案是:ZYNQ7020把软件灵活性和硬件并行性结合在了一起,特别适合图像处理这种“需要配置协议、又要高速运算”的场景。
PS端是两颗ARM Cortex-A9,主频可以到667MHz或更高,跑Linux或裸机都很方便。摄像头传感器往往需要通过I2C配置寄存器,这些工作放在PS里做非常合适。PL端是FPGA逻辑,可以把图像算法做成一个个并行模块。PS和PL之间通过AXI总线通信,最常见的方式是用VDMA(视频直接内存访问)把PS的DDR内存和PL的AXI-Stream接口打通,PS做帧缓存和显示控制,PL专门处理像素,各干各的。
对于入门项目,你完全可以把这套软硬协同的完整链路搭起来,也可以在前期让PS暂时靠边站,用PL内部ROM存一张测试图,先把图像算法验证通过,再接摄像头和显示。我个人推荐后一种方式:先PL后PS,不要一上来就搞全套,否则一旦图像不对,你根本分不清是算法错了还是链路没通。
1.3 项目整体架构:摄像头+灰度转换+算法链
这个项目完整的视频通路可以画成下面这个样子,我不用流程图,就用文字写给你看:
摄像头OV5640输出RGB数据,经过颜色空间转换模块变成灰度图,然后进入中值滤波模块去噪,再做一次膨胀或腐蚀形态学处理,最后送到HDMI显示。如果做二值化,通常还要在形态学之前加一个阈值判断模块。
几个模块之间用AXI-Stream协议或简单的valid握手信号连接。数据流是单向的,每个模块只对当前像素和周围邻域操作,天然适合流水线。灰度转换很快,原因是RGB转灰度公式 Y = 0.299R + 0.587G + 0.114B,在FPGA里可以近似成移位加法和乘法组合。中值滤波模块需要3x3窗口,膨胀腐蚀也需要3x3窗口,所以行缓存是可以复用的。处理好这个公共模块,整个项目就完成了一大半。
2. 三个图像算法的原理,用大白话讲清楚
2.1 中值滤波:9个像素排序为什么能去椒盐噪声
中值滤波的原理一句话就能说清:取一个像素周围3x3邻域里的9个像素,排序后取中间那个值,作为当前像素的输出。这个办法对付椒盐噪声特别有效,因为椒盐噪声是极端亮或极端暗的孤立点,排序后它会被排在两端,中间值自然不受影响。相比均值滤波,中值滤波还有个好习惯:它不会把边缘严重模糊掉,所以很多预处理场景里,它比均值滤波更常用。
在软件里,中值滤波可以用冒泡排序或更快的选择算法,但FPGA里不可能每来一个像素就停下来排9个数的序。常用的做法是排序网络,或者叫比较器网络。对于3x3窗口,经典策略是先对三行分别做三个数的排序,得到每行的最大值、中值、最小值;然后对三个最大值取最小值,对三个中值取中值,对三个最小值取最大值;最后再对这三个结果取中值。这个结果就是9个像素中的第5个值,也就是中值。
你可以把这个过程理解为一次“锦标赛”:先组内比,再交叉比,最后选出站在中间的那个人。硬件里都是纯组合逻辑,只要注意在比较级之间插入寄存器打拍,中值滤波器就可以做到每个时钟周期输出一个像素的处理结果,也就是所谓的全流水线。
2.2 膨胀与腐蚀:从二值图像的最大最小运算说起
形态学处理在图像处理里是个大家族,膨胀、腐蚀、开运算、闭运算都离不开它。如果你处理的是二值图像,膨胀就是看结构元素覆盖的区域里,只要有一个像素是前景,当前像素就输出前景;腐蚀则要求区域里所有像素都是前景,当前像素才输出前景。换成逻辑运算,膨胀就是“或”,腐蚀就是“与”。
如果你处理的是灰度图像,膨胀和腐蚀会变成更宽泛的定义:膨胀取邻域内的最大值,让图像变亮、亮区域扩张;腐蚀取邻域内的最小值,让图像变暗、暗区域扩张。很多人不理解凭什么“取最大值”就是膨胀,你想象一下,一张黑白图像里有一个白色小方块,如果每个像素都取周围3x3窗口里的最大值,白色区域周围的黑像素会被刷成白色,白方块自然就变大了,这就是膨胀。反过来取最小值,白区域边缘会被黑像素侵占,看起来就是缩小了,也就是腐蚀。
这个项目里我们用的是3x3全1结构元素,也就是说只看上下左右加对角一圈。3x3窗口数据一旦准备好了,膨胀腐蚀的计算就只是求9个数的最大值或最小值,和中值滤波里的排序网络相比甚至更简单。所以你会发现,三个算法共用同一个窗口,真正难的部分其实只有行缓存和窗口组织。
2.3 3x3窗口和行缓存,硬件图像处理的真正门槛
为什么很多人写FPGA图像处理,算法背得滚瓜烂熟,一写代码就懵?大概率是卡在“怎么拿到3x3窗口”这一步。由于图像数据是逐行流进来的,你想访问当前像素上方那一行的同列数据,就必须把数据延迟一行。一行有多少像素,就相当于延迟多少个时钟周期,这个延迟功能可以用行缓存来实现。
行缓存的硬件本质是FIFO或双口RAM,深度等于图像宽度,宽度等于像素位宽。拿两个FIFO串联起来,第一个FIFO存第一行数据,第二个FIFO存第二行数据,当前输入像素作为第三行。这样在任何一个时钟时刻,你都能同时拿到“当前行、上一行、上上一行”同一列的数据。再把这三行数据分别打三拍,横向也拉开三个像素,就组成了完整的3x3窗口。
我用一个更直白的类比:你看字的时候,眼睛一次只能落在一个字上,但如果你想看这个字前后左右各一个格子的字,就得把几行字同时摊开在桌上。行缓存干的活就是把桌上摊开的几行字对齐,窗口寄存器组则是用一排手帮你同时抓到9个格子。理解了这一步,后面读任何图像处理IP的源码都不会再晕。
3. 两种开发路线实操:HLS快速上手与Verilog深入底层
3.1 环境准备:Vivado版本、开发板、摄像头准备
做ZYNQ7020开发,软件上首选Vivado,版本只要不是特别老就行,我习惯用Vivado 2019.1以上版本,对Vivado HLS的支持也比较稳定。板子方面,市面上黑金、正点原子、米联客、ZYNQ官方开发板都行,核心都是XC7Z020,外设和引脚可能有区别,只要照着对应厂商的例程改约束文件就能用。
摄像头我建议用OV5640,因为资料多、分辨率高,虽然MIPI接口在入门时稍微有点麻烦,但这类板子一般都会引出DVP接口,OV5640的DVP模式也能跑,很多配套例程都是直接支持DVP。如果你只想验证图像算法,手头没摄像头也不要紧,完全可以生成一张测试图,用Python脚本转成十六进制文本文件,再用$readmemh读进仿真或ROM,效果一样能说明问题。显示部分用HDMI或者VGA都行,重点是看到输出图像的直观效果。
需要注意,ZYNQ7020涉及到PS端时,要生成硬件工程导出xsa文件,再在Vitis里写ARM程序。但入门阶段可以把PL当独立FPGA用,不一定要先跑PS。这样能省掉很多启动流程上的麻烦,让注意力集中在图像算法本身。
3.2 路线一:用Vivado HLS把C算法打包成IP
如果你C语言基础好,又急于先看到效果,我建议先用Vivado HLS趟一遍。HLS可以让你用C/C++写图像算法,然后综合转换成RTL,生成IP后在Vivado里调用。它特别适合算法原型验证,比如你想对比中值滤波和均值滤波的效果,用HLS写会快很多。
HLS里最基本的图像处理接口可以用hls::Stream加上hls::Mat,下面这段代码示意了核心结构:
#include <hls_stream.h> #include <hls_video.h> #define MAX_WIDTH 640 #define MAX_HEIGHT 480 void image_filter(hls::stream<ap_axiu<8,1,1,1>>& src, hls::stream<ap_axiu<8,1,1,1>>& dst, int rows, int cols) { #pragma HLS INTERFACE axis port=src #pragma HLS INTERFACE axis port=dst #pragma HLS INTERFACE s_axilite port=rows bundle=control #pragma HLS INTERFACE s_axilite port=cols bundle=control #pragma HLS INTERFACE ap_ctrl_none port=return hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC1> img_in; hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC1> img_median; hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC1> img_out; #pragma HLS STREAM variable=img_in dim=1 depth=512 hls::AXIvideo2Mat(src, img_in); hls::MedianBlur(img_in, img_median, 3); hls::Dilate(img_median, img_out, hls::Mat<3,3,unsigned char>()); hls::Mat2AXIvideo(img_out, dst); }这段代码调用了HLS库里的MedianBlur和Dilate,非常省事。但要注意,这也只是演示,实际工程中HLS库的MedianBlur只能对Mat类型操作,需要引入hls_video头文件。更常见的做法是自己写排序网络,像这样:
unsigned char median9(unsigned char w[3][3]) { unsigned char tmp[9]; // 复制窗口数据 for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) tmp[i*3+j] = w[i][j]; // 简单冒泡排序,示意用 for (int i = 0; i < 8; i++) for (int j = i+1; j < 9; j++) if (tmp[i] > tmp[j]) { unsigned char t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } return tmp[4]; }HLS综合这种代码时,会自动把循环展开成排序网络。不过HLS的自动流水需要多次pragma调优,新手可能对资源优化比较头疼。我的建议是:先用HLS快速知道算法效果对不对,再花时间用Verilog把它手写出来,两条路结合,既保持学习动力,又能真正掌握底层逻辑。
3.3 路线二:纯Verilog实现行缓存、排序网络和形态学
想要真正理解FPGA图像处理,纯Verilog实现是绕不开的。我下面把核心模块的结构说清楚,代码不追求完全可综合,但思路是完整可迁移的。
模块接口长这样:
module image_filter #( parameter DATA_BITS = 8, parameter LINE_WIDTH = 640, parameter LINE_NUM = 3 )( input wire clk, input wire rst_n, input wire i_valid, input wire [DATA_BITS-1:0] i_data, output wire o_valid, output wire [DATA_BITS-1:0] o_data );行缓存是最容易出问题的地方。我用两个FIFO串行,第1个FIFO输出line0,再接到第2个FIFO输出line1,输入像素本身算line2。为了让3行数据同时对齐到同一个列位置,还需要在FIFO输出后再打一拍或者打两拍,具体根据你选择的FIFO模式来定。FIFO的读使能不能乱拉,必须和输入valid同步。
wire [DATA_BITS-1:0] line0_data, line1_data, line2_data; assign line2_data = i_data; line_fifo #(.WIDTH(DATA_BITS), .DEPTH(LINE_WIDTH)) u_fifo0 ( .clk(clk), .rst_n(rst_n), .wr_en(i_valid), .din(i_data), .rd_en(i_valid), .dout(line0_data) ); line_fifo #(.WIDTH(DATA_BITS), .DEPTH(LINE_WIDTH)) u_fifo1 ( .clk(clk), .rst_n(rst_n), .wr_en(i_valid), .din(line0_data), .rd_en(i_valid), .dout(line1_data) );这里要注意,真实项目中FIFO的读数据会有延迟,所以需要再补两个移位寄存器来对齐窗口。比如你可以把line0_data、line1_data、line2_data分别连到三个8位移位寄存器链上,寄存器的二级输出就构成窗口的横坐标。为了便于理解,我直接用寄存器组表示窗口:
reg [DATA_BITS-1:0] win_reg [0:2][0:2]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) win_reg[i][j] <= 0; end else if (i_valid) begin // 每行向右移一列 for (int i = 0; i < 3; i++) begin win_reg[i][0] <= line? // 这里要根据实际连线 end end end这写成抽象示意可能不够严谨,但思路是:三行数据每来一个像素就整体右移一列,最右边的数据丢弃,窗口被不断刷新。如果你想复刻,建议自己画一个时序图,把每一行的tap点和寄存器链对齐。
排序网络可以单独封装成一个sort3模块,对三个数求最大、中、最小:
module sort3 #(parameter W = 8) ( input [W-1:0] a, b, c, output [W-1:0] max, mid, min ); assign max = (a > b && a > c) ? a : (b > c ? b : c); assign min = (a < b && a < c) ? a : (b < c ? b : c); assign mid = a + b + c - max - min; endmodule用mid = a + b + c - max - min虽然简单,但要当心加法进位面积偏大。入门做个教学实现没问题,真正做产品我建议全比较器打拍实现。对3x3窗口求中值的整体拼装,就是先三行分别求max/mid/min,再对三个max求一次sort3取min,对三个mid求一次sort3取mid,对三个min求一次sort3取max,最后再取这三个值的中间值。
形态学处理就比较直接。如果做灰度图像的膨胀,就是取9个窗口像素的最大值:
wire [DATA_BITS-1:0] dilate_out; max9 #(.W(DATA_BITS)) u_max9 ( .dout(dilate_out), .a(win_reg[0][0]), .b(win_reg[0][1]), .c(win_reg[0][2]), .d(win_reg[1][0]), .e(win_reg[1][1]), .f(win_reg[1][2]), .g(win_reg[2][0]), .h(win_reg[2][1]), .i(win_reg[2][2]) );腐蚀则是取最小值,逻辑上只是把求最大值的比较器改写为反向而已。如果做二值形态学,那更省事,直接用逻辑或和逻辑与,随便一个always块就能完成。
3.4 时序、复位与有效信号的编写要点
这个项目里最容易翻车的不是算法而是时序。我在最开始把所有控制信号都设计成“valid拉高时数据有效”,也就是简单的valid-only握手,暂时不引入ready。这样从摄像头模块到处理模块再到显示模块,一路都用valid作为使能,窗口寄存器和行缓存全部以valid为前提条件。
复位信号也很讲究。ZYNQ的PL端推荐用不复位的流水线设计,因为图像数据量太大,如果一复位就把寄存器全清零,图像会出现临界条件。通常只在配置完成后复位一次,数据通路本身用valid信号做门控,而不是频繁置位复位。这个习惯从一开始就养成,后面调试会轻松很多。
另外,三个连续的算法模块会增加延迟,比如行缓存延迟2拍、中值滤波打拍3拍、膨胀腐蚀再打拍3拍。最终的o_valid必须与o_data同步,否则输出图像会出现整体偏移。我的做法是专门写一个“延迟链模块”,用移位寄存器把valid延迟相同拍数,保证数据有效信号精准对齐,这个模块虽然简单但极其重要。
4. 验证调试:仿真、在线抓波、上板实测一条龙
4.1 用Testbench和BMP图像做数据驱动仿真
我把这个项目里最实用的仿真方法分享一下:不要只在仿真里给几个随机数,那样根本看不出滤波效果。正确做法是生成一张带噪声的标准测试图,比如在Lena图或者棋盘格上叠加椒盐噪声,再用Python把灰度图像数据写成hex文件,在Testbench里用$readmemh读进一个内存数组,然后按像素时钟逐个喂给模块。
Python转换脚本的核心逻辑非常简单:
from PIL import Image import numpy as np img = Image.open("lena_gray.bmp").convert("L") arr = np.array(img, dtype=np.uint8) with open("input.hex", "w") as f: for v in arr.flatten(): f.write(f"{v:02x}\n")Testbench里可以这样读取:
reg [7:0] frame_mem [0:640*480-1]; integer idx; initial begin $readmemh("input.hex", frame_mem); idx = 0; end always @(posedge clk) begin if (i_valid) begin i_data <= frame_mem[idx]; idx <= idx + 1; end end处理完的数据再用$fwrite写到文件里,用Python读回来对比原图,或者直接转成BMP查看。这样不用上板,就能直观看到中值滤波是否把噪声点去掉了,膨胀腐蚀是否让亮区域产生了变化。这一步能帮你节省大量调试时间。
4.2 ILA在线逻辑分析仪排查像素错位
仿真没问题不代表上板没问题,因为真实摄像头时序、跨时钟域、复位释放时机都会出幺蛾子。排查硬件问题最好用的工具就是ILA,在Vivado里给处理模块打上标记,综合后添加ILA核,抓取你关心的信号。
我经常抓的信号有三个:输入valid、行缓存输出、最终输出valid。如果发现输出valid出现毛刺,大概率是行缓存读使能和输入valid没有对齐;如果数据全是0或固定值,大概率是复位导致窗口寄存器被清零了。ILA里还能用tlast信号判断一帧有没有传播正确,如果你用的是AXI-Stream接口,帧尾信号错位会导致画面撕裂。
ILA调试有个小技巧:不要每次都把整个Vivado工程重新综合,可以在综合后用Set Up Debug的方式指定要抓的信号,然后重新实现。如果你的网表已经生成,这个操作比重头综合快很多。
4.3 资源占用与处理性能实测
我基于一个640x480灰度图做实测,处理链只包含中值滤波加膨胀腐蚀,资源占用大致如下:
| 模块 | LUT | FF | BRAM | DSP |
|---|---|---|---|---|
| 行缓存(2个FIFO) | 30左右 | 20左右 | 4 | 0 |
| 3x3窗口 | 约150 | 约280 | 0 | 0 |
| 中值排序网络 | 约260 | 约180 | 0 | 0 |
| 膨胀/腐蚀运算 | 约180 | 约120 | 0 | 0 |
| 合计估算 | 700左右 | 700左右 | 4 | 0 |
ZYNQ7020的PL资源完全没压力。性能上,这个数据通路全流水后,单像素吞吐率能做到一个时钟一个结果,在Vivado里综合频率跑到100MHz以上很容易。如果处理1080p@30,像素时钟大约为74.25MHz,所以你还会有一半以上余量,后续继续加高斯滤波、Sobel边缘检测都绰绰有余。
当然,如果你用的是HLS自动生成的IP,资源可能会比手写Verilog多20%到30%,但开发速度快很多。这就是一个取舍问题,我自己的排序是:项目时间紧用HLS,学习底层或者对资源敏感用Verilog。
5. 这些坑我替你踩过了:问题排查与避坑速查
5.1 图像错行:FIFO读写控制要跟valid走
很多人第一次跑起来,画面不是全黑就是像屏闪的马赛克,里面常常有斜线或错位。我遇到最多的原因就是FIFO的读使能没有和输入valid保持同步,导致读到了空数据或者旧数据。行缓存FIFO必须只在输入有效时读,并且读写使能最好都是同一个valid。如果你用的是Xilinx FIFO IP,要看清楚它的读数据延迟到底是“First-Word Fall-Through”还是标准模式,这决定了你需要额外打几拍来对齐。
调试方法也很简单:在ILA里同时抓FIFO空信号、读使能、输入valid,看看空信号在读使能拉高之前是不是已经变低了。如果读使能拉高时FIFO是空的,数据自然就错了。
5.2 中值滤波变成“模糊滤镜”的排序陷阱
我见过有初学者把中值滤波做成了均值滤波的效果,画面变糊但没有去掉椒盐噪声。原因很可能是排序网络只排了部分数据,没有真正取到9个数的中间值。举个例子,如果你只对每一行排序,然后取三行中值的中间值,这在某些像素分布下结果并不是真正的9点中值。
正确的排序网络一定要按照“三行排序、三列交叉排序、再取中间值”的顺序去做。如果担心理解偏差,可以先用Python写一个参考模型,把任意9个输入值跑一遍,和你的Verilog仿真结果对照,几个周期就能发现问题。
5.3 边界处理的两难:丢弃一行还是复制边缘
3x3窗口在图像边界处会越界,这时你有两个选择:一是直接丢弃最外圈一行和一列,输出分辨率从640x480变成638x478;二是复制边缘像素,让窗口在边界处依然有数据。入门项目我建议直接丢弃,原因很实在:实现简单,而且丢一圈边缘对视觉影响很小。为了控制输出坐标,你可以用行列计数器来判断当前像素是否处于边界区域,如果处于边界就不拉高输出valid。
如果你想做更完整的边界扩展,原理也不复杂,只是需要额外判断:当窗口中心在边界时,把缺失的窗口数据用最近的边缘像素补齐。这在HLS里可以调用边界处理函数,Verilog实现要麻烦一些,不是入门阶段必须掌握的。
5.4 时序收敛不了:排序网络必须打拍
中值排序网络如果全用组合逻辑直接算,理论上一个时钟能出结果,但9个比较器的组合链路很长,频率稍微提上去就会出现时序违例。解决办法很粗暴:在排序网络中间插入寄存器,把单拍大组合逻辑拆成两拍或者三拍流水。
代价是输出延迟多了几拍,但只要把valid信号跟着延迟同样拍数,数据依然完全对齐。这个“用valid延迟链应对流水线延迟”的思路贯穿整个FPGA图像处理,几乎每个模块都要用,建议专门封装一个小模块:
module delay_valid #(parameter N = 4) ( input wire clk, input wire i_valid, output wire o_valid ); reg [N-1:0] valid_shift; always @(posedge clk) begin valid_shift <= {valid_shift[N-2:0], i_valid}; end assign o_valid = valid_shift[N-1]; endmodule不要小看这个模块,它能帮你少掉一大半对不齐的烦恼。
5.5 HLS和Verilog模块对接时的握手问题
如果你采用HLS生成IP,再在Vivado里和Verilog模块混用,最常遇到的坑是AXI-Stream握手信号不匹配。HLS IP的AXI-Stream接口包含tdata、tvalid、tready、tlast四个主要信号,tready表示下游可以接收数据,tvalid表示数据有效,一级一级传播时可能因为tready拉低导致数据卡住。
新手最容易犯的错误是只连接tvalid和tdata,忽略tready,结果HLS IP根本跑不动。如果下游模块不支持ready信号,最简单的办法是在HLS IP后面加一个简单的AXI-Stream FIFO,用它隔离握手协议,保证数据能连续流动。这个FIFO在Vivado IP Catalog里直接有,配置很方便。
最后再分享一点我自己的实践体会
做了几次ZYNQ7820图像处理项目之后,我最大的体会是:这类项目真正的难点不在数学,而在数据流的“对齐感”。你写Verilog时脑海里要有一张时序图,清楚每个valid在每个时钟沿会在哪个位置,而不是等到仿真不对了再去猜。建议你拿到任何图像处理算法,先想清楚它需要几个窗口、几个行缓存、会产生几拍延迟,再动手写代码。
另外,如果你同时有软件和硬件基础,可以尝试把PS和PL协同起来:PS端用OpenCV做调试对比,PL端做实时处理,把两边的结果放在同一块屏幕上对比,会非常直观。这也是我常用的工作流,以后你从简单滤波做到人脸检测、目标追踪时,这套“PS+PL协同验证”的方法依旧通用。
先别急着追求复杂算法,把中值滤波、膨胀、腐蚀这三个算子彻底吃透,你已经比大多数FPGA新人强了。