FPGA实战(28):实时数据速率监测模块设计:双时钟乒乓架构与仿真加速技巧
1. 引言在网络数据包处理、视频流带宽监控、高速接口性能分析等场景中实时统计单位时间内的数据吞吐量是一项基础且关键的需求。通常我们需要在一个固定的时间窗口例如 1 秒内累计接收到的有效数据字节数并在窗口结束时更新输出速率值。本文介绍一个用 Verilog 实现的双时钟域数据速率监测模块它能够以Bytes/s为单位持续输出实时速率并采用乒乓状态机确保输出值稳定无毛刺。同时配套的 Testbench 展示了如何通过“加速计时”技巧大幅缩短仿真时间非常适合在 FPGA 验证环境中快速迭代。2. 模块端口与功能模块接口定义如下端口名方向位宽说明clk_100mI1100 MHz 高精度时钟用于生成 1 秒定时clkI1系统工作时钟数据采样时钟resetI1异步复位高有效rx_validI1输入数据有效标志每个时钟周期有效表示 1 个字节data_rateO32实时数据速率单位 Bytes/s功能核心在clk的每个上升沿采样rx_valid。利用clk_100m计数产生精确的 1 秒脉冲周期翻转。在 1 秒边界上将过去 1 秒内累计的字节数乘以 4代码中的左移 2 位后输出到data_rate。采用两级状态机交替工作避免在边界处输出不稳定值。3. 设计思路与关键代码解析3.1 双时钟域处理模块引入两个独立的时钟clk_100m仅用于计时器r_clk_count的递增。计数器从 0 计到9999_9999即 100M 个周期恰好 1 秒然后归零并翻转标志r_clk_valid。clk用于所有数据路径和状态机逻辑。r_clk_valid通过两级触发器同步到clk域得到r_clk_valid_dly0和r_clk_valid_dly1。r_clk_valid_dly1的上升沿/下降沿即代表 1 秒边界的到来。// 100MHz 计数器产生 1 秒翻转脉冲 always (posedge clk_100m) begin if (r_clk_count 32d9999_9999) begin r_clk_count 0; r_clk_valid ~r_clk_valid; // 每1秒翻转一次 end else r_clk_count r_clk_count 1; end // 同步到 clk 域 always (posedge clk) begin r_clk_valid_dly0 r_clk_valid; r_clk_valid_dly1 r_clk_valid_dly0; end3.2 乒乓状态机核心创新为避免在 1 秒边界的瞬间同时清空计数器并输出新值导致输出数据窗口混乱我们设计了两个交替工作的状态S0正常计数阶段。当r_clk_valid_dly1为高时表示上一个周期的结束立即将当前累计的r_data_count锁存到输出并清零计数器准备开始下一轮计数。S1同样是一个计数阶段但工作逻辑与 S0 互补。当r_clk_valid_dly1为低时即边界翻转后的状态执行相同的“锁存清零”操作。状态机在r_clk_valid_dly1的边沿上升沿或下降沿触发跳转从而实现两个状态轮流负责数据累计和输出更新。这种设计保证了每个 1 秒窗口内数据累计的完整性且输出值仅在边界时刻更新其余时间保持稳定无毛刺风险。状态转移条件assign p_st_s02p_st_s1_start (state_c P_ST_S0) (r_clk_valid_dly1); assign p_st_s12p_st_s0_start (state_c P_ST_S1) (r_clk_valid_dly1 1b0);3.3 计数与输出逻辑数据计数器r_data_count在clk域下工作其累加条件根据当前状态和r_clk_valid_dly1的值决定。以 S0 状态为例当r_clk_valid_dly1为高时执行清零边界到来。否则若rx_valid有效则累加 1。输出寄存器ro_data_rate同样在边界时刻被更新更新值为r_data_count左移 2 位即 ×4。这里之所以乘以 4是假设每个rx_valid脉冲代表 4 字节例如 32 位数据总线实际项目中可根据总线位宽调整但本设计将其作为可配置的放大系数。// 输出锁存S0 边界 else if (state_c P_ST_S0 r_clk_valid_dly1) ro_data_rate {r_data_count[29:0], 2b0}; // 等效 ×44. 创新点总结双时钟分离计时的精确性使用独立的 100MHz 时钟产生 1 秒基准不受系统时钟频率变化影响确保时间窗口严格为 1 秒。乒乓状态机保证输出平稳两个状态交替工作避免了在边界时刻同时清零和输出导致的冒险输出值在每个窗口结束后立即更新且保持一个完整周期稳定。Testbench 中的“加速计时”技巧在仿真中我们通过force强制修改内部计数器r_clk_count使其接近溢出值然后在下一个时钟周期释放从而在极短时间内触发 1 秒边界极大缩短仿真时间从秒级降至微秒级。这种方法对于验证长期累积行为非常高效。灵活的输出缩放通过左移操作实现倍数放大便于适配不同位宽的数据总线无需修改计数逻辑。5. 仿真验证与测试方案5.1 测试架构Testbench 主要包含两个独立时钟发生器100MHz 和系统时钟。复位逻辑。任务send_packet(num)连续发送num个rx_valid脉冲模拟数据包。任务accelerate_timer()强制计数器逼近阈值快速触发边界。5.2 测试流程Stage 1S0 区间发送 5 个有效脉冲期望速率 5 × 4 20 Bytes/s。调用加速任务等待状态跳转至 S1检查data_rate是否为 20。Stage 2S1 区间发送 8 个脉冲期望速率 8 × 4 32 Bytes/s。再次加速状态跳回 S0检查输出是否为 32。两个阶段完整覆盖了两个状态的计数和输出更新路径。测试结果与预期一致证明模块功能正确。5.3 关键修正提示在 Testbench 中访问状态机参数时应使用u_data_rate.P_ST_S0的形式而非反引号因为参数在模块内部定义通过实例名层级引用即可。wait(u_data_rate.state_c u_data_rate.P_ST_S1); // 正确写法6. 工程应用建议资源开销本模块仅占用少量寄存器计数器、状态机、同步器和组合逻辑适合部署在 FPGA 的逻辑资源中。扩展性若需要更高精度如毫秒级更新可调整r_clk_count的计数上限若数据位宽变化可修改输出左移位数。跨时钟域注意事项r_clk_valid的同步采用两级触发器已满足基本同步要求若clk频率较低需确保同步后的信号不会漏采本设计中r_clk_valid持续多个周期不存在漏采风险。7. 结语本文详细解析了一个实用的数据速率监测 Verilog 模块重点介绍了其双时钟计时、乒乓状态机以及仿真加速等设计亮点。通过配套的 Testbench我们可以快速验证其功能并将其集成到更复杂的系统级项目中。希望这篇博客能帮助读者理解实时带宽监测的实现技巧并启发更多关于高效验证方法的思考。data_rate.v完整代码//This module monitors the input data stream in real time and //updates the data transfer rate every 1 second, //with the output unit in Bytes/s. //This design is commonly used in scenarios such as //network packet processing and video stream bandwidth monitoring. module data_rate( input clk_100m , input clk , input reset , input rx_valid , output [31:0] data_rate ); /************************reg*********************/ reg ri_rx_valid ; reg [31:0] ro_data_rate ; reg [31:0] r_clk_count ; reg r_clk_valid ; reg r_clk_valid_dly0 ; reg r_clk_valid_dly1 ; reg [31:0] r_data_count ; /************************wire*********************/ /************************parameter***********************/ /************************fsm*********************/ reg [ (2 - 1):0] state_c ; reg [ (2 - 1):0] state_n ; parameter P_ST_S0 2b01 ; parameter P_ST_S1 2b10 ; always (posedge i_clk) begin if (i_rst) begin state_c P_ST_S0 ; end else begin state_c state_n; end end always (*) begin case(state_c) P_ST_S0 :begin if(p_st_s02p_st_s1_start) state_n P_ST_S1 ; else state_n state_c ; end P_ST_S1 :begin if(p_st_s12p_st_s0_start) state_n P_ST_S0 ; else state_n state_c ; end default : state_n P_ST_S0 ; endcase end assign p_st_s02p_st_s1_start state_cP_ST_S0 (r_clk_valid_dly1); assign p_st_s12p_st_s0_start state_cP_ST_S1 (r_clk_valid_dly1 d0); /************************combinelogic*******************/ assign i_clk clk ; assign i_rst reset ; assign data_rate ro_data_rate ; /************************inist***********************/ /************************always***********************/ always (posedge i_clk )begin if(i_rst) ri_rx_valid d0 ; else ri_rx_valid rx_valid ; end //r_clk_count always (posedge clk_100m )begin if(i_rst) r_clk_count d0 ; else if(r_clk_count d9999_9999) r_clk_count (d0) ; else r_clk_count r_clk_count d1; end //r_clk_valid always (posedge clk_100m )begin if(i_rst) r_clk_valid d0 ; else if(r_clk_count d9999_9999) r_clk_valid (~r_clk_valid) ; else r_clk_valid r_clk_valid ; end //reg r_clk_valid_dly0 always (posedge i_clk )begin if(i_rst) r_clk_valid_dly0 d0 ; else r_clk_valid_dly0 r_clk_valid ; end //reg r_clk_valid_dly1 always (posedge i_clk )begin if(i_rst) r_clk_valid_dly1 d0 ; else r_clk_valid_dly1 r_clk_valid_dly0 ; end //r_data_count always (posedge i_clk )begin if(i_rst) r_data_count d0 ; else if(state_c P_ST_S0 r_clk_valid_dly1) r_data_count d0 ; else if(state_c P_ST_S0 r_clk_valid_dly1 d0 ri_rx_valid) r_data_count (r_data_count d1) ; else if(state_c P_ST_S1 r_clk_valid_dly1 d0) r_data_count d0 ; else if(state_c P_ST_S1 r_clk_valid_dly1 ri_rx_valid) r_data_count (r_data_count d1) ; else r_data_count r_data_count ; end //ro_data_rate always (posedge i_clk )begin if(i_rst) ro_data_rate d0 ; else if(state_c P_ST_S0 r_clk_valid_dly1) ro_data_rate {r_data_count[29:0],2b0} ; else if(state_c P_ST_S1 r_clk_valid_dly1 d0) ro_data_rate {r_data_count[29:0],2b0} ; else ro_data_rate ro_data_rate ; end endmoduletb_data_rate.v完整代码timescale 1ns/1ps module tb_data_rate; // Signal Declaration // reg clk_100m; reg clk; reg reset; reg rx_valid; wire [31:0] data_rate; // Parameter // parameter CLK_100M_PERIOD 20; // 100MHz - 10ns period parameter CLK_SYS_PERIOD 20; // 50MHz System Clock // DUT Instantiation // data_rate u_data_rate ( .clk_100m (clk_100m), .clk (clk), .reset (reset), .rx_valid (rx_valid), .data_rate (data_rate) ); // Clock Reset Generation // // 100MHz Clock initial begin clk_100m 0; forever #(CLK_100M_PERIOD/2) clk_100m ~clk_100m; end // System Clock initial begin clk 0; forever #(CLK_SYS_PERIOD/2) clk ~clk; end // Reset Logic initial begin reset 1; #200; reset 0; end // Task Definition // // Task: 加速仿真时间 task accelerate_timer; begin (negedge clk_100m); // 强制内部计数器接近阈值 force u_data_rate.r_clk_count 32d9999_9990; (negedge clk_100m); release u_data_rate.r_clk_count; $display([%0t] Timer accelerated to trigger edge., $time); end endtask // Task: 产生 rx_valid 脉冲 task send_packet; input integer num; integer i; begin for(i0; inum; ii1) begin (posedge clk); rx_valid 1; (posedge clk); rx_valid 0; repeat(2) (posedge clk); end end endtask // Main Test Sequence // integer expected_rate; integer error_cnt; initial begin // Initialize rx_valid 0; error_cnt 0; expected_rate 0; // Wait for Reset complete wait(reset 0); repeat(10) (posedge clk); //------------------------------------------------------------ // Test Stage 1: State S0 Counting (Initial Interval) //------------------------------------------------------------ $display( Test Stage 1: S0 Interval ); // 发送 5 个 rx_valid 脉冲 send_packet(5); expected_rate 5 * 4; // 逻辑中做了左移2位(乘4)处理 // 加速计时器以触发状态翻转 accelerate_timer(); // 等待状态机跳转 S0 - S1 // 【修正点】去掉反引号直接使用分级名称访问 parameter wait(u_data_rate.state_c u_data_rate.P_ST_S1); repeat(5) (posedge clk); // 校验结果 if (data_rate ! expected_rate) begin $display(Error Stage 1: Expected Rate %d, Got Rate %d, expected_rate, data_rate); error_cnt error_cnt 1; end else begin $display(Success Stage 1: Data Rate %d, data_rate); end //------------------------------------------------------------ // Test Stage 2: State S1 Counting (Second Interval) //------------------------------------------------------------ $display( Test Stage 2: S1 Interval ); // 发送 8 个 rx_valid 脉冲 send_packet(8); expected_rate 8 * 4; // 加速计时器触发翻转 accelerate_timer(); // 等待状态机跳转 S1 - S0 // 【修正点】去掉反引号 wait(u_data_rate.state_c u_data_rate.P_ST_S0); repeat(5) (posedge clk); // 校验结果 if (data_rate ! expected_rate) begin $display(Error Stage 2: Expected Rate %d, Got Rate %d, expected_rate, data_rate); error_cnt error_cnt 1; end else begin $display(Success Stage 2: Data Rate %d, data_rate); end //------------------------------------------------------------ // Test End //------------------------------------------------------------ if (error_cnt 0) begin $display( Testbench PASSED ); end else begin $display( Testbench FAILED ); end #100; $stop; end endmodule附完整代码已在文首给出读者可自行复制并运行仿真观察输出波形以加深理解。如有疑问或优化建议欢迎在评论区交流。博客完