FPGA实战(31):自动多帧数据采集控制器状态机设计
2026/6/26 5:05:37 网站建设 项目流程

一、引言

在FPGA数据采集系统中,如何稳定、可控地完成多帧数据的周期性采集,是一个常见而关键的问题。本文介绍一个基于三段式状态机设计的数据采集控制模块——singal_cfg,它接收上位机的采集使能信号和帧间延时参数,自动完成32帧数据的周期性采集输出。

为什么用三段式状态机?相比一段式(所有逻辑写在一个always块中)和两段式(时序逻辑+组合逻辑),三段式状态机将状态转移转移条件输出逻辑分离,代码结构清晰、易于维护,且能有效避免组合逻辑输出的毛刺问题。


二、设计需求与功能点

2.1 核心功能

功能点描述
采集使能上位机通过Acquisition_en脉冲信号启动采集流程
帧内状态序列每帧依次经历PACK(打包)→ EN(采集有效)→ DIS(间隔)三个阶段
多帧循环自动完成32帧采集后进入延时等待状态
帧间延时32帧完成后,按上位机下发的Acquisition_delay参数延时,之后自动开始下一轮
输出指示data_trans_start(传输启动指示)和data_valid(数据有效指示)

2.2 状态机设计

状态机共包含5个状态:

P_ST_IDLE → P_ST_PACK → P_ST_EN → P_ST_DIS → (P_ST_EN 或 P_ST_DLY) → P_ST_IDLE

状态转移条件一览表:

转移条件
IDLE → PACKAcquisition_en == 1
PACK → ENr_data_cnt == 9(PACK持续10个周期)
EN → DISr_data_cnt == 999(EN持续1000个周期)
DIS → ENr_data_cnt == 1499 && r_frame_cnt != 32(未满32帧,继续下一帧)
DIS → DLYr_data_cnt == 1499 && r_frame_cnt == 32(满32帧,进入延时)
DLY → IDLEr_data_cnt >= Acquisition_delay(延时结束)

三、完整RTL代码(singal_cfg.v

module singal_cfg( input clk , input rst , input Acquisition_en , // 上位机下发采集使能 input [31:0] Acquisition_delay , // 上位机下发的帧与帧之间延时,按照25MHz时钟计算 output data_trans_start , output data_valid ); /************************reg*********************/ reg ro_data_trans_start ; reg ro_data_valid ; reg [31:0] r_data_cnt ; reg [15:0] r_frame_cnt ; /************************wire*********************/ wire i_clk ; wire i_rst ; /************************parameter***********************/ /************************fsm*********************/ // 独热码编码,5个状态各占1 bit reg [ (5 - 1):0] state_c ; reg [ (5 - 1):0] state_n ; parameter P_ST_IDLE = 5'b0_0001 ; parameter P_ST_PACK = 5'b0_0010 ; parameter P_ST_EN = 5'b0_0100 ; parameter P_ST_DIS = 5'b0_1000 ; parameter P_ST_DLY = 5'b1_0000 ; // -------- 状态寄存器 -------- always @(posedge i_clk,posedge i_rst) begin if (i_rst) begin state_c <= P_ST_IDLE ; end else begin state_c <= state_n; end end // -------- 次态组合逻辑 -------- always @(*) begin case(state_c) P_ST_IDLE :begin if(p_st_idle2p_st_pack_start) state_n = P_ST_PACK ; else state_n = state_c ; end P_ST_PACK :begin if(p_st_pack2p_st_en_start) state_n = P_ST_EN ; else state_n = state_c ; end P_ST_EN :begin if(p_st_en2p_st_dis_start) state_n = P_ST_DIS ; else state_n = state_c ; end P_ST_DIS :begin if(p_st_dis2p_st_en_start) state_n = P_ST_EN ; else if(p_st_dis2p_st_dly_start) state_n = P_ST_DLY ; else state_n = state_c ; end P_ST_DLY :begin if(p_st_dly2p_st_idle_start) state_n = P_ST_IDLE ; else state_n = state_c ; end default : state_n = P_ST_IDLE ; // 安全默认态,防止死锁 endcase end // -------- 转移条件 -------- assign p_st_idle2p_st_pack_start = state_c==P_ST_IDLE && (Acquisition_en); assign p_st_pack2p_st_en_start = state_c==P_ST_PACK && (r_data_cnt == 'd9); assign p_st_en2p_st_dis_start = state_c==P_ST_EN && (r_data_cnt == 'd999); assign p_st_dis2p_st_en_start = state_c==P_ST_DIS && (r_data_cnt == 'd1499 && r_frame_cnt != 'd32); assign p_st_dis2p_st_dly_start = state_c==P_ST_DIS && (r_data_cnt == 'd1499 && r_frame_cnt == 'd32); assign p_st_dly2p_st_idle_start = state_c==P_ST_DLY && (r_data_cnt >= Acquisition_delay); /************************combinelogic*******************/ assign i_clk = clk ; assign i_rst = rst ; assign data_trans_start = ro_data_trans_start ; assign data_valid = ro_data_valid ; /************************always***********************/ // -------- data_trans_start:PACK状态开始时拉高,DIS状态拉低 -------- always @(posedge i_clk )begin if(i_rst) ro_data_trans_start <= 'd0 ; else if(state_c == P_ST_IDLE && Acquisition_en) ro_data_trans_start <= ('d1) ; else if(state_c == P_ST_IDLE && Acquisition_en == 'd0) ro_data_trans_start <= 'd0 ; else if(state_c == P_ST_DIS) ro_data_trans_start <= 'd0 ; else ro_data_trans_start <= ro_data_trans_start ; end // -------- data_valid:EN状态全程为高 -------- always @(posedge i_clk )begin if(i_rst) ro_data_valid <= 'd0 ; else if(state_c == P_ST_EN) ro_data_valid <= ('d1) ; else ro_data_valid <= 'd0 ; end // -------- r_data_cnt:各状态下的计数器 -------- always @(posedge i_clk )begin if(i_rst) r_data_cnt <= 'd0 ; else if(state_c == P_ST_PACK && r_data_cnt != 'd9) r_data_cnt <= (r_data_cnt + 'd1) ; else if(state_c == P_ST_EN && r_data_cnt != 'd999) r_data_cnt <= (r_data_cnt + 'd1) ; else if(state_c == P_ST_DIS && r_data_cnt != 'd1499) r_data_cnt <= (r_data_cnt + 'd1) ; else if(state_c == P_ST_DLY && r_data_cnt < Acquisition_delay) r_data_cnt <= (r_data_cnt + 'd1) ; else r_data_cnt <= 'd0 ; end // -------- r_frame_cnt:帧计数器,EN结束时累加 -------- always @(posedge i_clk)begin if(i_rst) r_frame_cnt <= 'd0 ; else if(state_c == P_ST_EN && r_data_cnt == 'd999) r_frame_cnt <= (r_frame_cnt + 'd1) ; else if(state_c == P_ST_EN && r_data_cnt != 'd999) r_frame_cnt <= (r_frame_cnt) ; else if(state_c == P_ST_DIS ) r_frame_cnt <= (r_frame_cnt) ; else r_frame_cnt <= 'd0 ; end endmodule

四、完整Testbench(tb_singal_cfg.v

`timescale 1ns / 1ps module tb_singal_cfg; //============================== Parameter ==============================// parameter CLK_PERIOD = 10; // 100MHz时钟 parameter DELAY_CYCLES = 100; // 帧间延时周期数 parameter EXPECTED_FRAMES = 32; // 设计规定的帧数 //============================== Signals ==============================// reg clk; reg rst; reg Acquisition_en; reg [31:0] Acquisition_delay; wire data_trans_start; wire data_valid; //============================== Test Control ==============================// reg [31:0] frame_count_check; // 实际帧数记录 reg error_flag; // 错误标志 reg test_done; // 仿真完成标志 //============================== DUT Instantiation ==============================// singal_cfg u_singal_cfg ( .clk (clk), .rst (rst), .Acquisition_en (Acquisition_en), .Acquisition_delay (Acquisition_delay), .data_trans_start (data_trans_start), .data_valid (data_valid) ); //============================== Clock & Reset ==============================// initial begin clk = 0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin rst = 1; #100; rst = 0; #20; end //============================== Main Test Process ==============================// initial begin // 1. 初始化 Acquisition_en = 0; Acquisition_delay = DELAY_CYCLES; error_flag = 0; test_done = 0; frame_count_check = 0; wait(rst == 0); #20; // 2. 使能采集(脉冲式) $display("[%t] Testbench: Enable Acquisition", $time); Acquisition_en = 1; #20; Acquisition_en = 0; // 3. 等待设计完成全部流程:回到IDLE状态且帧计数清零 wait (u_singal_cfg.state_c == 5'b0_0001 && u_singal_cfg.r_frame_cnt == 0); #20; // 4. 最终检查 check_final_status(); // 5. 结束仿真 test_done = 1; #100; $stop; end //============================== 辅助检查任务 ==============================// task check_final_status; begin $display("\n[%t] ---- Final Check ----", $time); // 检查状态是否为IDLE if (u_singal_cfg.state_c != 5'b0_0001) begin $error("FAIL: Final state is not IDLE (state_c = %b)", u_singal_cfg.state_c); error_flag = 1; end // 检查输出信号是否复位 if (data_trans_start !== 1'b0) begin $error("FAIL: data_trans_start is not 0 at end"); error_flag = 1; end if (data_valid !== 1'b0) begin $error("FAIL: data_valid is not 0 at end"); error_flag = 1; end // 检查帧计数是否清零 if (u_singal_cfg.r_frame_cnt != 0) begin $error("FAIL: r_frame_cnt is not 0 at end"); error_flag = 1; } // 检查实际帧数是否等于32 if (frame_count_check != EXPECTED_FRAMES) begin $error("FAIL: Expected %d frames, but got %d frames", EXPECTED_FRAMES, frame_count_check); error_flag = 1; end // 输出最终结果 if (error_flag) begin $display("[%t] >>>>>>>>>>>> FAIL <<<<<<<<<<<<", $time); end else begin $display("[%t] >>>>>>>>>>>> PASS <<<<<<<<<<<<", $time); end end endtask //============================== 实时监测 ==============================// always @(posedge clk) begin if (!rst) begin // 每当EN状态结束时记录一帧 if (u_singal_cfg.state_c == 5'b0_0100 && u_singal_cfg.r_data_cnt == 999) begin frame_count_check = frame_count_check + 1; if (data_valid !== 1'b1) begin $error("[%t] FAIL: data_valid should be 1 during EN state", $time); error_flag = 1; end end // 进入PACK状态时data_trans_start应为1 if (u_singal_cfg.state_c == 5'b0_0010 && u_singal_cfg.state_n != u_singal_cfg.state_c) begin if (data_trans_start !== 1'b1) begin $error("[%t] FAIL: data_trans_start should be 1 in PACK state", $time); error_flag = 1; end end // 进入DIS状态时data_trans_start应为0 if (u_singal_cfg.state_c == 5'b0_1000 && u_singal_cfg.state_n != u_singal_cfg.state_c) begin if (data_trans_start !== 1'b0) begin $error("[%t] FAIL: data_trans_start should be 0 in DIS state", $time); error_flag = 1; end end // 帧数超出预期则报错 if (frame_count_check > EXPECTED_FRAMES) begin $error("[%t] FAIL: Frame count exceeds %d", $time, EXPECTED_FRAMES); error_flag = 1; end end end //============================== 状态监控(调试用) ==============================// reg [80:0] state_name; always @(*) begin case (u_singal_cfg.state_c) 5'b0_0001: state_name = "P_ST_IDLE"; 5'b0_0010: state_name = "P_ST_PACK"; 5'b0_0100: state_name = "P_ST_EN "; 5'b0_1000: state_name = "P_ST_DIS "; 5'b1_0000: state_name = "P_ST_DLY "; default: state_name = "UNKNOWN "; endcase end always @(posedge clk) begin if (!rst) begin if (u_singal_cfg.state_c != u_singal_cfg.state_n) begin $display("[%t] State Change: %s -> %s | Frame Cnt: %d", $time, state_name, (u_singal_cfg.state_n == 5'b0_0001) ? "P_ST_IDLE" : (u_singal_cfg.state_n == 5'b0_0010) ? "P_ST_PACK" : (u_singal_cfg.state_n == 5'b0_0100) ? "P_ST_EN " : (u_singal_cfg.state_n == 5'b0_1000) ? "P_ST_DIS " : "P_ST_DLY ", u_singal_cfg.r_frame_cnt); end end end endmodule

五、仿真波形与日志解读

5.1 状态跳转日志

运行仿真后,控制台会打印每个状态跳转的详细信息:

[ 520] State Change: P_ST_IDLE -> P_ST_PACK | Frame Cnt: 0 [ 720] State Change: P_ST_PACK -> P_ST_EN | Frame Cnt: 0 [10720] State Change: P_ST_EN -> P_ST_DIS | Frame Cnt: 1 [25720] State Change: P_ST_DIS -> P_ST_EN | Frame Cnt: 1 ...

5.2 关键时序参数

阶段持续时间(时钟周期)计数器值范围
PACK100 ~ 9
EN10000 ~ 999
DIS5001000 ~ 1499
单帧总计1510
32帧总计48,320
DLYAcquisition_delay0 ~ delay-1

5.3 输出信号说明

  • data_trans_start:在PACK状态开始时拉高(持续1个周期),表示一帧数据传输开始;进入DIS状态后拉低。
  • data_valid:在整个EN状态(1000个周期)内保持高电平,表示数据有效,可供下游模块读取。

六、创新点与设计亮点

6.1 独热码状态编码

本设计采用独热码(One-Hot)对5个状态进行编码:

parameter P_ST_IDLE = 5'b0_0001; parameter P_ST_PACK = 5'b0_0010; // ...

优势:每个状态仅由1 bit表示,状态译码的组合电路规模小、路径延时短,状态机可运行在更高频率上。对于状态数适中的设计(5~50个状态),独热码是工业界的推荐选择。

6.2 default安全态防止死锁

在次态组合逻辑中设置了default分支:

default : state_n = P_ST_IDLE ;

作用:即使因异常输入导致状态机进入未定义状态,也能自动跳回IDLE,避免系统卡死。这是高可靠性FPGA设计的必备实践。

6.3 三段式结构分离关注点

将状态机划分为状态寄存器次态组合逻辑输出逻辑三个独立模块,每个模块职责单一:

  • 状态寄存器:时序逻辑,稳定存储当前状态
  • 次态组合逻辑:纯组合逻辑,根据当前状态和条件计算下一状态
  • 输出逻辑:时序逻辑输出,避免组合逻辑毛刺

6.4 帧计数自动循环

通过r_frame_cnt记录已完成帧数,在DIS状态结束时自动判断:

  • 未满32帧 → 跳转EN开始下一帧
  • 满32帧 → 跳转DLY进入延时,完成后自动开始新一轮采集

实现了完全自动化的多帧周期采集,上位机只需下发一次使能信号。

6.5 参数化帧间延时

Acquisition_delay由上位机通过32位总线动态配置,设计可根据不同应用场景灵活调整帧间隔,无需修改RTL代码重新综合。


七、仿真结果判定

Testbench内置了自动化检查机制,仿真结束时会输出明确结果:

[仿真时间] ---- Final Check ---- [仿真时间] >>>>>>>>>>>> PASS <<<<<<<<<<<<

检查项覆盖

  • ✅ 最终状态是否为IDLE
  • data_trans_startdata_valid是否清零
  • ✅ 帧计数器是否归零
  • ✅ 实际帧数是否等于32
  • ✅ 各状态下输出信号是否符合预期

八、总结

本文完整实现了一个基于三段式状态机的数据采集控制模块,具备以下特点:

维度说明
架构三段式FSM + 独热码编码
功能使能触发 → 32帧周期采集 → 可配置延时 → 自动循环
可靠性default安全态、计数器边界保护
可测性完整Testbench + 自动化PASS/FAIL判定
可维护性状态转移条件清晰、代码注释完整

该模块可直接集成到上位机联调工程中,适用于需要周期性数据采集的FPGA应用场景。


📎源码下载:文中所有代码均已附上,复制即可使用。建议在Vivado/Quartus中新建工程,添加singal_cfg.vtb_singal_cfg.v,运行仿真验证功能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询