一只停表的Verilog实现

目录
  1. 总体思路和目标
  2. 分器件整理
    1. 主程序 stopwatch
    2. 数码管显示器件 scan
    3. 按键消抖器件 botton_check
    4. 脉冲发生器件 pulse_creator
    5. 时钟器件 click
    6. 状态控制器件 system_control
  3. 合并与调试
TOC

这是数字电路课的一个要求的实验,虽然理论上的过程很简单,但实际上踩了很多坑,于是想要记录下来

总体思路和目标

完成一只停表的设计和实现,这只表只有一个按钮,按一下进入计时,再按一下停止计时,再按一下复位。

使用的硬件是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 //stopwatch

注意串行变量和并行变量不能放在一排进行定义。同时,并行变量都需要单独定义,不能放在一排。


数码管显示器件 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//0
a <= 1;b <= 1;c <= 1;d <= 1;e <= 1;f <= 1;g <= 0;
end
if (!s0 && !s1 && !s2 && s3) begin//1
a <= 0;b <= 1;c <= 1;d <= 0;e <= 0;f <= 0;g <= 0;
end
if (!s0 && !s1 && s2 && !s3) begin//2
a <= 1;b <= 1;c <= 0;d <= 1;e <= 1;f <= 0;g <= 1;
end
if (!s0 && !s1 && s2 && s3) begin//3
a <= 1;b <= 1;c <= 1;d <= 1;e <= 0;f <= 0;g <= 1;
end
if (!s0 && s1 && !s2 && !s3) begin//4
a <= 0;b <= 1;c <= 1;d <= 0;e <= 0;f <= 1;g <= 1;
end
if (!s0 && s1 && !s2 && s3) begin//5
a <= 1;b <= 0;c <= 1;d <= 1;e <= 0;f <= 1;g <= 1;
end
if (!s0 && s1 && s2 && !s3) begin//6
a <= 1;b <= 0;c <= 1;d <= 1;e <= 1;f <= 1;g <= 1;
end
if (!s0 && s1 && s2 && s3) begin//7
a <= 1;b <= 1;c <= 1;d <= 0;e <= 0;f <= 0;g <= 0;
end
if (s0 && !s1 && !s2 && !s3) begin//8
a <= 1;b <= 1;c <= 1;d <= 1;e <= 1;f <= 1;g <= 1;
end
if (s0 && !s1 && !s2 && s3) begin//9
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,否则会有很长一段关于时钟的报错,其它端口可以在排版后再行指定。

DAR
SON