第二阶段 项目实践
第一章 FPGA设计思想
本文档编号:001500000080
需要看对应的视频,请点击视频编号:001500000089
1、本节是讲解详细讲解了如何正确的看懂 信号波形
1.1 看波形图的方法
本书的D触发器一章、怎么看FPGA波形一节中,讲述下如何看信号的波形,读者只需要记住一个规则:时钟上升沿看信号,看到的是信号变化前的值。有兴趣的读者,可以返回再细看那一章内容。
图 111
例如图111,在第4个时钟上升沿看信号a,则是看到a的值为1(变化前);看信号c,看到c的值为0(变化前);在第5个时钟上升沿看信号b,看到b的值为1,看信号c,看到c的值为1。
图 112
例如图112,在第5个时钟上升沿看信号dout,其值为1;看信号cnt,其值为1,而不是2。
以上就是看波形的方法,该方法的由来,可以参考本书的D触发器一章、怎么看FPGA波形一节的内容。当然,使用该方法是有前提的:所有信号都是同步信号;波形是理想的波形。
1.2 至简设计法的四种设计类型
学习FPGA,最关键的是学什么?
笔者发现,有部分读者将学习的重点放在接口知识、算法原理等理论知识层面。例如,学习串口的时候,把学习重点放在:什么是串口、串口有什么优势、什么时候用到串口等理论。至于串口代码,也是借鉴和模仿,原代码是什么结构,自己设计的代码也要有这种结构,原代码有什么信号,自己也要有这些信号等。如果该串口功能稍加改动,则陷入完全无从下手的状态。
每个工程师都有自己的代码风格,甚至有些工程师今天的风格和昨天的风格都会不一样。网络上的代码自然也是良莠不齐,一味靠模仿,能成长为高手,那就奇怪了。
明德扬认为,学习FPGA应该是为了提高自己的设计开发能力。学习串口,不是为了懂得这个串口,而是通过这个串口例子,学习其设计思路和方法,以便应用到其他接口上。
明德扬认为,设计思路和方法,不应过多过杂,而是形成自己的一套模式。今天学少林铁头功,明天学武当太极,后天学华山剑法,先不说精力问题,能达到精通状态吗?
明德扬研发了一套通用的设计方法:至简设计法。
至简设计法从宏观上,适应所有的功能设计需求。例如,无论是什么功能,我们都先将其转化成需求波形。然后在此基础上设计模块架构;在模块架构基础上设计信号。这步骤都是通用的、是固化的。
至简设计法在微观上,制定了实用的规范。详细到什么时候添加信号;怎么添加信号;添加信号名字是什么等,我们都做了详细的规定。
大部分的FPGA设计,明德扬将其归类下面讲述的4种类型。无论多复杂的功能,都是这4种类型的变种。下面通过4个典型案例的设计,来讲述至简设计法。
1.2.1 至简设计法设计类型1
案例1:当收到en=1后,dout产生一个宽度为10个时钟周期的高电平脉冲。图113是功能波形图。
图 113
根据看波形规则,在第3个时钟上沿的时候,看到en==1,根据功能要求,上升沿之后dout就会变为1。10个时钟周期后,即第13个时钟上升沿时,dout将变为0。
推理1:从功能要求中,看到数字10,我们就知道要计数,要使用计数器。
推理2:10个是指dout==1的次数为10个时钟周期,所以该计数器数的是dout==1的次数,因此看到dout==1时,计数器就会加1。
此外,明德扬还制定了计数器要遵守的原则。
原则1:初值一定为0。复位后,计数器一定要为0。
原则2:数到最后一个时,要及时清0。
根据上面2个推理和原则,补充计数器信号cnt,更新后的波形如图114。
图 114
从功能要求和波形图,我们确认,计数器cnt是对dout==1进行计数,并且一共数10个。为此,在GVIM编辑器中输 入“Jsq”并回车,将出现图115的代码。
图 115
在第13行,输入dout==1,在第14行代码中,输入10-1,这样就完成了计数器设计,如图 116。
图 116
add_cnt表示:计数器cnt加1条件。
end_cnt表示:计数器数到最后一个,也称之为结束条件。
图116第1~第11代码功能:时钟上升沿时,如果计数器加1条件有效,并且是数到最后一个,则计数器清零;如果计数器加1条件有效,但不是最后一个,则计数器就加1;其他时候,计数器就保持不变。
那么加1条件,即add_cnt是什么呢?在第13行进行了定义。该行代码表示,dout==1就是计数器的加1条件。
那么结束条件,即end_cnt是什么呢?在第14行进行了定义。该行代码表示,数到10个就结束。其中我们关注的是那个数字10,而-1是固定的格式。
add_cnt && cnt==10-1,含义是表示“数到第10个的时候”,add_cnt && cnt==x-1表示“数到第 x个的时候”。记住这个规则。end_cnt==1也表示数完了。
设计好计数器cnt后,我们就可以设计输出信号dout了。仔细分析dout,该信号有两个变化点:变1和变0。我们分 析原因,dout变1是由于收到en==1;dout变0,则是数到了10个或者是数完了。所以综上所述,dout的代码是:
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0 ; end else if(en==1) begin dout <= 1 ; end else if(end_cnt) begin dout <= 0 ; end end |
至此,我们完成了主体程序的设计,接下来补充module的其他部分。
将module的名称定义为my_ex1。并且我们已经知道该模块有4个信号:clk、rst_n、en和dout。为此,代码如下:
1 2 3 4 5 6 |
module my_ex1( clk , rst_n , en , dout ); |
其中clk、rst_n和en是输入信号,dout是输出信号,并且4个信号都是1比特的,根据这些信息,我们补充输入输出端口定义。代码如下:
1 2 3 4 |
input clk ; input rst_n ; input en ; output dout ; |
接下来定义信号类型。
cnt是用always产生的信号,因此类型为reg。cnt计数的最大值为9,需要用4根线表示,即位宽是4位。add_cnt和end_cnt都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:
1 2 3 |
reg [ 3:0] cnt ; wire add_cnt ; wire end_cnt ; |
dout是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg dout ; |
至此,整个代码的设计工作已经完成。整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
module my_ex1( clk , rst_n , en , dout ); input clk ; input rst_n ; input en ; output dout ;
reg [ 3:0] cnt ; wire add_cnt ; wire end_cnt ; reg dout ;
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt <= 0; end else if(add_cnt) begin if(end_cnt) &nbs p; cnt <= 0; else &nbs p; cnt <= cnt + 1; end end
assign add_cnt = (dout==1); assign end_cnt = add_cnt && cnt==10 -1 ;
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(en==1) begin dout <= 1; end else if(add_cnt && cnt==10-1)begin dout <= 0; end end
endmodule |
1.2.2 至简设计法设计类型2
例2. 当收到en=1后,dout间隔3个时钟后,产生宽度为2个时钟周期的高电平脉冲。
图 117
如上面波形图所示,在第3个时钟上升沿看到en==1,间隔 3个时钟后,dout变1,再过2个时钟后,dout变0。
根据案例1的经验,出现大于1的数字时,就需要计数。我们这里有数字2和3,建议的计数方式如下。
图 118
当然,其他计数方式最终也能实现功能。但明德扬的总结是上面方式最好,实现的代码将是最简的,其他方式则稍微复杂。
接下来判断计数器的加1条件。与案例1不同的是,计数器加1区域如下图阴影部分,但图中没有任何信号来指示此区域 。
图 119
为此,添加一个名字为“flag_add”的信号,刚好覆盖了阴影部分,如下图。
图 120
补充该信号后,计数器的加1条件就变为flag_add==1,并且是数5个。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin cnt <= 0; end else if(add_cnt) begin if(end_cnt) &nbs p; cnt <= 0; else &nbs p; cnt <= cnt+1 ; end end assign add_cnt = flag_add==1; assign end_cnt = add_cnt && cnt == 5-1 ; |
|
|
flag_add有2个变化点,变1和变0。变1的条件是收到en==1,变0的条件是计数器数完了,因此代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_add <= 0; end else if(en==1) begin flag_add <= 1; end else if(end_cnt) begin flag_add <= 0; end end |
dout也有2个变化点:变1和变0。变1的条件是“3个间隔之后”,也就是“数到3个的时候”;变0的条件是数完了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(add_cnt && cnt==3-1)begin dout <= 1; end else if(end_cnt) begin dout <= 0; end end |
至此,我们完成了主体程序的设计,接下来是补充module的其他部分。
将module的名称定义为my_ex2。并且我们已经知道该模块有4个信号:clk、rst_n、en和dout。为此,代码如下:
1 2 3 4 5 6 |
module my_ex2( clk , rst_n , en , dout ); |
其中clk、rst_n和en是输入信号,dout是输出信号,并且4个信号都是1比特的,根据这些信息,我们补充输入输出端口定义。代码如下:
1 2 3 4 |
input clk ; input rst_n ; input en ; output dout ; |
接下来定义信号类型。
cnt是用always产生的信号,因此类型为reg。cnt计数的最大值为4,需要用3根线表示,即位宽是3位。add_cnt和end_cnt都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:
1 2 3 |
reg [ 2:0] cnt ; wire add_cnt ; wire end_cnt ; |
dout是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg dout ; |
flag_add是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg flag_add ; |
至此,整个代码的设计工作已经完成。整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
module my_ex2( clk , rst_n , en , dout );
input clk ; input rst_n ; input en ; output dout ;
reg [ 2:0] cnt ; wire add_cnt ; wire end_cnt ; reg flag_add ; reg dout ;
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt <= 0; end else if(add_cnt) begin if(end_cnt) &nbs p; cnt <= 0; else &nbs p; cnt <= cnt + 1; end end
assign add_cnt = flag_add==1; assign end_cnt = add_cnt && cnt==5-1 ;
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_add <= 0; end else if(en==1) begin flag_add <= 1; end else if(end_cnt) begin flag_add <= 0; end end
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(add_cnt && cnt==3-1)begin dout <= 1; end else if(end_cnt) begin dout <= 0; end end
endmodule |
经过这个案例,我们做一下总结:在设计计数器的时候,如果计数区域没有信号来表示时,可补充一个信号flag_add。
1.2.3 至简设计法设计类型3
案例3. 当收到en1=1时,dout产生3个时钟周期的高电平脉冲;当收到en2==1时,dout产生2个周期的高电平脉冲 。下面波形图描述了该功能。
图 121
图中,第3个时钟上升沿收到en1==1,所以dout变1并且持续3个时钟周期;在第9个时钟上升沿看到en2==1,所以 dout变1并且持续2个时钟周期。注意,en1==1和en2==1的出现是没有顺序的。
有读者可能会问,如果en1==1和en2==1同时出现,或者说在dout==1期间,出现了en1==1或者en2==1,该怎么办?请不要考虑这种情况,本案例假设永远不会出现该情况。明德扬在模块划分规范时,会要求各个模块之间配合清楚,这有助于简化我们的设计,精简系统。
看到大于1的数字,就知道要计数。推荐的计数方式如下:
图 122
首先,不要用2个计数器分别计两种情况。这是因为这2个计数器都是不同时计数的,是可以合并的。同时,我们可以知道,这两种情况都是计算dout==1的次数。
在确认计数器数多少个时,我们遇到了问题。因为这个计数器有时候数到3个就清零(en1==1触发的波形),有时候数到2个就清零(en2==1触发 的波形)。此时,我们建议你用变量x代替,即数到x个。注意,verilog是没有变量的概念的,这个变量,是明德扬提出的一个设计概念,x本质上还是一个信号。
引入变量有什么用呢?设计计数器时就方便了,该计数器加1条件是dout==1,数x个就结束,因此代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt <= 0; end else if(add_cnt) begin if(end_cnt) &nbs p; cnt <= 0; else &nbs p; cnt <= cnt + 1; end end
assign add_cnt = dout==1; assign end_cnt = add_cnt && cnt==x-1 ; |
甚至我们还可以写出dout的代码,dout变1的条件是:en1==1或者en2==1;变0的条件是:计数器数完了。所以代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(en1==1 || en2==1)begin dout <= 1; end else if(end_cnt) begin dout <= 0; end end |
我们再设计一下变量x,我们知道计数器en1==1触发的时候数3个就清零,en2==1触发的时候数到2个就清零,为此增加一个信号flag_sel来区分这两种情况,flag_sel==0表示是en1==1触发的,flag_sel==1表示是en2==1触发的,波形如下:
图 123
flag_sel变0的条件是遇到en1==1,flag_sel变1的条件是遇到en2==1,为此flag_sel的代码如下。
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_sel <= 0; end else if(en1==1) begin flag_sel <= 0; end else if(en2==1) begin flag_sel <= 1; end end |
有了flag_sel,我们就好区分x的值了。 flag_sel为0时,x为3(数3个清零);flag_sel为1时,x为2(数2个清零),此时要用组合逻辑设计x,不然会出错的。代码如下:
1 2 3 4 5 6 |
always @(*)begin if(flag_sel==0) x = 3; else x = 2; end |
至此,本工程的主体程序已经设计完毕,本题,我们使用了变量x,这是明德扬的至简设计方法中的变量法。
主体程序完成后,我们补充模块的其他部分。
将module的名称定义为my_ex3。并且我们已经知道该模块有5个信号:clk、rst_n、en1、en2和dout。为此 ,代码如下:
1 2 3 4 5 6 7 |
module my_ex3( clk , rst_n , en1 , en2 , dout ); |
其中clk、rst_n、en1和en2是输入信号,dout是输出信号,并且5个信号都是1比特的,根据这些信息,我们补充输入输出端口定义。代码如下:
1 2 3 4 5 |
input clk ; input rst_n ; input en1 ; input en2 ; output dout ; |
接下来定义信号类型。
cnt是用always产生的信号,因此类型为reg。cnt计数的最大值为2,需要用2根线表示,即位宽是2位。add_cnt和end_cnt都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:
1 2 3 |
reg [ 1:0] cnt ; wire add_cnt ; wire end_cnt ; |
dout是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg dout ; |
flag_sel是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg flag_sel ; |
x是用always方式设计的,因此类型为reg,并且其值最大为3,用2根线表示即可。因此代码如下:
1 |
reg [ 1:0] x ; |
至此,整个代码的设计工作已经完成。整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
module my_ex3( clk , rst_n , en1 , en2 , dout );
input clk ; input rst_n ; input en1 ; input en2 ; output dout ;
reg [ 1:0] cnt ; wire add_cnt ; wire end_cnt ; reg dout ; reg flag_sel ; reg [ 1:0] x ;
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt <= 0; end else if(add_cnt) begin if(end_cnt) &nbs p; cnt <= 0; else &nbs p; cnt <= cnt + 1; end end
assign add_cnt = dout==1; assign end_cnt = add_cnt && cnt==x-1 ;
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(en1==1 || en2==1)begin dout <= 1; end else if(end_cnt) begin dout <= 0; end end
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_sel <= 0; end else if(en2==1) begin flag_sel <= 1; end else if(en1==1) begin flag_sel <= 0; end end
always @(*)begin if(flag_sel==0) x = 3; else x = 2; end
endmodule |
总结:设计时,我们不要受具体数字的影响,而是仔细识别信号的一致性动作,然后利用变量法来设计。这样就能设计出精妙的代码。
1.2.4 至简设计法设计类型4
案例4. 当收到en=1时,dout间隔1个时钟后,产生2个时钟周期的高电平脉冲,并且重复3次。
图 124
上面波形图显示了描述的功能。第3个时钟上升沿收到en==1,所以dout间隔1个时钟后变1并且持续2个时钟周期,这个动作重复3次,结束。
看到大于1的数字,就知道要计数。下面的计数方式非常普遍:
图 125
即用一个计数器,从头数到尾。这个计数器的设计很简单,但产生dout信号就不容易了。
明德扬推荐的计数方式如下:
图 126
利用2个计数器。cnt0就如案例2一样,数的是间隔和高电平时钟;而计数器cnt1数的是重复次数。
如案例2相同,需要添加信号flag_add来指示cnt0的加1区域,波形如下图。
图 127
所以cnt0的加1条件是flag_add==1,计数3个就清零。
仔细观察cnt1可以看到,每次cnt0数完后,cnt1就会加1。所以cnt1的加1条件是end_cnt0,计数3个就清零。从而我们可以设计出cnt0和cnt1的代码,输入Jsq2,即可调出模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt0 <= 0; end else if(add_cnt0) begin if(end_cnt0) &nbs p; cnt0 <= 0; else &nbs p; cnt0 <= cnt0 + 1; end end
assign add_cnt0 = flag_add==1; assign end_cnt0 = add_cnt0 && cnt0==3-1 ;
always @(posedge clk or negedge rst_n)begin if(!rst_n) begin cnt1 <= 0; end else if(add_cnt1) begin if(end_cnt1) &nbs p; cnt1 <= 0; else &nbs p; cnt1 <= cnt1 + 1; end end
assign add_cnt1 = end_cnt0; assign end_cnt1 = add_cnt1 && cnt1==3-1 ; |
flag_add有两个变化点:变1和变0。变1是因为en==1,变0是因为重复次数都完了,也就是end_cnt1。所以flag_add代码如下。
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_add <= 0; end else if(en==1) begin flag_add <= 0; end else if(end_cnt1) begin flag_add <= 1; end end |
dout有两个变化点:变1和变0。在cnt0数到1时(一个间隔)时变1,在cnt0数完时变0,所以dout的代码如下。
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(add_cnt0 && cnt0==1-1)begin dout <= 1; end else if(end_cnt0) begin dout <= 0; end end |
至此,本工程的主体程序已经设计完毕,之后需要读者补充信号定义、输入输出定义了。
将module的名称定义为my_ex3。并且我们已经知道该模块有5个信号:clk、rst_n、en和dout。为此,代码如下:
1 2 3 4 5 6 |
module my_ex4( clk , rst_ n , en , dout ); |
其中clk、rst_n、en是输入信号,dout是输出信号,并且4个信号都是1比特的,根据这些信息,我们补充输入输出端口定义。代码如下:
1 2 3 4 |
input clk ; input rst_n ; input en ; output dout ; |
接下来定义信号类型。
cnt0是用always产生的信号,因此类型为reg。cnt0计数的最大值为2,需要用2根线表示,即位宽是 2位。add_cnt0和end_cnt0都是用assign方式设计的,因此类型为wire。并且其值是 0或者1,1个线表示即可。因此代码如下:
1 2 3 |
reg [ 1:0] cnt0 ; wire add_cnt0 ; wire end_cnt0 ; |
cnt1是用always产生的信号,因此类型为reg。cnt1计数的 最大值为2,需要用2根线表示,即位宽是2位。 add_cnt1和end_cnt1都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:
1 2 3 |
reg [ 1:0] cnt1 ; wire add_cnt1 ; wire end_cnt1 ; |
dout是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg dout ; |
flag_add是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
1 |
reg flag_add ; |
至此,整个代码的设计工作已经完成。整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8 |
module my_ex4( clk , rst_n , en , dout );
input clk ; input rst_n ; input en ; output dout ;
reg [ 1:0] cnt0 ; wire add_cnt0 ; wire end_cnt0 ;
reg [ 1:0] cnt1 ; wire add_cnt1 ; wire end_cnt1 ; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt0 <= 0; end else if(add_cnt0) begin if(end_cnt0) &nbs p; cnt0 <= 0; else &nbs p; cnt0 <= cnt0 + 1; end end
assign add_cnt0 = flag_add==1; assign end_cnt0 = add_cnt0 && cnt0==3-1 ;
always @(posedge clk or negedge rst_n)begin if(!rst_n) begin cnt1 <= 0; end else if(add_cnt1) begin if(end_cnt1) &nbs p; cnt1 <= 0; else &nbs p; cnt1 <= cnt1 + 1; end end
assign add_cnt1 = end_cnt0; assign end_cnt1 = add_cnt1 && cnt1==3-1 ;
reg flag_add ;
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_add <= 0; end else if(en==1) begin flag_add <= 1; end else if(end_cnt1) begin flag_add <= 0; end end
reg dout ;
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(add_cnt0 && cnt0==1-1)begin dout <= 1; end else if(end_cnt0) begin dout <= 0; end end
endmodule |
本题中,我们设计了2个计数器 ,从而使得dout的设计非常简单。计数器的组合使用,对设计的复杂度有非常大的影响。合理和正确使用,将能设计出赏心悦目的代码。
1.3 至简设计法高效设计
上一节我们描述了四种情况下的设计方法。在阐述案例过程中,我们画出了大量的波形图。有读者可能会问,在工作中,我们是不是也需要先大量地画波形图,再来写代码呢?
不是的!工作中,我们要设计的系统更加的复杂,一个模块的信号也非常地多,如果我们每个模块都要画波形图,这不是明德扬提倡的至简设计。何况,功能一复杂,画出来的波形信号也是相当地多,也容易迷糊当中。
在上一节中,我们画波形图的目的,是为了让读者更清晰地理解功能、计数器和信号的关系。如果我们牢记明德扬的规则。或者熟练掌握后,波形存在心中即可。我们的设计将非常简单。
FPGA其实不是波形设计,而是功能设计,讲究的是逻辑,讲究的是因果关系。功能设计就是根据功能需求,编写我们的设计代码。我们以上一节中的案例4为例,说明什么叫功能设计。
案例4的功能要求是:当收到en=1时,dout间隔1个时钟后,产生2个时钟周期的高电平脉冲,并且重复3次。
由题意可知,要对“间隔” 和“高电平”个数进行计数,但没有信号表示“高隔”,为此想出补充一个信号 flag_add,用来表示计数区域。间隔时间+高电平时间,得到计数器数3个。
图 128
我们看到重复3次这一句话,这就说明还有一个计数器计数重复的次数。自然地想到,每完成一次就加1,一共加3次。得到代码如下。
图 129
在设计计数器0的时候,新增了信号flag_add。那进一步思考,什么时候要产生动作,那就让flag_add为1。自然, 从题意可知,en==1是开始,重复次数完了,那就结束,不用再产生信号。所以flag_add代码。
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin flag_add <= 0; end else if(en==1) begin flag_add <= 0; end else if(end_cnt1) begin flag_add <= 1; end end |
最后我们再来设计dout,由题意可知,每次均是间隔1个之后dout变1,2个时钟之后变0。那用什么来数这个1和2呢?cnt0。综合起来,就是说cnt0数到1个后,dout变1,数完后变0。
1 2 3 4 5 6 7 8 9 10 11 |
always @(posedge clk or negedge rst_n) begin if(rst_n==1'b0) begin dout <= 0; end else if(add_cnt0 && cnt0==1-1)begin dout <= 1; end else if(end_cnt0) begin dout <= 0; end end |
总结:从功能的文字描述中出发,根据功能要求来设计代码。在设计时,一定要理解清楚信号的因果关系,例如为什么变0,为什么变1,从功能说明中找答案。经常训练这种思考和设计方式,几分钟就能设计出精妙的代码,而且因果关系、逻辑关系清楚,几乎不存在出错的可能,从而写出所想即所得的代码。