通过案例分析可以发现复合计数并不是多此一举,反而是最简单的计数方式。因此,除计算输出10 毫秒 PWM 波的计数器外,本设计会使用一个计数 2 秒的计数器和一个表示次数的计数器。这是最适合本设计的计数器方案,在后续遇到问题时可以快速的定位到相应位置,从而避免很多麻烦。不论是简单设计还是复杂设计,至简设计法都会全面的考虑设计需求,在每一个环节采用最适合的设计方案,尽量为后续的步骤减少不必要的麻烦。
确定了三个计数器后来讨论一下每个计数器的实现。至简设计法的设计规则中有讲过,计数器的设计只考虑两个因素:加 1 条件和计数数量。只要确定好相应逻辑,就能完成计数器代码设计。首先来讨论计数 10 毫秒 PWM 波的计数器 cnt0,由于 cnt0 始终不停地进行计数,因此可以认为其加 1 条件是一直有效的,可写成:assign add_cnt0==1。
可能会有同学提出疑问:加 1 条件的概念是什么?这里以停车位来进行比喻,一般情况下对每个停车位置会进行对应编号,但是如果某个位置上放置了一块石头无法作为停车位时,该位置就不能获得对应的编号。反之则可以认为停车位编号的加 1 条件就是:对应位置上没有石头,其可以继续的进行编号,即 assign add_cnt0 = “没有石头”。因此如果在设计中计数器一直没有阻碍地进行计数工作,则可以认为加 1 条件是一直有效的。
接着讨论计数器 cnt0 的计数数量:本工程的工作时钟是 50MHz,即周期为 20ns,当计数器计数到第 10_000_000/20=500_000 个时,就代表 10 毫秒时间到了,因此 cnt0 的计数数量为 500_000。
确定好 cnt0 的加 1 条件和计数数量后开始进行代码编写,以往都是一行行的输入相应代码。但是至简设计法有一个小技巧,可以节省代码编写时间的同时在一定程度上降低了代码的出错率。至简设计法将日常代码中常用到的固定部分制作成模板,进行代码编程时可以调用相应模板后根据逻辑输入对应设计的变量将代码补充完整。这处就可以用模板编写计数器代码,感受一下这个炫酷的功能。打开 GVIM 工具,在命令模式下输入“:Mdyjsq”后点击回车就调出了对应模板,如下图所示。之后再将本案例中的变量填到模板里面,就可以得到完整正确的计数器代码。
按照上文方法,得出计数器 cnt0 的代码设计如下:
下面来设计记录 2 秒时间的计数器 cnt1。本工程的工作时钟是 50MHz,即周期为20ns,因此当计数器计数到第 2_000_000_000/20=100_000_000 个时,就代表 2 秒时间计时结束。这是第一种设计思路,至简设计法在这里也提供另外一种设计思路:与 cnt0 复合使用,以 10 毫秒为小周期,通过数 2_000_000_000/10_000_000=200 个 10 毫秒时间,就能确定 2 秒时间计时结束。在这种思路下可以得出计数器 cnt1 的加 1 条件是 end_cnt0,计数数量为 200。此时继续调用至简设计法模板,在 GVIM 命令模式下输入“:Mdyjsq”,点击回车后调出对应模板,将“add_cnt1”和“end_cnt1”补充完整,得到计数器 cnt1 的代码如下:
最后是次数计数器 cnt2 的设计。根据设计目标可知,每隔 2 秒为 1 次改变,计数器的值应加 1,即 cnt2 的加 1 条件为 end_cnt1;每个周期该计数器应计数 10 次,即 cnt2 的计数数量为 10。继续调用至简设计法模板,在命令模式下输入“:Mdyjsq”,点击回车,调出对应模板后将“add_cnt1”和
“end_cnt1”补充完整,得到计数器 cnt2 的代码如下:
确定好三个计数器的代码后来思考一下输出信号 led 的变化。回想设计目标可知 led 有两个变化点:变 0 和变 1。当 10 毫秒计数器计数到一定个数时 led 信号值变为 0,但由于占空比不断进行变化,该计数值也会发生变化。可以假设该值为 x,也就是当计数器数到第 x 个时,led 的值变 0。led 值变为 1 则是由于 10 毫秒计数时间到了,也就是当 end_cnt0 时,led 的值变 1 。依旧调用至简设计法模板,在编辑模式下输入“Shixu2”回车,调出模板并将代码补充完整,得出 led 信号的代码如下:
为什么Led的变零条件是add_cnt0&&cnt0==x-1?
在这个地方add_cnt0始终等于1 。写不写是无所谓的,但是写了绝对不会错。
看前面内容:
计数器规则 3:只有在加 1 条件有效时,才能表示计数器的计数值。
假定加 1 条件为 add_cnt,计数器当前值为 cnt,则
add_cnt&&cntx-1 表示计数器计数到 x 个。
cntx-1 不能表示计数器计数到 x 个。
计数器是从 0 开始计数的,因此计数器的默认值即初始值是 0。那么当计数器的值为 0 时要如何区分这是开始计数的第 1 个值,还是并未计数的默认值呢?
这种情况下可以通过加 1 条件来进行区分。当加 1 条件无效时,计数器值为 0 表示未开始计数,此时的 0 为默认值;当加 1 条件有效时,计数器值为 0 表示计的第 1 个数。同理,当 cntx-1,不能表示计数到 x;只有当 cntx-1 且加 1 条件有效时,才表示计数到 x。
因此,当 add_cnt&&cnt5-1 时,表示计数到 5 个。而当 add_cnt0 &&cnt==5-1 时,不能表示计数到 5 个
最后再来思考一下变量 x。x 代表 led 信号值变为 0 的条件,即 PWM 波变低的时刻。由于不同次数中 PWM 波的占空比是不断变化的,其对应的 x 值会有所变化。也就是说 x 的值与闪烁次数有关,即与计数器 cnt2 有关。在第 1 次闪烁(cnt2=0)时,led 信号在 9.5 毫秒时刻变为 0,即在第一次闪烁中,当 cnt0 数到第9_500_000/20=475_000 个时 led 信号变为 0。因此当 cnt2=0 时,x 的值为 4
75_000。同样的,第 2 次闪烁(cnt2=1)时,led 信号在 8.5 毫秒时刻变为 0。8_500_000/20=425_000,因此 cnt2=1 时,x=425_000。同理可得第三次 x=350_000,第四次 x=250_000,第五次 x=100_000,第六次 x=100_000,第七次 x=250_000,第八次 x=350_000,第九次 x=425_000,第十次 x=475_000。
综上所述,可得 x 的代码如下:
至此,主体程序已经完成。回顾一下思考过程会发现设计的每一步都要按照设计目标逐步展开,看似在讨论一个个小问题,但实际上每个问题都是围绕设计目标来进行讨论的,这也是本书最开始强调制定和理解设计目标的重要性的原因。
3.3 信号定义
下来将 module 补充完整,首先来定义信号类型。对类型 reg 和 wire 的判断总会有多余的联想,比如认为 reg 是寄存器,wire 是线;或者认为 reg 类型会综合成寄存器,wire 类型不会综合成寄存器。
但是这些其实和 reg 型还是 wire 型都并无关系。实际上对信号类型的判断不需要做任何的联想,只
要记住一个规则“用 always 实现的是 reg 型,其他都是 wire 型”就可以了。
cnt0 是用 always 产生的信号,因此类型为 reg。cnt0 计数的最大值为 500_000,需要用 19 根线表示,即位宽是 19 位。关于信号位宽的获取,至简设计法在此分享一个非常实用的技巧:打开计算器,点击“查看”,选择“程序员”模式,在“十进制”下将信号值输入,就会获得对应的信号位宽。如下图所示,将 cnt0 的值 500_000 输入,可以看出其位宽为 19。
综上所述,cnt0 的信号定义代码如下:
cnt1 也是用 always 产生的信号,因此类型为 reg。cnt1 计数的最大值为 200,需要用 8 根线表示,即位宽是 8 位。编辑模式下输入“Reg8”调用模板,得到代码表示如下:
同理,cnt2 信号也由 always 产生,其类型为 reg。cnt2 计数的最大值为 9,需要用 4 根线表示,251即位宽是 4 位。编辑模式下输入“Reg4”调用模板,得到代码表示如下:
add_cnt0 和 end_cnt0 都是用 assign 方式设计的,因此类型为 wire。其值是 0 或者 1,用 1 根线表示即可,位宽为 1。编辑模式下输入“Wire1”调用模板,得到代码表示如下:
add_cnt1 和 end_cnt1 也是用 assign 方式设计的,因此类型为 wire。其值是 0 或者 1,用 1 根线表示即可,位宽为 1。编辑模式下输入“Wire1”调用模板,得到代码表示如下:
同样,add_cnt2 和 end_cnt2 是用 assign 方式设计,类型为 wire。其值是 0 或者 1,用 1 根线表示即可,位宽为 1。编辑模式下输入“Wire1”调用模板,得到代码表示如下:
led 信号是用 always 方式设计的,因此类型为 reg。其值是 0 或者 1,用 1 根线表示即可,位宽为 1。编辑模式下输入“Reg1”调用模板,得到代码表示如下:
x 是用 always 方式设计的,因此类型为 reg。其值是最大是 475_000,需要 19 根线表示,即位宽为 19,其代码表示如下:
完整代码如下
module pwmled( clk, rst_n, led ); input clk; input rst_n; output led; reg [ 18:0] cnt0 ; wire add_cnt0 ; wire end_cnt0 ; reg [ 7:0] cnt1 ; wire add_cnt1 ; wire end_cnt1 ; reg [ 3 :0] cnt2 ; wire add_cnt2 ; wire end_cnt2 ; reg led; reg[18:0] x; always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin cnt0 <= 0; end else if(add_cnt0) begin if(end_cnt0) cnt0 <= 0; else cnt0 <= cnt0+1 ; end end assign add_cnt0 = 1; assign end_cnt0 = add_cnt0 && cnt0 == 500_000-1 ; always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin cnt1 <= 0; end else if(add_cnt1) begin if(end_cnt1) cnt1 <= 0; else cnt1 <= cnt1+1 ; end end assign add_cnt1 = end_cnt0; assign end_cnt1 = add_cnt1 && cnt1 == 200-1 ; always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin cnt2 <= 0; end else if(add_cnt2) begin if(end_cnt2) cnt2 <= 0; else cnt2 <= cnt2+1 ; end end assign add_cnt2 = end_cnt1; assign end_cnt2 = add_cnt2 && cnt2 == 10-1 ; always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin led <= 1; end else if(add_cnt0 && cnt0==x-1)begin led <= 0; end else if(end_cnt0)begin led <= 1; end end always @(*)begin if(cnt2==0)begin x=475_000; end else if(cnt2==1)begin x=425_000; end else if(cnt2==2)begin x=350_000; end else if(cnt2==3)begin x=250_000; end else if(cnt2==4)begin x=100_000; end else if(cnt2==5)begin x=100_000; end else if(cnt2==6)begin x=250_000; end else if(cnt2==7)begin x=350_000; end else if(cnt2==8)begin x=425_000; end else begin x=475_000; end end endmodule