基于CPIe的数据采集模块

基于CPIe的数据采集模块

基础知识

该模块有两个时钟域输入,分别为ADC时钟40Mhz;用户时钟125Mhz;

双通道采集数据;并带有开始采集标志位start_acq;

data_valid与data_ready形成握手信号机制;

带有状态信号:acq_in_progress;acq_complete;buffer_full;buffer_empty;

同时定义了2048 深度的缓冲区 (adc_buffer) 用于存储 ADC 数据

12 位是工业级 ADC 常见的精度,因此定义[11:0] adc_data_ch0,又因为双通道,所以每个采样周期产生24位原始数据;

PCIe、DDR 等高速接口通常采用 256 位或 512 位数据通路,256 位 = 32 字节(一个字节有8位),是 PCIe 和 DDR 的高效传输单元,这样将多个采样点打包成固定长度块可以减少协议开销

代码中每个 256 位数据块包含16 个采样点(有8 对双通道数据),每个采样点 16 位(实际 12 位,高 4 位补零),16 个采样点 × 16 位 = 256 位;缓冲区深度设置为2048,则总容量可以容纳:2048 × 16 位 = 32KB 或 1024 对采样点

代码展示:

//数据采集模块
module data_acquisition (
    input       adc_clk,       // ADC时钟 (40MHz),双通道采集
    input [11:0] adc_data_ch0,  // 通道0数据
    input [11:0] adc_data_ch1,  // 通道1数据
    input       adc_valid,     // ADC数据有效标志
    input       user_clk,      // 用户时钟 (125MHz)
    input       reset_n,
    input       start_acq,     // 开始采集标志位
    output      acq_in_progress,//指示系统目前是否处于采集过程状态
    output      acq_complete,//标志着系统一次完整的数据采集过程已完成
    output reg [255:0] data_out,
    output reg        data_valid,//指示 data_out 中的数据有效,作为数据输出的握手信号
    input         data_ready,//由外部模块发送的就绪信号,表示已准备好接收 data_out,与 data_valid 配合实现握手机制
    output reg [31:0] data_count,//记录已经输出的数据块总量(每成功打包256个数据形成一个数据块并输出就加一),用于之后的数据块索引
    output        buffer_full,//用于指示内部缓冲区即将填满,防止溢出
    output        buffer_empty//指示内部缓冲区已为空,防止无效数据读取
);

// 状态机
localparam IDLE        = 3'd0,//空闲状态,等待开始采集
           ACQUIRING   = 3'd1,//正在采集数据状态
           PACKING     = 3'd2,//打包数据状态
           READY       = 3'd3,//数据打包完成,等待外界形成握手信号用于可以被读取
           COMPLETE    = 3'd4;//表示整个过程完成

reg [2:0]  state, next_state;
reg [15:0] adc_buffer [0:2047];  // 双通道数据缓冲区 (可容纳2048个采样点),用于存储ADC数据
reg [15:0] buffer_wptr;//写指针
reg [15:0] buffer_rptr;//读指针
reg        buffer_full_int;
reg        buffer_empty_int;

// 跨时钟域同步
reg        start_acq_sync1, start_acq_sync2;
reg        adc_valid_sync1, adc_valid_sync2;
reg [11:0] adc_data_ch0_sync;
reg [11:0] adc_data_ch1_sync;

// 同步控制信号到ADC时钟域,使用两级触发器同步 (sync1, sync2) 防止亚稳态
//将 start_acq 信号从 user_clk 同步到 adc_clk 域
always @(posedge adc_clk or negedge reset_n) begin
    if (!reset_n) begin
        start_acq_sync1 <= 0;
        start_acq_sync2 <= 0;
    end else begin
        start_acq_sync1 <= start_acq;
        start_acq_sync2 <= start_acq_sync1;
    end
end

// 同步ADC数据到用户时钟域
//将 ADC 数据和有效信号从 adc_clk 同步到 user_clk 域
always @(posedge user_clk or negedge reset_n) begin
    if (!reset_n) begin
        adc_valid_sync1 <= 0;
        adc_valid_sync2 <= 0;
        adc_data_ch0_sync <= 0;
        adc_data_ch1_sync <= 0;
    end else begin
        adc_valid_sync1 <= adc_valid;
        adc_valid_sync2 <= adc_valid_sync1;
        adc_data_ch0_sync <= adc_data_ch0;
        adc_data_ch1_sync <= adc_data_ch1;
    end
end

// 状态机寄存器
always @(posedge user_clk or negedge reset_n) begin
    if (!reset_n) begin
        state <= IDLE;
        buffer_wptr <= 0;
        buffer_rptr <= 0;
        data_count <= 0;
        data_valid <= 0;
    end else begin
        state <= next_state;
        
        case (state)
            IDLE: begin//IDLE 状态:等待 start_acq_sync2 信号,初始化指针和计数器
                if (start_acq_sync2) begin
                    buffer_wptr <= 0;
                    buffer_rptr <= 0;
                    data_count <= 0;
                end
            end
            
            ACQUIRING: begin
                if (adc_valid_sync2 && buffer_wptr < 2046) begin
                    // 存储双通道ADC数据,将两个通道的数据交替存入缓冲区,每次写入后写指针加 2
                    adc_buffer[buffer_wptr]   <= adc_data_ch0_sync;
                    adc_buffer[buffer_wptr+1] <= adc_data_ch1_sync;
                    buffer_wptr <= buffer_wptr + 2;
                end
            end
            
            PACKING: begin
                // 打包数据到256位输出 (16个采样点: 8对双通道数据),读指针加 16
                data_out <= {adc_buffer[buffer_rptr+15],
                             adc_buffer[buffer_rptr+14],
                             adc_buffer[buffer_rptr+13],
                             adc_buffer[buffer_rptr+12],
                             adc_buffer[buffer_rptr+11],
                             adc_buffer[buffer_rptr+10],
                             adc_buffer[buffer_rptr+9],
                             adc_buffer[buffer_rptr+8],
                             adc_buffer[buffer_rptr+7],
                             adc_buffer[buffer_rptr+6],
                             adc_buffer[buffer_rptr+5],
                             adc_buffer[buffer_rptr+4],
                             adc_buffer[buffer_rptr+3],
                             adc_buffer[buffer_rptr+2],
                             adc_buffer[buffer_rptr+1],
                             adc_buffer[buffer_rptr]};
                buffer_rptr <= buffer_rptr + 16;
                data_valid <= 1;
                data_count <= data_count + 1;
            end
            
            READY: begin
                if (data_ready)//等待 data_ready 信号握手
                    data_valid <= 0;
            end
        endcase
    end
end

// 状态机逻辑,定义状态转移条件
always @(*) begin
    next_state = state;
    
    case (state)
        IDLE: begin
            if (start_acq_sync2)
                next_state = ACQUIRING;
        end
        
        ACQUIRING: begin
            if (buffer_wptr >= 2046)
                next_state = PACKING;
        end
        
        PACKING: begin
            next_state = READY;
        end
        
        READY: begin//数据被读取后,若缓冲区还有数据则继续打包,若缓冲区已空则标记完成
            if (data_ready && buffer_rptr >= 2046)
                next_state = COMPLETE;
            else if (data_ready)
                next_state = PACKING;
        end
        
        COMPLETE: begin
            if (!start_acq_sync2)
                next_state = IDLE;
        end
    endcase
end

// 缓冲区状态
assign buffer_full = buffer_full_int;
assign buffer_empty = buffer_empty_int;

always @(posedge user_clk or negedge reset_n) begin
    if (!reset_n) begin
        buffer_full_int <= 0;
        buffer_empty_int <= 1;
    end else begin
        buffer_full_int <= (buffer_wptr >= 2046);
        buffer_empty_int <= (buffer_rptr >= buffer_wptr);
    end
end

assign acq_in_progress = (state == ACQUIRING);
assign acq_complete = (state == COMPLETE);

endmodule

缓冲区深度(2048)设计原因

缓冲区设计目标
  1. 抗抖动能力

    • 假设 ADC 采样率 40MSPS(每秒40百万次采样),缓冲区可存储:2048/40M ≈ 51.2μs 数据(单通道)
    • 足够应对短暂的处理延迟或突发数据高峰
  2. 与传输带宽匹配

    • PCIe Gen2 x4 理论带宽 2GB/s(16Gbps)
    • 实际有效带宽约 480MB/s(考虑协议开销)
    • 缓冲区可支持连续传输:32KB/480MB ≈ 67μs
  3. 资源利用率

    • Xilinx Artix-7 FPGA 的 BRAM 通常为 36Kb / 块
    • 2048×16 位 = 32Kb,可高效利用 BRAM 资源
数据采集阶段
  1. ADC 以 40MHz 速率产生数据
  2. 每周期采集 2 个通道数据(共 24 位)
  3. 扩展为 16 位后存入缓冲区(占用 32 位)
  4. 缓冲区写指针每周期加 2
数据打包阶段(关键)
  1. 当缓冲区写入约 2046 个采样点时触发打包
  2. 每次从缓冲区读取 16 个采样点(32 个存储位置)
  3. 打包成 256 位数据块输出
  4. 缓冲区读指针每打包一次加 16

性能与资源平衡

带宽匹配
  • ADC 数据率:40MSPS × 2 通道 × 12 位 = 960Mbps(采样速率)
  • 输出数据率:960Mbps × (256/24) ≈ 10.24Gbps(原始数据率*打包比例=有效数据率)
  • 系统通过打包提高有效数据率,匹配高速接口需求
  • bps 是数据传输速率的基本单位,表示:bits per second(比特每秒)

  • 1Mbps = 1,000,000 位 / 秒
  • 1Gbps = 1,000,000,000 位 / 秒
缓冲区利用率
  • 最大存储量:2048 个采样点
  • 安全余量:预留 2 个位置避免指针溢出
  • 实际可用容量:2046 个采样点 ≈ 99.8% 利用率
FPGA 资源占用
  • BRAM 使用:32KB ≈ 1 个 36Kb BRAM(高效利用)
  • 逻辑资源:状态机和控制逻辑占用少量 LUT/FF

为什么不直接 “边采样边传输”?

1. 传输接口的 “最小数据单元” 限制
  • 多数高速接口(如 PCIe、以太网、USB)要求数据以 固定块大小(Packet/Burst) 传输,而非单个字节 / 样本。例如:
    • PCIe 最小传输单元是 128 字节(1024bit),若每次传 1 个 24 位采样点,需填充大量无效数据,浪费带宽。
    • 以太网帧至少 64 字节,传输 24bit 数据会导致 协议开销占比极高(如 99% 开销),实际有效速率趋近于 0。
  • 结论:逐个传输违背接口设计原则,效率极低。
2. 协议开销与时序成本不可忽视
  • 每次传输需经历 握手、寻址、同步、错误校验 等流程,单次传输耗时可能远大于采样间隔。
    例如:假设单次传输握手耗时 100ns,采样率 40MSPS(间隔 25ns),则传输耗时是采样间隔的 4 倍,必然导致采样数据积压。
  • 类比:这相当于每次送 1 封信就派 1 辆专车,油费(开销)比信本身(数据)更贵,显然不划算。
3. 硬件 DMA 机制的天然适配性
  • DMA(直接内存访问)控制器擅长搬运 连续批量数据,而非分散的单个样本。逐个传输需频繁唤醒 DMA,增加 CPU 中断负担,甚至导致系统卡顿。
  • 现代 ADC 芯片通常内置 FIFO 缓冲区,本身就支持 “先缓存后批量读取”,硬件设计上已倾向于打包传输。

打包传输的核心优势:用 “突发传输” 换 “整体效率”

1. 大幅降低协议开销,提升有效数据率
  • 本质:用 “短暂高速突发” 换取 “长期低开销”,类似快递集装柜:攒够一批再发车,虽然发车时有高速运输,但整体成本更低。
2. 匹配接口的 “突发模式” 特性
  • 高速接口(如 10Gbps 以太网、PCIe Gen3 x8)支持 突发传输(Burst Mode),在突发期间可达到理论峰值速率,但突发之间需要间隔(用于流控、时钟同步等)。
    • 例如:10.24Gbps 传输 2048 样本(49152bit)仅需 4.8μs,之后接口进入空闲,但此时 ADC 仍在持续采样,缓冲区继续填充,不会浪费接口带宽。
  • 若强行边采样边传输,反而会因频繁启停导致接口无法进入突发模式,实际速率可能连峰值的 10% 都达不到。
3. 简化系统时序设计,降低同步难度
  • 采样时钟(如 40MHz)与传输时钟(如 100MHz)通常不同步,直接实时传输需复杂的跨时钟域处理(如 FIFO 异步握手),而打包传输可通过 缓冲区深度一次性吸收时钟差异,大幅降低设计难度。

如何避免 “采样填满缓冲区再传输” 导致的 空白期?—— 双缓冲(Ping-Pong Buffer)机制

1. 双缓冲工作流程(核心技术):
  1. 缓冲区 A 负责实时接收 ADC 采样数据,填满后触发 DMA 传输至主机;
  2. 同时,缓冲区 B 已完成上一次传输,空出等待填充新的采样数据;
  3. 当缓冲区 A 传输完成时,缓冲区 B 可能已部分填充(取决于采样速率与传输速率的关系),交换两者角色,循环往复。
  4. 关键点:采样与传输在 不同缓冲区异步进行,表面上是 “填满再传”,实际是 “边填 A 边传 B”,无任何空白期。
2. 类比:流水线作业
  • 采样(工人 A)和传输(工人 B)各自有一个篮子:
    • 工人 A 不断往篮子 A 里放苹果(采样),篮子满(2048 个)后,工人 B 立刻搬去篮子 A 送货,同时工人 A 开始往篮子 B 放苹果;
    • 工人 B 送货(传输)仅需 4.8μs(搬苹果很快),回来时篮子 B 可能只放了 192 个苹果,继续等工人 A 放满,循环往复。
  • 表面上每个篮子都是 “满了才搬”,但两个篮子交替使用,实际生产过程是连续的,没有工人闲着。

短暂空白期的发生情况:缓冲区设计不合理时才会出现

若系统未采用双缓冲,且缓冲区深度不足(如单缓冲且深度很小),则会出现:

  1. 填充阶段:ADC 采样填满缓冲区(如 51.2μs),期间传输系统空闲;
  2. 传输阶段:缓冲区数据被送走(4.8μs),期间 ADC 被迫暂停采样(无空闲缓冲区可用),导致 51.2μs 工作 + 4.8μs 停顿 的周期性空白。

但在合理设计中(双缓冲 + 足够深度),这种情况 完全可避免,因为采样与传输是并行的,如同 “两条流水线同时工作”,没有停顿。


打包传输是 “效率最优解”,空白期可通过双缓冲消除

  1. 不直接边采样边传输:因接口特性和协议开销限制,逐个传输效率极低,硬件不支持;
  2. 打包传输的核心价值:利用突发模式提升有效数据率,匹配 DMA 和高速接口的设计逻辑;
  3. 消除空白期的关键:双缓冲机制让采样与传输异步并行,缓冲区 A 填充时传输缓冲区 B,交替进行,数据流动是连续的;
  4. 工程实践:几乎所有高速数据采集系统(如示波器、雷达)均采用 “采样→缓冲→批量传输” 架构,这是经过验证的成熟方案。

简言之:打包是为了 “跑快车时多拉货”,双缓冲则是为了 “让快车不停跑”,两者结合实现了高效与连续的统一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值