FPGA初学记录——可乐机系统搭建
野火征途Pro开发板教程——状态机拓展训练,可乐机系统搭建
文章目录
前言
最近在学习FPGA,因为研究生需要用到,所用的开发板是野火的征途Pro,也是在跟着野火的教程学习,学到了状态机,看到拓展训练,是一个比较综合的练习,可以回顾前面学的按键消抖和计数器的一些知识,还有一些基本的语法,感觉有必要记录下来,说不定以后会有用。
一、问题简述
以状态机为核心搭建一个小系统:以可乐机为背景,一瓶可乐的价格是2.5元。用按键控制投币(加入按键消抖功能),可以投0.5元硬币和1元硬币,投入0.5元后亮一个灯,投入1元后亮2个灯,投入1.5元亮3个灯,投入2元亮4个灯,如果投币后10s不再继续进行投币操作则可乐机回到初始状态。投入2.5元后可乐不找零,此时led灯实现双向流水操作,流水10s后自动停止。这里也有复位键,其功能是终止本次投币操作,使可乐机立刻回到初始状态。
二、功能解析和源代码
1.功能解析
1、首先要求是以状态机为核心,那么可能得写一个基本的状态机,分析题目可知有5个状态,分别是IDLE-HALF-ONE-ONE_HALF-TWO,具体解析可以参考野火的教程,有详细的解析,这里就不再过多赘述。
2、然后要求用按键控制投币,不同价格对应不同的灯亮,其实就是按键控制led的亮灭(加入按键消抖功能),这个好解决,不同的状态控制不同组合的led灯就行。
3、led灯组合形式有7种,初始状态-全灭、0.5元-亮1个灯、1元-亮2个灯、1.5元-亮三个灯、2元-亮四个灯,2.5元-单向流水灯,3元-双向流水灯。比较难的点主要在流水灯,单向流水灯野火教程里有详细解析,双向流水灯是流水灯那一章的拓展训练,搞懂原理分析之后可以解决。
4、流水10s后自动停止,虽然题干中说了挺多个10s后停止,但思路可以简单点,就是10s没操作就回到初始状态。只要没有投币,即按键没有按下,开始计数,计数期间只要有按键按下就计数清零,如果10s没操作,就设置一个标志位,利用这个标志位使得状态返回初始状态,灯全灭即可。因为10s有点长,为了提高调试的效率,所以改成了5s。
2.源代码——整体代码
先把整体代码奉上。
module complex_fsm_system
#(
parameter CNT_MAX = 20'd999_999, // 按键消抖计数器
parameter CNT_MAX_WATER = 25'd24_999_999, // 流水灯计数器
parameter CNT_MAX_INIT = 28'd249_999_999
)
(
input wire sys_clk, //系统时钟50MHz
input wire sys_rst_n, //全局复位
input wire pi_money_one, //投币1元
input wire pi_money_half, //投币0.5元
output wire [3:0] led_out // 输出控制led灯
);
//只有6种状态,使用独热码
parameter IDLE = 5'b00001;
parameter HALF = 5'b00010;
parameter ONE = 5'b00100;
parameter ONE_HALF = 5'b01000;
parameter TWO = 5'b10000;
/*10s归零 定义中间参数*/
reg [27:0] cnt_init;
reg cnt_init_flag;
/*流水灯 定义中间参数*/
reg [24:0] cnt ;
reg cnt_flag ;
reg cnt_flag_left;
/*按键消抖 定义中间参数*/
reg [19:0] cnt_20ms; // 按键消抖计数器
reg key_flag;//1:表示消抖后检测到按键被按下;0:表示没有检测到按键被按下
/*状态机 定义中间参数*/
reg [4:0] state;
reg [3:0] led_out_reg;
reg po_money; //1:表示找零;0:表示不找零
reg po_cola; //1:出可乐;0:不出可乐
wire [1:0] pi_money;
/**********************10s归零代码****************************/
//cnt_init:计数器计数10s
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_init <= 28'b0;
else if(pi_money_half == 1'b0 || pi_money_one == 1'b0)
cnt_init <= 28'b0;
else if(cnt_init == CNT_MAX_INIT)
cnt_init <= cnt_init;
else
cnt_init <= cnt_init + 1'b1;
//cnt_init_flag:计数器计数满10s标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_init_flag <= 1'b0;
else if(cnt_init == CNT_MAX_INIT)
cnt_init_flag <= 1'b1;
else
cnt_init_flag <= 1'b0;
/************************************************************************流水灯代码****************************************************************************/
//cnt:计数器计数500ms
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX_WATER)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//cnt_flag:计数器计数满500ms标志信号
//cnt_flag_left:左移的标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX_WATER - 1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag_left <= 1'b0;
else if((led_out_reg == 4'b1000 || led_out_reg == 4'b1111) && cnt_flag == 1'b1)
cnt_flag_left <= 1'b1;
else if(led_out_reg == 4'b0001 && cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
cnt_flag_left <= 1'b0;
/************************************************************************按键消抖代码****************************************************************************/
//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(pi_money_half == 1'b1 && pi_money_one == 1'b1)
cnt_20ms <= 20'b0;
else if((cnt_20ms == CNT_MAX) && ((pi_money_half == 1'b0) || (pi_money_one == 1'b0)))
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;
//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX-1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
/************************************************************************状态机代码****************************************************************************/
//pi_money:为了减少变量的个数,我们使用位拼接把输入的两个1bit信号拼接成1个2bit信号
//投币方式可以为:不投币(00)、投0.5元(01)、投1元(10),每次只投一个币
assign pi_money = {!pi_money_one&&key_flag,!pi_money_half&&key_flag};
//第一段状态机,描述当前状态state如何根据输入跳转到下一状态
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE : if(pi_money == 2'b01)
state <= HALF;
else if(pi_money == 2'b10)
state <= ONE;
else if(cnt_init_flag == 1'b1)
state <= IDLE;
else
state <= IDLE;
HALF : if(pi_money == 2'b01)
state <= ONE;
else if(pi_money == 2'b10)
state <= ONE_HALF;
else if(cnt_init_flag == 1'b1)
state <= IDLE;
else
state <= HALF;
ONE : if(pi_money == 2'b01)
state <= ONE_HALF;
else if(pi_money == 2'b10)
state <= TWO;
else if(cnt_init_flag == 1'b1)
state <= IDLE;
else
state <= ONE;
ONE_HALF : if(pi_money == 2'b01)
state <= TWO;
else if(pi_money == 2'b10)
state <= IDLE;
else if(cnt_init_flag == 1'b1)
state <= IDLE;
else
state <= ONE_HALF;
TWO : if(pi_money == 2'b01)
state <= IDLE;
else if(pi_money == 2'b10)
state <= IDLE;
else if(cnt_init_flag == 1'b1)
state <= IDLE;
else
state <= TWO;
default : state <= IDLE;
endcase
//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if(cnt_init_flag == 1'b1)
po_cola <= 1'b0;
else if((state == TWO && pi_money == 2'b01)||(state == TWO && pi_money == 2'b10)||(state == ONE_HALF && pi_money == 2'b10)||(state == IDLE && po_cola == 1'b1))
po_cola <= 1'b1;
else
po_cola <= 1'b0;
//第二段状态机,描述当前状态state和输入pi_money如何影响po_money输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_money <= 1'b0;
else if (cnt_init_flag == 1'b1)
po_money <= 1'b0;
else if ((state == TWO && pi_money == 2'b10) || (state == IDLE && po_money == 1'b1))
po_money <= 1'b1;
else if ((state == TWO && pi_money == 2'b01) || (state == IDLE && po_money == 1'b0))
po_money <= 1'b0;
//第二段状态机,描述当前状态state和输入pi_money如何影响led_out输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 4'b0000;
else if (state == IDLE && po_cola == 1'b0)
led_out_reg <= 4'b0000;
else if (state == HALF)
led_out_reg <= 4'b0001;
else if (state == ONE)
led_out_reg <= 4'b0011;
else if (state == ONE_HALF)
led_out_reg <= 4'b0111;
else if (state == TWO && po_cola == 1'b0)
led_out_reg <= 4'b1111;
else if (po_cola == 1'b1 && po_money == 1'b0)
// 单向流水
begin
if((led_out_reg == 4'b1000||led_out_reg == 4'b1111) && cnt_flag == 1'b1)
led_out_reg <= 4'b0001;
else if(cnt_flag == 1'b1)
led_out_reg <= led_out_reg << 1'b1; //左移
end
else if (po_cola == 1'b1 && po_money == 1'b1)
// 双向流水
begin
if((led_out_reg == 4'b1000||led_out_reg == 4'b1111) && cnt_flag == 1'b1)
led_out_reg <= 4'b0100;
else if(led_out_reg == 4'b0001 && cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
led_out_reg <= 4'b0010;
else if(cnt_flag == 1'b1 && cnt_flag_left == 1'b0)
led_out_reg <= led_out_reg << 1'b1;
else if(cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
led_out_reg <= led_out_reg >> 1'b1;
end
assign led_out = ~led_out_reg;
endmodule
2.源代码——状态机
同样先把代码奉上:非整体代码里的内容,只是一个基础,整体代码是在基础代码的基础上修改的
module complex_fsm
(
input wire sys_clk, //系统时钟50MHz
input wire sys_rst_n, //全局复位
input wire pi_money_one, //投币1元
input wire pi_money_half, //投币0.5元
output reg po_money, //1:表示找零;0:表示不找零
output reg po_cola //1:出可乐;0:不出可乐
);
//只有5种状态,使用独热码
parameter IDLE = 5'b00001;
parameter HALF = 5'b00010;
parameter ONE = 5'b00100;
parameter ONE_HALF = 5'b01000;
parameter TWO = 5'b10000;
reg [4:0] state;
wire [1:0] pi_money;
//pi_money:为了减少变量的个数,我们使用位拼接把输入的两个1bit信号拼接成1个2bit信号
//投币方式可以为:不投币(00)、投0.5元(01)、投1元(10),每次只投一个币
assign pi_money = {pi_money_one,pi_money_half};
//第一段状态机,描述当前状态state如何根据输入跳转到下一状态
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE : if(pi_money == 2'b01)
state <= HALF;
else if(pi_money == 2'b10)
state <= ONE;
else
state <= IDLE;
HALF : if(pi_money == 2'b01)
state <= ONE;
else if(pi_money == 2'b10)
state <= ONE_HALF;
else
state <= HALF;
ONE : if(pi_money == 2'b01)
state <= ONE_HALF;
else if(pi_money == 2'b10)
state <= TWO;
else
state <= ONE;
ONE_HALF : if(pi_money == 2'b01)
state <= TWO;
else if(pi_money == 2'b10)
state <= IDLE;
else
state <= ONE_HALF;
TWO : if(pi_money == 2'b01)
state <= IDLE;
else if(pi_money == 2'b10)
state <= IDLE;
else
state <= TWO;
default : state <= IDLE;
endcase
//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if((state == TWO && pi_money == 2'b01)||(state == TWO && pi_money == 2'b10)||(state == ONE_HALF && pi_money == 2'b10))
po_cola <= 1'b1;
else
po_cola <= 1'b0;
//第二段状态机,描述当前状态state和输入pi_money如何影响po_money输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_money <= 1'b0;
else if ((state == TWO)&&(pi_money == 2'b10))
po_money <= 1'b1;
else
po_money <= 1'b0;
endmodule
野火状态机的套路写法是新的二段式状态机,第一段是用时序逻辑描述当前状态并用组合逻辑描述下一状态;第二段描述状态输出。
第一个always块描述状态的转移为第一段状态机,第二个always块描述数据的输出为第二段状态机(如果我们遵循一个always块只描述一个变量的原则,如果有多个输出时第二段状态机就可以分为多个always块来表达,但理论上仍属于新二段状态机,多以几段式状态机并不是由always块的数量简单决定的)。
3.源代码——按键消抖控制灯
同样先把代码奉上:非整体代码里的内容,只是一个基础,整体代码是在基础代码的基础上修改的
module key_filter
#(
parameter CNT_MAX = 20'd999_999
)
(
input wire sys_clk,
input wire sys_rst_n,
input wire key_in1,
input wire key_in2,
output wire [1:0] led_out
);
reg [19:0] cnt_20ms;
reg key_flag; //key_flag为1时表示消抖后检测到按键被按下;key_flag为0时表示没有检测到按键被按下
reg [1:0] led_out_reg;
wire [1:0] key_in;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in1 == 1'b1 && key_in2 == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX && (key_in1 == 1'b0 || key_in2 == 1'b0))
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
assign key_in = {!key_in1&&key_flag,!key_in2&&key_flag};
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 2'b00;
else if(key_in == 2'b10)
led_out_reg <= 2'b10;
else if(key_in == 2'b01)
led_out_reg <= 2'b01;
else
led_out_reg <= 2'b00;
assign led_out = ~led_out_reg;
endmodule
基础代码是两个按键控制两个灯,主要是想尝试这个的写法
assign key_in = {!key_in1&&key_flag,!key_in2&&key_flag};
把两个按键的状态结合按键消抖结合在了一起,有点像合成一个整体,给人一种集成的感觉,写的时候感觉舒服些哈哈哈哈哈,整体代码里面没有直接按键控制灯,而是按键控制状态,状态控制灯。
4.源代码——流水灯
同样先把代码奉上:非整体代码里的内容,只是一个基础,整体代码是在基础代码的基础上修改的
单向流水灯
module water_led
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk , //系统时钟50Mh
input wire sys_rst_n , //全局复位
output wire [3:0] led_out //输出控制led灯
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg [24:0] cnt ;
reg cnt_flag ;
reg [3:0] led_out_reg ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//cnt:计数器计数500ms
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//cnt_flag:计数器计数满500ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX - 1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
//led_out_reg:led循环流水
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 4'b0001;
else if(led_out_reg == 4'b1000 && cnt_flag == 1'b1)
led_out_reg <= 4'b0001;
else if(cnt_flag == 1'b1)
led_out_reg <= led_out_reg << 1'b1; //左移
assign led_out = ~led_out_reg;
endmodule
双向流水灯
module water_led
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk, //系统时钟50MHz
input wire sys_rst_n, //全局复位
output wire [3:0] led_out // 输出控制led灯
);
reg [24:0] cnt;
reg cnt_flag;
reg cnt_flag_left;
reg [3:0] led_out_reg;
//cnt:计数器计数500ms
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//cnt_flag:计数器计数满500ms标志信号
//cnt_flag_left:左移的标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX - 1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag_left <= 1'b0;
else if(led_out_reg == 4'b1000 && cnt_flag == 1'b1)
cnt_flag_left <= 1'b1;
else if(led_out_reg == 4'b0001 && cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
cnt_flag_left <= 1'b0;
//led_out_reg:led循环流水
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 4'b0001;
else if(led_out_reg == 4'b1000 && cnt_flag == 1'b1)
led_out_reg <= 4'b0100;
else if(led_out_reg == 4'b0001 && cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
led_out_reg <= 4'b0010;
else if(cnt_flag == 1'b1 && cnt_flag_left == 1'b0)
led_out_reg <= led_out_reg << 1'b1;
else if(cnt_flag == 1'b1 && cnt_flag_left == 1'b1)
led_out_reg <= led_out_reg >> 1'b1;
assign led_out = ~led_out_reg;
endmodule
双向流水灯自己是利用了标志位。左循环到第四个灯的时候就让标志位为1,改变流水方向,如果有更简单的写法可以一起讨论吖,初学老感觉自己写的代码有些冗余。
5.源代码——10s归零(5s)
这个就没啥基础代码了,就是设定个计数器,计数满了设定标志位就行,可能要注意的就是计数的条件,得是按键没有操作的时候再进行计数,代码里的描述是给计数清零写了条件,然后else就是计数啦
/*10s归零 定义中间参数*/
reg [27:0] cnt_init;
reg cnt_init_flag;
/**********************10s归零代码****************************/
//cnt_init:计数器计数10s
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_init <= 28'b0;
else if(pi_money_half == 1'b0 || pi_money_one == 1'b0)
cnt_init <= 28'b0;
else if(cnt_init == CNT_MAX_INIT)
cnt_init <= cnt_init;
else
cnt_init <= cnt_init + 1'b1;
这里在调试的时候遇到一个小问题,在判断是否出可乐的代码上,我开始是这么写的
//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if((state == TWO && pi_money == 2'b01)||(state == TWO && pi_money == 2'b10)||(state == ONE_HALF && pi_money == 2'b10)||(state == IDLE && po_cola == 1'b1))
po_cola <= 1'b1;
else if(cnt_init_flag == 1'b1)
po_cola <= 1'b0;
else
po_cola <= 1'b0;
这样运行代码的时候发现,在进行流水灯的时候,无操作等待5s,并没有回到初始状态(即灯全灭),一直在循环,后来我改成了这样就行了。
//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_cola <= 1'b0;
else if(cnt_init_flag == 1'b1)
po_cola <= 1'b0;
else if((state == TWO && pi_money == 2'b01)||(state == TWO && pi_money == 2'b10)||(state == ONE_HALF && pi_money == 2'b10)||(state == IDLE && po_cola == 1'b1))
po_cola <= 1'b1;
else
po_cola <= 1'b0;
哈哈哈哈哈,比较神奇,就是换了一个顺序,这其实是个比较简单的问题,类似于优先级,算是比较细节吧,也不知道怎么说,等我以后组织组织语言,细节决定成败。
三、总结
大体内容就是这样啦,因为不是很熟练,整个功能的全部实现花了比较多的时间,每个功能或许能单独实现,但是整合在一起总会出现一些小问题,不过问题解决了心情还是很好滴,代码格式得注意可读性,一开始代码太乱了,给自己看糊涂了,最后整理下代码,定义定义放一块,功能功能放一块,看起来舒服多了,然后就是尽量不要用循环,一开始我看到10s我想要不循环算了,后来查了资料发现循环用多路,电路也会更加复杂,想了想就算了。
一开始我还想用以前写python的思路,把每个功能写成模块或者函数,直接调用就行,后来尝试了,问题好多,等学了更多的时候再试试这种思路吧,这种模块化设计的思路还挺重要的。