FPGA大作业实验报告
本实验报告分为实验目标,设计概述,细节详述,常见问题分析,总结,完整代码等部分。
实验目标
- 实验基本要求
设计一个简单的自动售饮料机的逻辑电路。它的投币口每次只能投入一枚五角或一元的硬币。投入一元五角钱硬币后机器自动给出一杯饮料。投入两元(两枚一元)硬币后,在给出饮料的同时找出一枚五角的硬币。 - 实验扩展要求
用数码管显示输入的金额,以及要找出的金额。
多物价系统。具体要求如下:
两个按钮,表示5角硬币和1元硬币。
可无限投入硬币,数码管动态显示当前金额(带小数)。
3个按钮,代表3种饮料:可口可乐2元,午后红茶3.5元,乌龙茶3元。
每种饮料初始存货各5罐。
当按下某种饮料按钮后,如果投入钱币金额足够,则减去相应的金额,并以数码管显示应找的钱币数目;如果不够,显示饮料价格并闪动,持续2秒,然后仍然显示当前金额;如果饮料数目不够,用数码管显示。当按下退币按钮后,显示应找的钱币数目。
设计概述
代码结构主要用了模块分解和函数复用的思想,虽然这是老生常谈的内容,但只有在实践的过程中(一次次debug中)才发现他们如此重要。
模块分解
一开始编写大作业的时候,因为Verilog里面并行的特性,想到什么功能就写一段代码,加一个always块。结果在debug的时候万分痛苦,在代码之间跳来跳去。
因此我后来把功能做了分块处理,一个功能就在一个大的always的代码块里面实现,方便调试,也可以解决一个reg变量不能在不同的always块里面赋值的问题。
自动售货机主要可以分为4个模块,分别为商品选择模块,投币模块,交易处理模块,数码管显示模块。其中前三个模块具有一定的时序关系,数码管显示模块与其余三个模块并行。
-
商品选择通过板子上的三个拨杆开关来实现,在代码中表示为如下变量:
input wire [2:0]sel_goods
拨动相应的开关即表示选择相应的商品。 -
投币通过板子上的两个按钮实现,在代码中表示为如下变量
input[1:0] key
key[1]
按一下表示输入1元,key[0]
按一下表示输入5角 -
当输入完金额后,按下板子上的一个按钮便进行交易结算,交易按钮在代码中表示为
input deal
交易结算在内部处理,假如交易成功,则显示找零;假如输入金额不足,数码管则会闪烁2秒;假如库存不足,则会显示当前的库存数量 -
假如在使用过程中出现了问题,比如想要重新输入金额,可以按rst键,这个按钮可以将自动售货机从任意状态跳转到最开始的商品选择状态。同时,这个按钮也可以同时起到退币的功能。rst按钮在代码中表示为
input rst
可用以下图来表示:
函数复用
函数复用主要是在数码管显示模块。
- 因为数码管的显示原理(以较高的频率不断显示单个数码管),显示一种状态就要有10-20行代码。
- 一开始写第一个模块的时候感觉还行,然而随着模块的增加和代码复杂度的升高,跟数码管显示有关的代码一遍又一遍的复制粘贴,导致数码管显示模块一度达到了100多行。满眼的
case,begin,end
和大量相似的代码让人感觉十分臃肿。 - 于是我将数码管显示的三个关键参数———显示数字,显示小数点,点亮的数码管,
分别写成了3个函数disp_num_get()
,dot_get()
,an_get()
, 用来精简代码。 - 具体的代码见下文细节详述
细节详述
Part1:变量定义
在这一部分中,主要对程序中用到的input, output
变量,以及程序中要用到但不参与输入输出的变量进行了定义。
Part2:商品选择
代码如下:
//=========================商品选择=============================================
//001代表可乐 对应的数字为20
//010代表红茶 对应的数字为35
//100代表乌龙茶 对应的数字为30
always @(posedge clk)
begin
case(sel_goods)
1:num[15:8] = 8'b00100000;
2:num[15:8] = 8'b00110101;
4:num[15:8] = 8'b00110000;
default:num[15:8] = 8'b00000000;
endcase
end
这一部分还是比较简单的,上文提到的input wire [2:0]sel_goods
为选择商品的变量,根据选择的商品种类,赋给num[15:8]
(对应左侧两个数码管)不同的值,以供数码管显示。
Part3: 输入金额
代码如下:
//=========================输入金额===========================================
//输入5角
always @(posedge key[0] or posedge rst)
begin
if (rst)
num_5_jiao = 4'b0;
else
begin
if (key[0])
num_5_jiao = num_5_jiao + 1;
end
end
//输入1元
always @(posedge key[1] or posedge rst)
begin
if (rst)
num_1_yuan = 4'b0;
else
begin
if (key[1])
num_1_yuan = num_1_yuan + 1;
end
end
// 汇总金额
always @(posedge clk)
begin
//个位数字(小数部分)只与5角的数量有关
//5角的个数为奇数,则为5;个数为偶数,则为0
if (num_5_jiao[0])
num[3:0] = 4'b0101;
else
num[3:0] = 4'b0;
//十位数字则与1元和5角的数量有关
//因为2个5角就是1元,所以只要取5角的个数的前三位
num[7:4] = num_1_yuan + num_5_jiao[3:1];
end
- 这一部分的实现思路是先记录
key[1]
和key[0]
输入1元和5角的个数,然后汇总总的输入金额,并把相应的值赋给num[7:4]
(对应右侧两个数码管),供数码管显示。
其中,在always @(posedge key[0] or posedge rst)
可以看到,只有当按下key[0]
或者rst
时,才会上升沿触发里面的代码执行。而在汇总金额的时候,采用clk
触发,保证5角和1元的数量变化能立刻改变输入金额的显示。 - 其实这一部分我一开始走了弯路,一开始想的是按钮按下直接改变对应的
num
里面的值,但这样就要考虑num[3:0]
的值和进位的关系。再加上按键遇到的问题(见后文)一直没法解决,以及一个reg变量只能在一个always块的约束,最终还是采用先记录个数再汇总金额的方法。
Part4: 交易处理
代码如下
//=========================交易===================================================
assign deal_success_flag = ch_flag & goods_flag;
always @(posedge deal or posedge rst)
begin
//按下rst按键,重置交易记录
if (rst)
begin
ch_flag <= 0;
deal_flag <= 0;
goods_flag <= 1;
end
else
begin
deal_flag <= 1;
//输入金额大于等于商品价格
if (num[7:0] >= num[15:8])
begin
changes[15:8] <= 8'b0;
//找零
if (num[3:0] >= num[11:8])
changes[7:0] <= num[7:0] - num[15:8];
else
begin
changes[3:0] <= 4'b0101;
changes[7:4] <= num[7:4] - num[15:12] - 1;
end
//找零标志位
ch_flag <= 1;
//减去库存
case(sel_goods)
1:
begin
if (num_goods[3:0] >= 1)
begin
num_goods[3:0] <= num_goods[3:0] - 1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
2:
begin
if (num_goods[7:4] >= 1)
begin
num_goods[7:4] <= num_goods[7:4] - 4'b1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
4:
begin
if (num_goods[11:8] >= 1)
begin
num_goods[11:8] <= num_goods[11:8] - 1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
default: num_goods <= num_goods;
endcase
end
//输入金额小于商品价格
else
ch_flag <= 0;
end
end
- 这一部分是处理交易的模块。这一块内容主要是要更新
assign deal_success_flag
,交易成功标志
ch_flag
,找零标志
goods_flag
,库存标志
changes
,找零金额
num_goods
,库存 - 第一行
assign deal_success_flag = ch_flag & goods_flag;
说明交易成功需要满足两个条件:输入金额大于等于商品价格,商品有库存。 - 当按下
rst
键时,触发posedge rst
,重置上次的交易结果。 - 当按下
deal
键时,触发posedge deal
,开始交易相关执行代码。
先比较输入金额和价格的关系,假如输入金额>=商品价格,则将输入金额-商品价格即为找零,同时更新找零标志,接着查看选择的商品的库存,更新库存数量和库存标志;假如输入金额<商品价格,那么直接更新找零标志,然后结束。
Part5: 库存查询
代码如下
always @(posedge clk)
begin
if (goods_num_query)
begin
case(sel_goods)
1:num_goods_disp = num_goods[3:0];
2:num_goods_disp = num_goods[7:4];
4:num_goods_disp = num_goods[11:8];
endcase
end
end
这部分其实我是顺手写的,本来除了库存查询以外,还计划通过按键实现库存加减的功能,后来觉得先把主要的功能实现再说,估计也不会新加别的功能了。。。
Part6: 数码管显示
代码较长,而且有不少相似的部分,因此具体见之后的完整代码。
- 这部分内容的大致思路为根据之前的交易处理得到的各种标志,得到需要显示的内容,将一些参数传给
clk_sw_7seg_sub
,从而显示不同的内容。 - 可以看到,基本每一种情况都会有下面三行代码,用来得到显示的内容
disp_dot = dot_get(clk_cnt, 4'b1101);
disp_num = disp_num_get(clk_cnt, changes);
an = an_get(clk_cnt);
其实这三个函数的原理基本一致,就以disp_num_get(clk_cnt, changes)
为例
//=========================得到显示数字函数=========================================
//根据计数器交替输出显示数字
function [3:0] disp_num_get;
input [23:0] clk_cnt;
input [16:0] num;
begin
case (clk_cnt[15:14])
0:disp_num_get = num[3:0];
1:disp_num_get = num[7:4];
2:disp_num_get = num[11:8];
3:disp_num_get = num[15:12];
endcase
end
endfunction
这么写是根据数码管的显示原理。要在四个数码管上同时显示不同的数字,需要以较高的频率依次点亮每个数码管。因此在这个函数中,根据输入的clk_cnt
中第16和15位的数字,实现分频的效果,并依次将要显示的数字传给clk_sw_7seg_sub
中的NUM
;再配合控制点亮数码管的an_get(clk_cnt)
,最终实现实时在数码管上显示数字。
- 至于闪烁的效果的实现,也是大致的思路,再加一个
blink_clk_cnt
计数器用来控制闪烁的时间。时钟是50MHZ,闪烁时间为2秒的话,就是要计数到100M,而一次闪烁计数为2^24,大约6次闪烁为2秒,因此将blink_clk_cnt[26] && blink_clk_cnt[25]
作为闪烁结束的条件。
编程中遇到的问题
- 要搞清楚
wire
和reg
类型的区别,一开始的报错大部分是因为这两个变量的类型搞错引起的。简单的说,wire
变量要用assign赋值,不能在always块里面赋值;而reg
变量是在always块里面赋值 - 一个
reg
变量只能在一个always里面赋值!!!这也是我一开始引发报错的最主要原因。假如想要在不同的always块改变一个reg
变量的值,我能想到的就是设两个变量,将一个变量作为沟通的桥梁。比如想让a在一个模块里面加1,然后在另外一个模块里面加1,可以这样写
always @(*posedge xx)
a_0 <= a_0 + 1;
always @(*posedge yy)
a_1 <= a_0 + 1;
- 在写输入金额部分的时候,我看网上的一些教程需要按键消抖,所以一开始我也写了按键消抖的模块,但调用模块的时候经常会报错,
not a net lvalue
之类的错误,好久都没有解决。然后我就把消抖模块给删了,发现还能用,就先搁置这个错误,以后再说。 - 我之前尝试过写成
always @(posedge key[0] or posedge key[1] or posedge rst)
这样的触发条件。但不知道为什么,如果这样写,在实际操作中,按下一个按键往往会得不到预想的结果,感觉触发了不止一次。可能是内部电路的问题,或者是消抖的问题?但是把触发条件分开写,则不会出现类似的问题。 - G12按钮在编译的时候可能会出现 I/O的错误,好像跟时间的输入有关,这时候要在.ucf文件里面加上一行
NET "deal" CLOCK_DEDICATED_ROUTE = FALSE;
总结
- 这次FPGA大作业给我带来的印象还是很深刻的,毕竟在这两周里在这个大作业上面花了好多时间。
- 从一开始为verilog的语法头疼、写几行代码就报错,到后面能较快地实现功能、并运用函数对代码进行优化,其中是在一次次debug中的成长。如何在较短的时间内学习一门陌生的语言并运用所学的知识完成特定的功能,是本次大作业给我的最大的收获。
- 在写代码的过程中我意识到,写代码之前既要对总体有规划,合理分解功能,又要快速建立原型,在一次次的迭代中实现最终的目标。这次大作业也为我处理好局部和整体的关系提供了宝贵的经验。
- 要多与他人交流,同学会给你不少有用的意见。
- 写代码写得心情不好的时候不妨先放一放,可能过一个晚上就会有新的想法出现。
完整代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
module main(key, clk, rst, a_to_g, an, sel_goods, deal, goods_num_query, disp_dot, led);
//=========================输入和输出===========================================
input[1:0] key; //投币按钮
input clk; //计时器
input rst; //重置交易记录和数码管显示
input deal; //交易按钮
output [6:0] a_to_g; //数码管数字显示
output reg[3:0] an; //用来点亮数码管
output reg disp_dot; //小数点
input wire [2:0]sel_goods; // 用来选择商品 001代表可乐 011代表红茶 100代表乌龙茶
input goods_num_query; //库存查询
output reg [7:0] led;//灯光效果!
//=========================变量================================================
//把金额以角为单位
//num[15:12]表示商品价格的十位
//num[11:8]表示商品价格的个位
//num[7:4]表示输入金额的十位
//num[3:0]表示输入金额的个位
reg [15:0] num = 16'b0;
reg [3:0]disp_num; //数码管显示数字
reg [11:0]num_goods = 12'h555; //初始设置库存都为
reg [23:0]clk_cnt = 24'b0; //时钟计数
reg [26:0]blink_clk_cnt = 27'b0;//用于计数闪烁时间
reg [15:0] changes; //找零金额
reg ch_flag = 0; //找零标志位
reg goods_flag = 1; //库存标志位
reg deal_flag = 0; //交易是否进行的标志位
wire deal_success_flag; //交易是否成功的标志
reg [3:0] num_goods_disp; //用于显示库存
reg [4:0] num_5_jiao = 4'b0; //投入的5角的数量
reg [4:0] num_1_yuan = 4'b0; //投入的1元的数量
reg blink_flag = 1; //闪烁标志
reg blink_rst;//闪烁重置
//=========================商品选择=============================================
//001代表可乐 对应的数字为20
//010代表红茶 对应的数字为35
//100代表乌龙茶 对应的数字为30
always @(posedge clk)
begin
case(sel_goods)
1:num[15:8] = 8'b00100000;
2:num[15:8] = 8'b00110101;
4:num[15:8] = 8'b00110000;
default:num[15:8] = 8'b00000000;
endcase
end
//=========================输入金额===========================================
//输入5角
always @(posedge key[0] or posedge rst)
begin
if (rst)
num_5_jiao = 4'b0;
else
begin
if (key[0])
num_5_jiao = num_5_jiao + 1;
end
end
//输入1元
always @(posedge key[1] or posedge rst)
begin
if (rst)
num_1_yuan = 4'b0;
else
begin
if (key[1])
num_1_yuan = num_1_yuan + 1;
end
end
// 汇总金额
always @(posedge clk)
begin
//个位数字(小数部分)只与5角的数量有关
//5角的个数为奇数,则为5;个数为偶数,则为0
if (num_5_jiao[0])
num[3:0] = 4'b0101;
else
num[3:0] = 4'b0;
//十位数字则与1元和5角的数量有关
//因为2个5角就是1元,所以只要取5角的个数的前三位
num[7:4] = num_1_yuan + num_5_jiao[3:1];
end
//=========================交易===================================================
assign deal_success_flag = ch_flag & goods_flag;
always @(posedge deal or posedge rst)
begin
//按下rst按键,重置交易记录
if (rst)
begin
ch_flag <= 0;
deal_flag <= 0;
goods_flag <= 1;
blink_rst <= 1;
end
else
begin
blink_rst <= 0;
deal_flag <= 1;
//输入金额大于等于商品价格
if (num[7:0] >= num[15:8])
begin
changes[15:8] <= 8'b0;
//找零
if (num[3:0] >= num[11:8])
changes[7:0] <= num[7:0] - num[15:8];
else
begin
changes[3:0] <= 4'b0101;
changes[7:4] <= num[7:4] - num[15:12] - 1;
end
//标志位
ch_flag <= 1;
//减去库存
case(sel_goods)
1:
begin
if (num_goods[3:0] >= 1)
begin
num_goods[3:0] <= num_goods[3:0] - 1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
2:
begin
if (num_goods[7:4] >= 1)
begin
num_goods[7:4] <= num_goods[7:4] - 4'b1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
4:
begin
if (num_goods[11:8] >= 1)
begin
num_goods[11:8] <= num_goods[11:8] - 1;
goods_flag <= 1;
end
else
goods_flag <= 0;
end
default: num_goods <= num_goods;
endcase
end
//输入金额小于商品价格
else
ch_flag <= 0;
end
end
//=========================库存查询以及库存改变=====================================
always @(posedge clk)
begin
if (goods_num_query)
begin
case(sel_goods)
1:num_goods_disp = num_goods[3:0];
2:num_goods_disp = num_goods[7:4];
4:num_goods_disp = num_goods[11:8];
endcase
end
end
//=========================数码管显示==============================================
always @(posedge clk)
begin
if (clk_cnt == 24'hFFFFFF)
clk_cnt <= 24'b0;
else
clk_cnt <= clk_cnt + 1;
//重置闪烁标志
if (blink_rst)
blink_flag = 1;
//未进行交易时
if (!deal_flag)
begin
//查询库存
if (goods_num_query)
begin
disp_num = disp_num_get(clk_cnt, num_goods_disp);
an = an_get(clk_cnt);
end
else
begin
disp_dot = dot_get(clk_cnt, 4'b0101);
disp_num = disp_num_get(clk_cnt, num);
an = an_get(clk_cnt);
end
end
//交易后
else
begin
//交易成功,显示零钱
if (deal_success_flag)
begin
//交易成功的灯光闪烁效果
case (clk_cnt[23:21])
0:led = 8'b00000001;
1:led = 8'b00000010;
2:led = 8'b00000100;
3:led = 8'b00001000;
4:led = 8'b00010000;
5:led = 8'b00100000;
6:led = 8'b01000000;
7:led = 8'b10000000;
endcase
disp_dot = dot_get(clk_cnt, 4'b1101);
disp_num = disp_num_get(clk_cnt, changes);
an = an_get(clk_cnt);
end
//金额不足交易失败,闪烁数字6次,大约2秒
else if (!ch_flag)
begin
if (blink_flag)
begin
blink_clk_cnt = blink_clk_cnt + 1;
if (blink_clk_cnt[26] && blink_clk_cnt[25])
begin
blink_flag = 0;
blink_clk_cnt = 0;
end
if (clk_cnt[23])
begin
disp_dot = dot_get(clk_cnt, 4'b0101);
disp_num = disp_num_get(clk_cnt, num);
an = an_get(clk_cnt);
end
end
else
begin
disp_dot = dot_get(clk_cnt, 4'b0101);
disp_num = disp_num_get(clk_cnt, num);
an = an_get(clk_cnt);
end
end
//库存不足交易失败,显示库存,肯定是0啊
else if(!goods_flag)
begin
disp_dot = dot_get(clk_cnt, 4'b1111);
disp_num = disp_num_get(clk_cnt, 16'b0);
an = an_get(clk_cnt);
end
end
end
//=========================得到显示数字函数=========================================
//根据计数器交替输出显示数字
function [3:0] disp_num_get;
input [23:0] clk_cnt;
input [16:0] num;
begin
case (clk_cnt[15:14])
0:disp_num_get = num[3:0];
1:disp_num_get = num[7:4];
2:disp_num_get = num[11:8];
3:disp_num_get = num[15:12];
endcase
end
endfunction
//=========================小数点显示函数==========================================
function dot_get;
input [23:0] clk_cnt;
input [3:0] dot;
begin
case (clk_cnt[15:14])
0:dot_get = dot[0];
1:dot_get = dot[1];
2:dot_get = dot[2];
3:dot_get = dot[3];
endcase
end
endfunction
//=========================数码管交替点亮函数=======================================
//根据计数器交替输出应该点亮的数码管
function [3:0] an_get;
input [23:0] clk_cnt;
begin
case (clk_cnt[15:14])
0:an_get = 4'b1110;
1:an_get = 4'b1101;
2:an_get = 4'b1011;
3:an_get = 4'b0111;
endcase
end
endfunction
clk_sw_7seg_sub A1( .NUM(disp_num),
.a_to_g(a_to_g));
endmodule
//=========================数码管显示模块===========================================
module clk_sw_7seg_sub(
input [3:0]NUM,
output reg[6:0]a_to_g
);
always @(*)
case(NUM)
0:a_to_g=7'b0000001;
1:a_to_g=7'b1001111;
2:a_to_g=7'b0010010;
3:a_to_g=7'b0000110;
4:a_to_g=7'b1001100;
5:a_to_g=7'b0100100;
6:a_to_g=7'b0100000;
7:a_to_g=7'b0001111;
8:a_to_g=7'b0000000;
9:a_to_g=7'b0000100;
'hA: a_to_g=7'b0001000;
'hB: a_to_g=7'b1100000;
'hC: a_to_g=7'b0110001;
'hD: a_to_g=7'b1000010;
'hE: a_to_g=7'b0110000;
'hF: a_to_g=7'b0111000;
default: a_to_g=7'b0000001;
endcase
endmodule
网友评论