FPGA实战(31):自动多帧数据采集控制器状态机设计

📅 2026/6/26 5:05:43
FPGA实战(31):自动多帧数据采集控制器状态机设计
一、引言在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 1PACK → ENr_data_cnt 9PACK持续10个周期EN → DISr_data_cnt 999EN持续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.vmodule 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 5b0_0001 ; parameter P_ST_PACK 5b0_0010 ; parameter P_ST_EN 5b0_0100 ; parameter P_ST_DIS 5b0_1000 ; parameter P_ST_DLY 5b1_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_cP_ST_IDLE (Acquisition_en); assign p_st_pack2p_st_en_start state_cP_ST_PACK (r_data_cnt d9); assign p_st_en2p_st_dis_start state_cP_ST_EN (r_data_cnt d999); assign p_st_dis2p_st_en_start state_cP_ST_DIS (r_data_cnt d1499 r_frame_cnt ! d32); assign p_st_dis2p_st_dly_start state_cP_ST_DIS (r_data_cnt d1499 r_frame_cnt d32); assign p_st_dly2p_st_idle_start state_cP_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_startPACK状态开始时拉高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_validEN状态全程为高 -------- 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四、完整Testbenchtb_singal_cfg.vtimescale 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 5b0_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 ! 5b0_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 ! 1b0) begin $error(FAIL: data_trans_start is not 0 at end); error_flag 1; end if (data_valid ! 1b0) 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 5b0_0100 u_singal_cfg.r_data_cnt 999) begin frame_count_check frame_count_check 1; if (data_valid ! 1b1) 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 5b0_0010 u_singal_cfg.state_n ! u_singal_cfg.state_c) begin if (data_trans_start ! 1b1) 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 5b0_1000 u_singal_cfg.state_n ! u_singal_cfg.state_c) begin if (data_trans_start ! 1b0) 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) 5b0_0001: state_name P_ST_IDLE; 5b0_0010: state_name P_ST_PACK; 5b0_0100: state_name P_ST_EN ; 5b0_1000: state_name P_ST_DIS ; 5b1_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 5b0_0001) ? P_ST_IDLE : (u_singal_cfg.state_n 5b0_0010) ? P_ST_PACK : (u_singal_cfg.state_n 5b0_0100) ? P_ST_EN : (u_singal_cfg.state_n 5b0_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 ~ 9EN10000 ~ 999DIS5001000 ~ 1499单帧总计1510—32帧总计48,320—DLYAcquisition_delay0 ~ delay-15.3 输出信号说明data_trans_start在PACK状态开始时拉高持续1个周期表示一帧数据传输开始进入DIS状态后拉低。data_valid在整个EN状态1000个周期内保持高电平表示数据有效可供下游模块读取。六、创新点与设计亮点6.1 独热码状态编码本设计采用独热码One-Hot对5个状态进行编码parameter P_ST_IDLE 5b0_0001; parameter P_ST_PACK 5b0_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_start和data_valid是否清零✅ 帧计数器是否归零✅ 实际帧数是否等于32✅ 各状态下输出信号是否符合预期八、总结本文完整实现了一个基于三段式状态机的数据采集控制模块具备以下特点维度说明架构三段式FSM 独热码编码功能使能触发 → 32帧周期采集 → 可配置延时 → 自动循环可靠性default安全态、计数器边界保护可测性完整Testbench 自动化PASS/FAIL判定可维护性状态转移条件清晰、代码注释完整该模块可直接集成到上位机联调工程中适用于需要周期性数据采集的FPGA应用场景。源码下载文中所有代码均已附上复制即可使用。建议在Vivado/Quartus中新建工程添加singal_cfg.v和tb_singal_cfg.v运行仿真验证功能。