基于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)设计原因
缓冲区设计目标
-
抗抖动能力:
- 假设 ADC 采样率 40MSPS(每秒40百万次采样),缓冲区可存储:2048/40M ≈ 51.2μs 数据(单通道)
- 足够应对短暂的处理延迟或突发数据高峰
-
与传输带宽匹配:
- PCIe Gen2 x4 理论带宽 2GB/s(16Gbps)
- 实际有效带宽约 480MB/s(考虑协议开销)
- 缓冲区可支持连续传输:32KB/480MB ≈ 67μs
-
资源利用率:
- Xilinx Artix-7 FPGA 的 BRAM 通常为 36Kb / 块
- 2048×16 位 = 32Kb,可高效利用 BRAM 资源
数据采集阶段
- ADC 以 40MHz 速率产生数据
- 每周期采集 2 个通道数据(共 24 位)
- 扩展为 16 位后存入缓冲区(占用 32 位)
- 缓冲区写指针每周期加 2
数据打包阶段(关键)
- 当缓冲区写入约 2046 个采样点时触发打包
- 每次从缓冲区读取 16 个采样点(32 个存储位置)
- 打包成 256 位数据块输出
- 缓冲区读指针每打包一次加 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. 双缓冲工作流程(核心技术):
- 缓冲区 A 负责实时接收 ADC 采样数据,填满后触发 DMA 传输至主机;
- 同时,缓冲区 B 已完成上一次传输,空出等待填充新的采样数据;
- 当缓冲区 A 传输完成时,缓冲区 B 可能已部分填充(取决于采样速率与传输速率的关系),交换两者角色,循环往复。
- 关键点:采样与传输在 不同缓冲区异步进行,表面上是 “填满再传”,实际是 “边填 A 边传 B”,无任何空白期。
2. 类比:流水线作业
- 采样(工人 A)和传输(工人 B)各自有一个篮子:
- 工人 A 不断往篮子 A 里放苹果(采样),篮子满(2048 个)后,工人 B 立刻搬去篮子 A 送货,同时工人 A 开始往篮子 B 放苹果;
- 工人 B 送货(传输)仅需 4.8μs(搬苹果很快),回来时篮子 B 可能只放了 192 个苹果,继续等工人 A 放满,循环往复。
- 表面上每个篮子都是 “满了才搬”,但两个篮子交替使用,实际生产过程是连续的,没有工人闲着。
短暂空白期的发生情况:缓冲区设计不合理时才会出现
若系统未采用双缓冲,且缓冲区深度不足(如单缓冲且深度很小),则会出现:
- 填充阶段:ADC 采样填满缓冲区(如 51.2μs),期间传输系统空闲;
- 传输阶段:缓冲区数据被送走(4.8μs),期间 ADC 被迫暂停采样(无空闲缓冲区可用),导致 51.2μs 工作 + 4.8μs 停顿 的周期性空白。
但在合理设计中(双缓冲 + 足够深度),这种情况 完全可避免,因为采样与传输是并行的,如同 “两条流水线同时工作”,没有停顿。
打包传输是 “效率最优解”,空白期可通过双缓冲消除
- 不直接边采样边传输:因接口特性和协议开销限制,逐个传输效率极低,硬件不支持;
- 打包传输的核心价值:利用突发模式提升有效数据率,匹配 DMA 和高速接口的设计逻辑;
- 消除空白期的关键:双缓冲机制让采样与传输异步并行,缓冲区 A 填充时传输缓冲区 B,交替进行,数据流动是连续的;
- 工程实践:几乎所有高速数据采集系统(如示波器、雷达)均采用 “采样→缓冲→批量传输” 架构,这是经过验证的成熟方案。
简言之:打包是为了 “跑快车时多拉货”,双缓冲则是为了 “让快车不停跑”,两者结合实现了高效与连续的统一。