案例分析
在本小节中,我们将为大家介绍一些常见的时序逻辑电路,包括 FPGAOL 的数码管显示、移位寄存器等。此外,我们还会介绍一些现实中可以使用时序逻辑电路解决的问题,从需求出发,一步步设计组合逻辑电路,并进行编程。
3.1 FPGAOL 的七段数码管显示
Lab3 中已经有对七段数码管的结构解释。在学过时序逻辑电路后,我们可以更深一步地运用这些数码管来实现一些简单的项目。
首先,我们复习一下 Lab3 所学习的关于 FPGAOL 的七段数码管显示知识。
FPGAOL 平台一共拥有 9 个七段数码管,但细心的同学会发现 9 个数码管中最左边的与其他八个数码管画风不太一样,接下来会一一向你们解释。
首先是最左边的单个七段数码管(其实是八段,其中一个是小数点)的架构
单个七段数码管
我们可以看到数码管的每一段都是一个发光二极管,每一个发光二极管由 A~G 和 DP 命名,如果其中任意一个输入是高电平的时候,发光二极管就会发光,只要控制 A~G 和 DP 信号的电平高低就可以控制整个数码管的显示。
(注意这个七段数码管是与 led 灯组复用,所以同学在看到 led 随之亮起的时候不要惊讶)
以下是一个示例
Single_Segment.v |
---|
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 | module Single_Segment (
input [3:0] num,
output reg [7:0] led_sig // led_sig[0] = A, led_sig[1] = B, etc.
);
always @(*) begin
case(num)
4'd0 : led_sig = 8'b00111111;
4'd1 : led_sig = 8'b00000110;
4'd2 : led_sig = 8'b01011011;
4'd3 : led_sig = 8'b01001111;
4'd4 : led_sig = 8'b01100110;
4'd5 : led_sig = 8'b01101101;
4'd6 : led_sig = 8'b01111101;
4'd7 : led_sig = 8'b00000111;
4'd8 : led_sig = 8'b01111111;
4'd9 : led_sig = 8'b01101111;
4'd10 : led_sig = 8'b01110111;
4'd11 : led_sig = 8'b01111100;
4'd12 : led_sig = 8'b00111001;
4'd13 : led_sig = 8'b01011110;
4'd14 : led_sig = 8'b01111001;
4'd15 : led_sig = 8'b01110001;
endcase
end
endmodule
|
接下来我们考虑剩下的八个数码管,它们实际上是一个组,以下是结构图。
八个分时复用七段数码管
仔细观察上面的结构图,我们可以发现所有数码管的输入是公用的,由一个数码管译码器提供,这个数码管译码器的逻辑与与上面的 example
一样,将数据d[3]~d[0](相当于上面的 num
)输出成了数码管的信号源(相当于上面的 led_sig
)
而输出与上面的单个数码管始终接地不同,是分离的连在一个 3-8 译码器的反向输出端上,由这里的逻辑不难发现,八个数码管在单一时刻只有一个数码管能够发光并显示 d[3]~d[0] 所表示的数。
如 Lab3 中所说,为了利用好所有的数码管,我们通常采用分时复用的方式轮流点亮每个数码管,并保证一个数码管的刷新频率为 50Hz。理论可行,我们直接开始实践。
例子:可复位时钟
请设计一个精度为 0.1 秒的计时器,用 4 位数码管显示出来,数码管从高到低,分别表示分钟、秒钟十位、秒钟个位、十分之一秒,该计时器采用按钮作为复位功能。
TIMER.v |
---|
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 | module TIMER (
input clk,rst,
output [3:0] seg_data,
output [2:0] seg_an
);
reg [3:0] outm ;
reg [3:0] outss;
reg [3:0] outsg;
reg [3:0] outst;
reg [39:0] count;
initial begin
outm = 4'H0;
outss = 4'H0;
outsg = 4'H0;
outst = 4'H0;
count = 40'H0;
end
always@ (posedge clk) begin
if (rst) begin
count <= 40'H1;
end
else begin
if (count != 40'D6000000000)
count <= count + 40'H1;
else
count <= 40'H1;
end
end
/*使用 Lab3 实验练习中编写的数码管显示模块*/
Segment segment(
.clk (clk),
.rst (rst),
.output_data ({outm, outss, outsg, outst}),
.output_valid (8'HFF), // 如果你没有实现,可以不需要这个端口
.seg_data (seg_data),
.seg_an (seg_an)
);
always@ (posedge clk) begin
if(rst) begin
outm <= 4'H0;
outss <= 4'H0;
outsg <= 4'H0;
outst <= 4'H0;
end
else begin
outm <= (count % 40'D6000000000 != 0) ? outm :
(outm != 4'H5) ? outm + 1 :
4'H0 ;
outss <= (count % 40'D1000000000 != 0) ? outss :
(outss != 4'H5) ? outss + 1 :
4'H0 ;
outsg <= (count % 40'D100000000 != 0) ? outsg :
(outsg != 4'H9) ? outsg + 1 :
4'H0 ;
outst <= (count % 40'D10000000 != 0) ? outst :
(outst != 4'H9) ? outst + 1 :
4'H0 ;
end
end
endmodule
|
提醒
请不要在练习中直接使用上面的代码,因为 %
运算符会带来极大的时间开销,导致最终上板的运行结果异常。
Segment 模块
如果你没有或无法正确实现 Lab3 中的 Segment 模块,可以与助教联系以获得可用的模块代码。
在编写好源文件后,我们可以根据上面的八段数码管结构图来编写我们的 xdc 约束文件。
constraints.xdc |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | ## CLOCK
set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports {clk}]
## BUTTON
set_property -dict {PACKAGE_PIN F20 IOSTANDARD LVCMOS33} [get_ports { rst }];
## SEG DATA
set_property -dict { PACKAGE_PIN F18 IOSTANDARD LVCMOS33 } [get_ports {seg_data[0]}]
set_property -dict { PACKAGE_PIN E18 IOSTANDARD LVCMOS33 } [get_ports {seg_data[1]}]
set_property -dict { PACKAGE_PIN B20 IOSTANDARD LVCMOS33 } [get_ports {seg_data[2]}]
set_property -dict { PACKAGE_PIN A20 IOSTANDARD LVCMOS33 } [get_ports {seg_data[3]}]
## SEG AN
set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVCMOS33 } [get_ports {seg_an[0]}]
set_property -dict { PACKAGE_PIN A19 IOSTANDARD LVCMOS33 } [get_ports {seg_an[1]}]
set_property -dict { PACKAGE_PIN F19 IOSTANDARD LVCMOS33 } [get_ports {seg_an[2]}]
|
接下来烧写比特流并上板就可以看到一个运行得很好但并无啥用的时钟了。
3.2 移位寄存器
在数字电路中,移位寄存器(英語:shift register)是一种在若干相同时间脉冲下工作的以触发器级联为基础的器件,每个触发器的输出接在触发器链的下一级触发器的「数据」输入端,使得电路在每个时间脉冲内依次向左或右移动一个比特,在输出端进行输出。
而在本实验中,我们设定的移位寄存器与上面所定义的有些差异,我们通过按钮来实现移位寄存,当我们点击btn的时候,程序会进行边缘检测,读入数据(din)并进行移位,从而实现移位存储的功能。
Shift_Reg |
---|
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 | module Shift_Reg(
input clk,
input rst,
input en,
input [3:0] din,
output [3:0] seg_data,
output [2:0] seg_an,
output [7:0] led_sig
);
reg [31:0] shift_reg; //创建 32 位寄存器分成八个四位寄存器用来级联成移位寄存器
reg [3:0] out_data;
initial begin
shift_reg = 32'H0;
out_data = 4'H0;
end
/*使用 Lab3 实验练习中编写的数码管显示模块*/
Segment segment(
.clk (clk),
.rst (rst),
.output_data (shift_reg),
.output_valid (8'HFF),
.seg_data (seg_data),
.seg_an (seg_an)
);
/*使用上面示例中展示的单个数码管显示模块*/
Single_Segment single_segment(
.num (out_data),
.led_sig (led_sig)
);
wire pos_en;
/*使用 Lab3 的边沿检测模块*/
edge_capture en_pos_capture(
.clk(clk),
.rst(rst),
.sig_in(en),
.pos_edge(pos_en),
.neg_edge()
);
always@ (posedge clk) begin
if (pos_en) begin
shift_reg <= {shift_reg[27:0], din};
out_data <= shift_reg[31:28];
end
end
endmodule
|
此工程需要用到几乎所有的 FPGAOL 外设,除了 uart,希望以下 xdc 能让同学们加深对外设使用的理解。
constraints.xdc |
---|
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 | set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports {clk}]
## 输入使能
set_property -dict {PACKAGE_PIN F20 IOSTANDARD LVCMOS33} [get_ports { en }];
## 移位寄存器的输出值
set_property -dict { PACKAGE_PIN B15 IOSTANDARD LVCMOS33 } [get_ports {led_sig[0]}]
set_property -dict { PACKAGE_PIN B16 IOSTANDARD LVCMOS33 } [get_ports {led_sig[1]}]
set_property -dict { PACKAGE_PIN C13 IOSTANDARD LVCMOS33 } [get_ports {led_sig[2]}]
set_property -dict { PACKAGE_PIN B13 IOSTANDARD LVCMOS33 } [get_ports {led_sig[3]}]
set_property -dict { PACKAGE_PIN A15 IOSTANDARD LVCMOS33 } [get_ports {led_sig[4]}]
set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVCMOS33 } [get_ports {led_sig[5]}]
set_property -dict { PACKAGE_PIN A13 IOSTANDARD LVCMOS33 } [get_ports {led_sig[6]}]
set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 } [get_ports {led_sig[7]}]
## 移位寄存器的输入值(单个七段数码管输出)
set_property -dict { PACKAGE_PIN B17 IOSTANDARD LVCMOS33 } [get_ports {din[0]}]
set_property -dict { PACKAGE_PIN B18 IOSTANDARD LVCMOS33 } [get_ports {din[1]}]
set_property -dict { PACKAGE_PIN D17 IOSTANDARD LVCMOS33 } [get_ports {din[2]}]
set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports {din[3]}]
## 复位使能,为sw[7]
set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports { rst }];
## 移位寄存器存储值(八个七段数码管输出)
## SEG DATA
set_property -dict { PACKAGE_PIN F18 IOSTANDARD LVCMOS33 } [get_ports {seg_data[0]}]
set_property -dict { PACKAGE_PIN E18 IOSTANDARD LVCMOS33 } [get_ports {seg_data[1]}]
set_property -dict { PACKAGE_PIN B20 IOSTANDARD LVCMOS33 } [get_ports {seg_data[2]}]
set_property -dict { PACKAGE_PIN A20 IOSTANDARD LVCMOS33 } [get_ports {seg_data[3]}]
## SEG AN
set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVCMOS33 } [get_ports {seg_an[0]}]
set_property -dict { PACKAGE_PIN A19 IOSTANDARD LVCMOS33 } [get_ports {seg_an[1]}]
set_property -dict { PACKAGE_PIN F19 IOSTANDARD LVCMOS33 } [get_ports {seg_an[2]}]
|
3.3 实际应用
3.3.1 交通信号灯
例子:交通信号灯
要求设计一个交通信号灯,保证按绿灯黄灯红灯交替发光。
相信大部分同学在看到这道题后都不会去捣鼓所谓的状态转换图和卡诺图,毕竟在理论课上已经深受折磨,所以在这个简单案例中,我们也不会进行相关分析,而是把重点放在用一个简单例子来加深同学们对三段式 Verilog 时序电路的理解。
注意
我们建议,运用 FSM 来设计时序逻辑电路时,不管状态机多么简单,信号量多么少,都尽量采用三段式 Verilog 代码框架进行设计。
首先定义状态变量以及状态名称
reg [1:0] current_state, next_state;
localparam RED = 2'd0;
localparam YELLOW = 2'd1;
localparam GREEN = 2'd2;
接下来编写第一段:状态更新。假定 reset
信号的效果是清除之前所有的输入,恢复初始状态。则按下 reset
后状态机应当跳转到 \(GREEN\)。
always @(posedge clk) begin
if (reset)
current_state <= GREEN;
else
current_state <= next_state;
end
接下来编写第二段:状态转移。本案例中状态转换图是一个很简单的循环,根据状态转换图,我们可以编写如下的代码:
always @(*) begin
next_state = current_state;
case (current_state)
GREEN:
next_state = YELLOW;
RED:
next_state = GREEN;
YELLOW:
next_state = RED;
endcase
end
最后,我们编写第三段:输出。
assign green = current_state == GREEN;
assign yellow = current_state == YELLOW;
assign red = current_state == RED;
3.3.2 投币机
例子:投币机
要求设计一个自动饮料售卖机,饮料单价 10 分钱,硬币有 5 分和 10 分两种。假定一次只能投入一枚硬币,请设计状态机,考虑可能的情况,并计算找零。
设计过程如下:
第一步 确定输入输出。
A=1 表示投入 5 分钱,B=1 表示投入 10 分钱,Y=1 表示弹出饮料,Z=1 表示找零。
第二步 确定电路状态。
S0 表示售卖机里还没有钱币,S1 表示已经投了 5 分钱。
注意这里不存在其他的情况,比如已经投了 10 分钱,那已经足够完成本次交易,电路应该回归初始的 S0 状态,所以只会有 S0 和 S1 这两种状态。
第三步 画状态转移图。

投币机的 FSM
在 S0 状态时:
-
不投钱,则 AB=00,此时肯定不会弹出饮料,也不会找零,因此 YZ=00,此时状态也不会跳转,保持为 S0。
-
投入 5 分钱,则 AB=10,此时还不够 10 分钱,因此不会弹出饮料,也不会找零,因此 YZ=00,此时状态将跳转为 S1。
-
投入 10 分钱,则 AB=01,此时售卖机中已经达到 10 分钱,因此会弹出饮料,但不会找零,因此 YZ=10,完成交易后回到初始状态 S0,等待进行下一次交易。
-
同时投入 5 分钱和 10 分钱的情况假设不会发生,因此 AB=11 的情况,默认 YZ=00,并回到初始态 S0。
S1 状态时:
-
不投钱,则 AB=00,此时肯定不会弹出饮料,也不会找零,因此 YZ=00,此时状态也不会跳转,保持为 S1。
-
投入 5 分钱,则 AB=10,此时售卖机中已经达到 10 分钱,因此会弹出饮料,但不会找零,因此 YZ=10,完成交易后回到初始状态 S0,等待进行下一次交易。
-
投入 10 分钱,则 AB=01,此时售卖机中已经达到 15 分钱,因此会弹出饮料,同时会找零,因此 YZ=11,完成交易后回到初始状态 S0,等待进行下一次交易。
-
同时投入 5 分钱和 10 分钱的情况假设不会发生,因此 AB=11 的情况,默认 YZ=00,并回到初始态 S0。
完善的设计思路
说实话,一般人手中有两枚硬币,肯定直接投 10 分钱就好了,哪有先投 5 分钱再投 10 分钱的呢?是不是傻?
但做设计需要考虑这种特殊的情况,那可能是这个顾客忘了自己有两枚硬币,也可能是上个顾客投了 5 分钱后,发现身上只有 5 分钱就走了,然后第二个顾客过来捡了便宜(虽然有点扯)。
所以只要有可能发生的情况,在设计时都必须考虑到,尤其是实际的项目中比这复杂的情况都要尽量考虑到,而这些地方恰恰是容易发生问题,产生 bug 的地方,需要尤其认真仔细。
第四步 画出卡诺图。

投币机的卡诺图
第五步 卡诺图化简并输出计算公式。

投币机的状态转化式
最后的 Verilog 代码编写给同学留到实验练习中进行完成。