🎉欢迎来到FPGA专栏~状态机基础知识
- ☆* o(≧▽≦)o *☆嗨~我是小夏与酒🍹
- ✨博客主页:小夏与酒的博客
- 🎈该系列文章专栏:FPGA学习之旅
- 文章作者技术和水平有限,如果文中出现错误,希望大家能指正🙏
- 📜 欢迎大家关注! ❤️
🎉 目录-状态机基础知识
- 一、效果演示
- 二、状态机基础知识
- 三、Hello例程分析
- 四、简单例程分析
- 4.1 使用状态机实现流水灯
- 4.2 使用状态机实现循迹小车的pwm
一、效果演示
🔸Hello状态机例程:
RTL视图:
状态转移:
🔸 流水灯状态机例程:
使用小精灵V2实现的效果:
小精灵V2基础使用记录:【FPGA-Spirit_V2】小精灵V2开发板初使用。
RTL视图:
状态转移:
🔸循迹小车pwm状态机例程:
二、状态机基础知识
状态机全称是有限状态机(Finite State Machine,FSM),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
状态机分为摩尔(Moore)型有限状态机与米利(Mealy)型有限状态机。摩尔状态机的输出只由输入确定(不直接依赖于当前状态)。米利有限状态机的输出不止与其输入有关,还与它的当前状态相关,这也是与摩尔有限状态机的不同之处。
对于状态机的描述方式,可分为一段式、两段式以及三段式。
一段式:整个状态机写到一个 always 模块里面。在该模块中既描述状态转移,又描述状态的输入和输出。
两段式:用两个 always 模块来描述状态机。其中一个 always 模块采用同步时序描述状态转移,另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律及其输出。
三段式:在两个 always 模块描述方法基础上,使用三个 always 模块。一个 always 模块采用同步时序描述状态转移,一个 always 采用组合逻辑判断状态转移条件,描述状态转移规律,另一个 always 模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。
本篇文章主要以三个简单案例为主入门状态机,需要深入学习状态机相关的知识可以参考大佬的文章:FPGA状态机(一段式、二段式、三段式)、摩尔型(Moore)和米勒型(Mealy)。
三、Hello例程分析
Hello例程说明:使用状态机对“Hello”
字符串进行检测,当检测到完整的“Hello”
字符串时,改变led的电平。
状态转移过程说明:在状态H
时,当检测到字符H
,跳转到状态e
,否则,一直保持在H状态
;在状态e
时,当检测到字符e
,跳转到状态l
,否则,跳转回H状态
;…同理,在状态o
时,当检测到字符o
,跳转到状态H
,同时改变led的电平,否则,跳转回H状态
且不改变led电平。
为了便于转移过程的分析,使用独热码对状态进行编码:
localparam ST_H = 5'b00001;
localparam ST_e = 5'b00010;
localparam ST_la = 5'b00100;
localparam ST_lb = 5'b01000;
localparam ST_o = 5'b10000;
同时,需要定义一个状态机寄存器,用于判断当前的状态:
//状态机寄存器
reg[4:0] curr_st;
由于对“Hello”字符串的检测分为5个状态,使用独热码进行编码占用了5个位宽,因此也需要定义一个5个位宽的状态机寄存器。
为了便于模块的维护和管理,在此定义好led的亮和灭:
//定义led状态
parameter led_on = 1'b0;
parameter led_off = 1'b1;
根据上述状态转移的说明分析,编写状态机主程序:
//状态机主程序
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
curr_st <= ST_H;
else begin
case(curr_st)
ST_H:begin
if(data == "H")
curr_st <= ST_e;
else
curr_st <= ST_H;
end
ST_e:begin
if(data == "e")
curr_st <= ST_la;
else
curr_st <= ST_H;
end
ST_la:begin
if(data == "l")
curr_st <= ST_lb;
else
curr_st <= ST_H;
end
ST_lb:begin
if(data == "l")
curr_st <= ST_o;
else
curr_st <= ST_H;
end
ST_o:begin
if(data == "o")
curr_st <= ST_H;
else
curr_st <= ST_H;
end
default:curr_st <= ST_H;
endcase
end
end
“Hello”例程的完整代码:
FSM_Hello.v:
/
//模块:状态机例程-Hello
//作者:CSDN-小夏与酒
module FSM_Hello(
input Clk,
input Rst_n,
input [7:0]data,
output reg led
);
//状态机状态定义
//使用独热码的编码方式
//写法一:
localparam ST_H = 5'b00001;
localparam ST_e = 5'b00010;
localparam ST_la = 5'b00100;
localparam ST_lb = 5'b01000;
localparam ST_o = 5'b10000;
//写法二:
// localparam
// ST_H = 5'b00001,
// ST_e = 5'b00010,
// ST_la = 5'b00100,
// ST_lb = 5'b01000,
// ST_o = 5'b10000;
//状态机寄存器
reg[4:0] curr_st;
//定义led状态
parameter led_on = 1'b0;
parameter led_off = 1'b1;
//状态机主程序
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
curr_st <= ST_H;
else begin
case(curr_st)
ST_H:begin
if(data == "H")
curr_st <= ST_e;
else
curr_st <= ST_H;
end
ST_e:begin
if(data == "e")
curr_st <= ST_la;
else
curr_st <= ST_H;
end
ST_la:begin
if(data == "l")
curr_st <= ST_lb;
else
curr_st <= ST_H;
end
ST_lb:begin
if(data == "l")
curr_st <= ST_o;
else
curr_st <= ST_H;
end
ST_o:begin
if(data == "o")
curr_st <= ST_H;
else
curr_st <= ST_H;
end
default:curr_st <= ST_H;
endcase
end
end
//给led赋值,如果进入ST_o状态,并且data = "o",则led电平改变
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led <= led_off;
else if(curr_st == ST_o && data == "o")
led <= led_on;
else
led <= led_off;
end
endmodule
RTL视图:
编写测试激励文件:
FSM_Hello_tb.v:
`timescale 1ns/1ns
`define clock_period 20
module FSM_Hello_tb;
reg Clk;
reg Rst_n;
reg [7:0]ASCII;
wire led;
FSM_Hello FSM_Hello0(
.Clk(Clk),
.Rst_n(Rst_n),
.data(ASCII),
.led(led)
);
initial Clk = 1;
always #(`clock_period/2) Clk = ~Clk;
initial begin
Rst_n = 0;
ASCII = 0;
#(`clock_period*20);
Rst_n = 1;
#(`clock_period*20 + 1);
ASCII = "I";
#(`clock_period);
ASCII = "A";
#(`clock_period);
ASCII = "M";
#(`clock_period);
ASCII = "X";
#(`clock_period);
ASCII = "H";
#(`clock_period);
ASCII = "E";
#(`clock_period);
ASCII = "M";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "H";
#(`clock_period);
ASCII = "E";
#(`clock_period);
ASCII = "L";
#(`clock_period);
ASCII = "L";
#(`clock_period);
ASCII = "O";
#(`clock_period);
ASCII = "H";
#(`clock_period);
ASCII = "e";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "o";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "H";
#(`clock_period);
ASCII = "e";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "l";
#(`clock_period);
ASCII = "o";
#(`clock_period);
ASCII = "l";
#(`clock_period);
$stop;
end
endmodule
仿真结果:
四、简单例程分析
4.1 使用状态机实现流水灯
使用Verilog实现一个流水灯并不难,但是使用状态机的方式实现,就提供了新的编程思路。
在状态机的文章中加入流水灯的实现,主要是为了学习 【小月电子】 大佬的状态机写法风格,大佬博客主页链接:Moon_3181961725。
先上完整代码:
FSM_01_led.v:
/
//模块:状态机例程-LED-分析
//作者:CSDN-小夏与酒
module FSM_01_led(
input Clk,
input Rst_n,
output reg led1,
output reg led2,
output reg led3
);
//定义状态机
parameter ST1 = 1;
parameter ST2 = 2;
parameter ST3 = 3;
reg[3:0] curr_st;//状态机寄存器
reg[7:0] cnt1;//定义计数寄存器
reg[7:0] cnt2;//定义计数寄存器
reg[7:0] cnt3;//定义计数寄存器
//状态机主程序
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
curr_st <= ST1;
else case(curr_st)
ST1:begin
if(cnt1 == 8'd9)
curr_st <= ST2;
else;
end
ST2:begin
if(cnt2 == 8'd9)
curr_st <= ST3;
else;
end
ST3:begin
if(cnt3 == 8'd9)
curr_st <= ST1;
else;
end
default:;
endcase
end
//状态机ST1的计数器,当状态机等于ST1时,cnt1加1,否则cnt1等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt1 <= 8'b0;
else if(curr_st == ST1)
cnt1 <= cnt1 + 1'b1;
else
cnt1 <= 8'b0;
end
//状态机ST2的计数器,当状态机等于ST2时,cnt2加1,否则cnt2等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt2 <= 8'b0;
else if(curr_st == ST2)
cnt2 <= cnt2 + 1'b1;
else
cnt2 <= 8'b0;
end
//状态机ST3的计数器,当状态机等于ST3时,cnt3加1,否则cnt3等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt3 <= 8'b0;
else if(curr_st == ST3)
cnt3 <= cnt3 + 1'b1;
else
cnt3 <= 8'b0;
end
//给LED1赋值,当状态等于ST1时,LED1等于0,即点亮LED灯,否则LED1等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led1 <= 1'b1;
else if(curr_st == ST1)
led1 <= 1'b0;
else
led1 <= 1'b1;
end
//给LED2赋值,当状态等于ST2时,LED2等于0,即点亮LED灯,否则LED2等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led2 <= 1'b1;
else if(curr_st == ST2)
led2 <= 1'b0;
else
led2 <= 1'b1;
end
//给LED3赋值,当状态等于ST3时,LED3等于0,即点亮LED灯,否则LED3等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led3 <= 1'b1;
else if(curr_st == ST3)
led3 <= 1'b0;
else
led3 <= 1'b1;
end
endmodule
RTL视图与状态转移:
FSM_01_led_tb.v:
`timescale 1ns/1ns
`define clock_period 20
module FSM_01_led_tb;
reg Clk;
reg Rst_n;
wire led1;
wire led2;
wire led3;
FSM_01_led FSM_01_led0(
.Clk(Clk),
.Rst_n(Rst_n),
.led1(led1),
.led2(led2),
.led3(led3)
);
initial Clk = 1;
always #(`clock_period/2) Clk = ~Clk;
initial begin
Rst_n = 0;
#(`clock_period*30);
Rst_n = 1;
#(`clock_period*300);
$stop;
end
endmodule
仿真结果:
实现效果:
在上板验证前记得修改计数器的计数值,即:
/
//模块:状态机例程-LED
//作者:CSDN-小夏与酒
module FSM_01_led(
input Clk,
input Rst_n,
output reg led1,
output reg led2,
output reg led3
);
//定义状态机
parameter ST1 = 1;
parameter ST2 = 2;
parameter ST3 = 3;
reg[3:0] curr_st;//状态机寄存器
reg[24:0] cnt1;//定义计数寄存器
reg[24:0] cnt2;//定义计数寄存器
reg[24:0] cnt3;//定义计数寄存器
//定义计数值范围
parameter cnt_max = 25'd24_999_999; //定时器最大值
parameter cnt_min = 25'd0; //定时器最小值
parameter cnt_add = 1'b1; //定时器定时增量
//定义led状态
parameter led_on = 1'b0;
parameter led_off = 1'b1;
//状态机主程序
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
curr_st <= ST1;
else case(curr_st)
ST1:begin
if(cnt1 == cnt_max)
curr_st <= ST2;
else;
end
ST2:begin
if(cnt2 == cnt_max)
curr_st <= ST3;
else;
end
ST3:begin
if(cnt3 == cnt_max)
curr_st <= ST1;
else;
end
default:;
endcase
end
//状态机ST1的计数器,当状态机等于ST1时,cnt1加1,否则cnt1等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt1 <= cnt_min;
else if(curr_st == ST1)
cnt1 <= cnt1 + cnt_add;
else
cnt1 <= cnt_min;
end
//状态机ST2的计数器,当状态机等于ST2时,cnt2加1,否则cnt2等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt2 <= cnt_min;
else if(curr_st == ST2)
cnt2 <= cnt2 + cnt_add;
else
cnt2 <= cnt_min;
end
//状态机ST3的计数器,当状态机等于ST3时,cnt3加1,否则cnt3等于0
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
cnt3 <= cnt_min;
else if(curr_st == ST3)
cnt3 <= cnt3 + cnt_add;
else
cnt3 <= cnt_min;
end
//给LED1赋值,当状态等于ST1时,LED1等于0,即点亮LED灯,否则LED1等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led1 <= led_off;
else if(curr_st == ST1)
led1 <= led_on;
else
led1 <= led_off;
end
//给LED2赋值,当状态等于ST2时,LED2等于0,即点亮LED灯,否则LED2等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led2 <= led_off;
else if(curr_st == ST2)
led2 <= led_on;
else
led2 <= led_off;
end
//给LED3赋值,当状态等于ST3时,LED3等于0,即点亮LED灯,否则LED3等于1,关闭LED灯
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
led3 <= led_off;
else if(curr_st == ST3)
led3 <= led_on;
else
led3 <= led_off;
end
endmodule
学习方法:
将状态机主程序分割开来单独写,会使整个程序思路清晰。状态机主程序部分:
//状态机主程序
always@(posedge Clk or negedge Rst_n)begin
if(!Rst_n)
curr_st <= ST1;
else case(curr_st)
ST1:begin
if(cnt1 == 8'd9)
curr_st <= ST2;
else;
end
ST2:begin
if(cnt2 == 8'd9)
curr_st <= ST3;
else;
end
ST3:begin
if(cnt3 == 8'd9)
curr_st <= ST1;
else;
end
default:;
endcase
end
4.2 使用状态机实现循迹小车的pwm
关于FPGA实现简单的循迹小车链接:【FPGA-Spirit_V2】基于FPGA的循迹小车-小精灵V2开发板。
现在讲解小车中的pwm模块:
关于模块的端口列表:
module ctrl_moto_pwm(
input clk,//时钟50M
input rst_n,//复位,低电平有效
input [7:0] spd_high_time,//输入高电平持续时间
input [7:0] spd_low_time,//输入低电平持续时间
output period_fini,//一个pwm周期结束的标志位
output reg pwm//脉冲信号
);
该模块需要输入时钟信号、复位信号、高电平持续时间和低电平持续时间;输出一个pwm周期结束的标志位和pwm信号。
定义pwm产生的三个状态:
//状态机
parameter idle = 8'h0;//空闲状态
parameter step_high = 8'h1;//脉冲高电平状态,当为该状态时,pwm为高电平
parameter step_low = 8'h2;//脉冲低电平状态,当为该状态时,pwm为低电平
为了debug的方便,在模块中加入判断产生了一个pwm的标志位:
//产生一个pwm周期的标志位,当一个pwm产生后,输出高电平,否则输出低电平
assign period_fini = (step_low_cnt == step_low_time)?1'b1:1'b0;
同样,与上文相同,为了编程思路的清晰,将状态机主程序单独写:
//状态机主程序
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
curr_st <= idle;
else case(curr_st)
idle:curr_st <= step_high;
step_high:begin
//当高电平计数时间到达输入值时,进行状态跳转
if(step_high_cnt == step_high_time)
curr_st <= step_low;
else;
end
step_low:begin
//当低电平计数时间到达输入值时,进行状态跳转
if(step_low_cnt == step_low_time)
curr_st <= step_high;
else;
end
default:;
endcase
end
在不同的状态下(空闲、高电平、低电平),pwm的信号输出:
//该always块描述pwm的输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pwm <= 0;
else if(curr_st == idle)
pwm <= 0;
else if(curr_st == step_high)
pwm <= 1;
else if(curr_st == step_low)
pwm <= 0;
else
pwm <= 1;
end
完整代码如下:
ctrl_moto_pwm.v:
//脉冲生成模块,通过控制输出脉冲频率及占空比来控制小车的速度
module ctrl_moto_pwm(
input clk,//时钟50M
input rst_n,//复位,低电平有效
input [7:0] spd_high_time,//输入高电平持续时间
input [7:0] spd_low_time,//输入低电平持续时间
output period_fini,//一个pwm周期结束的标志位
output reg pwm//脉冲信号
);
//状态机
parameter idle = 8'h0;//空闲状态
parameter step_high = 8'h1;//脉冲高电平状态,当为该状态时,pwm为高电平
parameter step_low = 8'h2;//脉冲低电平状态,当为该状态时,pwm为低电平
reg [7:0] curr_st;
reg [10:0] step_high_time;
reg [10:0] step_low_time;
reg [10:0] step_high_cnt;
reg [10:0] step_low_cnt;
//产生一个pwm周期的标志位,当一个pwm产生后,输出高电平,否则输出低电平
assign period_fini = (step_low_cnt == step_low_time)?1'b1:1'b0;
//将输入值(高、低电平持续时间)存入寄存器中
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
step_high_time <= 0;
step_low_time <= 0;
end
else begin
step_high_time <= spd_high_time;
step_low_time <= spd_low_time;
end
end
//状态机主程序
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
curr_st <= idle;
else case(curr_st)
idle:curr_st <= step_high;
step_high:begin
//当高电平计数时间到达输入值时,进行状态跳转
if(step_high_cnt == step_high_time)
curr_st <= step_low;
else;
end
step_low:begin
//当低电平计数时间到达输入值时,进行状态跳转
if(step_low_cnt == step_low_time)
curr_st <= step_high;
else;
end
default:;
endcase
end
//高电平持续时间计数器,当持续时间到达输入值时,进行状态跳转
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
step_high_cnt <= 0;
else if(curr_st == idle)
step_high_cnt <= 0;
else if(curr_st == step_high)
step_high_cnt <= step_high_cnt + 1;
else
step_high_cnt <= 0;
end
//低电平持续时间计数器,当持续时间到达输入值时,进行状态跳转
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
step_low_cnt <= 0;
else if(curr_st == idle)
step_low_cnt <= 0;
else if(curr_st == step_low)
step_low_cnt <= step_low_cnt + 1;
else
step_low_cnt <= 0;
end
//该always块描述pwm的输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pwm <= 0;
else if(curr_st == idle)
pwm <= 0;
else if(curr_st == step_high)
pwm <= 1;
else if(curr_st == step_low)
pwm <= 0;
else
pwm <= 1;
end
endmodule
pwm产生模块的代码中我每一部分都写了对应的注释,可供大家参考,该模块可以直接在需要用到pwm的地方直接调用。
该模块对应的RTL视图和状态转移:
我们编写一个简单的测试激励文件:
ctrl_moto_pwm_tb.v:
`timescale 1ns/1ns
`define clock_period 20
module ctrl_moto_pwm_tb;
reg clk;
reg rst_n;
reg [7:0]spd_high_time;
reg [7:0]spd_low_time;
wire period_fini;
wire pwm;
ctrl_moto_pwm Uctrl_moto_pwm0(
.clk(clk),//时钟50M
.rst_n(rst_n),//复位,低电平有效
.spd_high_time(spd_high_time),//输入高电平持续时间
.spd_low_time(spd_low_time),//输入低电平持续时间
.period_fini(period_fini),//一个pwm周期结束的标志位
.pwm(pwm)//脉冲信号
);
initial clk = 1;
always #(`clock_period/2) clk = ~clk;
initial begin
rst_n = 0;
spd_high_time = 0;
spd_low_time = 0;
#(`clock_period*100);
rst_n = 1;
spd_high_time = 15;
spd_low_time = 5;
#(`clock_period*2000);
$stop;
end
endmodule
仿真结果:
🧸结尾
- ❤️ 感谢您的支持和鼓励! 😊🙏
- 📜您可能感兴趣的内容:
- 【FPGA-Spirit_V2】基于FPGA的循迹小车-小精灵V2开发板
- 【Verilog HDL】FPGA-Verilog文件的基本结构
- 【Arduino TinyGo】【最新】使用Go语言编写Arduino-环境搭建和点亮LED灯
- 【全网首发开源教程】【Labview机器人仿真与控制】Labview与Solidworks多路支配关系-四足爬行机器人仿真与控制