FPGA实战(32):多通道ADC数据打包模块设计

📅 2026/6/26 3:59:02
FPGA实战(32):多通道ADC数据打包模块设计
1. 为什么需要ad_pack在上一篇文章中我们介绍了单通道打包模块ad_chan_pack它负责将一路ADC的I/Q数据按固定帧格式打包并通过异步FIFO送出。实际系统中我们往往需要同时处理多路DDC数字下变频输出的数据比如8个通道每个通道包含I/Q两路每路32位。如果让每个通道独立输出打包数据会占用过多总线资源也不利于统一管理。ad_pack模块正是为了解决这一问题而设计的顶层聚合单元。它聚合8个通道的数据每个通道使用独立的ad_chan_pack实例各自完成帧打包。轮询读取通过状态机轮流从各个通道的FIFO中读取数据合并成一路连续数据流输出。支持DMA流控通过dma_fifo_progfull信号感知下游FIFO状态实现反压防止数据溢出。可配置帧长度通过参数pack_length设定每帧有效数据点数默认32012。集成测试模式trans_data_sel可切换为内部测试数据便于调试。自动帧间延时控制内部例化singal_cfg模块根据上位机下发的Acquisition_delay控制帧与帧之间的间隔。本文将围绕ad_pack.v及其测试平台tb_ad_pack.v展开详细讲解设计思路和实现细节。2. ad_pack模块功能概述2.1 顶层接口信号名方向位宽说明clk_adcinput1ADC采样时钟写FIFO用clk_transinput1传输时钟读FIFO和输出用rstinput1异步复位高有效trans_data_selinput10ADC数据1测试数据Acquisition_eninput1上位机下发采集使能Acquisition_delayinput[31:0]帧间延时按25MHz时钟计算ddc_eninput[7:0]各通道DDC数据有效使能bit位对应通道ddc_datainput[255:0]8个通道的I/Q数据每个通道32位拼接为256位dma_fifo_progfullinput1下游DMA FIFO可编程满标志流控pack_valid_enoutput1输出数据有效使能同时为读使能pack_valid_dataoutput[31:0]输出的打包数据2.2 内部结构ad_pack内部例化了以下子模块singal_cfg产生打包启动脉冲data_trans_start和数据有效信号data_valid根据Acquisition_en和Acquisition_delay控制帧启动时序。8个ad_chan_pack每个通道独立打包FIFO写侧使用clk_adc读侧使用clk_trans。每个通道输出自己的pack_valid、pack_valid_data以及FIFO状态。data_rate统计输出数据速率单位为Bytes/s用于性能监控。顶层模块的核心任务是轮询8个通道的FIFO将有效数据读出并合并输出同时处理流控和帧切换。2.3 FIFO IP核配置3. 核心设计思路3.1 四状态FSM顶层采用4个状态控制轮询和复位P_ST_IDLE空闲等待任意通道的FIFO非满w_fifo_prog_full[0]为1表示可写实际条件为w_fifo_prog_full[0]需注意是满标志应该用非满触发—— 仔细看代码中跳转条件p_st_idle2p_st_read_start state_cP_ST_IDLE (w_fifo_prog_full[0]);这里w_fifo_prog_full[0]是FIFO可编程满标志通常为1表示几乎满此处用做启动条件似乎有误但可能是设计意图当某通道FIFO达到可编程满阈值时说明有数据可读于是进入READ状态。实际使用中需根据FIFO的编程满阈值设定来配合。P_ST_READ读取数据状态。在此状态下根据当前通道号r_chan_cnt使能对应通道的rd_fifo_ready信号从该通道FIFO读取一个数据。同时计数r_data_cnt当读够pack_length个数据后切换下一通道或进入结束状态。P_ST_JUDGE判断状态仅1拍用于更新通道号随后回到READ。P_ST_END结束状态当所有8个通道都读完一轮后进入等待r_cnt计数到1000即帧间延时后回到IDLE同时产生r_fifo_rst复位信号复位所有子模块的FIFO。状态转移条件IDLE → READw_fifo_prog_full[0]有效至少有一个通道FIFO非空实际是检测通道0的满标志这可能只针对通道0但所有通道共享同一个启动故认为是整体启动条件。READ → JUDGE当前通道读完pack_length个数据且不是最后一个通道。READ → END当前通道读完且是最后一个通道通道7。JUDGE → READ无条件1拍后回到READ此时通道号已1。END → IDLEr_cnt 1000即帧间延时结束。3.2 通道轮询与读控制r_chan_cnt当前正在读取的通道号0~7。r_data_cnt当前通道已读出的数据个数。r_rd_fifo_ready8位寄存器每位对应一个通道的读使能。在READ状态下只有当前通道的对应位为1其他为0。r_pack_ddc_en将ddc_en经过两级同步后寄存用于在ADC时钟域使能打包。r_sample_cnt在ADC时钟域累加的采样计数器每个通道共用实际帧中采样计数是全局的。轮询过程从通道0开始每个通道连续读出pack_length个数据完成后切换下一通道。当所有8个通道读完进入END状态然后IDLE等待下一帧启动。3.3 流控机制下游流控dma_fifo_progfull为高时表示下游DMA FIFO即将满此时顶层应停止读取。代码中在READ状态下如果dma_fifo_progfull为1则r_rd_fifo_ready[r_chan_cnt]被清零暂停读取当该信号恢复为0且数据未读完时继续使能。上游流控每个ad_chan_pack子模块的FIFO都有prog_full和full标志通过w_fifo_prog_full向量反馈给顶层用于启动或暂停但当前设计中只在IDLE时检测通道0的prog_full未用作实时流控这是一个可优化的点。3.4 参数化打包长度通过parameter pack_length 32d32012可灵活配置每帧的有效数据点数适应不同场景如雷达脉冲宽度、FFT点数等。3.5 测试模式trans_data_sel为1时ad_chan_pack内部生成递增测试数据不依赖外部ADC方便独立调试验证。3.6 帧间延时singal_cfg模块根据Acquisition_delay控制两次data_trans_start脉冲的间隔从而实现帧间延时。顶层END状态也通过r_cnt计数1000个周期产生r_fifo_rst复位所有FIFO确保下一帧开始前FIFO处于干净状态。4. 关键代码解析4.1 状态机跳转逻辑assign p_st_idle2p_st_read_start state_cP_ST_IDLE (w_fifo_prog_full[0]); assign p_st_read2p_st_judge_start state_cP_ST_READ (r_data_cnt P_PACK_LENGTH r_chan_cnt ! d7); assign p_st_read2p_st_end_start state_cP_ST_READ (r_data_cnt P_PACK_LENGTH r_chan_cnt d7); assign p_st_judge2p_st_read_start state_cP_ST_JUDGE (1); assign p_st_end2p_st_idle_start state_cP_ST_END (r_cnt d1000);4.2 通道切换与数据计数always (posedge i_clk) begin if(i_rst || r_fifo_rst) r_chan_cnt d0; else if(state_c P_ST_IDLE) r_chan_cnt d0; else if(state_c P_ST_READ r_data_cnt P_PACK_LENGTH r_chan_cnt d7) r_chan_cnt d0; else if(state_c P_ST_READ r_data_cnt P_PACK_LENGTH r_chan_cnt ! d7) r_chan_cnt r_chan_cnt 1; else if(state_c P_ST_END) r_chan_cnt d0; else r_chan_cnt r_chan_cnt; end4.3 读使能生成含流控always (posedge i_clk) begin if(i_rst || r_fifo_rst) r_rd_fifo_ready d0; else if(state_c P_ST_IDLE) r_rd_fifo_ready d0; else if(state_c P_ST_JUDGE) r_rd_fifo_ready d0; else if(state_c P_ST_READ dma_fifo_progfull) r_rd_fifo_ready[r_chan_cnt] 1b0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt P_PACK_LENGTH) r_rd_fifo_ready[r_chan_cnt] 1b0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt ! P_PACK_LENGTH) r_rd_fifo_ready[r_chan_cnt] 1b1; else if (state_c P_ST_END) r_rd_fifo_ready d0; else r_rd_fifo_ready r_rd_fifo_ready; end4.4 输出数据选择在READ状态下根据当前使能的通道w_ddc_pack_en实际上就是r_rd_fifo_ready的独热形式选择对应通道的pack_valid_data输出。5. 仿真测试平台tb_ad_pack.v测试平台模拟了以下场景产生100MHz和50MHz双时钟复位后释放。使能Acquisition_en设置Acquisition_delay 10缩短帧间延时以加速仿真。使能所有8个通道ddc_en 8hFF。模拟ADC数据递增每个时钟周期增加固定值。模拟dma_fifo_progfull信号在数据传送过程中拉高一段时间检验流控是否生效。运行足够长时间观察输出数据流。打印状态机状态、rd_fifo_ready等信号。监控逻辑包括检测帧头0xCC33AA55然后检测通道标识0x55AA33xx最后检测帧尾0x7E7E7E7E并记录每个通道的数据个数。统计总包数和错误计数。5.1 关键监控代码always (posedge clk_trans) begin if (!rst pack_valid_en) begin total_packets total_packets 1; if (pack_valid_data 32hCC33_AA55) data_count 0; else if (pack_valid_data[31:8] 24h55_AA33) begin current_chan pack_valid_data[7:0]; $display([%t] Frame Start detected for Channel %0d, $time, pack_valid_data[7:0]); end else if (pack_valid_data 32h7E7E_7E7E) $display([%t] Frame End detected for Channel %0d (Data count: %0d), $time, current_chan, data_count); else data_count data_count 1; end end6. 完整代码6.1 ad_pack.vmodule ad_pack#( parameter pack_length 32d32012 )( input clk_adc , input clk_trans , input rst , input trans_data_sel , //传输数据选择 input Acquisition_en , //上位机下发采集使能 input [31:0] Acquisition_delay , //上位机下发的帧与帧之间延时 input [7:0] ddc_en , input [255:0] ddc_data , input dma_fifo_progfull , output pack_valid_en , output [31:0] pack_valid_data ); /************************ reg *********************/ reg ro_pack_valid_en ; reg [31:0] ro_pack_valid_data ; reg [7:0] r_pack_ddc_en ; reg [255:0] r_pack_ddc_data ; reg [31:0] r_sample_cnt ; reg [7:0] r_rd_fifo_ready ; reg [7:0] r_chan_cnt ; reg [31:0] r_data_cnt ; reg r_fifo_rst ; reg [15:0] r_cnt ; /************************ wire *********************/ wire i_clk ; wire i_rst ; wire w_data_trans_start ; wire w_data_valid ; wire [7:0] w_fifo_prog_full ; wire [7:0] w_fifo_prog_empty ; wire [7:0] w_fifo_full ; wire [7:0] w_ddc_pack_en ; wire [31:0] w_ddc_pack_data[7:0] ; wire [7:0] w_ddc_pack ; wire w_ad_pack_valid ; wire [31:0] w_data_rate ; wire [7:0] w_wr_rst_busy ; /************************ parameter ***********************/ localparam P_PACK_LENGTH pack_length ; /************************ FSM *********************/ reg [ (4-1):0] state_c ; reg [ (4-1):0] state_c_dly0 ; reg [ (4-1):0] state_c_dly1 ; reg [ (4-1):0] state_n ; parameter P_ST_IDLE 0 ; parameter P_ST_READ 1 ; parameter P_ST_JUDGE 2 ; parameter P_ST_END 3 ; always (posedge i_clk) begin if (i_rst) state_c P_ST_IDLE; else state_c state_n; end always (*) begin case(state_c) P_ST_IDLE : if(p_st_idle2p_st_read_start) state_n P_ST_READ; else state_n state_c; P_ST_READ : if(p_st_read2p_st_judge_start) state_n P_ST_JUDGE; else if(p_st_read2p_st_end_start) state_n P_ST_END; else state_n state_c; P_ST_JUDGE: if(p_st_judge2p_st_read_start) state_n P_ST_READ; else state_n state_c; P_ST_END : if(p_st_end2p_st_idle_start) state_n P_ST_IDLE; else state_n state_c; default : state_n P_ST_IDLE; endcase end assign p_st_idle2p_st_read_start state_cP_ST_IDLE (w_fifo_prog_full[0]); assign p_st_read2p_st_judge_start state_cP_ST_READ (r_data_cnt P_PACK_LENGTH r_chan_cnt ! d7); assign p_st_read2p_st_end_start state_cP_ST_READ (r_data_cnt P_PACK_LENGTH r_chan_cnt d7); assign p_st_judge2p_st_read_start state_cP_ST_JUDGE (1); assign p_st_end2p_st_idle_start state_cP_ST_END (r_cnt d1000); /************************ combinational logic *******************/ assign i_clk clk_trans ; assign i_rst rst ; assign w_ad_pack_valid (~w_wr_rst_busy ) ; assign pack_valid_en ro_pack_valid_en w_ddc_pack[r_chan_cnt] ; assign pack_valid_data ro_pack_valid_data ; /************************ submodules *******************/ singal_cfg singal_cfg_u0( .clk ( clk_adc ), .rst ( i_rst || r_fifo_rst ), .Acquisition_en ( Acquisition_en ), .Acquisition_delay ( Acquisition_delay ), .data_trans_start ( w_data_trans_start ), .data_valid ( w_data_valid ) ); genvar i; generate for (i0; i 8; ii1) begin: pack ad_chan_pack ad_chan_pack_u0( .clk_adc ( clk_adc ), .clk_trans ( clk_trans ), .rst ( i_rst || r_fifo_rst ), .fifo_rst ( r_fifo_rst ), .data_trans_start ( w_data_trans_start ), .data_valid ( w_data_valid ), .trans_data_sel ( trans_data_sel ), .ad_pack_valid ( w_ad_pack_valid ), .adc_en ( r_pack_ddc_en[i] ), .adc_data ( r_pack_ddc_data[32*i : 32] ), .sample_cnt ( r_sample_cnt ), .chan_num ( i ), .rd_fifo_ready ( r_rd_fifo_ready[i] ), .pack_fifo_prog_full ( w_fifo_prog_full[i] ), .pack_fifo_prog_empty ( w_fifo_prog_empty[i] ), .pack_fifo_full ( w_fifo_full[i] ), .pack_valid_en ( w_ddc_pack_en[i] ), .pack_valid_data ( w_ddc_pack_data[i] ), .pack_valid ( w_ddc_pack[i] ), .w_wr_rst_busy ( w_wr_rst_busy[i] ) ); end endgenerate data_rate data_rate_u0( .clk_100m (clk_trans ), .clk (clk_trans ), .reset (rst ), .rx_valid (ro_pack_valid_en ), .data_rate (w_data_rate ) ); /************************ always blocks ***********************/ // ro_pack_valid_en always (posedge i_clk) begin if(i_rst || r_fifo_rst) ro_pack_valid_en d0; else if (state_c P_ST_END) ro_pack_valid_en d0; else if(w_ddc_pack_en 8b0000_0001 || w_ddc_pack_en 8b0000_0010 || w_ddc_pack_en 8b0000_0100 || w_ddc_pack_en 8b0000_1000 || w_ddc_pack_en 8b0001_0000 || w_ddc_pack_en 8b0010_0000 || w_ddc_pack_en 8b0100_0000 || w_ddc_pack_en 8b1000_0000) ro_pack_valid_en d1; else ro_pack_valid_en d0; end // ro_pack_valid_data always (posedge i_clk) begin if(i_rst || r_fifo_rst) ro_pack_valid_data d0; else if (state_c P_ST_END) ro_pack_valid_data d0; else if (state_c P_ST_READ) begin case(w_ddc_pack_en) 8b0000_0001: ro_pack_valid_data w_ddc_pack_data[0]; 8b0000_0010: ro_pack_valid_data w_ddc_pack_data[1]; 8b0000_0100: ro_pack_valid_data w_ddc_pack_data[2]; 8b0000_1000: ro_pack_valid_data w_ddc_pack_data[3]; 8b0001_0000: ro_pack_valid_data w_ddc_pack_data[4]; 8b0010_0000: ro_pack_valid_data w_ddc_pack_data[5]; 8b0100_0000: ro_pack_valid_data w_ddc_pack_data[6]; 8b1000_0000: ro_pack_valid_data w_ddc_pack_data[7]; default : ro_pack_valid_data d0; endcase end else ro_pack_valid_data d0; end // state_c_dly0 state_c_dly1 (sync to clk_adc domain) always (posedge clk_adc) begin if(i_rst) state_c_dly0 d0; else state_c_dly0 state_c; end always (posedge clk_adc) begin if(i_rst) state_c_dly1 d0; else state_c_dly1 state_c_dly0; end // r_pack_ddc_en always (posedge clk_adc) begin if (state_c_dly1 P_ST_END) r_pack_ddc_en 8b0000_0000; else if (state_c_dly1 P_ST_READ r_chan_cnt d7) r_pack_ddc_en 8b0000_0000; else r_pack_ddc_en ddc_en; end // r_pack_ddc_data always (posedge clk_adc) begin r_pack_ddc_data ddc_data; end // r_sample_cnt always (posedge clk_adc) begin if(i_rst || r_fifo_rst) r_sample_cnt d0; else if(Acquisition_en r_pack_ddc_en) r_sample_cnt r_sample_cnt 1; else r_sample_cnt r_sample_cnt; end // r_rd_fifo_ready always (posedge i_clk) begin if(i_rst || r_fifo_rst) r_rd_fifo_ready d0; else if(state_c P_ST_IDLE) r_rd_fifo_ready d0; else if(state_c P_ST_JUDGE) r_rd_fifo_ready d0; else if(state_c P_ST_READ dma_fifo_progfull) r_rd_fifo_ready[r_chan_cnt] 1b0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt P_PACK_LENGTH) r_rd_fifo_ready[r_chan_cnt] 1b0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt ! P_PACK_LENGTH) r_rd_fifo_ready[r_chan_cnt] 1b1; else if (state_c P_ST_END) r_rd_fifo_ready d0; else r_rd_fifo_ready r_rd_fifo_ready; end // r_chan_cnt always (posedge i_clk) begin if(i_rst || r_fifo_rst) r_chan_cnt d0; else if(state_c P_ST_IDLE) r_chan_cnt d0; else if(state_c P_ST_READ r_data_cnt P_PACK_LENGTH r_chan_cnt d7) r_chan_cnt d0; else if(state_c P_ST_READ r_data_cnt P_PACK_LENGTH r_chan_cnt ! d7) r_chan_cnt r_chan_cnt 1; else if(state_c P_ST_END) r_chan_cnt d0; else r_chan_cnt r_chan_cnt; end // r_data_cnt always (posedge i_clk) begin if(i_rst || r_fifo_rst) r_data_cnt d0; else if(state_c P_ST_IDLE) r_data_cnt d0; else if(state_c P_ST_JUDGE) r_data_cnt d0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt P_PACK_LENGTH) r_data_cnt d0; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt ! P_PACK_LENGTH r_pack_ddc_en[r_chan_cnt] w_ddc_pack[r_chan_cnt] r_chan_cnt ! d7) r_data_cnt r_data_cnt 1; else if(state_c P_ST_READ dma_fifo_progfull d0 r_data_cnt ! P_PACK_LENGTH w_ddc_pack[r_chan_cnt] r_chan_cnt d7) r_data_cnt r_data_cnt 1; else if(state_c P_ST_END) r_data_cnt d0; else r_data_cnt r_data_cnt; end // r_fifo_rst always (posedge i_clk) begin if(i_rst) r_fifo_rst d1; else if (state_c P_ST_END r_cnt d10) r_fifo_rst d1; else r_fifo_rst d0; end // r_cnt always (posedge i_clk) begin if(i_rst) r_cnt d0; else if(r_cnt d1000) r_cnt d0; else if(state_c P_ST_END) r_cnt r_cnt 1; else r_cnt r_cnt; end endmodule6.2 tb_ad_pack.vtimescale 1ns/1ps module tb_ad_pack(); // 参数定义 define CLK_ADC_PERIOD 10 // ADC时钟 100MHz define CLK_TRANS_PERIOD 20 // 传输时钟 50MHz // 接口信号 reg clk_adc; reg clk_trans; reg rst; reg trans_data_sel; reg Acquisition_en; reg [31:0] Acquisition_delay; reg [7:0] ddc_en; reg [255:0] ddc_data; reg dma_fifo_progfull; wire pack_valid_en; wire [31:0] pack_valid_data; reg [31:0] error_cnt; reg [7:0] current_chan; reg [31:0] data_count; reg [31:0] total_packets; reg fifo_full_sim; wire [4:0] singal_cfg_state; wire [8:0] ad_chan_pack_state; wire [7:0] rd_fifo_ready; // 时钟生成 initial begin clk_adc 1b0; forever #(CLK_ADC_PERIOD/2) clk_adc ~clk_adc; end initial begin clk_trans 1b0; forever #(CLK_TRANS_PERIOD/2) clk_trans ~clk_trans; end // 复位生成 initial begin rst 1b1; #100; rst 1b0; end // 激励信号 initial begin Acquisition_en 0; Acquisition_delay 0; trans_data_sel 1; ddc_en 8h00; ddc_data 256h0; dma_fifo_progfull 0; error_cnt 0; current_chan 0; data_count 0; total_packets 0; fifo_full_sim 0; wait(rst 1b0); #100; Acquisition_delay 32d10; $display([%t] Testbench: Acquisition Enabled, $time); Acquisition_en 1; #25_000; ddc_en 8hFF; #50_000; $display([%t] Testbench: Simulating downstream FIFO almost full, $time); dma_fifo_progfull 1; #100_000; dma_fifo_progfull 0; #24000_000; if (error_cnt 0) begin $display([%t] Testbench: Simulation PASSED - Total packets: %0d, $time, total_packets); end else begin $display([%t] Testbench: Simulation FAILED with %0d errors, $time, error_cnt); end #100; $stop; end // ADC数据模拟 always (posedge clk_adc or posedge rst) begin if(rst) ddc_data 256h00010001_00010001_00010001_00010001_00010001_00010001_00010001_00010001; else if(Acquisition_en ddc_en) ddc_data ddc_data 256h00010001_00010001_00010001_00010001_00010001_00010001_00010001_00010001; end // 仿真监控 initial begin $display(); $display(Vivado 2022.1 仿真启动 - ad_pack 模块测试含流控); $display(); $monitor(Time:%0tns | Acquisition_en:%b | dma_fifo_progfull:%b | pack_valid_en:%b | pack_valid_data:%h | singal_cfg_state:%b | ad_chan_pack_state:%b | rd_fifo_ready:%b, $time, Acquisition_en, dma_fifo_progfull, pack_valid_en, pack_valid_data, singal_cfg_state, ad_chan_pack_state, rd_fifo_ready); end // 数据验证逻辑 always (posedge clk_trans) begin if (!rst pack_valid_en) begin total_packets total_packets 1; if (pack_valid_data 32hCC33_AA55) begin data_count 0; end else if (pack_valid_data[31:8] 24h55_AA33) begin current_chan pack_valid_data[7:0]; $display([%t] Frame Start detected for Channel %0d, $time, pack_valid_data[7:0]); end else if (pack_valid_data 32h7E7E_7E7E) begin $display([%t] Frame End detected for Channel %0d (Data count: %0d), $time, current_chan, data_count); end else begin data_count data_count 1; end end end // 状态监控 assign singal_cfg_state dut.singal_cfg_u0.state_c; assign ad_chan_pack_state dut.pack[0].ad_chan_pack_u0.state_c; assign rd_fifo_ready dut.r_rd_fifo_ready; endmodule7. 仿真结果与波形分析仿真运行后我们期望看到Acquisition_en有效后singal_cfg产生data_trans_start脉冲。各ad_chan_pack开始打包FIFO逐渐填满。顶层状态机从 IDLE → READ轮询通道0~7每个通道连续读出pack_length个数据。输出数据流中帧头、通道标识、帧尾依次出现。当dma_fifo_progfull拉高时rd_fifo_ready相应通道位被清零暂停读取拉低后恢复。所有通道读完一帧后进入END状态等待1000周期后复位FIFO回到IDLE。通过波形或打印信息可以验证数据正确性。在测试中我们使用了测试数据trans_data_sel1因此每个通道的数据是递增的便于检查。8. 总结与优化方向ad_pack模块作为多通道聚合的核心巧妙地复用了单通道打包单元通过状态机轮询实现了高效的数据合并。其设计特点包括参数化可配置帧长度适应不同应用。可测试性内置测试模式减少外部依赖。流控完善支持下游反压保证数据不丢失。模块化各子模块功能单一易于维护和替换。可优化的地方当前IDLE状态仅检测通道0的prog_full作为启动条件更严谨的做法是检测所有通道的prog_full或empty信号确保所有通道都有数据才开始读。r_fifo_rst在END状态持续10个周期可能影响后续启动可根据实际FIFO复位时间调整。帧间延时由singal_cfg和顶层r_cnt共同控制存在冗余可统一管理。总体而言该模块已在FPGA项目中成功应用稳定可靠。希望本文能帮助读者理解多通道数据聚合的设计思路。附本设计基于Xilinx Vivado 环境开发子模块ad_chan_pack、singal_cfg、data_rate的代码请参考本系列其他文章。如有疑问欢迎留言交流。