一、引言
在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 → PACK | Acquisition_en == 1 |
| PACK → EN | r_data_cnt == 9(PACK持续10个周期) |
| EN → DIS | r_data_cnt == 999(EN持续1000个周期) |
| DIS → EN | r_data_cnt == 1499 && r_frame_cnt != 32(未满32帧,继续下一帧) |
| DIS → DLY | r_data_cnt == 1499 && r_frame_cnt == 32(满32帧,进入延时) |
| DLY → IDLE | r_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 关键时序参数
| 阶段 | 持续时间(时钟周期) | 计数器值范围 |
|---|---|---|
| PACK | 10 | 0 ~ 9 |
| EN | 1000 | 0 ~ 999 |
| DIS | 500 | 1000 ~ 1499 |
| 单帧总计 | 1510 | — |
| 32帧总计 | 48,320 | — |
| DLY | Acquisition_delay | 0 ~ 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_start和data_valid是否清零 - ✅ 帧计数器是否归零
- ✅ 实际帧数是否等于32
- ✅ 各状态下输出信号是否符合预期
八、总结
本文完整实现了一个基于三段式状态机的数据采集控制模块,具备以下特点:
| 维度 | 说明 |
|---|---|
| 架构 | 三段式FSM + 独热码编码 |
| 功能 | 使能触发 → 32帧周期采集 → 可配置延时 → 自动循环 |
| 可靠性 | default安全态、计数器边界保护 |
| 可测性 | 完整Testbench + 自动化PASS/FAIL判定 |
| 可维护性 | 状态转移条件清晰、代码注释完整 |
该模块可直接集成到上位机联调工程中,适用于需要周期性数据采集的FPGA应用场景。
📎源码下载:文中所有代码均已附上,复制即可使用。建议在Vivado/Quartus中新建工程,添加
singal_cfg.v和tb_singal_cfg.v,运行仿真验证功能。