赞
踩
上个视频FPGA图像处理的一些基础知识,FPGA是如何实现最高实时性的?相比于GPU的优势在哪?https://www.bilibili.com/video/BV1Ba411k7FA讲了用FPGA做图像处理的一些基础知识。这个视频开始讲具体的例子,比如最简单的直方图统计该如何用FPGA实时的实现呢?
上上个视频:《FPGA图像处理中二值算子的一些妙用https://www.bilibili.com/video/BV1WY411L7Bd》里介绍的“无限次元”这个小软件中就有画图像直方图的功能。这个软件的代码已经传到了上图所示的库中。
becomequantum (becomequantum) · GitHub 代码在这
统计图像直方图数据的C#代码非常简单,除了遍历图像数据的双循环之外,就只有上图中画红框里的一行代码。其中“R谱”是长度为256的整型数组,因为一般图像数据都是8位,值为0到255。这个数组就是用来统计这256种图像亮度数值在一副图像里总共出现了多少次。所以它的代码就是用图像的某个亮度值做为这个统计数组的索引,先把它读出来,加上1,再写回去。也就是每遍历一个像素,就进行了一次对统计数组的“读——加1——写回”操作,在CPU的程序里这个操作的代码就只需上图中的一句话,但要用FPGA实现就没这么简单了。
因为在软件代码中,“变量”、“数组”这些都是逻辑上的概念,我们写软件代码只需要编写对它们的操作,至于代码在具体执行时,数组所需的内存是怎么分配的?变量被对应到了CPU里的哪个寄存器?用i做为索引去读数组时,这个数又是如何从内存被读到CPU里的?等等这些问题我们在写软件代码时完全都不用考虑。所以软件代码实现“read modify write”操作就只需这一句。
但用FPGA这个可编程硬件去实现时问题就不一样了,我们要写代码直接操作硬件,那变量、数组在FPGA硬件上分别对应着啥呢?这个问题可能都会让初学者懵圈一下。上面这个统计数组在FPGA里一般用Block Ram来实现。也就是数组对应着Ram,数组的索引就对应着Ram的读写地址。要用FPGA实现这个功能,就必须先了解FPGA中Block Ram的生成和使用方法。
上图是赛灵思Vivado里Block Memory Generator的基础选项界面,其中Memory Type要选True Dual Port Ram。这个双口Ram是啥意思呢?为啥实现这个统计功能必须要用双口的呢?真双口Ram有两个独立的读写端口可以同时对Ram里的数据进行读写,后面会讲到为什么必须要用这种双口的。
那如果Ram能有两个端口同时读写,是不是就会产生一个问题:如果两个端口在某个时钟周期读写数据的地址相同时会发生什么?如上图所示。在这种情况下双口Ram的行为有三种可以选择。其中“Write First”和“Read First”是主要的两种。“Read First”的意思是出现这种情况,读出来的是该地址已存的数据,“Write First”模式下读出来的将会是刚写的数据。
这个统计功能是会遇到上述情况的,要选默认的“Write First”模式,原因后面会说。另外在上面这个页面上,“Primitives Output Register”选项要取消掉,勾上了数据读出来会多延迟一个周期,这会让代码更复杂,而本视频讲的是最简单的实现方式。
端口的位宽设为18,这意味着计数的最大值为2的18次方,可能会不够用。深度设为1024,不是256就够了吗,为啥要1024呢?那是由于设为256用掉的同样也是一个18k BRam,因为Block Ram是有固定大小的、论个算的,要用就是整数个。不是只用四分之一就能省下四分之三的。所以深度干脆设为1024,这样如果图像数据是10位时也能用。
最后在其它选项页,要把上图红框里的选项勾上,值默认为0即可。这样可以让Ram里所有初值都为0。在FPGA里寄存器和Ram都是可以赋初值的,FPGA上电加载比特文件时这些初值都会被加载进去。所以FPGA代码里可以不写:用一个外来的复位信号复位寄存器这样的代码,在设计电路板时也不用整一个引脚做为复位信号的输入。
Ram设置好之后直接来看代码,如上图所示,除掉前面的输入输出和后面的模块例化代码,实现统计核心功能的代码也就红框里的三行,这就是最简单的写法。不过这和C sharp代码比已经看起来复杂了很多,而且对新手并不友好。如果对Block Ram的时序理解的不够清楚的话,写代码很容易出错。我最初写Block Ram读写代码时也经常犯错,在经历了N次犯错和调试后,现在写时才能做到思路清晰,很少出错。
代码的仿真结果如上图所示。在仿真界面里,我们是可以通过直接查看仿真结束后Ram里的数据来看看结果对不对。先得像上图这样找到“iCountArray”这个Ram实例存数据的变量在哪,看来就是这个“memory”了,用上面的搜索功能可以快速找到。可以看到它0地址里的数是2,1地址里的是3,它的意义就是,在DataEn数据使能信号升起的这段时间里,PixelData中出现了0两次,1三次。我们可以对照右边的波形图数一下,看看是不是这样。先来数0,的确是出现了2次。再来数1,的确出现了3次。再往后2出现了1次。3出现了1次。4出现了2次。5出现了3次。6出现了4次。由此可见Ram里的统计数据和测试波形是能完全对上的,这说明上面代码的功能是正确的。
再来回头看代码,模块的输入除了时钟之外就三个,VSYNC在VGA时序里是垂直同步信号,其实就是示意一帧图像开始和结束的信号,我喜欢管它叫帧有效信号。把它放在这里是因为统计完一帧的数据后要进行数据的读出和清零工作,而这项工作是要趁着帧有效信号没有使能时进行的。这部分代码比较简单,所以上面没写,整个计数器在VSYNC降下后计数,并用它作为Ram地址把数据读出来,同时清零即可。注意,不一定非要写状态机去实现。
DataEn是数据使能、有效信号。使能信号是硬件电路里的特色常见信号,主要用于示意数据信号在何时有效。在图像数据里,DataEn一般会在一行数据的开始升起,一行有多少个像素就会升起多少个时钟周期。PixelData就是对应的像素数据,在上面的C-Sharp代码中可以看到,像素数据会被做为索引去读统计数组里的统计数据,读出来,加上1,再写回去。统计数组在FPGA里变成了上面代码中的“iCountArray”这个双口Ram,那PixelData这个信号就应该被做为Ram的读地址,用来读出Ram中已有的数据。所以在代码里PixelData直接被连到了“iCountArray”A端口的地址上。那么A端口的作用就是读数据,写功能就用不上了,所以写使能和写数据这两个输入都被置零了。A端口的输出连到Cnt信号上,被加1之后就连到了B端口的写数据输入端口,这就是在把加1的数据写回。
由此可见Ram好像也必须有两个端口才能完成这个任务啊,A端口用于读之后就不能再用于写回了,因为每个周期都有新的数据来,而且值可能都是不同的,所以每个周期都要去读数据,自然就没有空余周期再让它写数据。写数据就只能用B端口,而且每个周期也都有数据要写回。为啥Ram要有双端口就是这个原因,因为有些任务没有独立双端口就完成不了。
统计Ram用于读出的A端口信号都连好了,用于写回的B端口数据信号也连好了,接下来要理解的就是B端口的写使能和写地址信号该怎么弄。把某个地址的数据读出来加1后写回的还是原地址,所以B端口的地址应该和A端口是一样的,都要连PixelData,但时序上会有不同。因为最基本的Ram读数据时序,如果在配置时没有勾选任何输出寄存的话,数据的读出是要比相应的地址给出是晚一个周期的。数据读出后还加了1,但用assign写的加1并不会导致再延迟一个周期。所以写回的数据是比读它的地址延迟了一个周期,那么写回的地址就相应的把读地址延迟一个周期就可以了。所以会看到代码中B端口地址连的就是PixelData延迟一个周期之后的信号,那么B端口的写使能信号自然也要比PixelData的使能信号DataEn延迟一个周期。
代码就解读完了,再来看一下仿真波形,如上图所示。Cnt计数信号就是douta,计数加一信号就是dinb,它只是加了1,时序上并没有延迟。PixelData做为地址读出的信号在前几个周期都为零,因为Ram里的初始数据都是零。直到粉色箭头所示的地方,当使能信号升起后第五个周期数据为2时,下一个周期读出的数据才变为1,这是因为2之前已经出现了一次,计数已经加了1。从这里我们可以看出Ram读数据的时序,也就是数据的出现要比相应的地址给出延迟一个周期。
当同一个数连续出现时,就会出现两个端口读写地址相同的情况,如上图蓝色箭头所示,4出现的第二个周期就是这个情况。这时就体现出Write First模式的必要性,因为此时如向下的红色箭头所指,Ram里地址4上的数据还是0,没有来得及更新为1。所以如向右上的红色箭头所示,此时需要读出的就是这个周期刚要被写进去的数据1,而不是Ram现存的数据0。再看红色向下箭头的右边,Ram里地址4的数据到下一周期才变为1,由此可见写Ram时,Ram里存的数据发生变化也是要延迟一个周期的。
用Verilog编程硬件相对于写软件代码来说,麻烦就麻烦在需要考虑具体硬件信号的产生和连接,以及它们的时序。所以“硬件编程”其实不是在“编程”而是在用写语言的方式画电路图。学写Verilog一定要把“我在写程序”这个观念转变为“我其实是在画电路图”,不然遇到一些问题你可能就无法理解。比如在Verilog里会遇到“multi driver”多重驱动这个报错,如果你用软件编程的思维,就会觉得我先写A = 0,把A赋值为零,再写A=1,这能有啥问题呢?但你用画电路图的思维一想,写A=0不是在赋值,是把信号A连到地,A=1是把信号A连到电源。A显然不能同时连到电源和地,这样不就短路了吗?所以如果你在代码里没用if else写清楚A何时为0,何时为1,只是先写assign A=0,再写assign A=1。那这就会犯多重驱动这个错误,因为一个信号不能同时被两个信号驱动,想想也好理解,除了会造成短路之外,同时给它连两个信号,你到底要让它等于谁呢?
另外写Verilog代码还要学会流水线思维。在上面这个例子中,每个时钟周期都会来一个新数据,对每个数据都要进行读出加一再写回这两步操作,每步操作都需要一个周期。那这就有问题了,每个时钟周期要处理一个数据,但处理一个数据却要两个周期,这明显时间不够用啊?这时就要用到流水线思维了,把所需的硬件资源加倍,在本例中就是要用两个端口,一个端口读,一个端口写,形成读写流水线,这样对于一个数据来说,读写的确需要两个周期,但读写这两条流水线是同时在工作的,加起来就是一个周期能完成一次读写操作,是能满足每个周期处理完一个数据的要求的。
本文通过直方图数据统计这个例子讲了双口Block Ram的时序和使用方法,在FPGA图像处理中,Block Ram是会经常用到的,所以学会它的使用是入门必需的。其实我之前一直没写过直方图统计的FPGA代码,上面的代码是为了做这个视频刚写的。因为直方图统计要一帧之后才有结果,而且是整体信息的统计,对于图像的实时处理并没有啥用。但在有些场合它是有用的,比如在相机里都能查看照片的直方图,这能帮助你了解照片的整体曝光情况,能更清晰的看出是否有过曝或欠曝光。当你设计相机类产品时,如果想要做到能自动调整曝光时间,那就要通过直方图来看当前的曝光情况,然后进行反馈调整。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。