前言
本文章为FPGA设计的实例,基于Verilog,来源于学习FPGA中进行的一项练习,为了能够更快更好的学习FPGA。这也是一项比较全面的一次练习。
设计思路
平台:Quartus;仿真:Altera_modelsim
使用状态机进行设计,通过按键进行状态的跳转,电路接口如图所示:
设计一个有A,B,C三种商品的自动售货机,通过按键进行选择,状态跳转,以及投币,找零的流程。
状态机的设计:
IDLE(空闲):数码管上显示 A3b5C1;
PICK(挑选):通过按键1进行选择ABC商品,通过按键2选择购买的数量;
BUY(购买计算):选择好商品以及数量以后,计算出需要的总金额并显示在数码管上,通过按键1,按键2进行投币;
ZL(找零):投币金额减去商品的总金额,将找零的金额显示在数码管上;
CH(出货):当投币金额>=商品总金额后,LED灯闪烁以及数码管显示FFFFFF。
代码部分
控制模块代码
/**************************************************************
@File : ctrl.v
@Time : 2024/03/26 10:06:48
@Author : huangmo777x7
@EditTool: VS Code
@Font : UTF-8
@Function: 自动售货机控制模块
**************************************************************/
module ctrl (
input clk ,
input rst_n ,
input [2:0] key_in ,
output reg [23:0] display_data ,
output reg [5:0] display_point ,
output reg [5:0] display_mask ,
output reg [3:0] led
);
//中间信号定义
localparam IDLE = 5'b00001, //空闲状态
PICK = 5'b00010, //选择商品以及数量
BUY = 5'b00100, //进行投币,key_in[1]+1 key_in[2]+5
ZL = 5'b01000, //找零 投币-金额=找的钱
CH = 5'b10000; //商品出货
reg [4:0] state ;
wire idle2pick ;
wire pick2buy ;
wire buy2zl ;
wire buy2ch ;
wire zl2ch ;
wire ch2idle ;
reg [5:0] pick_sel;
reg [3:0] a,b,c;
wire [6:0] sum;
wire [3:0] sum_g,sum_s;
reg [6:0] pay_sum;
wire [3:0] pay_sum_g,pay_sum_s;
wire [6:0] diff_sum;
wire [3:0] diff_sum_g,diff_sum_s;
reg [26:0] cnt_delay;
wire add_delay_cnt,end_delay_cnt;
parameter TIME_2S = 100_000_000;
reg [24:0] cnt_mask;
wire add_mask_cnt,end_mask_cnt;
parameter TIME_500MS = 25_000_000;
wire [23:0] display_idle ;
wire [23:0] display_pick ;
wire [23:0] display_buy ;
wire [23:0] display_zl ;
wire [23:0] display_ch ;
reg [22:0] cnt_led;
wire add_led_cnt,end_led_cnt ;
parameter TIME_100MS = 5_000_000 ;
/******************************************************************
状态机设计
*******************************************************************/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
state <= IDLE;
else case(state)
IDLE : if(idle2pick) state <= PICK ;
PICK : if(pick2buy) state <= BUY ;
BUY : if(buy2zl) state <= ZL ;
else if(buy2ch) state <= CH ;
ZL : if(zl2ch) state <= CH ;
CH : if(ch2idle) state <= IDLE ;
endcase
end
assign idle2pick = state == IDLE && key_in[0];
assign pick2buy = state == PICK && key_in[0];
assign buy2zl = state == BUY && pay_sum > sum;
assign buy2ch = state == BUY && pay_sum == sum;
assign zl2ch = state == ZL && end_delay_cnt ;
assign ch2idle = state == CH && end_delay_cnt ;
/******************************************************************
各个状态功能
*******************************************************************/
//通过按键1进行数码管选位
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pick_sel <= 6'b000001;
else if(key_in[1] && state == PICK)
pick_sel <= {pick_sel[3:0],pick_sel[5:4]};
else if(pick2buy)
pick_sel <= 6'b000001;
end
//通过按键2进行计数
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
a <= 'b0;
b <= 'b0;
c <= 'b0;
end
else if(key_in[2] && state == PICK)
case(pick_sel)
6'b000001 : if(c == 9) c <= 0; else c <= c + 1 ;
6'b000100 : if(b == 9) b <= 0; else b <= b + 1 ;
6'b010000 : if(a == 9) a <= 0; else a <= a + 1 ;
endcase
else if(ch2idle)begin
a <= 'b0;
b <= 'b0;
c <= 'b0;
end
end
//sum 商品的总价
assign sum = 3*a + 5*b + 1*c;
assign sum_g = sum % 10;
assign sum_s = sum / 10;
//pay_sum 投币的金额
always@(posedge clk or negedge rst_n)
if(!rst_n)
pay_sum <= 'b0;
else if(key_in[2] && state == BUY)
pay_sum <= pay_sum + 5;
else if(key_in[1] && state == BUY)
pay_sum <= pay_sum + 1;
else if(ch2idle)
pay_sum <= 'b0;
assign pay_sum_g = pay_sum % 10;
assign pay_sum_s = pay_sum / 10;
//找零=投币的金额-商品的总价
assign diff_sum = pay_sum - sum;
assign diff_sum_g = diff_sum % 10;
assign diff_sum_s = diff_sum / 10;
//ZL和CH状态的延时计数器
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt_delay <= 'd0;
else if(add_delay_cnt)begin
if(end_delay_cnt)
cnt_delay <= 'd0;
else
cnt_delay <= cnt_delay + 1'b1;
end
assign add_delay_cnt = ((state == ZL) || (state == CH));
assign end_delay_cnt = add_delay_cnt && cnt_delay == TIME_2S - 1 ;
/******************************************************************
显示数据输出
*******************************************************************/
//各个状态数码管所显示的
always@(*)
case(state)
IDLE : begin display_data = display_idle ; display_point = 6'b010100; end
PICK : begin display_data = display_pick ; display_point = 6'b010100; end
BUY : begin display_data = display_buy ; display_point = 6'b001000; end
ZL : begin display_data = display_zl ; display_point = 6'b000000; end
CH : begin display_data = display_ch ; display_point = 6'b000000; end
default : begin display_data = display_idle ; display_point = 6'b111111; end
endcase
assign display_idle = {4'd10,4'd3,4'd11,4'd5,4'd12,4'd1};
assign display_pick = {4'd10,a,4'd11,b,4'd12,c};
assign display_buy = {4'd13,sum_s,sum_g,4'd5,pay_sum_s,pay_sum_g};
assign display_zl = {4'd1,4'd1,4'd1,4'd14,diff_sum_s,diff_sum_g};
assign display_ch = {4'd15,4'd15,4'd15,4'd15,4'd15,4'd15};
//掩码
always@(posedge clk or negedge rst_n)
if(!rst_n)
display_mask <= 6'b111111;
else case(state)
PICK : if(end_mask_cnt)
case(pick_sel)
6'b000001 : begin display_mask <= {5'b11111,~display_mask[0]}; end
6'b000100 : begin display_mask <= {3'b111,~display_mask[2],2'b11}; end
6'b010000 : begin display_mask <= {1'b1,~display_mask[4],4'b1111}; end
default : ;
endcase
ZL : display_mask <= 6'b000111;
default : display_mask <= 6'b111111;
endcase
//选择数码管闪烁计数器
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt_mask <= 'd0;
else if(add_mask_cnt)begin
if(end_mask_cnt)
cnt_mask <= 'd0;
else
cnt_mask <= cnt_mask + 1'b1;
end
assign add_mask_cnt = 1;
assign end_mask_cnt = add_mask_cnt && cnt_mask == TIME_500MS - 1 ;
/******************************************************************
出货时LED流水闪烁
*******************************************************************/
always@(posedge clk or negedge rst_n)
if(!rst_n)
led <= 'b0;
else if(state == CH && end_led_cnt)
if(led == 'b0 || led == 4'b0001)
led <= 4'b1000;
else
led <= (led >> 1);
else if(state != CH)
led <= 'b0;
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt_led <= 'd0;
else if(add_led_cnt)begin
if(end_led_cnt)
cnt_led <= 'd0;
else
cnt_led <= cnt_led + 1'b1;
end
assign add_led_cnt = 1;
assign end_led_cnt = add_led_cnt && cnt_led == TIME_100MS - 1 ;
endmodule
仿真代码
/**************************************************************
@File : ctrl_tb.v
@Time : 2024/03/26 15:46:14
@Author : haungmo777x7
@EditTool: VS Code
@Font : UTF-8
@Function: 仿真ctrl模块文件
**************************************************************/
`timescale 1ns/1ps
module ctrl_tb ();
parameter CLK_CYCLE = 20;
reg sys_clk,sys_rst_n;
always #(CLK_CYCLE/2) sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b1;
sys_rst_n = 1'b0;
#(CLK_CYCLE*2);
sys_rst_n = 1'b1;
end
reg [2:0] key_in; // 按键
wire [3:0] led; //LED灯
defparam ctrl.TIME_2S = 200; //重定义时间,让仿真快一些
defparam ctrl.TIME_500MS = 50; //重定义时间,让仿真快一些
defparam ctrl.TIME_100MS = 10; //重定义时间,让仿真快一些
ctrl ctrl(
/* input */.clk (sys_clk) ,
/* input */.rst_n (sys_rst_n) ,
/* input [2:0] */.key_in (key_in ) ,
/* output */.led (led)
);
initial begin
key_in = 0;
#(CLK_CYCLE*20);
//进入状态(激活售货机)
my_key(0);
//买三件C
my_key(2);
my_key(2);
my_key(2); //3
//切换到B商品
my_key(1);
//买两件B
my_key(2);
my_key(2); //10
//切换到A
my_key(1);
//买1件A
my_key(2); //3
//结算
my_key(0);
//故意多给1块 17
my_key(1);
my_key(1);
my_key(2);
my_key(2);
my_key(2);
#2000;
$stop;
end
task my_key;
input [1:0] num;
begin
key_in[num] = 1;
#(CLK_CYCLE*1);
key_in[num] = 0;
#(CLK_CYCLE*1000);
end
endtask
endmodule
仿真结果
仿真结果也是调试了很多次代码,结果如图所示,将数码管驱动模块以及按键消抖模块加入即可上板进行验证。
上板验证效果:
111