FPGA的设计经常讲究的原则是自顶向下,我们也遵从这个原则。
首先通过前面两章(加上MATLAB那章)的学习,我们应该知道了设计的中值滤波要实现什么功能,接下来要做的就是明确我们设计的实现结构框架应该是什么?需要分为哪几个模块?
先说说我的思路,老规矩结合实际的设计直接上图,开局一张图,设计全靠编哈哈。
这是在vivado设计出来生成的模块组成图也是原理图,图中含有了一部分的控制信号,看起来比较繁琐,下面我简化一下,
这里可以清晰的看到实际的数据流,还可以看到组成中值滤波设计的模块主要有4大个块,分别是顶层mid_top、数据存储模块RAM_picture、3X3矩阵生成模块mid3X3、最后是矩阵滤波模块midfilter。
整个设计工作的流程:
首先图像数据输入顶层当中,紧接着数据直接进入到数据存储模块RAM.picture进行存储,然后数据经过mid3X3模块生成3x3的矩阵模板,接着将这个3x3的矩阵模板送入midfilter模块进行滤波处理,最后将滤波后的数据输出,每个模块都实现唯一的功能,这样的思路能够很好的降低设计难度和理解难度。
接下来我们先看简单的模块设计实现。
顶层的设计就比较简单,它的作用就是将各个模块连接起来。值得注意的是在设计顶层的时候,最好不要包含逻辑功能设计模块也就是always和assign功能块,只用结构化的描述方式设计也就是模块调用例化的方式,这样的方式主要也是让我们的设计有较强的可阅读性。
上代码,看一下实际的设计:
module mid_top#(
parameter picture_size =71200,
picture_row=200,
picture_col=356,
data_width =24
)(
input clk,rst_n,rst_ram,
input [data_width-1:0]data_in,
output flag,
output [data_width-1:0]data_out
);
wire [31:0] add;
wire [data_width-1:0] ram_data;
wire mid3x3_done;
wire [data_width-1:0]c1,c2,c3,c4,c5,c6,c7,c8,c9;
RAM_picture m0(
.clk(clk),
.rst_n(rst_ram),
.data_in(data_in),
.address_out(add),
.data_out(ram_data)
);
mid3x3 m1(
.clk(clk),
.rst_n(rst_n),
.flag(flag),
.data_in(ram_data),
.c1(c1),.c2(c2),.c3(c3),.c4(c4),.c5(c5),.c6(c6),.c7(c7),.c8(c8),.c9(c9),模板矩阵
.address_out(add),
.mid3x3_done(mid3x3_done)
);
midfilter m2(
.clk(clk),
.rst_n(rst_n),
.mid3x3_done(mid3x3_done),
.c1(c1),.c2(c2),.c3(c3),.c4(c4),.c5(c5),.c6(c6),.c7(c7),.c8(c8),.c9(c9),模板矩阵
.flag(flag),
.data_out(data_out) ///滤波后输出数据
);
endmodule
顶层的设计是不是非常简单,首先利用parameter函数对基础的常量定义了4个,分别是图层的大小picture_size=71200、图层的行数picture_row、列数picture_col、一个像素的大小位宽data_width。
parameter picture_size =71200,
picture_row=200,
picture_col=356,
data_width =24
接着是输入输出端口的定义,时钟是clk,它的大小在前一章的tb里面可以改,然后是两个复位信号rst_n、rst_ram,解释一下两个复位的原因,主要因为在我们生成3x3的矩阵时候,往往需要取跨行和跨列的数据组成矩阵模板,而我们输入的数据是图像的从左边到右边,从上往下,一个一个数据写进RAM,所以就需要RAM至少提前存入当前中心点像素下一行的数据,矩阵模板才能取到这个数据。为了达到这个效果,实现的方式就是让RAM_piucture先启动存图像数据。当存完第一行数据乃至第二行第三行...后,再启动后面的mid3X3和滤波模块,开始第一个模板的处理,而rst就是相当于启动信号。
输入数据端口是data_in,输出数据端口是data_out,然后是 flag信号,这个flag信号上一章也说过,代表一个像素点滤波数据处理完成了。
input clk,rst_n,rst_ram,
input [data_width-1:0]data_in,
output flag,
output [data_width-1:0]data_out
然后是连线的定义,其中add代表由mid3X3模块产生用来控制RAM输出的矩阵地址,一个一个的取出对应坐标的模板矩阵的9个数,ram_data就是对应地址读出的像素点数据,mid3x3_done代表的是模板产生成功,通知midfilter模块可以开始滤波了,而c1到c9就代表的是由mid3x3产生的矩阵的9个数。
wire [31:0] add;
wire [data_width-1:0] ram_data;
wire mid3x3_done;
wire [data_width-1:0]c1,c2,c3,c4,c5,c6,c7,c8,c9;
最后就是模块实例化调用,同时将各个模块对应端口连接起来。
这个模块的功能作用前面都描述差不多了,就是存储图像数据,然后根据相应的读地址输出像素数据。对于这个模块的设计需要注意的一点是你的图片大小,我们通过PL(programmable logic)端verilog语言方式设计的的RAM都是利用芯片的内部存储寄存器生成的,简单的说芯片实现RAM的资源空间有限,太大了就会出现这个警告,不对是错误:
所以在设计的时候我选择了356*200大小的图片,而不是像之前很清晰但很大的的图片,实在是空间有限。解决这个问题的方法有很多,但是目前我们设计的重点不在这里,就不再过多的纠结。
上这部分的代码:
module RAM_picture#(
parameter picture_size =71200,
data_width =24
)
(
input clk,rst_n,
input [data_width-1:0] data_in,
input [31:0] address_out,
output reg [data_width-1:0]data_out
);
reg [data_width-1:0] ram_data[picture_size-1:0];
reg [31:0] address_in;
integer i;
always@(posedge clk)
if(!rst_n)
begin
address_in<=0;
for(i=0;i<=picture_size;i=i+1)
ram_data[i]<=0;
end
else
begin
address_in<=address_in+1;
ram_data[address_in]<=data_in;
end
always@(posedge clk)
if(!rst_n)
data_out<=0;
else
data_out<=ram_data[address_out];
endmodule
设计的代码逻辑也很简单,对于常量和端口大家就可以根据我前面的分析步骤类推。功能逻辑主要是通过定义了一个宽度为[data_width-1:0] 深度为[picture_size-1:0]的寄存器ram_data对数据进行存取。
写:在存数据也就是写入数据的时候,只需要将数据一个一个的写进来,所以写数据的写地址是依次累加的,一个时钟进来一个数据,同时写地址加一。这里用了一个for循环对所有的寄存器初始化为0。
读:在取数据也就是读出数据的时候,需要根据输入的读地址,读取相应地址的数据就行了。
这一章主要是向大家介绍了中值滤波设计的顶层结构,然后十分详细的设计了顶层模块和数据存储模块。这两个模块都比较简单,其中的数据存储模块大家应该能直接自己写出来。顶层的话每个模块的具体信号还没实际设计过,可能写不出来。不过我们设计一般都是先把框架搭出来,具体的模块写好了再在顶层中例化调用,调整细节,所以不急,后面应该还有两个章节就可以完成整个的设计了。
打工人,奥利给!!!