代码在这:https://github.com/becomequantum/Kryon
算法演示动画:https://www.bilibili.com/video/av26067000
FPGA实现的连通域识别算法升级
Verilog是一种思维方式
先来谈一下怎样才能学好Verilog这个问题。有人说学Verilog很难,好像比C语言还要难学。有一定难度是真的,但并没有比别的语言更难学。我们刚开始学C语言的时候也觉得C语言很难,直到我们把思维方式转变过来了,把微机原理学好了,能模拟CPU的运行方式来思考问题了,就会发现C语言也没那么难了。所以这里面存在一个思维方式的转换的过程。这对于学Verilog来说也是一样的,只不过Verilog比C语言还要更加底层,我们只掌握了CPU的思维模式还不行,还需要再往下学一层“硬件电路的思维模式”,才能更好的掌握硬件编程语言。
我对学习的一个经验总结就是,如果你想要很好的掌握某一个层面的知识技能,那就必须要往下再学一个更基础的层面。比如C语言是软件层面的,理论上你不知道CPU的工作原理也能编程。但要成为高手也还是必须对更下层的微机原理、编译原理等有深入的了解。再往下一层,也就是数字电路层,对于软件编程来说已经不太重要了。但要学好Verilog,则又必须再往下学好数字电路这一层。也就是要了解什么是时序电路,组合电路,RTL,什么是触发器的建立时间和保持时间等这些重要概念。至于触发器是由什么样的门电路构成的,逻辑门的版图又是怎么画的,这样更底层的知识其实对学Verilog来说也不太重要,但要是学芯片设计,这些又很重要。
总的来说现在会硬件编程的人才少是因为之前微电子专业培养的人太少了。而计算机专业的想来用FPGA那自然会觉得Verilog难学,因为他们可能没学过数字电路这些基础知识,或者学过也早忘了。所以如果现在想学硬件编程,而以前也没有学过数字电路的基础,那就先得补充点基础知识,再通过实践训练,也能很快掌握。Verilog没有比C更难,能学好C的肯定也能学好Verilog。但这需要你再进行一次思维方式的转换和训练。如果你直接把写C语言的方式套用来写Verilog上,那就是大错特错了,它们不是同一个层面的。
多实践,表掉进概念的坑
刚开始学Verilog的时候可能会发现有些概念很难理解。比如Verilog和VHDL有什么区别?阻塞赋值和非阻塞赋值有什么区别?什么是可综合和不可综合?
初学时可能看了很多书和文章却还是搞不清楚这些概念。其实要弄明白这些概念的关键不是去调研看别人的解释,而是要自己去实践。网上写文章解释这些概念的人未必自己就搞的很清楚。比如Verilog和VHDL我就认为它们之间只是形式上有些区别,一个简洁一点一个啰嗦一些,本质上没啥区别,换汤不换药。能看懂Verilog去看VHDL也没问题,我还干过手动把VHDL改为Verilog的事情,也就是复制粘贴然后改改关键字并删掉一些东西就行了。能这样就改过来说明它们之间就只有形式上的区别。网上的那些说它们之间区别的帖子,把它们之间的区别说的似乎有很大,但我觉得这些都是在瞎扯。
阻塞赋值和非阻塞赋值。呵呵,我也不知道当初发明硬件建模语言的人为啥要整出这样一些让人费解的术语。要知道,有些学术术语如果用大白话说实际上是很简单的,那些搞研究的人估计是为了故作高深,所以要发明一些新的让人看不懂的术语来显得自己好像水平很高。所以大家千万不要被术语给唬住了。为啥要用阻塞和非阻塞这两个术语来描述对组合逻辑电路和触发器的模拟,这个我也不明白。我只知道=和<=在Verilog中是如何使用的。=是用在always@(*)块和assign语句中写组合逻辑电路的。<=只用在always@(posedge clk)块中用来写寄存器。always@(*)和assign之间没啥区别,都生成组合逻辑电路。只是有时组合逻辑比较复杂,用assign语句一句话写不完时会用always@(*)。区别就是always@(*)块中被赋值的信号要被定义成reg,而assign中被赋值的信号则必须是wire,但它们却都是生成组合逻辑电路。这就是Verilog一点不严谨的地方。不过这也没啥大问题,就是容易把初学者搞糊涂。有人喜欢把组合电路和时序电路在代码中分开来写,比如在always@(*)中写NextState = 一堆组合逻辑,然后再在always@(posedge clk)中只写 State <=NextState。不过我嫌这样写罗索,所以在我写的代码中就只会出现always@(posedge clk) 和assign。
可综合和不可综合可以直接理解为,可综合的就是用来写实际电路模块的,不可综合就是用来写仿真测试激励的。可综合的就是前面说的always@(posedge clk),always@(*),assign,再加上function块这几种语句。function块是用来描述纯组合电路的,是可综合的。比如你要在代码中经常用到求最大值这个功能,就可以写一个function [:] Max;。initial,task,for循环,#n延时,repeat(n)@等这些都只会在写测试激励时出现,是不可综合的。可综合的和不可综合的语句都能在测试激励中写。这样一说不就很清楚了。
必须了解图像处理算法的实现细节
现在调用OpenCV或Matlab中现成的图像处理函数就可以做图像处理。但这样只能说你会用这些图像处理算法,并不能说你会写图像处理算法。因为这些算法具体是怎么处理图像数据的,怎样进行计算的你并不知道。要想用FPGA做图像处理,首先你得先会写图像处理算法,不管你用什么语言写,关键是不能直接调用现成的函数,而是要自己能写出一个像素、一个像素点的处理过程。然后还需要知道你写的图像处理算法中,哪些是适合用FPGA来进行处理的。或者说用FPGA进行图像处理,和进行各种计算的优势到底再哪里。如果发现的确可以用FPGA加速,再来进行FPGA编程实现。
关于FPGA的优势到底在哪里,可以参考我在知乎上的这个回答:《FPGA图像处理的前景如何?》。和以下这两篇文章:《FPGA,你为什么这么牛?》《专访 | UIUC陈德铭教授:「万能芯片」FPGA与深度学习》。
FPGA做图像处理的技巧都在Block Ram的使用上
FPGA的最大优势就是能对数据进行并行流水线处理。而实现这一点的关键就是要用FPGA内部的Block Ram对数据进行边缓存边处理。注意,进行流水线处理是用不到DDR的,DDR没有Ram那么高的实时性,只能用来缓存大量数据。要知道FPGA接的DDR速度和容量是远没有CPU上接的DDR快的。所以要发挥FPGA并行流水线处理的优势,其所用的算法也必须并行流水线化。把CPU上的算法照搬到FPGA中,然后接个DDR当内存,这样的做法并不能发挥FPGA的优势。FPGA的优势是并行流水线。那什么样的算法可以并行流水线化呢?简单的说只需要顺序读取数据进行处理的算法都可以。比如像图像处理中用NxN的算子进行滤波,取边缘,膨胀腐蚀等。这些都是很适合用FPGA进行处理的。有些算法看似不是顺序读取数据的,但改造一下之后也可以。比如连通域识别,具体可见我的另一篇文章《FPGA实现的连通域识别算法升级》。
那么用FPGA进行NxN的算子法图像处理具体是怎样实现的呢?以3x3的算子为例,3x3的算子要同时取3行的数据,所以先要用FPGA里面的Block Ram缓存上两行的数据。当这一行数据来的时候同时去读取Ram里缓存的上两行数据,并把这3行数据一起移入3x3的移位寄存器中,然后对这3x3个寄存器中的值进行你所需要的算子运算。之后再把这新一行的数据存回Ram中,原先最上面的那一行数据就被覆盖丢弃了。简单的说流程就是这样的,N行的算子只需要缓存N-1行数据。Block Ram是FPGA里最重要的资源,所以能省则省。
具体如何写大家可以去参考我开源的代码,其实也没有多复杂,代码并不长。LineBuffer.v这个模块是负责控制Block Ram读写的,它并没有把Block Ram模块包含进去,是因为Block Ram是需要你自己用ISE或Vivado根据你的算法需要来生成的。OperatorNxN.v这个模块包含了LineBuffer.v和Block Ram模块,负责把数据移入移位寄存器并进行算子的计算。
这个Ram就相当于数组,在软件编程中我们获取数组中的数据只要写个A[n]数据就来了,不需要关心任何细节问题。但在Verilog硬件编程中,数据是怎么写入Ram中的,然后又是怎么读出来的都需要你去描述,这里面关键要处理的就是Ram的读写时序问题。所以在Verilog代码中,进行算子计算的这块代码看起来是和C语言中的差不多的。Verilog中多的就是对Ram的读写操作和移位寄存这块。要想用FPGA进行图像处理,要学会的也就是这些操作。
这几个代码大家新建个工程把它们添加进去,并自己生成同样大小的Block Ram就能跑仿真了,测试激励和测试用的文本图像文件都有。生成Block Ram时要注意选True Dual Port Ram,宽度和深度和我的代码中标注的一样。输出不需要用寄存器缓存,ISE中默认没有勾选,Vivado中勾上了,要去掉。注意FPGA中的Block Ram是有最小单位的,Xilinx 6系中是9k,7系中是18k,这就意味着如果你在7系中生成一个18x1025或19x1024的Ram就要消耗两个18K的Block Ram模块。所以生成时要注意看最后的报告,告诉你到底用了多少块。
所以也就这么几个不长的代码就把FPGA进行图像处理的基本技巧都包括了,没有多么的复杂。用这几个看似简单的模块进行组合,能实现的功能还是挺多的。