【数电】北邮电子宠物机设计小结

摘要

小时候可能玩过宠物孵化玩具,可以通过按键交互实现宠物的孵化,喂养等有趣的功能。本次实验尝试基于可编程器件实验板DTCE-EDA_V设计了电子宠物孵化器的系统构架与并用代码实现宠物孵化功能。基于Verilog实现了包含顶层模块EPEt,LCD1602,img,music,seg,rand,button在内的7大模块。

关键词

关键词 :电子宠物孵化器,Verilog,FPGA,多模块。

项目要求

在数字实验板上的 8×8 点阵上模拟一个电子宠物“孵化器”的工作过程。

基本要求

  1. SW7 为总开关,SW7=‘0’时,所有器件不显示,SW7 拨到‘1’时,8×8 点阵显示一只 黄色的“蛋”;
  2. SW0 为温度选择键,SW0=‘1’时,LD2(黄灯)亮,表示孵化器温度适合孵化, SW0=‘0’时,LD0(蓝灯)亮,表示孵化器温度偏低,无法孵化;
  3. BTN0 为孵化启动键,按下后如果温度合适(SW0=‘1’),8×8 点阵显示“蛋”的形状 每 2 秒钟发生一次变化,变化顺序如图 1~图 6;变化过程中,如果温度偏低 (SW0=‘1’)则变化停止,等到温度合适后继续变化,如果温度偏低持续时间超过 5 秒,则“蛋”变成绿色,表示孵化失败;
  4. 当“蛋”的形状变化到图 6 后,孵化过程不再受温度影响,每 2 秒依次显示图 7、图 8 和图 9 动物图案,表示孵化成功;
  5. 孵化出的动物有蛇、小鸡、海龟、恐龙 4 种,具体孵化结果为四种动物中的随机一 种,4 种动物图案如下:
  6. 按下 BTN0 后开始计时,记录整个孵化时间,孵化成功或失败后计时停止,同时 用 DISP7~DISP6 两个数码管显示计时;
  7. 孵化成功或失败后按动 BTN0 即可开始新的孵化过程。

图片1.png
图片2.png
图片3.png

提高要求

  1. 为相关状态和过程配加合适的音效或音乐;
  2. 自拟其他功能。

系统设计

从整体上而言,整个系统解耦为七大模块。

设计思路
首先对整个系统进行初步构想:将系统视为黑盒,其输入输出应该包含:

  • 控制类

    • 电源开关
    • “孵化”启动/重启按钮
    • 温度选择开关
  • 显示类

      • 蜂鸣器
      • 8*8双色LED点阵(孵化图案)
      • 数码管(总计孵化时间)
      • 发光LED阵列(左半部分提示进入下一阶段的时间,右半部分提示温度状态)
      • LCD1602(文字提示)

考虑到状态的切换问题,使用状态机来管理孵化进程是一个看起来很不错的解决方案。于是我们可以考虑设计一个寄存器变量pet_state,使用数字代替表示不同的孵化状态。

另外,为了方便代码的调试管理,并且为未来可能的进一步开发提供灵活性和便捷性,应该将整个系统的功能代码解耦分离,分解为多个功能各异的模块。本次实验内,我将整个系统大致功能代码分类如下:

  • 从面向对象编程的设计原理来说,应该把显示逻辑从主逻辑中分离。于是我们应该分离出LCD1602,img,seg三大模块。这三者的显示都有扫描,译码,显示三大步骤。
  • 蜂鸣器的声波生成应该也要单独分离。
  • 随机数生成器可以作为一个单独的模块,当业务拓展时,仅需改变接口位宽即可满足发展的需求。
  • 按键消抖功能应该单独分离出一个模块。这样子就可以实现多按键时代码轻松复用。
  • 整个系统存在需要高频率的部分(8*8点阵,LCD1602,蜂鸣器),所以时钟频率选择10MHz。

总体框图

图片4.png

分块设计

按键消抖模块button

按键消抖模块的设计灵感来源于00-19计数器的实验。我们的目的是通过延迟计时的方式滤除不稳定的抖动,这种抖动一般在30ms之内。

图片5.png
图片6.png

我们可以设计模块端口如下:

module button(
    input wire btn, // 按钮
    input wire CLK, // 时钟信号
    input wire RST, // 复位信号
    output reg btn_deb // 去抖后的按钮
);
  • 寄存器变量cnt负责CLK计数,产生30ms计时。
  • 寄存器变量btn_deb是消抖后的稳定输出
  • 寄存器变量stable_flag是按键稳定的标志,这个变量可以排除稳定后的长按信号,使得btn_deb的输出为长一个时钟的瞬时脉冲。

在顶层模块EPet相关代码整合如下:

wire btn0_deb; // 按钮0去抖后输出
/* 按钮去抖 */
button u0(
    .btn(btn0),
    .CLK(clk),
    .RST(rst),
    .btn_deb(btn0_deb)
);

button模块综合代码如下:

/*
    按钮去抖模块解读
*/

module button(
    input wire btn, // 按钮
    input wire CLK, // 时钟信号
    input wire RST, // 复位信号
    output reg btn_deb // 去抖后的按钮
);

parameter TIME_30MS = 'd300000; //30ms消抖

reg [18:0] cnt; // 计数器
reg stable_flag; // 稳定标志

always @(posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
      cnt <= 'd0;
    end else if (cnt == TIME_30MS) begin
      cnt <= 'd0;
    end else if (btn == 1'b1) begin
      cnt <= cnt + 'd1;
    end else begin
      cnt <= 'd0;
    end
end

always @(posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
      stable_flag <= 1'b0;
    end else if (btn == 1'b1 && cnt == TIME_30MS) begin
      stable_flag <= 1'b1;
    end else if (btn == 1'b0) begin
      stable_flag <= 1'b0;
    end
end

always @(posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
      btn_deb <= 1'b0;
    end else if (stable_flag == 1'b0 && cnt == TIME_30MS) begin
      btn_deb <= 1'b1;
    end else begin
      btn_deb <= 1'b0;
    end
end
endmodule

数码管显示模块seg

本次实验用实验板的数码管是并联在一起的,所以我们需要通过扫描的方式输出数字。

图片7.png
图片8.png
图片9.png

设计思路同样遵循00-19计数器,不同的是待显示数字并不是先输入后分离的,而是在顶层模块EPet中就已经实现的分离(因为计时时候就是按照个位和十位分开累加的)。具体可看模块例化部分:

/* 数码管显示时间 */
seg u4(
    .power(power),
    .CLK(clk),
    .num({seg_display_2[3:0], seg_display_1[3:0]}), //是个位和十位的拼接
    .seg(seg[6:0]),
    .cat(seg_cat[7:0])
);

综上,seg模块代码如下:

/*
    数码管模块解读附录
    num: 四位宽一个数字,一共二位数字,8位宽
*/

module seg(
    input wire power, // 电源开关
    input wire [7:0] num, // 待显示的数字
    input wire CLK, // 时钟信号
    output reg [6:0] seg, // 数码管
    output reg [7:0] cat // 位选

);

reg select = 'd0; // 当前显示的数码管编号
reg [3:0] bin; // 待显示的数字的二进制表示

// 扫描

always @ (posedge CLK or negedge power) begin
    if (power == 1'b0) begin
        select <= 1'b0;
    end else begin
        select <= ~select;
    end
end

always @ (*) begin
    if (power == 1'b0) begin // 电源关闭时,数码管全灭
        cat <= 8'b1111_1111;
    end else begin
        case (select)
            1'b0: cat <= 8'b1111_1110;
            1'b1: cat <= 8'b1111_1101;
            default: cat <= 8'b1111_1111;
        endcase
    end
end

// 译码

always @ (*) begin // 分离显示数字
    case (select)
        2'b00: bin = num[3:0]; // 个位
        2'b01: bin = num[7:4]; // 十位
        default: bin = 4'b0000;
    endcase
end

always @ (*) begin
    case (bin)
        4'd0: seg = 7'b1111110; // 共阴极,低电平有效
        4'd1: seg = 7'b0110000;
        4'd2: seg = 7'b1101101;
        4'd3: seg = 7'b1111001;
        4'd4: seg = 7'b0110011;
        4'd5: seg = 7'b1011011;
        4'd6: seg = 7'b1011111;
        4'd7: seg = 7'b1110000;
        4'd8: seg = 7'b1111111;
        4'd9: seg = 7'b1111011;
        default: seg = 7'b0000000;
    endcase
end

endmodule

随机数生成模块rand

这里的随机数并不是严格意义上的随机数,而是伪随机数。

在数电课上我们已经学过线性伪随机数序列(M序列),于是我们可以使用代码实现这一线性反馈移位寄存器(LFSR)序列。

图片10.png

这里我们设定M序列位宽为8,反馈多项式可以任意选取,本次实验我选择的反馈函数为:

$$rand_0=rand_7⊕rand_2⊕rand_1$$

我们设定每个时钟刻LFSR更新一次,并调试好随机种子(RAND_SEED本次实验选用8'd0100_0010)这样我们就可以实现随机了。(虽然我们的随机种子是固定设好的,但是每次我们启动孵化的时间,孵化过程的长短都不可能严格一致,这样就导致从上电到截取随机数的这段时间内所含时钟刻随机,进而造成结果随机)

综上,模块代码如下:

/*
    随机数生成器模块解读附录
    线性反馈移位寄存器(LFSR)
    M序列:反馈多项式取7,2,1
*/

module rand(
    input wire RST, // 复位信号
    input wire CLK, // 时钟信号
    output reg [7:0] rand // 随机数
);

parameter RAND_SEED = 8'd0100_0010; // 随机数种子

initial begin
    rand = RAND_SEED;
end

always @ (posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
      rand <= 3'd0;
    end else begin
      rand[0] <= rand[7] ^ rand[2] ^ rand[1];
      rand[1] <= rand[0];
      rand[2] <= rand[1];
      rand[3] <= rand[2];
      rand[4] <= rand[3];
      rand[5] <= rand[4];
      rand[6] <= rand[5];
      rand[7] <= rand[6];
    end
end

endmodule

需要注意的是,rand输出是一直在随着时钟变化的。为了获得可用的随机数,我们可以任意截取某一时刻的随机数作为输出结果,虽然输出位宽为7,但是我们并不需要这么多位宽的随机数。我们可以仅仅截取8位中的2位即可(因为整个序列随机并且每个位数的01分布几率均匀,数学推导可知截取的子序列也是均匀随机的)。这一代码逻辑在顶层模块EPet中实现:

// some codes were ignored
rand_num[1:0] <= rand_num_current[7:6]; // 保存随机数
// ……又是一些代码
case (rand_num[1:0]) // 随机宠物
     2'b00: img_index <= 'd8; // 蛇
     2'b01: img_index <= 'd9; // 鸡
     2'b10: img_index <= 'd10; // 龟
     2'b11: img_index <= 'd11; // 龙
endcase

蜂鸣器发声模块music

实验板为无源蜂鸣器,所以需要我们手动进行 PWM调制

图片11.png
图片12.png

我们以音阶C2为基准,参照表格设定中音DO-高音DO频率如下(位于顶层模块EPet中):

// 音阶定义
parameter DO = 12'd524;
parameter RE = 12'd587;
parameter MI = 12'd659;
parameter FA = 12'd698;
parameter SO = 12'd784;
parameter LA = 12'd880;
parameter TI = 12'd988;
parameter DO_H = 12'd1047;

为了使得蜂鸣器发声,我们仅需要产生相应频率的矩形波即可。
由于 $T=\frac{1}{f}$ ,有计数上限:

$$n_{\frac{1}{2}}=\frac{T_{note}}{2T_{CP}}=\frac{f_{CP}}{2f_{note}}$$

使用计数器即可很简单地产生(不大于时钟频率的)任意频率波形(除以2是为了使得占空比为50%)。

综合模块代码如下:

/*
    无源蜂鸣器模块解读附录
    
*/

module music(
    input wire power, // 电源开关
    input wire CLK, // 时钟信号
    input wire RST, // 复位信号
    input wire PLAY, // 播放信号
    input wire [11:0] not_freq, // 目标频率
    output reg beep // 蜂鸣器
);

reg [23:0] cnt = 'd0; // 分频计时器

parameter clk_freq = 'd10_000_000;

always @ (posedge CLK) begin
    if (cnt > clk_freq / not_freq / 2) begin
        if (power == 1'b1 && PLAY == 1'b1) begin
            beep <= ~beep;
        end else begin
            beep <= 1'b0;
        end
        cnt <= 'd0;
    end else begin
        cnt <= cnt + 'd1;
    end
end

endmodule

music模块仅仅是为了发声,其控制信号主要有两个:播放控制信号play和目标音阶频率not_freq。

实际控制发声的功能逻辑写在了EPet里:

/* 音乐 */

reg play = 1'd0; // 播放标志
reg [11:0] not_freq = 12'd524; // 声音频率

// 音阶定义
parameter DO = 12'd524;
parameter RE = 12'd587;
parameter MI = 12'd659;
parameter FA = 12'd698;
parameter SO = 12'd784;
parameter LA = 12'd880;
parameter TI = 12'd988;
parameter DO_H = 12'd1047;

music u5(
    .CLK(clk),
    .RST(rst),
    .beep(beep),
    .PLAY(play),
    .not_freq(not_freq[11:0]),
    .power(power)
);

always @ (posedge clk) begin
    if (power == 1'b1) begin
        case (pet_state)
            4'b0001: begin // 孵化中
                if (cnt1s == TIME_1S) begin
                    if (temp_sw) begin // 适宜温度
                        not_freq <= DO;
                    end else begin // 冷
                        not_freq <= RE;
                    end
                    play <= ~play;
                end
            end
            4'b0010: begin // 孵化完成
                if (img_index < 'd8) begin // 孵化完成音乐
                    case (img_index)
                        'd5: not_freq <= LA;
                        'd6: not_freq <= TI;
                        'd7: not_freq <= DO_H;
                        default: ;
                    endcase
                    if (cnt1s == TIME_1S) begin
                        play <= ~play;
                    end
                end else begin
                    play <= 1'b0;
                end
            end
            4'b0011: begin
                play <= 1'b0;
            end
            default: play <= 1'b0;
        endcase
    end else begin
        play <= 1'b0;
    end
end

8*8点阵显示模块img

类似数码管,也是通过扫描的方式显示。

设计亮点:

  • 使用数组的方式储存图像数据,容易修改,简单直观
  • 暴露显示接口img_index,只需要提供图片序号即可直接显示,与主逻辑解耦
  • 实际实验板红色分量远远大于绿色分量,若简单粗暴地直接设定黄色(红绿同时显示),实际效果是偏向红色有些难以区分的。为了让黄色更黄,有设置红绿分量调节常量COLOR_YELLOW_DIV,经过实践测定设置为5比较合适(绿色5份,红色1份)
  • 巧妙设置了“仅显示绿色”模式,很方便的完成了失败时的显示而不必修改原图像数据。每个图像仅需准备一份双色版本即可,大大减少了存储使用量

综合模块代码如下:

/*
    8x8LED点阵模块解读附录
    类似数码管,也是通过扫描的方式显示图像
*/

module img(
    input wire power, // 电源开关
    input wire [3:0] img_index, // 图像编号
    input wire green_mode, // 仅显示绿色模式
    input wire CLK, // 时钟信号
    input wire RST, // 复位信号
    output reg [7:0] img_row, // 图像行
    output reg [7:0] img_col_red, // 图像列(红色)
    output reg [7:0] img_col_green // 图像列(绿色)

);

reg [2:0] cnt = 'd0; // 位选计数器
reg [5:0] cnt_red = 'd0; // 绿色分量计数器
parameter COLOR_YELLOW_DIV = 'd5; // 黄色分量中绿色分量占比(绿色n份,红色1份)

/*  预先设定好的图像集
    第一个 [63:0] 数据宽度 8*8*2 = 128
    第二个 [1:0] 图像编号
*/
reg [127:0] image [0:11];

// 初始化
initial begin
    image [0] <= { // 蛋0
        {
            { //绿灯
                8'b0000_0000,
                8'b0000_0000,
                8'b0001_1000,
                8'b0011_1100,
                8'b0011_1100,
                8'b0001_1000,
                8'b0000_0000,
                8'b0000_0000
            },
            { // 红灯
                8'b0000_0000,
                8'b0000_0000,
                8'b0001_1000,
                8'b0011_1100,
                8'b0011_1100,
                8'b0001_1000,
                8'b0000_0000,
                8'b0000_0000
            }
        }
    };
    // 这里省略了其他图像的数据……

end

// 扫描
always @ (posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
        cnt <= 3'b000;
    end else begin
        if (power == 1'b1) begin
            if (cnt == 3'b111) begin
                cnt <= 3'b000;
                // 黄色占空比调节
                if (cnt_red == COLOR_YELLOW_DIV) begin
                    cnt_red <= 'd0;
                end else begin
                    cnt_red <= cnt_red + 'd1;
                end
            end else begin
                cnt <= cnt + 3'b001;
            end
        end else begin
            cnt <= 3'b000;
        end
    end 
end

// 显示(行的控制)
always @ (posedge CLK) begin
    if (power == 1'b1) begin
        case (cnt)
            3'b000: img_row <= 8'b1111_1110;
            3'b001: img_row <= 8'b1111_1101;
            3'b010: img_row <= 8'b1111_1011;
            3'b011: img_row <= 8'b1111_0111;
            3'b100: img_row <= 8'b1110_1111;
            3'b101: img_row <= 8'b1101_1111;
            3'b110: img_row <= 8'b1011_1111;
            3'b111: img_row <= 8'b0111_1111;
        endcase
    end else begin
        img_row <= 8'b1111_1111;
    end
end

// 显示(列的控制)
always @ (posedge CLK) begin
    case (cnt)
        3'b000: begin
            img_col_red <= image[img_index][7:0];
            img_col_green <= image[img_index][71:64];
        end
        3'b001: begin
            img_col_red <= image[img_index][15:8];
            img_col_green <= image[img_index][79:72];
        end
        3'b010: begin
            img_col_red <= image[img_index][23:16];
            img_col_green <= image[img_index][87:80];
        end
        3'b011: begin
            img_col_red <= image[img_index][31:24];
            img_col_green <= image[img_index][95:88];
        end
        3'b100: begin
            img_col_red <= image[img_index][39:32];
            img_col_green <= image[img_index][103:96];
        end
        3'b101: begin
            img_col_red <= image[img_index][47:40];
            img_col_green <= image[img_index][111:104];
        end
        3'b110: begin
            img_col_red <= image[img_index][55:48];
            img_col_green <= image[img_index][119:112];
        end
        3'b111: begin
            img_col_red <= image[img_index][63:56];
            img_col_green <= image[img_index][127:120];
        end
    endcase

    // 仅显示绿色模式
    if (green_mode == 1'b1) begin
        img_col_red <= 8'b0000_0000;
    end else begin
        // 黄色占空比调节
        if (cnt_red != COLOR_YELLOW_DIV) begin
            img_col_red <= 8'b0000_0000;
        end else begin
            img_col_green <= 8'b0000_0000;
        end
    end
end

endmodule

LCD1602显示模块

LCD1602管脚如下:

图片13.png

其中我们仅仅需要注意数据脚,RS脚和R/W脚(本次实验我们并不需要从LCD读,所以R/W可以直接固定为0)。

LCD1602储存有以下三种:

  • DDRAM:显示数据RAM(可写入)
  • CGRAM:用户可自定义的字模(可写入,00H-0FH)
  • CGROM:内置的常用字模(不可写入,ASCII: 20H-7FH, 日文、希腊:A0H-FFH)

图片14.png
图片15.png
图片16.png
图片17.png
图片18.png
图片19.png
图片20.png
图片21.png
图片22.png

上面是LCD1602的指令集。可见,这里也可以通过状态机的方式控制LCD1602.
为了适配任意时钟,将周期设置为参数TIME_20MS,TIME_500HZ方便例化。

综合代码如下:

/*
    LCD1602显示屏模块解读附录
    DDRAM:显示数据RAM(可写入)
    CGRAM:用户可自定义的字模(可写入,00H-0FH)
    CGROM:内置的常用字模(不可写入,ASCII: 20H-7FH, 日文、希腊:A0H-FFH)
*/

module LCD1602( // 1602液晶显示屏模块
    input wire [127:0] row_1, // row_1[127:0] 为第一行显示内容
    input wire [127:0] row_2, // row_2[127:0] 为第二行显示内容
    input wire CLK, // 时钟信号
    input wire RST, // 复位信号
    output reg LCD_E, // 使能端,当其为下降沿时执行命令
    output reg LCD_RS, // 数据/指令选择端,为0时输入指令,为1时输入数据
    output reg [7:0] LCD_DATA, // 数据输出端(DB7-DB0)
    output reg LCD_RW // 读写选择端,为0时写入,为1时读取

);

// 初始化
initial begin
    LCD_RW <= 1'b0; //读写选择端设置为始终写入
end

// 时钟CLK频率:10MHz = 10^-7s = 10^-4ms

parameter TIME_20MS= 'd20_0000; //需要20ms上电稳定(初始化)
reg [17:0] cnt_20ms;
reg delay_done = 1'b0;

parameter TIME_500HZ = 'd20000; //工作周期
reg [14:0] cnt_500hz;
reg write_flag;

//状态机有40种状态,此处用了格雷码,一次只有一位变化(在二进制下)
parameter IDLE=8'h00;
parameter SET_FUNCTION=8'h01;
parameter DISP_OFF=8'h03;
parameter DISP_CLEAR=8'h02;
parameter ENTRY_MODE=8'h06;
parameter DISP_ON=8'h07;
parameter ROW1_ADDR=8'h05;
parameter ROW1_0=8'h04;
parameter ROW1_1=8'h0C;
parameter ROW1_2=8'h0D;
parameter ROW1_3=8'h0F;
parameter ROW1_4=8'h0E;
parameter ROW1_5=8'h0A;
parameter ROW1_6=8'h0B;
parameter ROW1_7=8'h09;
parameter ROW1_8=8'h08;
parameter ROW1_9=8'h18;
parameter ROW1_A=8'h19;
parameter ROW1_B=8'h1B;
parameter ROW1_C=8'h1A;
parameter ROW1_D=8'h1E;
parameter ROW1_E=8'h1F;
parameter ROW1_F=8'h1D;
parameter ROW2_ADDR=8'h1C;
parameter ROW2_0=8'h14;
parameter ROW2_1=8'h15;
parameter ROW2_2=8'h17;
parameter ROW2_3=8'h16;
parameter ROW2_4=8'h12;
parameter ROW2_5=8'h13;
parameter ROW2_6=8'h11;
parameter ROW2_7=8'h10;
parameter ROW2_8=8'h30;
parameter ROW2_9=8'h31;
parameter ROW2_A=8'h33;
parameter ROW2_B=8'h32;
parameter ROW2_C=8'h36;
parameter ROW2_D=8'h37;
parameter ROW2_E=8'h35;
parameter ROW2_F=8'h34;
 
reg [5:0] c_state; //当前状态
reg [5:0] n_state; //下一状态

// 初始化,上电稳定

always @ (posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
        cnt_20ms <= 1'b0; //复位
    end else if (cnt_20ms == TIME_20MS) begin
        delay_done <= 1'b1; //到达20ms时置1
    end else begin
        cnt_20ms <= cnt_20ms + 1'b1; //未到达20ms时加1
    end

end

// 工作周期分频(LCD1602工作周期500Hz)

always @ (posedge CLK or posedge RST) begin
    if (RST == 1'b1) begin
        cnt_500hz <= 1'b0;
    end else if( delay_done == 1'b1 ) begin
        if(cnt_500hz == TIME_500HZ) begin
            cnt_500hz <= 1'b0;
        end else begin
            cnt_500hz <= cnt_500hz + 1'b1;
        end
    end else begin
        cnt_500hz <= 1'b0;
    end
end

always @ (posedge CLK or posedge RST) begin //使能端,每个工作周期一次下降沿,执行一次命令(/2是为了避免在数据写入未完成时就执行指令)
    if (RST == 1'b1) begin
        LCD_E <= 1'b0;
    end else if (cnt_500hz < TIME_500HZ/2) begin
        LCD_E <= 1'b1;
    end else begin
        LCD_E <= 1'b0;
    end
end

always @ (posedge CLK or posedge RST) begin //每到一个工作周期,write_flag置高一周期
    if (RST == 1'b1) begin
        write_flag <= 1'b0;
    end else if (cnt_500hz == TIME_500HZ) begin
        write_flag <= 1'b1;
    end else begin
        write_flag <= 1'b0;
    end
end

// 状态机
 
//state 指令集对应:RS, R/W, DB7, DB6, DB5, DB4, DB3, DB2, DB1, DB0

always @ (posedge CLK or posedge RST) begin
    if(RST == 1'b1) begin
        c_state <= IDLE;
    end else if(write_flag) begin //每一个工作周期改变一次状态
        c_state <= n_state;
    end
end

always @ (*) begin
    case (c_state) //循环进行扫描显示
        IDLE:n_state=SET_FUNCTION;
        SET_FUNCTION:n_state=DISP_OFF;
        DISP_OFF:n_state=DISP_CLEAR;
        DISP_CLEAR:n_state=ENTRY_MODE;
        ENTRY_MODE:n_state=DISP_ON;
        DISP_ON:n_state=ROW1_ADDR;
        ROW1_ADDR:n_state=ROW1_0;
        ROW1_0:n_state=ROW1_1;
        ROW1_1:n_state=ROW1_2;
        ROW1_2:n_state=ROW1_3;
        ROW1_3:n_state=ROW1_4;
        ROW1_4:n_state=ROW1_5;
        ROW1_5:n_state=ROW1_6;
        ROW1_6:n_state=ROW1_7;
        ROW1_7:n_state=ROW1_8;
        ROW1_8:n_state=ROW1_9;
        ROW1_9:n_state=ROW1_A;
        ROW1_A:n_state=ROW1_B;
        ROW1_B:n_state=ROW1_C;
        ROW1_C:n_state=ROW1_D;
        ROW1_D:n_state=ROW1_E;
        ROW1_E:n_state=ROW1_F;
        ROW1_F:n_state=ROW2_ADDR;
        ROW2_ADDR:n_state=ROW2_0;
        ROW2_0:n_state=ROW2_1;
        ROW2_1:n_state=ROW2_2;
        ROW2_2:n_state=ROW2_3;
        ROW2_3:n_state=ROW2_4;
        ROW2_4:n_state=ROW2_5;
        ROW2_5:n_state=ROW2_6;
        ROW2_6:n_state=ROW2_7;
        ROW2_7:n_state=ROW2_8;
        ROW2_8:n_state=ROW2_9;
        ROW2_9:n_state=ROW2_A;
        ROW2_A:n_state=ROW2_B;
        ROW2_B:n_state=ROW2_C;
        ROW2_C:n_state=ROW2_D;
        ROW2_D:n_state=ROW2_E;
        ROW2_E:n_state=ROW2_F;
        ROW2_F:n_state=ROW1_ADDR;
        default:;
    endcase
end

// RS端控制

always @ (posedge CLK or posedge RST) begin
    if(RST == 1'b1) begin
        LCD_RS <= 1'b0;
    end else if(write_flag == 1'b1) begin //当状态为七个指令任意一个,将RS置为指令输入状态
        if(
            (n_state == SET_FUNCTION) ||
            (n_state == DISP_OFF) ||
            (n_state == DISP_CLEAR) ||
            (n_state == ENTRY_MODE) ||
            (n_state == DISP_ON) ||
            (n_state == ROW1_ADDR) ||
            (n_state == ROW2_ADDR)
        ) begin
            LCD_RS<=1'b0; //为0时输入指令,为1时输入数据
        end else begin
            LCD_RS<=1'b1;
        end
    end
end

// 显示控制

always @ (posedge CLK or posedge RST) begin
    if(RST == 1'b1) begin
        LCD_DATA <= 1'b0;
    end else if(write_flag == 1'b1) begin
        case (n_state)
            // x态:信号数值不确定
            // z态:高阻态
            IDLE:LCD_DATA<=8'hxx;
            //8'b0011_1000,工作方式设置:DL=1(DB4,8位数据接口),N=1(DB3,两行显示),L=0(DB2,5x8点阵显示).
            SET_FUNCTION:LCD_DATA<=8'h38;
            //8'b0000_1000,显示开关设置:D=0(DB2,显示关),C=0(DB1,光标不显示),D=0(DB0,光标不闪烁)
            DISP_OFF:LCD_DATA<=8'h08;
            //8'b0000_0001,清屏
            DISP_CLEAR:LCD_DATA<=8'h01;
            //8'b0000_0110,进入模式设置:I/D=1(DB1,写入新数据光标右移),S=0(DB0,显示不移动)
            ENTRY_MODE:LCD_DATA<=8'h06;
            //8'b0000_1100,显示开关设置:D=1(DB2,显示开),C=0(DB1,光标不显示),D=0(DB0,光标不闪烁)
            DISP_ON:LCD_DATA<=8'h0c;
            //8'b1000_0000,设置DDRAM地址:00H->1-1,第一行第一位
            ROW1_ADDR:LCD_DATA<=8'h80;
            //将输入的row以每8-bit拆分,分配给对应的显示位
            ROW1_0:LCD_DATA<=row_1[127:120];
            ROW1_1:LCD_DATA<=row_1[119:112];
            ROW1_2:LCD_DATA<=row_1[111:104];
            ROW1_3:LCD_DATA<=row_1[103: 96];
            ROW1_4:LCD_DATA<=row_1[ 95: 88];
            ROW1_5:LCD_DATA<=row_1[ 87: 80];
            ROW1_6:LCD_DATA<=row_1[ 79: 72];
            ROW1_7:LCD_DATA<=row_1[ 71: 64];
            ROW1_8:LCD_DATA<=row_1[ 63: 56];
            ROW1_9:LCD_DATA<=row_1[ 55: 48];
            ROW1_A:LCD_DATA<=row_1[ 47: 40];
            ROW1_B:LCD_DATA<=row_1[ 39: 32];
            ROW1_C:LCD_DATA<=row_1[ 31: 24];
            ROW1_D:LCD_DATA<=row_1[ 23: 16];
            ROW1_E:LCD_DATA<=row_1[ 15:  8];
            ROW1_F:LCD_DATA<=row_1[  7:  0];
            ROW2_ADDR:LCD_DATA<=8'hc0;//8'b1100_0000,设置DDRAM地址:40H->2-1,第二行第一位
            ROW2_0:LCD_DATA<=row_2[127:120];
            ROW2_1:LCD_DATA<=row_2[119:112];
            ROW2_2:LCD_DATA<=row_2[111:104];
            ROW2_3:LCD_DATA<=row_2[103: 96];
            ROW2_4:LCD_DATA<=row_2[ 95: 88];
            ROW2_5:LCD_DATA<=row_2[ 87: 80];
            ROW2_6:LCD_DATA<=row_2[ 79: 72];
            ROW2_7:LCD_DATA<=row_2[ 71: 64];
            ROW2_8:LCD_DATA<=row_2[ 63: 56];
            ROW2_9:LCD_DATA<=row_2[ 55: 48];
            ROW2_A:LCD_DATA<=row_2[ 47: 40];
            ROW2_B:LCD_DATA<=row_2[ 39: 32];
            ROW2_C:LCD_DATA<=row_2[ 31: 24];
            ROW2_D:LCD_DATA<=row_2[ 23: 16];
            ROW2_E:LCD_DATA<=row_2[ 15:  8];
            ROW2_F:LCD_DATA<=row_2[  7:  0];
        endcase
    end
end

endmodule

经过封装后很方便使用,在EPet中简单设置一个状态机即可更新显示内容:

/* LCD1602显示 */

reg [127:0] row_1; // 第一行显示内容
reg [127:0] row_2; // 第二行显示内容
reg lcd_rst; // LCD1602刷新信号

LCD1602 u1(
    .row_1(row_1[127:0]),
    .row_2(row_2[127:0]),
    .CLK(clk),
    .RST(lcd_rst),
    .LCD_E(LCD_E),
    .LCD_RS(LCD_RS),
    .LCD_DATA(LCD_DATA),
    .LCD_RW(LCD_RW)
);

// LCD1602内容刷新
always @ (posedge clk) begin
    if (power == 1'b0) begin
        row_1 <= "*- POWER  OFF -*";
        row_2 <= "-=> Switch 7 <=-";
    end else begin
        case (pet_state)
            4'b0000: begin // 孵化未开始
                row_1 <= "*- POWER  ON  -*";
                row_2 <= "-=> Button 0 <=-";
            end
            4'b0001: begin // 孵化中
                if (temp_sw) begin // 正常温度
                    row_1 <= ">-  Hatching  -<";
                    row_2 <= "-=>   WARM   <=-";
                end else begin // 异常温度
                    row_1 <= ">-  Hatching  -<";
                    row_2 <= "-=>  FROZEN  <=-";
                end
            end
            4'b0010: begin // 孵化完成
                row_1 <= ">-  Hatching  -<";
                row_2 <= "-=> Success! <=-";
            end
            4'b0011: begin // 孵化失败
                row_1 <= ">-  Hatching  -<";
                row_2 <= "-=>  Failed  <=-";
            end
            default: ;
        endcase
    end
end

顶层模块EPet

整个顶层模块的核心在于状态机的设置。

首先是设置了一个全局状态变量:

reg [3:0] pet_state; // 宠物状态
/*
    宠物状态说明
    0: 孵化未开始
    1: 孵化中
    2: 孵化完成
    3: 孵化失败
*/

另一个重点的状态转移:

/* 宠物状态机 */

always @ (posedge clk) begin
    if (power == 1'b1) begin
        case (pet_state)
            4'b0000: begin // 孵化未开始
                if (btn0_deb == 1'b1) begin
                    pet_state <= 4'b0001; // 孵化中
                end
            end
            4'b0001: begin // 孵化中
                if (hatch_state_flag == 1'b1) begin
                    hatch_state_flag <= 1'b0; // 重置孵化结束标志
                end
                img_green_mode <= 1'b0; // 显示彩色模式
                if (img_index == 'd5) begin
                    pet_state <= 4'b0010; // 孵化完成
                end else if (temp_sw == 1'b0 && hatch_cnt == TIME_FROZEN_DEAD) begin
                    pet_state <= 4'b0011; // 孵化失败
                end
            end
            4'b0010: begin // 孵化完成(包含蛋裂动画)
                if (btn0_deb == 1'b1 && img_index > 'd7) begin
                    hatch_state_flag <= 1'b1;
                    pet_state <= 4'b0001; // 孵化中
                end
            end
            4'b0011: begin // 孵化失败
                img_green_mode <= 1'b1; // 仅显示绿色模式
                if (btn0_deb == 1'b1) begin
                    hatch_state_flag <= 1'b1;
                    pet_state <= 4'b0001; // 孵化中
                end
            end
            default: ;
        endcase
    end else begin
        pet_state <= 4'b0000; // 孵化未开始
        hatch_state_flag <= 1'b1;
    end
end

孵化中的计时为了避免多驱动的错误,和img显示写在一起:

// 孵化中计时

parameter TIME_NEXT_IMG = 'd2;
parameter TIME_FROZEN_DEAD = 'd5;

reg [23:0] cnt1s = 'd0; // 1s计数器
reg [2:0] hatch_cnt = 'd0; // 孵化计数器
reg [3:0] seg_display_1 = 'd0; // 显示计数器(个位)
reg [3:0] seg_display_2 = 'd0; // 显示计数器(十位)
reg temp_sw_flag = 1'b0; // 温度切换标志(1:正常温度/0:异常温度)
reg hatch_state_flag = 1'b0; // 孵化状态切换标志

always @ (posedge clk) begin // 1s分频
    if (power == 1'b1) begin
        if (hatch_state_flag == 1'b1) begin
            cnt1s <= 'd0; // 重置1s计数器
        end
        if (cnt1s == TIME_1S) begin
            cnt1s <= 'd0;
        end else begin
            cnt1s <= cnt1s + 'd1;
        end
    end else begin
        cnt1s <= 'd0; // 重置1s计数器
    end
end

always @ (posedge clk) begin // 显示计时
    if (power == 1'b1 && pet_state == 4'b0001) begin // 电源 + 孵化中
        if (hatch_state_flag == 1'b1) begin
            seg_display_1 <= 'd0; // 重置显示计时器
            seg_display_2 <= 'd0;
        end
        if (cnt1s == TIME_1S) begin
            if (seg_display_1 == 'd9) begin
                seg_display_1 <= 'd0;
                seg_display_2 <= seg_display_2 + 'd1;
            end else begin
                seg_display_1 <= seg_display_1 + 'd1;
            end
        end
    end else if (power == 1'b0) begin // 关机
        seg_display_1 <= 'd0; // 重置显示计时器
        seg_display_2 <= 'd0;
    end
end

always @ (posedge clk) begin // 8*8LED点阵显示
    if (power == 1'b1) begin // 开机
        case (pet_state)
            4'b0001: begin // 孵化中
                if (hatch_state_flag == 1'b1) begin
                    hatch_cnt <= 'd0; // 重置孵化计时器
                    img_index <= 'd0; // 重置图像编号
                end
                if (temp_sw) begin // 切换温度时重置计时器
                    if (temp_sw_flag == 1'b0) begin
                        temp_sw_flag <= 1'b1;
                        hatch_cnt <= 'd0;
                    end
                end else begin
                    if (temp_sw_flag == 1'b1) begin
                        temp_sw_flag <= 1'b0;
                        hatch_cnt <= 'd0;
                    end
                end
                if (cnt1s == TIME_1S) begin
                    if (temp_sw) begin // 正常温度的处理
                        if (hatch_cnt == TIME_NEXT_IMG - 'd1) begin
                            hatch_cnt <= 'd0;
                            img_index <= img_index + 'd1;
                        end else begin
                            hatch_cnt <= hatch_cnt + 'd1;
                        end
                    end else begin // 异常温度的处理
                        if (hatch_cnt < TIME_FROZEN_DEAD) begin
                            hatch_cnt <= hatch_cnt + 'd1;
                        end
                    end
                end
            end
            4'b0010: begin // 孵化完成
                if (img_index < 'd8) begin // 蛋裂动画播放
                    if (cnt1s == TIME_1S) begin
                        if (hatch_cnt == TIME_NEXT_IMG - 'd1) begin
                            hatch_cnt <= 'd0;
                            img_index <= img_index + 'd1;
                        end else begin
                            hatch_cnt <= hatch_cnt + 'd1;
                        end
                    end
                    rand_num[1:0] <= rand_num_current[7:6]; // 保存随机数
                end else begin
                    case (rand_num[1:0]) // 随机宠物
                        2'b00: img_index <= 'd8; // 蛇
                        2'b01: img_index <= 'd9; // 鸡
                        2'b10: img_index <= 'd10; // 龟
                        2'b11: img_index <= 'd11; // 龙
                    endcase
                end
            end
            4'b0011: begin // 孵化失败
                hatch_cnt <= 'd0; // 重置孵化计时器
            end
        endcase
    end else begin // 关机
        hatch_cnt <= 'd0; // 重置孵化计时器
        img_index <= 'd0; // 重置图像编号
    end
end

温度/时间提示以及完整的代码详见附录。

仿真波形及波形分析

原始报告含各个仿真波形的分析,这里偷个懒就不放了。

图片23.png
图片24.png
图片25.png
图片26.png
图片27.png

故障及问题分析

太长了,偷懒不放了OwO

总结和结论

本次实验丰富了我的硬件设计经历。了解了很多器件的原理并学会了读说明书/使用手册。这次实验让我运用了很多编程设计的技巧和思想,并且让我意识到了很多以前没有意识到的知识漏洞,提升了知行合一的学科素养。

此外,本次实验让我进一步掌握了Quartus的使用方法,了解了ModelSim的仿真文件编写方式。而由于Quartus本身的可视化做的很糟糕,我还学习了如何在vscode下比较优雅地编辑verilog,安装了CTags并排除了ModelSim原先的安装授权问题。这些经历也锻炼了我学习并接收新事物的能力。

附录

完整代码

太长了,偷懒不放了OwO

参考资料

原报告有,此略

评论区
头像
    头像
    wu先生
      

    不明觉历呀。

      头像
      Nickwald
        
      @wu先生

      期末验收报告是这样的

文章目录