跳转至

案例分析

在本小节中,我们将为大家介绍一些常见的时序逻辑电路,包括 FPGAOL 的数码管显示、移位寄存器等。此外,我们还会介绍一些现实中可以使用时序逻辑电路解决的问题,从需求出发,一步步设计组合逻辑电路,并进行编程。

3.1 FPGAOL 的七段数码管显示

Lab3 中已经有对七段数码管的结构解释。在学过时序逻辑电路后,我们可以更深一步地运用这些数码管来实现一些简单的项目。

首先,我们复习一下 Lab3 所学习的关于 FPGAOL 的七段数码管显示知识。

FPGAOL 平台一共拥有 9 个七段数码管,但细心的同学会发现 9 个数码管中最左边的与其他八个数码管画风不太一样,接下来会一一向你们解释。

首先是最左边的单个七段数码管(其实是八段,其中一个是小数点)的架构

Image title

单个七段数码管

我们可以看到数码管的每一段都是一个发光二极管,每一个发光二极管由 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'b01110011;
    endcase
end

endmodule

接下来我们考虑剩下的八个数码管,它们实际上是一个组,以下是结构图。

Image title

八个分时复用七段数码管

仔细观察上面的结构图,我们可以发现所有数码管的输入是公用的,由一个数码管译码器提供,这个数码管译码器的逻辑与与上面的 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
set_property -dict { PACKAGE_PIN E3    IOSTANDARD LVCMOS33 } [get_ports { clk }];
set_property -dict { PACKAGE_PIN B18   IOSTANDARD LVCMOS33 } [get_ports { rst }];

set_property -dict { PACKAGE_PIN A14   IOSTANDARD LVCMOS33 } [get_ports { seg_data[0] }];
set_property -dict { PACKAGE_PIN A13   IOSTANDARD LVCMOS33 } [get_ports { seg_data[1] }];
set_property -dict { PACKAGE_PIN A16   IOSTANDARD LVCMOS33 } [get_ports { seg_data[2] }];
set_property -dict { PACKAGE_PIN A15   IOSTANDARD LVCMOS33 } [get_ports { seg_data[3] }];
set_property -dict { PACKAGE_PIN B17   IOSTANDARD LVCMOS33 } [get_ports { seg_an[0] }];
set_property -dict { PACKAGE_PIN B16   IOSTANDARD LVCMOS33 } [get_ports { seg_an[1] }];
set_property -dict { PACKAGE_PIN A18   IOSTANDARD LVCMOS33 } [get_ports { seg_an[2] }];

接下来烧写比特流并上板就可以看到一个运行得很好但并无啥用的时钟了。

3.2 移位寄存器

在数字电路中,移位寄存器(英語:shift register)是一种在若干相同时间脉冲下工作的以触发器级联为基础的器件,每个触发器的输出接在触发器链的下一级触发器的「数据」输入端,使得电路在每个时间脉冲内依次向左或右移动一个比特,在输出端进行输出。

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 (en_pos) 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
set_property -dict { PACKAGE_PIN E3    IOSTANDARD LVCMOS33 } [get_ports { clk }];

## 输入使能
set_property -dict { PACKAGE_PIN B18   IOSTANDARD LVCMOS33 } [get_ports { en }];

## 移位寄存器的输出值
set_property -dict { PACKAGE_PIN C17   IOSTANDARD LVCMOS33 } [get_ports { led_sig[0] }];
set_property -dict { PACKAGE_PIN D18   IOSTANDARD LVCMOS33 } [get_ports { led_sig[1] }];
set_property -dict { PACKAGE_PIN E18   IOSTANDARD LVCMOS33 } [get_ports { led_sig[2] }];
set_property -dict { PACKAGE_PIN G17   IOSTANDARD LVCMOS33 } [get_ports { led_sig[3] }];
set_property -dict { PACKAGE_PIN D17   IOSTANDARD LVCMOS33 } [get_ports { led_sig[4] }];
set_property -dict { PACKAGE_PIN E17   IOSTANDARD LVCMOS33 } [get_ports { led_sig[5] }];
set_property -dict { PACKAGE_PIN F18   IOSTANDARD LVCMOS33 } [get_ports { led_sig[6] }];
set_property -dict { PACKAGE_PIN G18   IOSTANDARD LVCMOS33 } [get_ports { led_sig[7] }];

## 移位寄存器的输入值(单个七段数码管输出)
set_property -dict { PACKAGE_PIN D14   IOSTANDARD LVCMOS33 } [get_ports { din[0] }];
set_property -dict { PACKAGE_PIN F16   IOSTANDARD LVCMOS33 } [get_ports { din[1] }];
set_property -dict { PACKAGE_PIN G16   IOSTANDARD LVCMOS33 } [get_ports { din[2] }];
set_property -dict { PACKAGE_PIN H14   IOSTANDARD LVCMOS33 } [get_ports { din[3] }];

## 复位使能
set_property -dict { PACKAGE_PIN H14   IOSTANDARD LVCMOS33 } [get_ports { rst }];

## 移位寄存器存储值(八个七段数码管输出)
set_property -dict { PACKAGE_PIN A14   IOSTANDARD LVCMOS33 } [get_ports { seg_data[0] }];
set_property -dict { PACKAGE_PIN A13   IOSTANDARD LVCMOS33 } [get_ports { seg_data[1] }];
set_property -dict { PACKAGE_PIN A16   IOSTANDARD LVCMOS33 } [get_ports { seg_data[2] }];
set_property -dict { PACKAGE_PIN A15   IOSTANDARD LVCMOS33 } [get_ports { seg_data[3] }];
set_property -dict { PACKAGE_PIN B17   IOSTANDARD LVCMOS33 } [get_ports { seg_an[0] }];
set_property -dict { PACKAGE_PIN B16   IOSTANDARD LVCMOS33 } [get_ports { seg_an[1] }];
set_property -dict { PACKAGE_PIN A18   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 这两种状态。

第三步 画状态转移图

Image title

投币机的 FSM

在 S0 状态时:

  1. 不投钱,则 AB=00,此时肯定不会弹出饮料,也不会找零,因此 YZ=00,此时状态也不会跳转,保持为 S0。

  2. 投入 5 分钱,则 AB=10,此时还不够 10 分钱,因此不会弹出饮料,也不会找零,因此 YZ=00,此时状态将跳转为 S1。

  3. 投入 10 分钱,则 AB=01,此时售卖机中已经达到 10 分钱,因此会弹出饮料,但不会找零,因此 YZ=10,完成交易后回到初始状态 S0,等待进行下一次交易。

  4. 同时投入 5 分钱和 10 分钱的情况假设不会发生,因此 AB=11 的情况,默认 YZ=00,并回到初始态 S0。

S1 状态时:

  1. 不投钱,则 AB=00,此时肯定不会弹出饮料,也不会找零,因此 YZ=00,此时状态也不会跳转,保持为 S1。

  2. 投入 5 分钱,则 AB=10,此时售卖机中已经达到 10 分钱,因此会弹出饮料,但不会找零,因此 YZ=10,完成交易后回到初始状态 S0,等待进行下一次交易。

  3. 投入 10 分钱,则 AB=01,此时售卖机中已经达到 15 分钱,因此会弹出饮料,同时会找零,因此 YZ=11,完成交易后回到初始状态 S0,等待进行下一次交易。

  4. 同时投入 5 分钱和 10 分钱的情况假设不会发生,因此 AB=11 的情况,默认 YZ=00,并回到初始态 S0。

完善的设计思路

说实话,一般人手中有两枚硬币,肯定直接投 10 分钱就好了,哪有先投 5 分钱再投 10 分钱的呢?是不是傻?

但做设计需要考虑这种特殊的情况,那可能是这个顾客忘了自己有两枚硬币,也可能是上个顾客投了 5 分钱后,发现身上只有 5 分钱就走了,然后第二个顾客过来捡了便宜(虽然有点扯)。

所以只要有可能发生的情况,在设计时都必须考虑到,尤其是实际的项目中比这复杂的情况都要尽量考虑到,而这些地方恰恰是容易发生问题,产生 bug 的地方,需要尤其认真仔细。

第四步 画出卡诺图

Image title

投币机的卡诺图

第五步 卡诺图化简并输出计算公式

Image title

投币机的状态转化式

最后的 Verilog 代码编写给同学留到实验练习中进行完成。


最后更新: November 16, 2023

评论