这是数字电路课的一个要求的实验,虽然理论上的过程很简单,但实际上踩了很多坑,于是想要记录下来
总体思路和目标 完成一只停表的设计和实现,这只表只有一个按钮,按一下进入计时,再按一下停止计时,再按一下复位。
使用的硬件是Bsys3的FPGA,软件是Vivado。
它的状态机简单明了,也只需要几个分频器,理论上是很简单的,但对于我这种刚开始上手的玩家,还是有很多意料之外的难题。通过这个实验,我学到了很多乱七八糟的知识。
分器件整理
主程序 stopwatch 在开始实验的时候,我犯的第一个错误,就是把所有器件写在了一个文件夹。
在vscode里,缩放功能并不能完全把一个module缩小在一行,所以代码看起来就界限不太明显,也不方便单独器件的调试。
在几天后的另一个实验里,我才知道了有task和function的存在。
主程序只需要负责连接各大器件罢了。
其中,pulse_creator制造不同大小的脉冲。botton_check对按键进行滤波,因为按键需要消抖处理。system_control是状态控制模块,click为时钟输出,scan将时钟的BCD显示转换为数码管的输出。
为了调试方便,我定义了很多输出,这些输出将控制一系列LED的亮灭,通过这些信息,我可以快速判断器件的运行情况。
其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module stopwatch ( input clk,btnc, output [6 :0 ]led_segs_n,[3 :0 ]led_sel_n, output wire bt,work,clear,dot, output wire [7 :0 ] msec ); wire [6 :0 ]led_segs_n;wire [3 :0 ]led_sel_n;wire work,clear,pulse_10ms,pulse_120hz,pulse_1ms,over_flow;wire [7 :0 ] sec;pulse_creator pulse_creator(.clk (clk),.pulse_10ms (pulse_10ms),.pulse_1ms (pulse_1ms),.pulse_120hz (pulse_120hz)); botton_check botton_check (.bt (bt),.clk (pulse_10ms),.btnc (btnc)); system_control system_control(.bt (bt),.work (work),.clear (clear),.pulse_10ms (pulse_10ms),.over_flow (over_flow)); click click(.pulse_1ms (pulse_1ms),.clear (clear),.work (work),.sec (sec),.msec (msec),.over_flow (over_flow)); scan scan(.pulse_120hz (pulse_120hz),.sec (sec),.msec (msec),.led_segs_n (led_segs_n),.led_sel_n (led_sel_n),.dot (dot)); endmodule
注意串行变量和并行变量不能放在一排进行定义。同时,并行变量都需要单独定义,不能放在一排。
数码管显示器件 scan 对于always语句,最好使用统一的时钟模块,不能用一个计数器去控制另一个模块的刷新,即always用来检测改变量的时候最好不用来计数。
数码管的管脚一般有8个,7个用于控制数字的笔画亮灭,1个控制点的亮灭。
另外还有4个管脚,负责控制到底是哪一个数字亮灭,这里也要注意是共阳还是共阴。
小心查看说明书,共阳和共阴的01是反的,如果发现数码管显示的几乎全是8或者0,考虑是否是因为显示的01反了。
代码如下:
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 module scan ( input pulse_120hz,[7 :0 ]sec,[7 :0 ]msec, output [6 :0 ]led_segs_n,[3 :0 ]led_sel_n,dot ); wire [6 :0 ]led_segs_n;reg [3 :0 ]led_sel_n;reg dot;reg [2 :0 ]num;reg [3 :0 ]ss;initial begin num = 0 ; led_sel_n = 4'b01 ; end bcd_decoder bcd_decoder(.led_segs_n (led_segs_n),.ss (ss)); always @(posedge pulse_120hz) begin case (num) 3'b000 : begin led_sel_n = 4'b1110 ; ss = msec[3 :0 ]; dot = 1 ; end 3'b001 : begin led_sel_n = 4'b1101 ; ss = msec[7 :4 ]; dot = 1 ; end 3'b010 : begin led_sel_n = 4'b1011 ; ss = sec[3 :0 ]; dot = 0 ; end 3'b011 : begin led_sel_n = 4'b0111 ; ss = sec[7 :4 ]; dot = 1 ; end default ; endcase end always @(posedge pulse_120hz) begin num <= num + 1 ; if (num == 3'b100 )begin num <= 0 ; end end endmodule
以及附加的另一个器件(负责十进制BCD到数码管的转换):
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 module bcd_decoder(ss,led_segs_n); input [3 :0 ]ss; output reg [6 :0 ]led_segs_n; reg a,b,c,d,e,f,g; reg s0,s1,s2,s3; always @(ss) begin s0 <= ss[3 ];s1 <= ss[2 ];s2 <= ss[1 ];s3 <= ss[0 ]; if (!s0 && !s1 && !s2 && !s3) begin a <= 1 ;b <= 1 ;c <= 1 ;d <= 1 ;e <= 1 ;f <= 1 ;g <= 0 ; end if (!s0 && !s1 && !s2 && s3) begin a <= 0 ;b <= 1 ;c <= 1 ;d <= 0 ;e <= 0 ;f <= 0 ;g <= 0 ; end if (!s0 && !s1 && s2 && !s3) begin a <= 1 ;b <= 1 ;c <= 0 ;d <= 1 ;e <= 1 ;f <= 0 ;g <= 1 ; end if (!s0 && !s1 && s2 && s3) begin a <= 1 ;b <= 1 ;c <= 1 ;d <= 1 ;e <= 0 ;f <= 0 ;g <= 1 ; end if (!s0 && s1 && !s2 && !s3) begin a <= 0 ;b <= 1 ;c <= 1 ;d <= 0 ;e <= 0 ;f <= 1 ;g <= 1 ; end if (!s0 && s1 && !s2 && s3) begin a <= 1 ;b <= 0 ;c <= 1 ;d <= 1 ;e <= 0 ;f <= 1 ;g <= 1 ; end if (!s0 && s1 && s2 && !s3) begin a <= 1 ;b <= 0 ;c <= 1 ;d <= 1 ;e <= 1 ;f <= 1 ;g <= 1 ; end if (!s0 && s1 && s2 && s3) begin a <= 1 ;b <= 1 ;c <= 1 ;d <= 0 ;e <= 0 ;f <= 0 ;g <= 0 ; end if (s0 && !s1 && !s2 && !s3) begin a <= 1 ;b <= 1 ;c <= 1 ;d <= 1 ;e <= 1 ;f <= 1 ;g <= 1 ; end if (s0 && !s1 && !s2 && s3) begin a <= 1 ;b <= 1 ;c <= 1 ;d <= 0 ;e <= 0 ;f <= 1 ;g <= 1 ; end led_segs_n[6 ] <= !a;led_segs_n[5 ] <= !b;led_segs_n[4 ] <= !c;led_segs_n[3 ] <= !d;led_segs_n[2 ] <= !e; led_segs_n[1 ] <= !f;led_segs_n[0 ] <= !g; end endmodule
按键消抖器件 botton_check 对一个按键来说,虽然有按下是高,松开为低这样的理论表现,其实际表现总会产生一些小脉冲。让这样的信号直接输入系统,会让系统误认为进行了多次按压,结果导致实际表现不好,会跳状态。
因此,对按键信号的消抖是必不可少的。
消抖的思路就是对信号进行多次检测,在一段间隔中始终保持高电平则认为按下按键一次。
我尝试了多种消抖方式,就结果来看,有两种方式被证实是可行的。
一种是在状态机中插入缓冲状态,通过状态的过渡达到目的。
一种是写一个器件实现对按键信号的滤波处理,通过多次循环达到目的。
最后实现的方式是第二种,因为这种方式对状态机依赖小,可以不用对我的主程序作太大修改。
在调试过程中,建议先把按键检测的阈值提高,通过人手动长按检查程序是否运行正确,然后再逐步调低阈值,避免因为人眼无法观察到灯光闪动而误以为成功,实则不然。
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 always @(posedge clk) begin if (btnc) begin i <= i + 1 ; if (i >= 5 )begin bt <= 1 ; end else begin bt <= 0 ; end end else begin i <= 0 ; bt <= 0 ; end end endmodule
脉冲发生器件 pulse_creator 该器件输入FPGA自带的100MHz的震荡时钟,通过计数器分频的方式分成需要的频率,供其它器件驱使。
这里采用的是integer计数的方式,通过定义不同的i,j,k实现,这里基本没有难度,不再赘述。
代码如下:
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 module pulse_creator( input clk, output reg pulse_10ms,pulse_1ms,pulse_120hz ); integer i;integer j;integer k;initial begin i = 0 ;j = 0 ;k = 0 ; pulse_10ms = 0 ; pulse_1ms = 0 ; pulse_120hz = 0 ; end always @(posedge clk) begin i <= i+1 ; if (i == 100000 )begin i <= 0 ; pulse_10ms <= (pulse_10ms)?0 :1 ; end end always @(posedge clk) begin j <= j+1 ; if (j == 1000000 )begin j <= 0 ; pulse_1ms <= (pulse_1ms)?0 :1 ; end end always @(posedge clk) begin k <= k+1 ; if (k == 83333 )begin k <= 0 ; pulse_120hz <= (pulse_120hz)?0 :1 ; end end endmodule
时钟器件 click 该器件将纯粹的时钟信号转换为BCD编码的数字信号,本质上相当于是一个计数器,但是用逻辑语言实现就会非常简单。
可以看到,对于一个串行数据[7:0]sec,也可以分成两块分别处理,就像[3:0]sec,[7:4]sec。
其代码如下:
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 module click( input pulse_1ms,clear,work, output [7 :0 ]sec,[7 :0 ]msec,over_flow ); reg [7 :0 ]sec; reg [7 :0 ]msec; reg over_flow;initial begin over_flow <= 0 ; sec <= 0 ; msec <= 0 ; end always @(posedge pulse_1ms) begin if (clear) begin sec <= 0 ; msec <= 0 ; over_flow <= 0 ; end if (work & !over_flow)begin msec[3 :0 ] = msec[3 :0 ] + 1 ; if (msec[3 :0 ] == 4'd10 )begin msec[3 :0 ] <= 0 ; msec[7 :4 ] <= msec[7 :4 ] + 1 ; end if (msec[7 :4 ] == 4'd6 )begin msec[7 :4 ] <= 0 ; sec[3 :0 ] <= sec[3 :0 ] + 1 ; end if (sec[3 :0 ] == 4'd10 )begin sec[3 :0 ] <= 0 ; sec[7 :4 ] <= sec[7 :4 ] + 1 ; end if (sec[7 :4 ] == 4'd6 )begin over_flow <= 1 ; end end end endmodule
状态控制器件 system_control 该模块是一个简易的状态控制模块,由于这个停表的状态过于简单,所以只需要按顺序设计即可,没有区分状态转移和输出控制模块。
需要注意的是,在always内部,代码是有顺序的,不正确的代码顺序会有严重后果。
其代码如下:
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 module system_control( input bt,pulse_10ms,over_flow, output reg work,clear ); reg [1 :0 ]state;initial begin state = 2'b00 ; work = 0 ; clear = 1 ; end always @(posedge bt) begin case (state) 2'b00 : begin state = 2'b01 ; work <= 1 ; clear <= 0 ; end 2'b01 : begin state = 2'b10 ; work <= 0 ; clear <= 0 ; end 2'b10 : begin state = 2'b00 ; work <= 0 ; clear <= 1 ; end endcase if (over_flow)begin state = 2'b00 ; work <= 0 ; clear <= 1 ; end end endmodule
合并与调试 将上面的代码都放在一个文件中,或放在一个文件夹下,并指定器件stopwatch为top器件即可。
在调试中,注意需要提前设置时钟信号clk的专用端口为W5,否则会有很长一段关于时钟的报错,其它端口可以在排版后再行指定。