FPGA学习(四)------状态机重写LED流水灯并仿真
目录
- FPGA学习(四)------状态机重写LED流水灯并仿真
- 一、状态机编程思想
- 二、LED流水灯仿真实现
- 三、实现效果
- 四、CPLD和FPGA芯片主要技术区别
- 五、hdlbitsFPGA------组合逻辑学习
- 六、总结
一、状态机编程思想
1、状态机要素
可以把状态机的要素分为4个要素,即:现态、条件、动作、次态。"现态"和"条件"是因,"动作"和"次态"是果。
(1)现态:是指当前所处状态;
(2)条件:又称为"事件"。当条件被满足时,将会触发一个动作,或者执行一次状态的迁移。
(3)动作:条件满足后执行的动作。动作不是必须的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
(4)次态:条件满足后要迁移往的新状态。"次态"是相对于"现态"而言的,"次态"一旦被激活,就转变成新的"现态"了。
2、状态迁移图

(1)状态框:用方框表示状态,包括所谓的"现态"和"次态";
(2)条件及迁移箭头:用箭头表示状态迁移的方向,并在该箭头上标注触发条件;
(3)节点圆圈:当多个箭头指向一个状态时,可以用节点符号(小圆圈)连接汇总;
(4)动作框:用椭圆框表示;
(5)附加条件判断框:用六角菱形框表示;
3、状态机写法


二、LED流水灯仿真实现
1、代码实现
flow_led.v:
module flow_led (
input wire clk, // 系统时钟输入(假设50MHz)
input wire rst_n, // 异步低电平复位信号(0复位,1工作)
output reg [7:0] leds // 8位LED输出,控制8个LED灯
);
// ==============================================
// 状态定义:使用独热码(one-hot)或二进制编码
// 这里使用3位二进制编码表示8个状态
// ==============================================
parameter S0 = 3'b000; // 第1个LED亮
parameter S1 = 3'b001; // 第2个LED亮
parameter S2 = 3'b010; // 第3个LED亮
parameter S3 = 3'b011; // 第4个LED亮
parameter S4 = 3'b100; // 第5个LED亮
parameter S5 = 3'b101; // 第6个LED亮
parameter S6 = 3'b110; // 第7个LED亮
parameter S7 = 3'b111; // 第8个LED亮
// ==============================================
// 内部信号定义
// ==============================================
reg [2:0] current_state; // 当前状态寄存器
reg [2:0] next_state; // 下一状态组合逻辑
reg [25:0] counter; // 26位计数器,用于分频产生延时
// ==============================================
// 第一段:状态寄存器(时序逻辑)
// 功能:在时钟上升沿或复位下降沿更新当前状态
// ==============================================
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 异步复位,回到初始状态
current_state <= S0;
counter <= 26'd0;
end
else begin
// 当计数器达到50_000_000时(约0.2秒@50MHz)切换状态
if (counter == 26'd50_000_000) begin
current_state <= next_state; // 状态转移
counter <= 26'd0; // 计数器清零
end
else begin
counter <= counter + 1; // 计数器递增
end
end
end
// ==============================================
// 第二段:下一状态逻辑(组合逻辑)
// 功能:根据当前状态确定下一状态
// ==============================================
always @(*) begin
case (current_state)
S0: next_state = S1; // 状态0→状态1
S1: next_state = S2; // 状态1→状态2
S2: next_state = S3; // 状态2→状态3
S3: next_state = S4; // 状态3→状态4
S4: next_state = S5; // 状态4→状态5
S5: next_state = S6; // 状态5→状态6
S6: next_state = S7; // 状态6→状态7
S7: next_state = S0; // 状态7→状态0(循环)
default: next_state = S0; // 默认回到初始状态
endcase
end
// ==============================================
// 第三段:输出逻辑(时序逻辑)
// 功能:根据当前或下一状态产生输出信号
// 注意:这里使用时序逻辑输出可以避免毛刺
// ==============================================
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时点亮第一个LED
leds <= 8'b0000_0001;
end
else if (counter == 26'd50_000_000) begin
// 在状态切换时刻更新LED输出
case (next_state)
S0: leds <= 8'b0000_0001; // LED0亮
S1: leds <= 8'b0000_0010; // LED1亮
S2: leds <= 8'b0000_0100; // LED2亮
S3: leds <= 8'b0000_1000; // LED3亮
S4: leds <= 8'b0001_0000; // LED4亮
S5: leds <= 8'b0010_0000; // LED5亮
S6: leds <= 8'b0100_0000; // LED6亮
S7: leds <= 8'b1000_0000; // LED7亮
default: leds <= 8'b0000_0001; // 默认LED0亮
endcase
end
// 其他时候保持LED状态不变
end
endmodule
测试代码 tb_flow_led.v:
`timescale 1ns / 1ps
module tb_flow_led();
// 输入信号
reg clk; // 时钟信号
reg rst_n; // 复位信号
// 输出信号
wire [7:0] leds; // LED输出
// ==============================================
// 实例化被测模块
// 注意:模块名称改为flow_led以匹配设计文件
// ==============================================
flow_led uut (
.clk(clk),
.rst_n(rst_n),
.leds(leds)
);
// ==============================================
// 时钟生成(50MHz)
// ==============================================
initial begin
clk = 0;
forever #10 clk = ~clk; // 10ns半周期=20ns周期(50MHz)
end
// ==============================================
// 测试激励
// ==============================================
initial begin
// 初始化信号
rst_n = 0; // 初始复位
// 波形记录开始
$dumpfile("tb_flow_led.vcd");
$dumpvars(0, tb_flow_led);
// 释放复位(100ns后)
#100 rst_n = 1;
$display("Simulation started at %0t ns", $time);
// 观察LED流水效果
// 每个状态持续1秒(50,000,000个周期)
// 观察8个完整周期(8秒模拟时间)
#8_000_000_000; // 400ms观察时间(原代码有误,应为8秒=8*50,000,000*20ns)
$display("Simulation finished at %0t ns", $time);
$stop;
end
endmodule
2、modesim仿真
- 点击Processing-->Start-->Start test bench template writer,这时我们的文件夹之下已经有了一个flow_led.vt文件

-
对刚才生成的文件进行修改(将测试代码粘贴到flow_led.vt),然后再次编译
`timescale 1ns / 1ps
module tb_flow_led();
// 输入信号
reg clk; // 时钟信号
reg rst_n; // 复位信号// 输出信号
wire [7:0] leds; // LED输出// ==============================================
// 实例化被测模块
// 注意:模块名称改为flow_led以匹配设计文件
// ==============================================
flow_led uut (
.clk(clk),
.rst_n(rst_n),
.leds(leds)
);// ==============================================
// 时钟生成(50MHz)
// ==============================================
initial begin
clk = 0;
forever #10 clk = ~clk; // 10ns半周期=20ns周期(50MHz)
end// ==============================================
// 测试激励
// ==============================================
initial begin
// 初始化信号
rst_n = 0; // 初始复位// 波形记录开始 $dumpfile("tb_flow_led.vcd"); $dumpvars(0, tb_flow_led); // 释放复位(100ns后) #100 rst_n = 1; $display("Simulation started at %0t ns", $time); // 观察LED流水效果 // 每个状态持续1秒(50,000,000个周期) // 观察8个完整周期(8秒模拟时间) #8_000_000_000; // 400ms观察时间(原代码有误,应为8秒=8*50,000,000*20ns) $display("Simulation finished at %0t ns", $time); $stop;
end
endmodule
-
现在设置仿真,点击Tools-->Options-->EDA ToolsOptions选项,点击浏览Modelsim安装目录下的Win64或者Win32

- 然后对仿真文件设置点击Assignments-->Settings,再点击下面的Simulation按照如图设置

-
按照顺序先编辑名字,然后浏览刚才的.vt文件,最后点击Add添加达到如下图效果,再点击OK
-
点击如下图按钮,然后点击仿真,会自动跳转Modelsim

三、实现效果
1、仿真结果

2、硬件结果
状态机流水灯
四、CPLD和FPGA芯片主要技术区别
CPLD(Complex Programmable Logic Device)
和 FPGA(Field-Programmable Gate Array)
都是可编程逻辑器件,用于实现数字电路。它们有一些相似之处,但也有一些关键的区别:
-
结构和架构:
CPLD
:CPLD通常由可编程逻辑模块(PLM)、可编程寄存器数组(PRA)以及时钟管理电路组成。它们使用固定的资源连接,具有相对较小的逻辑单元,通常适用于中规模的逻辑设计。
FPGA
:FPGA由可编程逻辑块(CLB)、可编程片上内存(Block RAM)、可编程I/O资源和其他特定功能模块组成。FPGA提供了更大的逻辑容量和更灵活的资源分配,适用于大规模和高度并行的逻辑设计。 -
适用场景:
CPLD
:适用于中等规模、低功耗、低成本的逻辑设计。通常用于控制和接口电路、时序逻辑等应用。
FPGA
:适用于大规模、高性能、灵活性要求高的逻辑设计。常用于数字信号处理、通信、图像处理等复杂的应用。 -
配置技术:
CPLD
:使用EEPROM、Flash等非挥发性存储器进行配置。配置速度较快,但无法实时重配置。
FPGA
:使用SRAM(静态随机存储器)进行配置。具有动态重配置的能力,支持实时修改设计。 -
时序和时钟管理:
CPLD
:通常具有固定的时序延迟,时钟管理相对简单。适合于一些实时控制应用。
FPGA
:时序性能高度可配置,具有更复杂的时钟管理结构。适用于高性能、高时序精度的应用。 -
成本:
CPID
:成本相对较低,适合一些成本敏感的应用。
FPGA
:成本较高,但提供更大规模、更高性能的逻辑容量。
6.功耗:
CPLD
:通常具有较低的功耗,适合一些对功耗敏感的应用。
FPGA
:由于更大的逻辑容量和灵活性,通常具有较高的功耗。
总体而言,CPLD和FPGA都是灵活的可编程逻辑器件,但它们在适用场景、资源结构、配置技术等方面存在显著的差异,因此在选择时需要根据具体的应用需求和性能要求做出合适的选择。

五、hdlbitsFPGA------组合逻辑学习
1、创建一个D触发器
A D flip-flop 是一个存储 bit 并定期更新的电路,位于 clock signal的(通常)正边沿。

当使用 clocked always 块时,logic synthesizer 会创建 D flip-flops。D flip-flop 是 "blob of combinational logic followed a flip-flop" 的最简单形式,其中 combinational logic 部分只是一根线。
module top_module (
input clk, // 时钟输入,用于时序电路
input d, // 数据输入
output reg q // 数据输出(需要声明为 reg 类型)
);
// 使用时钟控制的 always 块
// 在每个 clk 的上升沿将 d 的值赋给 q
// 时钟控制的 always 块应使用非阻塞赋值(<=)
always @(posedge clk) begin
q <= d;
end
endmodule

2、简单状态转换
以下是具有 1 个输入、1 个输出和 4 个状态的 Moore 状态机的状态转换表。使用以下状态编码:A=2'b00、B=2'b01、C=2'b10、D=2'b11。
仅实现此状态机的状态转换逻辑和输出逻辑 (组合逻辑部分)。给定当前状态 (),根据状态转换表计算 和 output ()。state``next_state``out
州 | 下一个状态 | 输出 | |
---|---|---|---|
in=0 | in=1 | ||
一个 | 一个 | B | 0 |
B | C | B | 0 |
C | 一个 | D | 0 |
D | C | B | 1 |
module top_module(
input in,
input [1:0] state,
output [1:0] next_state,
output out); //
parameter A=2'b00, B=2'b01, C=2'b10, D=2'b11;
// State transition logic: next_state = f(state, in)
always @(*) begin
case(state)
A: next_state = in ? B : A;
B: next_state = in ? B : C;
C: next_state = in ? D : A;
D: next_state = in ? B : C;
default: next_state = A;
endcase
end
// Output logic: out = f(state) for a Moore state machine
assign out = (state == D); // 仅在D状态输出1
endmodule
3、4位移位寄存器
构建一个 4 位移位寄存器 (右移位),具有 asynchronous reset、synchronous load 和 enable。
areset
:将 shift 寄存器重置为零。load
:使用data[3:0]
加载移位寄存器,而不是移位。ena
:向右移动(q[3]
变为零,q[0]
移出并消失)。q
:移位寄存器的内容。
如果 load
和 ena
inputs都置位 (1),则 load
input 具有更高的优先级
module top_module(
input clk,
input areset, // 异步高电平复位(复位为0)
input load, // 同步加载
input ena, // 右移使能
input [3:0] data, // 并行加载数据
output reg [3:0] q // 移位寄存器输出
);
always @(posedge clk or posedge areset) begin
if (areset) begin
// 异步复位,优先级最高
q <= 4'b0;
end
else if (load) begin
// 同步加载,优先级次之
q <= data;
end
else if (ena) begin
// 右移操作
q <= {1'b0, q[3:1]}; // q[3]变为0,其余位右移
end
// 如果没有使能信号,保持当前值
end
endmodule

4、计数器1-12
设计一个具有以下输入和输出的 1-12 计数器:
-
重置同步高电平有效复位,强制计数器为 1
-
使将 counter 设置为高位以运行
-
时钟正边沿触发时钟输入
-
**问[3:0]**计数器的输出
-
**c_enable、c_load、c_d[3:0]**控制信号进入提供的 4 位计数器,因此可以验证作是否正确。
module top_module (
input clk,
input reset, // 同步高电平复位
input enable, // 计数使能
output [3:0] Q, // 计数器输出(1-12)
output c_enable, // 连接到4位计数器的使能
output c_load, // 连接到4位计数器的加载
output [3:0] c_d // 连接到4位计数器的加载数据
);// 实例化4位计数器 count4 the_counter ( .clk(clk), .enable(c_enable), .load(c_load), .d(c_d), .Q(Q) ); // 控制逻辑 always @(*) begin if (reset) begin // 复位时强制计数器为1 c_enable = 1'b0; c_load = 1'b1; c_d = 4'd1; end else if (enable) begin if (Q == 4'd12) begin // 达到12时重新加载1 c_enable = 1'b0; c_load = 1'b1; c_d = 4'd1; end else begin // 正常计数 c_enable = 1'b1; c_load = 1'b0; c_d = 4'd0; // 不使用 end end else begin // 不使能时保持当前值 c_enable = 1'b0; c_load = 1'b0; c_d = 4'd0; // 不使用 end end
endmodule

5、边缘捕获寄存器
对于 32-bit 向量中的每个 bit,当 input 信号从一个 clock cycle 中的 1 变为下一个 clock cycle 的 0 时捕获。"Capture" 意味着输出将保持 1 ,直到 register 被重置(同步重置)。
每个输出位的行为类似于 SR 触发器: 输出位应在 1 到 0 转换发生后的周期内设置 (至 1)。当 reset 为高电平时,output bit 应在正 clock edge 重置(为 0)。如果上述两个事件同时发生,则 reset 优先。在下面示例波形的最后 4 个周期中,'reset' 事件比 'set' 事件早一个周期发生,因此这里没有冲突。
module top_module (
input clk,
input reset,
input [31:0] in,
output [31:0] out
);
// 存储上一个时钟周期的输入值
reg [31:0] prev_in;
always @(posedge clk) begin
prev_in <= in; // 记录上一个周期的输入值
if (reset) begin
out <= 32'b0; // 同步复位,优先级最高
end
else begin
// 检测每个bit的下降沿(1->0)
out <= out | (prev_in & ~in);
end
end
endmodule

六、总结
通过这次完整的状态机流水灯实验,我对数字电路设计有了更深入的理解和实践经验。在Quartus中实现状态机控制的流水灯让我掌握了如何将理论状态转换图转化为实际可综合的Verilog代码,特别是状态编码和状态转移逻辑的编写技巧。使用ModelSim进行功能仿真时,我学会了如何编写有效的测试激励文件,并通过波形分析验证设计的正确性,这培养了我的调试能力。在HDLbits平台练习组合逻辑部分让我对FPGA的基本构建模块有了更扎实的理解,特别是如何用简洁的代码描述复杂的逻辑功能。整个实验过程让我认识到,良好的状态机设计不仅需要正确的功能实现,还要考虑时序约束和资源优化。从最初的代码编写到最终的硬件验证,我深刻体会到数字系统设计需要严谨的逻辑思维和反复的调试验证,这些经验对我后续学习更复杂的FPGA设计打下了坚实基础。