摘要
小时候可能玩过宠物孵化玩具,可以通过按键交互实现宠物的孵化,喂养等有趣的功能。本次实验尝试基于可编程器件实验板DTCE-EDA_V设计了电子宠物孵化器的系统构架与并用代码实现宠物孵化功能。基于Verilog实现了包含顶层模块EPEt,LCD1602,img,music,seg,rand,button在内的7大模块。
关键词
关键词 :电子宠物孵化器,Verilog,FPGA,多模块。
项目要求
在数字实验板上的 8×8 点阵上模拟一个电子宠物“孵化器”的工作过程。
基本要求
- SW7 为总开关,SW7=‘0’时,所有器件不显示,SW7 拨到‘1’时,8×8 点阵显示一只 黄色的“蛋”;
- SW0 为温度选择键,SW0=‘1’时,LD2(黄灯)亮,表示孵化器温度适合孵化, SW0=‘0’时,LD0(蓝灯)亮,表示孵化器温度偏低,无法孵化;
- BTN0 为孵化启动键,按下后如果温度合适(SW0=‘1’),8×8 点阵显示“蛋”的形状 每 2 秒钟发生一次变化,变化顺序如图 1~图 6;变化过程中,如果温度偏低 (SW0=‘1’)则变化停止,等到温度合适后继续变化,如果温度偏低持续时间超过 5 秒,则“蛋”变成绿色,表示孵化失败;
- 当“蛋”的形状变化到图 6 后,孵化过程不再受温度影响,每 2 秒依次显示图 7、图 8 和图 9 动物图案,表示孵化成功;
- 孵化出的动物有蛇、小鸡、海龟、恐龙 4 种,具体孵化结果为四种动物中的随机一 种,4 种动物图案如下:
- 按下 BTN0 后开始计时,记录整个孵化时间,孵化成功或失败后计时停止,同时 用 DISP7~DISP6 两个数码管显示计时;
- 孵化成功或失败后按动 BTN0 即可开始新的孵化过程。
提高要求
- 为相关状态和过程配加合适的音效或音乐;
- 自拟其他功能。
系统设计
从整体上而言,整个系统解耦为七大模块。
设计思路
首先对整个系统进行初步构想:将系统视为黑盒,其输入输出应该包含:
控制类
- 电源开关
- “孵化”启动/重启按钮
- 温度选择开关
显示类
声
- 蜂鸣器
光
- 8*8双色LED点阵(孵化图案)
- 数码管(总计孵化时间)
- 发光LED阵列(左半部分提示进入下一阶段的时间,右半部分提示温度状态)
- LCD1602(文字提示)
考虑到状态的切换问题,使用状态机来管理孵化进程是一个看起来很不错的解决方案。于是我们可以考虑设计一个寄存器变量pet_state,使用数字代替表示不同的孵化状态。
另外,为了方便代码的调试管理,并且为未来可能的进一步开发提供灵活性和便捷性,应该将整个系统的功能代码解耦分离,分解为多个功能各异的模块。本次实验内,我将整个系统大致功能代码分类如下:
- 从面向对象编程的设计原理来说,应该把显示逻辑从主逻辑中分离。于是我们应该分离出LCD1602,img,seg三大模块。这三者的显示都有扫描,译码,显示三大步骤。
- 蜂鸣器的声波生成应该也要单独分离。
- 随机数生成器可以作为一个单独的模块,当业务拓展时,仅需改变接口位宽即可满足发展的需求。
- 按键消抖功能应该单独分离出一个模块。这样子就可以实现多按键时代码轻松复用。
- 整个系统存在需要高频率的部分(8*8点阵,LCD1602,蜂鸣器),所以时钟频率选择10MHz。
总体框图
分块设计
按键消抖模块button
按键消抖模块的设计灵感来源于00-19计数器的实验。我们的目的是通过延迟计时的方式滤除不稳定的抖动,这种抖动一般在30ms之内。
我们可以设计模块端口如下:
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
本次实验用实验板的数码管是并联在一起的,所以我们需要通过扫描的方式输出数字。
设计思路同样遵循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)序列。
这里我们设定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调制
。
我们以音阶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管脚如下:
其中我们仅仅需要注意数据脚,RS脚和R/W脚(本次实验我们并不需要从LCD读,所以R/W可以直接固定为0)。
LCD1602储存有以下三种:
- DDRAM:显示数据RAM(可写入)
- CGRAM:用户可自定义的字模(可写入,00H-0FH)
- CGROM:内置的常用字模(不可写入,ASCII: 20H-7FH, 日文、希腊:A0H-FFH)
上面是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
温度/时间提示以及完整的代码详见附录。
仿真波形及波形分析
原始报告含各个仿真波形的分析,这里偷个懒就不放了。
故障及问题分析
太长了,偷懒不放了OwO
总结和结论
本次实验丰富了我的硬件设计经历。了解了很多器件的原理并学会了读说明书/使用手册。这次实验让我运用了很多编程设计的技巧和思想,并且让我意识到了很多以前没有意识到的知识漏洞,提升了知行合一的学科素养。
此外,本次实验让我进一步掌握了Quartus的使用方法,了解了ModelSim的仿真文件编写方式。而由于Quartus本身的可视化做的很糟糕,我还学习了如何在vscode下比较优雅地编辑verilog,安装了CTags并排除了ModelSim原先的安装授权问题。这些经历也锻炼了我学习并接收新事物的能力。
附录
完整代码
太长了,偷懒不放了OwO
参考资料
原报告有,此略
不明觉历呀。
期末验收报告是这样的