跳转至

猜数字游戏

还记得第一次课上我们演示的猜数字小游戏吗?在本方向中,我们将带大家自顶向下地实现这个项目。

2.2.1 游戏介绍

猜数字小游戏的基本过程如下:首先,程序随机生成 3 个介于 0 ~ 5 之间的、互不相同的数码,由玩家进行猜测。玩家每次通过拨码开关(Switches)输入自己猜测的数据序列,按下按钮表明输入完毕。随后,程序检查玩家最近的三次输入和生成的数码是否匹配,并通过 LED 给出对应的反馈结果。玩家在看到结果之后,会再次按下按钮以继续输入自己的猜测。在玩家操作同时,数码管(Segments)会显示一个倒计时,在倒计时结束之前如果匹配正确则视为用户胜利,否则会视为用户失败。之后,用户再次按下按钮,以开始一局新的游戏。

编码开关的输入方式为:

sw[0] - 输入 0
sw[1] - 输入 1
sw[2] - 输入 2
sw[3] - 输入 3
sw[4] - 输入 4
sw[5] - 输入 5

开关的输入为上升沿有效,在其被拨上去时视作输入了一个对应的数字,将开关从开启状态拨动到关闭状态不会触发任何输入动作。按下按钮后,我们仅将最近的三次输入作为用户的最终输入,例如:假定当前的开关状态为 sw = 8'B0011_0011,经过下面的一系列操作:

sw = 8'B0011_0011   // 初始状态
sw = 8'B0010_0011   // 拨下 sw[4]
sw = 8'B0010_1011   // 拨上 sw[3]
sw = 8'B0010_1111   // 拨上 sw[2]
sw = 8'B0010_1110   // 拨下 sw[0]
sw = 8'B0011_1110   // 拨上 sw[5]
sw = 8'B0011_1111   // 拨上 sw[0]

随后按下按钮,则此时程序接收到的输入序列为 12'H250。那么如果用户输入了限制范围之外的数值,例如拨动了 sw[7] 应当怎么处理呢?这个问题我们可以之后再考虑。

按钮的作用

这里我们让按钮作为用户输入完成的确定信号。因此,在用户通过开关输入数据时,后续的匹配模块并不会接收到数据;仅当用户按下按钮后,匹配模块才会接收到用户的最近三次输入信息。

匹配阶段需要比较用户猜测的结果和目标结果之间的相似程度。考虑到我们只有三位数,所有可能的结果可以拆成下面几种情况:

  • 一个数字都没对;
  • 只对了一个数,且位置不正确;
  • 只对了一个数,但位置正确;
  • 对了两个数,但位置都不正确;
  • 对了两个数,但一个位置正确,一个不正确;
  • 对了两个数,且位置都正确;
  • 对了三个数,且位置都不正确;
  • 对了三个数,但一个位置正确,两个不正确;
  • 对了三个数,且位置都正确。

我们可以使用 led[5:0] 表示上面的九种情况。对应的关系如下:

  • led[2:0]:用于指示当前正确但位置不正确的数字数目。其中

    • led[2] 亮起代表 3 个数都正确但位置均不正确
    • led[1] 亮起代表有 2 个数正确但位置均不正确
    • led[0] 亮起代表有 1 个数正确但位置不正确
    • 如果均不亮起代表没有正确的数字
  • led[5:3]:用于指示当前正确且位置正确的数字数目。其中

    • led[5] 亮起代表 3 个数位置都正确
    • led[4] 亮起代表有 2 个数位置正确
    • led[3] 亮起代表有 1 个数位置正确
    • 如果均不亮起代表没有位置正确的数字。

例如,如果用户输入为 023,而当前游戏的答案为 520,则 LED 灯的结果为 led[5:0] = 6'b001_001,即对了两个数,但一个位置正确,一个不正确。如果用户输入为 052,则 LED 灯的结果为 led[5:0] = 6'b000_100,即 3 个数都正确但位置均不正确。九种情况的完整对应关系如下:

  • 一个数字都没对:led[5:0] = 6'b000_000
  • 只对了一个数,且位置不正确:led[5:0] = 6'b000_001
  • 只对了一个数,但位置正确:led[5:0] = 6'b001_000
  • 对了两个数,但位置都不正确:led[5:0] = 6'b000_010
  • 对了两个数,但一个位置正确,一个不正确:led[5:0] = 6'b001_001
  • 对了两个数,且位置都正确:led[5:0] = 6'b010_000
  • 对了三个数,且位置都不正确:led[5:0] = 6'b000_100
  • 对了三个数,但一个位置正确,两个不正确:led[5:0] = 6'b001_010
  • 对了三个数,且位置都正确:led[5:0] = 6'b100_000(这种情况会一闪而过,因为游戏已经胜利了,LED 会全部亮起)

当游戏胜利时,LED 灯会全部亮起,数码管显示 32'H88888888;当游戏失败时,LED 灯会全部熄灭,数码管显示 32'H44444444。为了增加游戏的动态效果,我们让 LED 在用户输入的过程中(还没有按下按钮表示输入完成)呈现 Lab3 所示的流水灯效果。

提示

游戏的流程如下图所示:

Image title

2.2.2 系统设计

根据以上的内容,我们可以设计如下图所示的电路结构。

Image title

整个项目可以分为如下的几个部分:

  • 外设输入。包括:开关输入处理模块、串口输入处理模块等;
  • 外设输出。包括:数码管显示模块、串口输出处理模块等;
  • 计时器。包括:核心计时器、计时复位控制模块等;
  • 数据检查模块。包括:随机数生成器、结果检测模块等;
  • 中央控制器。

我们将分为基础版和进阶版两部分进行说明。

★ 2.2.3 基础内容

为了实现最为基础的功能,Basic 版本的猜数字小游戏硬件结构如下图所示:

Image title

其中,rst 信号由 sw[7] 引出,作为全局复位信号。当 rst 信号为高电平时,系统内所有的寄存器都会被复位。LED_flow 模块为 Lab3 中编写的流水灯模块。

2.2.3.1 开关输入

首先,我们需要确定用户拨动开关的时机以及对应的编号,这就是 SW_Input 模块的作用。模块的端口约定如下:

SW_Input
1
2
3
4
5
6
7
8
module Input(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,
    input                   [ 7 : 0]            sw,

    output      reg         [ 3 : 0]            hex,
    output                  [ 0 : 0]            pulse
);

其中 sw 是来自开发板的开关输入,hex 信号用于指示当前拨动的开关编号,范围是 4'd04'd7。之所以使用 4bits 位宽是为了便于之后的扩展。pulse 信号为时钟同步的高电平脉冲,当某开关由关闭变为开启状态时,pulse 会发出一个时钟周期的高电平信号。

模块的代码框架如下:

SW_Input
 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 Input(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,
    input                   [ 7 : 0]            sw,

    output      reg         [ 3 : 0]            hex,
    output                  [ 0 : 0]            pulse
);
// 三级寄存器边沿检测
reg [7:0] sw_reg_1, sw_reg_2, sw_reg_3;
always @(posedge clk) begin
    if (rst) begin
        sw_reg_1 <= 0;
        sw_reg_2 <= 0;
        sw_reg_3 <= 0;
    end
    else begin
        // TODO:补充边沿检测的代码
    end
end

wire [7:0] sw_change = // TODO:检测上升沿

// TODO:编写代码,产生 hex 和 pulse 信号。
// Hint:这两个信号均为组合逻辑产生。

endmodule

下面是对应的示例波形图(图中存在误差,实际的信号应均为时钟同步的。所有的数据均为十六进制):

Image title

完成 SW_Input 模块后,我们需要引入一个移位寄存器,用于存储用户最近的开关输入情况。

Shift_Reg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module ShiftReg(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,
    input                   [ 3 : 0]            hex,
    input                   [ 0 : 0]            pulse,
    output      reg         [31 : 0]            dout
);
always @(posedge clk) begin
    if (rst)
        dout <= 0;
    else if (pulse)
        dout <= {{dout[27: 0]}, {hex}};
end
endmodule

这样就实现了用户输入的保存。

题目 2-A-1

请根据以上内容,按要求完成本项练习

2.2.3.2 数据检查模块

数据检查模块 Check 用于确定当前用户的输入是否与游戏目标相符,并给出对应的检测结果。模块的端口定义如下:

Check.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module Check(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,

    input                   [11 : 0]            input_number,
    input                   [11 : 0]            target_number,
    input                   [ 0 : 0]            start_check,

    output                  [ 5 : 0]            check_result
);

其中,input_number 为来自用户的输入,target_number 为本局游戏的目标,start_check 信号来自控制单元。当 start_check 信号为高电平时,模块会给出对应的 check_result。在这里,check_result 就是上面提到的 LED 信号。例如:check_result = 6'B001_001 代表有两个数字正确,其中一个数字位置正确,另一个位置不正确。

Check 模块的代码框架如下:

Check.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
module Check(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,

    input                   [11 : 0]            input_number,
    input                   [11 : 0]            target_number,
    input                   [ 0 : 0]            start_check,

    output                  [ 5 : 0]            check_result
);
// 模块内部用寄存器暂存输入信号,从而避免外部信号突变带来的影响
reg [11:0] current_input_data, current_target_data;
always @(posedge clk) begin
    if (rst) begin
        current_input_data <= 0;
        current_target_data <= 0;
    end
    else if (start_check) begin 
        current_input_data <= input_number;
        current_target_data <= target_number;
    end
end

// 使用组合逻辑产生比较结果
wire [3:0] target_number_3, target_number_2, target_number_1;
wire [3:0] input_number_3, input_number_2, input_number_1;
assign input_number_1 = current_input_data[3:0];
assign input_number_2 = current_input_data[7:4];
assign input_number_3 = current_input_data[11:8];
assign target_number_1 = current_target_data[3:0];
assign target_number_2 = current_target_data[7:4];
assign target_number_3 = current_target_data[11:8];

reg i1t1, i1t2, i1t3, i2t1, i2t2, i2t3, i3t1, i3t2, i3t3;
always @(*) begin
    i1t1 = (input_number_1 == target_number_1);
    i1t2 = (input_number_1 == target_number_2);
    i1t3 = (input_number_1 == target_number_3);
    i2t1 = (input_number_2 == target_number_1);
    i2t2 = (input_number_2 == target_number_2);
    i2t3 = (input_number_2 == target_number_3);
    i3t1 = (input_number_3 == target_number_1);
    i3t2 = (input_number_3 == target_number_2);
    i3t3 = (input_number_3 == target_number_3);
end

// TODO:按照游戏规则,补充 check_result 信号的产生逻辑

endmodule
Tips

实际上,Check 模块可以被设计成一个纯粹的组合逻辑模块,即对于任何给定的 target_numberinput_number,都能异步地给出 check_result 信号。你可以自行选择自己喜欢的实现方式。

提示

Basic 版本的猜数字游戏无法随机生成题目,因此在例化 Check 模块时,可以先暂时将 target_number 设定成一个固定常数。

题目 2-A-2

请根据以上内容,按要求完成本项练习

2.2.3.3 计时器

计时器模块 Timer 用于控制倒计时的开启与关闭。模块的端口定义如下:

Timer.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module Timer(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,

    input                   [ 0 : 0]            set,
    input                   [ 0 : 0]            en,

    output                  [ 7 : 0]            minute,
    output                  [ 7 : 0]            second,
    output                  [11 : 0]            micro_second,

    output                  [ 0 : 0]            finish
);

其中,set 信号用于设置计时器的起始值。当 set 信号有效时,内部的计时器会被设置为 01 分 00 秒 000 毫秒。en 信号来自控制单元。当 en 信号有效时,计时器会持续倒计时;当 en 信号为低电平时,计时器会暂停倒计时(而不是清零或复位)。finish 信号在内部计时器值为 0 分 0 秒 000 毫秒时为高电平,用于告知控制单元此时已经计时完毕。minutesecondmicro_second 分别对应当前计时器的分、秒、微秒数值。

提醒

上述所有的数值均为二进制表示,而不是 BCD 码表示。也就是说,minute 的上限值是 8'D598'H37,而不是 8'B0101_1001

计时器内部的核心为串行计时单元和一个状态机。串行计时单元基于如下的 Clock 模块:

Clock.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
module Clock # (
    parameter               WIDTH               = 32,
    parameter               MIN_VALUE           = 0,
    parameter               MAX_VALUE           = 999,
    parameter               SET_VALUE           = 500    
) (
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,

    input                   [ 0 : 0]            set,
    input                   [ 0 : 0]            carry_in,
    output      reg         [ 0 : 0]            carry_out,
    output      reg         [WIDTH-1: 0]        value
);

always @(posedge clk) begin
    if (rst)
        value <= 0;
    else if (set)
        value <= SET_VALUE;
    else if (carry_in) begin
        if (value <= MIN_VALUE)
            value <= MAX_VALUE;
        else
            value <= value - 1;
    end
end
always @(*)
    carry_out = (carry_in) && (value == MIN_VALUE);
endmodule

Clock 模块是串行计数器的基本模块。其中 carry_in 是来自低位的进位信号,carry_out 是向高位的进位信号。Clock 模块仅在 carry_in 有效时倒计时一次,仅在倒计时达到下限 MIN_VALUE 时发出一次 carry_out 信号。下面是计时器的基本结构:

Image title

毫秒计数器每 1ms 跳动一次,当减少到 0 时触发秒计数器跳动一次;秒计数器为 0 时触发分计数器跳动一次,依次类推,这样就实现了串行计数。

计时器内部的状态机则比较简单,仅包含两个状态。

Image title

计时器在计时状态下才会启动毫秒计时器,用于产生 micro_second_clock 的 carry_in 信号;在停止状态下,该信号将保持为 0。

Timer 模块的代码框架如下:

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
66
67
68
69
70
71
72
73
74
75
module Timer(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,

    input                   [ 0 : 0]            set,
    input                   [ 0 : 0]            en,

    output                  [ 7 : 0]            minute,
    output                  [ 7 : 0]            second,
    output                  [11 : 0]            micro_second,

    output                  [ 0 : 0]            finish
);

reg current_state, next_state;
localparam ON = 1;
localparam OFF = 0;

always @(posedge clk) begin
    if (rst)
        current_state <= OFF;
    else
        current_state <= next_state;
end
// TODO: Finish the FSM


localparam TIME_1MS = 100_000_000 / 1000;
reg [31 : 0] counter_1ms;
// TODO: Finish the counter

wire carry_in[2:0];
Clock # (
    .WIDTH                  (8)    ,
    .MIN_VALUE              (0)    ,
    .MAX_VALUE              (59)   ,
    .SET_VALUE              (1)      
) minute_clock (
    .clk                    (clk),
    .rst                    (rst),
    .set                    (set),
    .carry_in               (carry_in[2]),
    .carry_out              (finish),
    .value                  (minute)
);
Clock # (
    .WIDTH                  (8)   ,
    .MIN_VALUE              (0)   ,
    .MAX_VALUE              (59)  ,
    .SET_VALUE              (0)      
) second_clock (
    .clk                    (clk),
    .rst                    (rst),
    .set                    (set),
    .carry_in               (carry_in[1]),
    .carry_out              (carry_in[2]),
    .value                  (second)
);
Clock # (
    .WIDTH                  (12)   ,
    .MIN_VALUE              (0)    ,
    .MAX_VALUE              (999)  ,
    .SET_VALUE              (0)      
) micro_second_clock (
    .clk                    (clk),
    .rst                    (rst),
    .set                    (set),
    .carry_in               (carry_in[0]),
    .carry_out              (carry_in[1]),
    .value                  (micro_second)
);
// TODO: what's carry_in[0] ?


endmodule

题目 2-A-3

请根据以上内容,按要求完成本项练习

2.2.3.4 核心控制器

Control 模块用于控制整个电路的正常工作。其端口约定如下:

Control.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module Control (
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,
    input                   [ 0 : 0]            btn,

    input                   [ 5 : 0]            check_result,
    output      reg         [ 0 : 0]            check_start,
    output      reg         [ 0 : 0]            timer_en,
    output      reg         [ 0 : 0]            timer_set,
    input                   [ 0 : 0]            timer_finish,

    output      reg         [ 1 : 0]            led_sel,
    output      reg         [ 1 : 0]            seg_sel
);

你需要结合数据通路与各个子模块的定义,自行设计 Control 模块中的状态机,并确定各个信号的产生逻辑。

题目 2-A-4

请根据以上内容,按要求完成本项练习

至此,我们已经实现了 Basic 版本的大部分内容。

题目 2-A-5

请根据以上内容,按要求完成本项练习

2.2.4 进阶内容

进阶内容是在 Basic 版本的基础上进行的细节添加。

2.2.4.1 BCD 显示

你可能已经注意到了,目前数码管的倒计时是十六进制的格式。这是由于我们从 Timer 模块直接将结果 current_time 连到了数码管的输出上,而 Segment 模块是直接将 output_data 的某几位作为 seg_data 的输出的。

现在,我们需要让数码管的倒计时显示为 8421BCD 编码的格式。为此我们需要在 Timer 模块和 Segment 模块之间添加一个 Hex2BCD 模块。该模块可以将输入的十六进制(其实也就是二进制)数据转换为对应的十进制 BCD 码。例如:

8'H0A -> 10 -> 8'B0001_0000
8'H1C -> 28 -> 8'B0010_1000
8'H20 -> 32 -> 8'B0011_0010

关于如何进行编码转换,你可以参考这篇文章这篇文章的相关内容进行设计。

Tips

本题允许使用循环语句。但如果你使用了循环语句,那么需要在检查时向助教介绍这段代码实际对应的电路结构。

2.2.4.2 闪烁的倒计时

Lab3 的选择性必做题目 3 里,我们设计了一个带有掩码功能的 Segment 模块。现在,我们希望基于该模块添加倒计时闪烁的功能。描述如下:

  • 剩余时间大于 10s:不闪烁
  • 剩余时间大于 3s 且不超过 10s:以 1Hz 的频率闪烁(数码管亮起 0.5s,熄灭 0.5s)
  • 剩余时间不超过 3s:以 2Hz 的频率闪烁(数码管亮起 0.25s,熄灭 0.25s)
  • 胜利或失败界面:不闪烁

请自行设计模块并修改数据通路,实现上述的功能。

2.2.4.3 随机数生成器

现在的猜数字游戏只有一道题目,因此只能进行一局游戏。为了支持更为多元的游戏模式,我们希望每一局游戏的答案都能「随机」产生。这就是随机数生成器的作用了。

首先,我们需要明确题目可能的数目。对于六个数码组成的无重复数值,总共有 \(6\times5\times4=120\) 种不同的结果。我们使用一个 reg 型数组 targets 进行存储:

reg [11:0] targets [0: 127];
always @(posedge clk) begin
    // 这里用 initial 也可以,只要让 targets 中的数值保持不变即可。
    targets[0] <= 12'h012;
    targets[1] <= 12'h013;
    targets[2] <= 12'h014;
    targets[3] <= 12'h015;
    targets[4] <= 12'h021;
    targets[5] <= 12'h023;
    targets[6] <= 12'h024;
    targets[7] <= 12'h025;
    targets[8] <= 12'h031;
    targets[9] <= 12'h032;
    targets[10] <= 12'h034;
    targets[11] <= 12'h035;
    targets[12] <= 12'h041;
    targets[13] <= 12'h042;
    targets[14] <= 12'h043;
    targets[15] <= 12'h045;
    targets[16] <= 12'h051;
    targets[17] <= 12'h052;
    targets[18] <= 12'h053;
    targets[19] <= 12'h054;

    targets[20] <= 12'h102;
    targets[21] <= 12'h103;
    targets[22] <= 12'h104;
    targets[23] <= 12'h105;
    targets[24] <= 12'h120;
    targets[25] <= 12'h123;
    targets[26] <= 12'h124;
    targets[27] <= 12'h125;
    targets[28] <= 12'h130;
    targets[29] <= 12'h132;
    targets[30] <= 12'h134;
    targets[31] <= 12'h135;
    targets[32] <= 12'h140;
    targets[33] <= 12'h142;
    targets[34] <= 12'h143;
    targets[35] <= 12'h145;
    targets[36] <= 12'h150;
    targets[37] <= 12'h152;
    targets[38] <= 12'h153;
    targets[39] <= 12'h154;

    targets[40] <= 12'h201;
    targets[41] <= 12'h203;
    targets[42] <= 12'h204;
    targets[43] <= 12'h205;
    targets[44] <= 12'h210;
    targets[45] <= 12'h213;
    targets[46] <= 12'h214;
    targets[47] <= 12'h215;
    targets[48] <= 12'h230;
    targets[49] <= 12'h231;
    targets[50] <= 12'h234;
    targets[51] <= 12'h235;
    targets[52] <= 12'h240;
    targets[53] <= 12'h241;
    targets[54] <= 12'h243;
    targets[55] <= 12'h245;
    targets[56] <= 12'h250;
    targets[57] <= 12'h251;
    targets[58] <= 12'h253;
    targets[59] <= 12'h254;

    targets[60] <= 12'h301;
    targets[61] <= 12'h302;
    targets[62] <= 12'h304;
    targets[63] <= 12'h305;
    targets[64] <= 12'h310;
    targets[65] <= 12'h312;
    targets[66] <= 12'h314;
    targets[67] <= 12'h315;
    targets[68] <= 12'h320;
    targets[69] <= 12'h321;
    targets[70] <= 12'h324;
    targets[71] <= 12'h325;
    targets[72] <= 12'h340;
    targets[73] <= 12'h341;
    targets[74] <= 12'h342;
    targets[75] <= 12'h345;
    targets[76] <= 12'h350;
    targets[77] <= 12'h351;
    targets[78] <= 12'h352;
    targets[79] <= 12'h354;

    targets[80] <= 12'h401;
    targets[81] <= 12'h402;
    targets[82] <= 12'h403;
    targets[83] <= 12'h405;
    targets[84] <= 12'h410;
    targets[85] <= 12'h412;
    targets[86] <= 12'h413;
    targets[87] <= 12'h415;
    targets[88] <= 12'h420;
    targets[89] <= 12'h421;
    targets[90] <= 12'h423;
    targets[91] <= 12'h425;
    targets[92] <= 12'h430;
    targets[93] <= 12'h431;
    targets[94] <= 12'h432;
    targets[95] <= 12'h435;
    targets[96] <= 12'h450;
    targets[97] <= 12'h451;
    targets[98] <= 12'h452;
    targets[99] <= 12'h453;

    targets[100] <= 12'h501;
    targets[101] <= 12'h502;
    targets[102] <= 12'h503;
    targets[103] <= 12'h504;
    targets[104] <= 12'h510;
    targets[105] <= 12'h512;
    targets[106] <= 12'h513;
    targets[107] <= 12'h514;
    targets[108] <= 12'h520;
    targets[109] <= 12'h521;
    targets[110] <= 12'h523;
    targets[111] <= 12'h524;
    targets[112] <= 12'h530;
    targets[113] <= 12'h531;
    targets[114] <= 12'h532;
    targets[115] <= 12'h534;
    targets[116] <= 12'h540;
    targets[117] <= 12'h541;
    targets[118] <= 12'h542;
    targets[119] <= 12'h543;
end

这样,每一局游戏只需要随机生成一个范围在 0 ~ 119 之间的下标 index,即可得到对应的游戏目标 targets[index] 了。

下面,我们给出 Random.v 模块的基础端口代码。

Random.v
1
2
3
4
5
6
module Random(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rst,
    input                   [ 0 : 0]            generate_random,
    output                  [11 : 0]            random_data
);

其中:generate_random 信号来自控制模块,高电平有效。在时钟上升沿,如果发现 generate_random 信号为 1,则需要给出一个新的 12bits 随机数 random_data。一个最为简单的递增实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
reg [7 : 0] index;
always @(posedge clk) begin
    if (rst)
        index <= 0;
    else if (generate_random) begin
        if (index < 119)
            index <= index + 1;
        else
            index <= 0;
    end
end
assign random_data = target[index];

这样,每一局游戏的答案以 120 为周期进行循环。你可以在为 target 初始化时将数据随机排序(而不是现在的顺序排序),从而得到更加随机的结果。

接下来,我们需要引入硬件的伪随机。一个直观的思路就是将上一次的 index、用户输入 sw 以及当前系统运行的时间(可以维护一个计数器)作为因变量,控制 index 的产生逻辑。例如:

always @(posedge clk) begin
    if (rst)
        index <= 0;
    else
        index <= (index_old + sw_seed + timer_seed) % 120;
end

现在,由于每一次用户的输入序列不同,以及对应的游戏时间不同,游戏仅有 rst 后的第一次答案固定,之后的答案都将随机产生。你需要将结合了用户输入 sw 以及当前系统运行的时间的 Random 模块正确接入通路,实现随机的题目生成功能。为此你可能需要为 Random 模块增加一些端口。

2.2.4.4 串口命令

为什么串口方向的练习会与串口没关系呢?这不就来了嘛!

我们希望为猜数字小游戏增加一些串口交互。你需要添加如下的内容:

  • a;:在串口界面输出当前游戏的答案 target_number。此时通过开关输入答案后游戏可以正常进入胜利状态。
  • n;:开始一局新的游戏。此时游戏的答案需要发生变化,计时器及核心状态机也需要改动。注意:n; 命令与 rst 信号的功能并不等价。
  • p;:暂停计时器。输入后游戏的计时器暂停工作,其他模块则正常进行。再次输入后计时器则会继续工作。

你可以参考串口通信的教程完成本部分设计。为此,你需要自行设计部分模块,并修改数据通路。

题目 2-A-6

请根据以上内容,按要求完成本项练习


最后更新: December 21, 2023

评论

Authors: wintermelon008