数字电子技术B
课程设计报告
(2023-2024第1学期)
课题名称:饮料自动贩卖机
一、系统总体设计方案
1.1功能描述
自动售饮料机,此饮料机仅提供一种饮料,每瓶售价1.5元,有按键,每按一次表示购买瓶数加一。按键之前确保复位信号处于高电平状态,否则按键无效。投币器只能接受1元和5角硬币,每按一次1元或5角按键表示1元或5角的个数加一。具有找零功能,当投币数和饮料数输入完毕后,拨动购买开关,如果钱数和购买瓶数合理则提示用户取走饮料和找零的指示灯亮起,并且会显示投币的钱数和找零数在数码管上。如果钱数和购买瓶数不合理,则代表出错的指示灯亮起,钱数全数退回,找零数为投币钱数。购买完毕后,将购买开关拨回,复位信号拨到低电平复位后拨回高电平,结束购买。
1.2 设计原理及设计思路
初始状态有一个复位信号,低电平有效。故执行流程时需把复位信号处于高电平。首先通过3个防抖动按键将元的个数、角的个数和饮料的个数通过计数器得到它们的二进制数形式。然后得这些二进制数的8421bcd码的形式(便于后面的计算),使用一个12位二进制数存储投币的钱和找零的钱,[11: 8] 存储十位,[7: 4] 存储个位,[3: 0] 存储小数,且全部用8421bcd码存储。得到投币的钱,再计算找零的钱,存储方式和存储投币的钱相同。购买开关关闭的时候,只有投币的钱数会正常显示,找零的钱部分显示的是00.0。当有了投币的钱和找零的钱后,拨动购买开关,首先进行判断,如果投币的钱少于购买饮料所需要的钱,则出错指示灯亮起,此次购买失败;如果投币的钱可以购买则提示用户取走饮料喝找零的指示灯亮起。然后会通过模6计数器、分频器、译码器等分配数据,得到位选信号和对应的段选信号,并且如果是个位的时候,小数点dp会亮起。从而在数码管上显示投币钱数和找零钱数。
1.3 verilog程序代码设计
curriculum_design模块
module curriculum_design(
input clk, // 50MHz时钟信号
input yuan_en, // 元 角 饮料的使能信号
input jiao_en,
input cola_num_en,
input buy, // 买信号
input rst, // 复位信号
output reg take_cola, // 拿走饮料信号
output reg take_change, // 拿走找零信号
output reg error, // 出错信号
output reg [7: 0] SEG, // 数码管
output [6: 1] DIG // 位选信号
);
wire [2: 0] yuan_num; // 元数目的二进制大小
wire [2: 0] jiao_num; // 角数目的二进制大小
wire [2: 0] cola_num; // 饮料数目的二进制大小
wire [11: 0] change; // 找零 每四位存储一个8421bcd码 分别表示十位个位小数位
wire [7: 0] change_yuan;// 存储找零的整数部分
wire clk_1kHz; // 分频获得的1kHz时钟信号
wire [2: 0] SEL; // 选择信号
wire [11: 0] money; // 钱 每四位存储一个8421bcd码 分别表示十位个位小数位
wire [3: 0] yuan; // 元的8421bcd码
wire [3: 0] jiao; // 角的8421bcd码
wire [3: 0] cola; // 饮料的8421bcd码
reg [3: 0] data; // 存储显示的数据
reg [2: 0] jiao_to_yuan; // 角转换成元并只取整数部分
get_num module1(clk, yuan_en, yuan_num, rst); // 通过按钮防抖动获取元的二进制大小
get_num module2(clk, jiao_en, jiao_num, rst); // 通过按钮防抖动获取角的二进制大小
get_num module3(clk, cola_num_en, cola_num, rst); // 通过按钮防抖动获取饮料的二进制大小
get_1kHz get(clk, clk_1kHz); // 得到分频的1kHz时钟信号
// 将角转换成元
always@(posedge clk or negedge rst)
begin
if(!rst)
jiao_to_yuan <= 3'b000;
else
jiao_to_yuan <= jiao_num>>1;
end
// 获取各个数据
to_bcd t1(yuan_num, yuan, clk); // 获取元的8421bcd码
to_bcd t2(jiao_to_yuan, jiao, clk); // 获取角的8421bcd码
to_money t5(jiao_num, yuan, jiao, money, clk); // 获取总钱数
to_bcd t3(cola_num, cola, clk); // 获取饮料的8421bcd码
to_change t4(cola_num, yuan_num, jiao_num, change[3:0], change_yuan); // 获取找零整数数量及小数数量
_4bit_to_8bcd t6(change_yuan, change[11:4], clk); //将找零整数部分的二进制转为bcd编码
// 初步判断是否购买出错
always@(posedge clk or negedge rst)
begin
if(!rst)
begin
error <= 1'b0;
take_cola <= 1'b0;
take_change <= 1'b0;
end
else
begin
// 确定购买
if(buy)
begin
// 如果总钱数少于价钱
if((2 * yuan_num + jiao_num) < (cola_num * 3))
begin
error <= 1'b1;
take_cola <= 1'b0;
take_change <= 1'b0;
end
else
// 总钱数大于等于价钱
begin
error <= 1'b0;
take_cola <= 1'b1;
take_change <= 1'b1;
end
end
else
// 未确定购买
begin
error <= 1'b0;
take_cola <= 1'b0;
take_change <= 1'b0;
end
end
end
counter6 c(clk_1kHz, SEL);
// 分配数据
always@(SEL)
begin
case(SEL)
3'b000:
begin
// 未确定购买 找零部分为0
if(!buy)
data <= 4'b0000;
else
begin
// 如果出错 找零为总钱数
if(error)
begin
data <= money[3: 0]; // 小数部分
end
else
data <= change[3: 0];
end
end
3'b001:
begin
if(!buy)
data <= 4'b0000;
else
begin
if(error)
begin
data <= money[7: 4]; // 个位部分
end
else
begin
data <= change[7: 4];
end
end
end
3'b010:
begin
if(!buy)
data <= 4'b0000;
else
begin
if(error)
begin
data <= money[11: 8]; // 十位部分
end
else
begin
data <= change[11: 8];
end
end
end
3'b011:
begin
data <= money[3: 0]; // 小数部分
end
3'b100:
begin
data <= money[7: 4]; // 个位部分
end
3'b101:
begin
data <= money[11: 8]; // 十位部分
end
endcase
end
// 将数据译成数码管数据
always@(data)
begin
case(data)
4'b0000: SEG <= 8'b1111_1100; // 0
4'b0001: SEG <= 8'b0110_0000; // 1
4'b0010: SEG <= 8'b1101_1010; // 2
4'b0011: SEG <= 8'b1111_0010; // 3
4'b0100: SEG <= 8'b0110_0110; // 4
4'b0101: SEG <= 8'b1011_0110; // 5
4'b0110: SEG <= 8'b1011_1110; // 6
4'b0111: SEG <= 8'b1110_0000; // 7
4'b1000: SEG <= 8'b1111_1110; // 8
4'b1001: SEG <= 8'b1111_0110; // 9
default: SEG <= 8'bx;
endcase
// 如果是总钱和找零的个位 小数点应亮起
if(SEL == 3'b001 || SEL == 3'b100)
SEG[0] <= 1'b1;
end
// 确定位选
choose ch(SEL, DIG);
endmodule
_4bit_to_8bcd模块
module _4bit_to_8bcd(
input [7: 0] bin_in,
output reg [7: 0] bcd_out,
input clk
);
reg [3: 0] ones;
reg [3: 0] tens;
integer i;
always@(*)
begin
ones = 4'd0;
tens = 4'd0;
for(i = 7; i >= 0; i = i - 1)
begin
if(ones >= 4'd5) ones = ones + 4'd3;
if(tens >= 4'd5) tens = tens + 4'd3;
tens = {tens[2: 0] , ones[3]};
ones = {ones[2: 0], bin_in[i]};
end
bcd_out = {tens, ones};
end
endmodule
choose模块
module choose(SEL, DIG); // 确定位选
input [2: 0] SEL;
output reg [6: 1] DIG;
always@(SEL)
begin
case(SEL)
3'b000: DIG <= 6'b000001;
3'b001: DIG <= 6'b000010;
3'b010: DIG <= 6'b000100;
3'b011: DIG <= 6'b001000;
3'b100: DIG <= 6'b010000;
3'b101: DIG <= 6'b100000;
default: DIG <= 6'bx;
endcase
end
endmodule
counter6模块
module counter6(clk_1kHz, SEL); // 模6计数器
input clk_1kHz;
output reg [2: 0] SEL;
reg [2: 0] counter;
always@(posedge clk_1kHz)
begin
if(counter < 3'b101)
begin
counter <= counter + 1'b1;
end
else
begin
counter <= 3'b000;
end
SEL <= counter;
end
endmodule
get_1kHz模块
module get_1kHz(clk, clk_1kHz); // 分频
input clk;
output reg clk_1kHz;
reg [15: 0] counter;
parameter num = 50000; // 1kHz一个周期有50MHz50000个周期
always@(posedge clk)
begin
if(counter < num / 2)
begin
counter <= counter + 16'b1;
clk_1kHz <= 1'b0;
end
else if(counter < num - 1)
begin
counter <= counter + 16'b1;
clk_1kHz <= 1'b1;
end
else
counter <= 16'b0;
end
endmodule
get_num模块
module get_num(clk, key_in, num, rst_n); // 获取个数
input clk; // 时钟50MHz
input key_in; // 按键输入
input rst_n;
reg key_state; // 按键状态 高电平为按下 低电平为未按下状态
reg key_flag; // 经消抖后可确认的按下动作
output reg [2: 0] num = 0;
parameter UP = 2'b00, // 松开稳定时
FILTER0 = 2'b01, // 按下毛刺时
DOWN = 2'b10, // 按下稳定时
FILTER1 = 2'b11; // 松开毛刺时
// 20ms计数器
reg en_counter; // 计数使能
reg [19:0] cnt; // 计数
reg cnt_full; // 计数器是否满
//只有当计数使能为高电平的时候 计数器才会计数
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
cnt <= 0;
else if(en_counter)
cnt <= cnt + 1'b1;
else
cnt <= 0;
end
// 计数满信号
always@(posedge clk or negedge rst_n) // 当clk接50MHz的信号时 相当于clk在1s内进行了50M次计数 相邻上升沿相间(1/50M)s
begin
if(!rst_n)
cnt_full <= 1'b0;
else if(cnt == 20'd1000000)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
end
// 判断边沿模块
reg key_tmp0, key_tmp1; // 将输入信号寄存一个节拍 分别按键上升沿和下降沿的产生
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
key_tmp0 <= 1'b1;
key_tmp1 <= 1'b1;
end
else
begin
key_tmp0 <= key_in; // key_in按键输入
key_tmp1 <= key_tmp0;
end
end
wire pedge, nedge;
assign nedge = (!key_tmp0) & key_tmp1; // 下降沿(下一clk时为0,上一clk时为1)
assign pedge = key_tmp0 & (!key_tmp1); // 上升沿(下一clk时为1,上一clk时为0)
// 状态机模块
reg [1:0] state; // 所有状态由2位二进制数存储
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
state <= UP;
en_counter <= 1'b0; // 计数器归零
key_state <= 1'b0; // 按键未按下
key_flag <= 1'b0;
end
else
begin
case(state)
UP: // 低电平(松开稳定)
begin
key_flag <= 1'b0; // 按键未按下,不计
key_state <= 1'b0; // 按键松开状态
en_counter <= 1'b0; // 计数器归零
if(pedge) // 检测到上升沿 进入下一个状态同时打开计数器
begin
state <= FILTER0;
en_counter <= 1'b1; // 计数器使能
end
else
state <= state; //保持目前状态
end
FILTER0: // 上升沿抖动(按下毛刺)
begin
if(cnt_full) // 计数满 说明达到稳定状态 关闭计数器 进入下一个状态
begin
state <= DOWN;
en_counter <= 1'b0; // 计数器归零
key_flag <= 1'b1; // 按键可确认已按下
key_state <= 1'b1; // 按键按下状态
end
else if(nedge) // 检测到下将沿(毛刺) 跳回UP状态同时关闭计数器
begin
en_counter <= 1'b0; //计数器归零
state <= UP;
end
else
state <= state; // 保持目前状态
end
DOWN: // 高电平(按下稳定)
begin
key_flag <= 1'b0; // 一个按键周期测到一次就行 现在可清零了(后面代码编写只在意其posedge)
if(nedge) // 检测到下降沿 进入下一个状态同时打开计数器
begin
state <= FILTER1;
en_counter <= 1'b1; // 计数器使能
end
else
state <= state; // 保持目前状态
end
FILTER1: // 下降沿抖动(松开毛刺)
begin
if(cnt_full)
begin
state <= UP;
key_state <= 1'b0;
end
else
begin
if(pedge)
begin
en_counter <= 1'b0; // 计数器归零
state <= DOWN;
end
else
state <= state; // 保持目前状态
end
end
default:
state <= UP;
endcase
end
end
// 计数值输出与译码器模块
always@(posedge key_flag,negedge rst_n) // key_flag:经消抖后可确认的按下动作
begin
if(!rst_n)
num <= 0;
else if(num < 3'b111)
num <= num + 1'b1;
end
endmodule
to_bcd模块
module to_bcd(
input [2:0] bin_in,
output reg [3:0] bcd_out,
input clk
); // 转为8421bcd码
reg [3:0] ones;
integer i;
always @(*) begin
ones = 4'd0;
for(i = 2; i >= 0; i = i - 1)
begin
if (ones >= 4'd5) ones = ones + 4'd3;
ones = {ones[2:0],bin_in[i]};
end
bcd_out = {ones};
end
endmodule
to_change模块
module to_change(cola_num, yuan_num, jiao_num, change, change_yuan); // 获取找零整数数量及小数数量
input [2: 0] cola_num; //饮料数量的二进制码
input [2: 0] yuan_num; //投入1元数量的二进制码
input [2: 0] jiao_num; //投入5角的二进制码
reg [7:0] change_temp; //找零(多少个5角)
output reg [3: 0] change; //找零的小数位8421bcd码
output [7:0] change_yuan;
always@(yuan_num or jiao_num or cola_num)
begin
if(2 * yuan_num + jiao_num >= 3 * cola_num)
change_temp = 2 * yuan_num + jiao_num - 3 * cola_num; //找多少个5角
else
change_temp = 2 * yuan_num + jiao_num;
change[3:0] = (change_temp[0]==1)?4'b0101:4'b0000;
change_temp = change_temp>>1; //找多少个1元
end
assign change_yuan = change_temp;
endmodule
to_money模块
module to_money(jiao_num, yuan, jiao, money, clk); // 获取money
input [2: 0] jiao_num;
input [3: 0] yuan;
input [3: 0] jiao;
input clk;
output reg [11: 0] money;
always@(posedge clk)
begin
// 如果角数目为偶数
if(jiao_num[0] == 0)
begin
money[3: 0] <= 4'b0000;
end
else
money[3: 0] <= 4'b0101;
// 如果产生进位
if(yuan[3: 0] + jiao[3: 0] > 4'b1001)
begin
money[11: 8] <= 4'b1010;
money[7: 4] <= yuan[3: 0] + jiao[3: 0] - 4'b1010;
end
else
begin
money[11: 8] <= 4'b0000;
money[7: 4] <= yuan[3: 0] + jiao[3: 0];
end
end
endmodule
1.4 状态转换图与状态转换表
状态转换图
状态转换表(1)
状态转换表(2)
1.5 状态编码方案
本程序一共有四个状态,分别用二进制编码00、01、10、11来代表四个状态。在时钟clk上升沿和复位信号rst_n下降沿触发的always语句块中,根据当前State值的不同来执行不同的程序逻辑:当rst_n处于低电平时,将state赋为UP,计数器归零且按键状态赋为高电平未按下状态;rst_n处于高电平,state为00也就是UP时,按键属于松开稳定状态且将计数器归零,按键状态赋为高电平未按下状态,当检查到上升沿时进入下一个状态同时打开计数器,否则保持当前状态不变。state为01也就是FILTER0时,按键处于上升沿抖动状态,当计数器记满后说明按键达到稳定状态并且关闭计数器,进入下一个状态同事计数器归零,按键状态赋为低电平按下状态,同时按键确认按下动作赋值为高电平;计数器没满且检测到下降沿毛刺时跳回UP状态同时关闭计数器,其他时刻保持当前状态不变。当按下稳定时,state为10也就是DOWN时,按键确认按下动作清零,如果检测到下降沿时进入下一个状态同时打开计时器,否则保持当前状态不变。state为11也就是FILTER1时,按键处于下降沿抖动的情况,如果计数器记满说明松开状态稳定,按键状态赋为高电平松开状态;如果计数器没计满且检测到上升沿毛刺时跳回DOWN状态且将计数器归零,否则保持当前状态不变。
1.6 输出表
输出信号 | 输出信号作用 |
take_cola | 提醒用户拿取饮料 |
take_change | 提醒用户拿取零钱 |
error | 零钱不够,报错 |
SEG[7: 0] | 用于数码管显示 |
DIG[6: 1] | 数码管位选信号 |
1.7 总体电路图
二、调试步骤
仿真代码:
`timescale 10 ns/ 1 ns
module curriculum_design_vlg_tst();
reg buy;
reg clk;
reg cola_num_en;
reg jiao_en;
reg yuan_en;
reg rst;
wire [6:1] DIG;
wire [7:0] SEG;
wire error;
wire take_change;
wire take_cola;
curriculum_design i1 (
.DIG(DIG),
.SEG(SEG),
.buy(buy),
.rst(rst),
.clk(clk),
.cola_num_en(cola_num_en),
.error(error),
.jiao_en(jiao_en),
.take_change(take_change),
.take_cola(take_cola),
.yuan_en(yuan_en)
);
initial
begin
clk = 1'b0;
yuan_en = 1'b0;
jiao_en = 1'b0;
cola_num_en = 1'b0;
buy = 1'b0;
end
always
begin
#1
clk = ~clk; //50MHz时钟
end
always
begin
rst = 1;
#1_000_000;
cola_num_en = 1; #3_000_000; //选了三瓶饮料
cola_num_en = 0; #3_000_000;
cola_num_en = 1; #3_000_000;
cola_num_en = 0; #3_000_000;
cola_num_en = 1; #3_000_000;
cola_num_en = 0; #3_000_000;
yuan_en = 1; #3_000_000; //投入1元
yuan_en = 0; #3_000_000;
yuan_en = 1; #3_000_000; //投入1元
yuan_en = 0; #3_000_000;
yuan_en = 1; #3_000_000; //投入1元
yuan_en = 0; #3_000_000;
yuan_en = 1; #3_000_000; //投入1元
yuan_en = 0; #3_000_000;
yuan_en = 1; #3_000_000; //投入1元
yuan_en = 0; #3_000_000;
jiao_en = 1; #3_000_000; //投入5角
jiao_en = 0; #3_000_000;
jiao_en = 1; #3_000_000; //投入5角
jiao_en = 0; #3_000_000;
jiao_en = 1; #3_000_000; //投入5角
jiao_en = 0; #3_000_000;
jiao_en = 1; #3_000_000; //投入5角
jiao_en = 0; #3_000_000;
jiao_en = 1; #3_000_000; //投入5角
jiao_en = 0; #3_000_000;
buy = 1; #3_000_000; //按下购买
buy = 0; #3_000_000;
end
endmodule
三、测试数据波形及分析说明
波形图总览:(本波形图rst未呈现,始终保持rst = 1)
cola_num_en为选择饮料使能开关,每一个上升沿说明购买瓶数+1;
yuan_en为投1元硬币使能开关,每一个上升沿说明投币1元;
jiao_en为投5元硬币使能开关,每一个上升沿说明投币5角;
buy为购买按钮,高电平说明决定购买,下降沿说明购买结束;
change为找零值,用3位十六进制表示8421BCD码,当buy为1时在数码管上译码显示相应的找零值。
波形分析:
刚开始,按下三次cola_num_en,表示要购买3瓶饮料。
投币前,数码管上六位均显示数字“0”(SEG信号为11111100)。
投币1元后,数码管左边3位显示“01.0”(SEG信号为01100001),表示投币1元。由于还未按下buy,投币需要4.5才能找零,数码管右边三位(找零的三位数字)显示“000”。
投币2元后,数码管左边3位显示“02.0”(SEG信号为110111011),表示投币1元。由于还未按下buy,数码管右边三位(找零的三位数字)显示“000”。
不断投币,当投入5个1元和5个5角后,此时应当找零5+2.5-3*1.5=3元;
打开buy开关后,左侧三位数码管显示投币“07.5”,右侧三位数码管显示找零“03.0”。同时take_cola和take_change亮起,提醒用户拿走饮料和零钱;
随后关闭buy开关,表示购买结束,准备下次购买。(rst需置低电平重置数据)
四、引脚分配表(电路中的信号名称->主板器件名称->引脚号PIN)
信号名 | 主板器件 | PIN | 信号名 | 主板器件 | PIN | |
clk | 50MHz | PIN_90 | SEG[4] | d | PIN_111 | |
cola_num_en | Key8/SW8/LED16 | PIN_43 | SEG[5] | c | PIN_104 | |
jiao_en | Key4/SW4/LED12 | PIN_32 | SEG[6] | b | PIN_100 | |
yuan_en | Key5/SW5/LED13 | PIN_42 | SEG[7] | a | PIN_112 | |
error | LED2 | PIN_52 | DIG[1] | SEG1 | PIN_126 | |
take_change | LED1 | PIN_50 | DIG[2] | SEG2 | PIN_115 | |
take_cola | LED0 | PIN_46 | DIG[3] | SEG3 | PIN_125 | |
buy | Key0/SW0/LED8 | PIN_24 | DIG[4] | SEG5 | PIN_121 | |
SEG[0] | dp | PIN_86 | DIG[5] | SEG6 | PIN_113 | |
SEG[1] | g | PIN_103 | DIG[6] | SEG7 | PIN_120 | |
SEG[2] | f | PIN_110 | ||||
SEG[3] | e | PIN_106 |
五、结论
5.1 系统特点及存在的问题
特点:
此系统有操作简单、逻辑简单、功能实现模块化、可循环使用、代码有一定的健壮性等特点。用户在购买的时候只需通过五个按钮:元、角、饮料的计数按钮和购买按钮以及复位按钮即可实现购买饮料和找零的功能。实现功能的逻辑也较简单,便于后期的维护。此系统的多个功能采用模块化设计,层次分明,逻辑性强,便于后期功能的扩展和修改。此系统是可以面向多个用户循环使用的,在一个用户拨动购买按钮后,只需重新拨回购买按钮,并进行复位后。即可开始下一轮的购买。此系统的代码具有一定的健壮性,当一个用户拨动购买按钮后,如果再次按下元、角、饮料的计数按钮,这时元、角、饮料的个数是不会变的,即拨动购买按钮后,元、角、饮料的个数不会发生改变。且当拨回购买按钮后,拨动复位按钮给一个的复位信号,将上一个用户输入的元、角、饮料个数以及根据这些个数计算出来的投币钱数和找零钱数全部清零,从而实现下一个用户的功能正常实现。
问题:此系统元、角、饮料个数全部得在7个以内,超过7个便不会再计数。
5.2 功能扩展
在计数方面,还可以增加一个按钮实现对应元、角、饮料的计数减少,减到零的时候不能再减。
在个数方面,可以适当增加元、角、饮料个数的上限。
六、本系统使用说明
1.准备使用
确保购买按钮处于拨回状态,复位按钮处于高电平状态。
2.输入元、角、饮料个数阶段
按下对应的按钮,元、角、饮料个数会开始从0计数,到8后为到达最大值。数码管会左边会显示投币钱数,右边因为购买按钮还未拨动,找零分布显示的是00.0。
3.拨动购买按钮
如果投币钱数小于需要钱数,出错指示灯会亮起,此次购买失败。投币钱数全数返回。如果符合要求,则提示用户取走饮料喝找零的指示灯亮起,数码管找零部分也会显示找零钱数。
4.购买结束
拨回购买按钮,拨动复位按钮,使数据全部复位后拨回复位按钮。