在高速数据采集应用中,时间往往是最大的挑战。当采样率要求达到1MHz时,如果使用传统的单线串行SPI接口去依次读取多个ADC通道的数据,几乎是一个“不可能完成的任务”。为了突破这一瓶颈,一个有效的解决方案是采用 “多线并行移位” 的架构。
本文提供的控制器代码模板正是基于这一思路:它让6个ADC通道共享同一个SPL时钟(SCLK)和片选信号(CS),但为每个通道配备独立的MISO数据引脚。这样一来,在一个SPI时钟周期内,所有6个通道的数据可以同步被采样和移位,极大地缓解了单一线序带来的时间压力,为后续的信号处理留出了宝贵的裕量。该设计假设FPGA的系统时钟为100MHz,并在此频率下将SPI时钟(SCLK)分频至50MHz,以满足在1微秒(us)的采样周期内完成转换和读取的需求。
module adc_spi_controller (
input wire clk, // 系统时钟 100MHz
input wire rst_n, // 异步复位
input wire trigger, // 1MHz 触发脉冲 (由定时器产生)
// ADC 物理接口
output reg adc_cs, // 片选
output reg adc_sclk, // SPI 时钟
output reg adc_cnvst, // 转换开始信号
input wire adc_busy, // ADC 忙信号
input wire [5:0] adc_miso, // 6路并行数据线
// 用户接口
output reg [95:0] data_out, // 6通道 x 16bit = 96bit
output reg data_valid // 数据有效标志
);
// 状态机定义
localparam ST_IDLE = 3'd0,
ST_CONV = 3'd1,
ST_WAIT = 3'd2,
ST_READ = 3'd3,
ST_DONE = 3'd4;
reg [2:0] state;
reg [7:0] cnt_bit; // 比特计数 (0-15)
reg [7:0] cnt_wait; // 等待计数器
reg [15:0] shift_reg [5:0]; // 6个移位寄存器
// SPI 时钟生成 (100MHz 分频得到 50MHz)
reg sclk_en;
always @(posedge clk) begin
if (state == ST_READ)
adc_sclk <= ~adc_sclk;
else
adc_sclk <= 1'b1; // CPOL=1 示例
end
// 状态机逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= ST_IDLE;
adc_cnvst <= 1'b0;
adc_cs <= 1'b1;
data_valid <= 1'b0;
end else begin
case (state)
ST_IDLE: begin
data_valid <= 1'b0;
if (trigger) begin
adc_cnvst <= 1'b1; // 发起转换
state <= ST_CONV;
end
end
ST_CONV: begin
// CNVST 保持几个周期 (根据 datasheet)
adc_cnvst <= 1'b0;
state <= ST_WAIT;
end
ST_WAIT: begin
// 等待 BUSY 下降沿或固定转换时间
if (!adc_busy) begin
adc_cs <= 1'b0;
cnt_bit <= 0;
state <= ST_READ;
end
end
ST_READ: begin
// 在 SCLK 的上升沿采样数据 (假设 CPHA=0)
if (adc_sclk == 1'b0) begin // 即将变高
shift_reg[0] <= {shift_reg[0][14:0], adc_miso[0]};
shift_reg[1] <= {shift_reg[1][14:0], adc_miso[1]};
shift_reg[2] <= {shift_reg[2][14:0], adc_miso[2]};
shift_reg[3] <= {shift_reg[3][14:0], adc_miso[3]};
shift_reg[4] <= {shift_reg[4][14:0], adc_miso[4]};
shift_reg[5] <= {shift_reg[5][14:0], adc_miso[5]};
if (cnt_bit == 15) begin
state <= ST_DONE;
end else begin
cnt_bit <= cnt_bit + 1;
end
end
end
ST_DONE: begin
adc_cs <= 1'b1;
data_out <= {shift_reg[5], shift_reg[4], shift_reg[3],
shift_reg[2], shift_reg[1], shift_reg[0]};
data_valid <= 1'b1;
state <= ST_IDLE;
end
endcase
end
end
endmodule
设计要点与原理剖析
1. 为什么要采用并行移位(6路 MISO)架构?
让我们来算一笔时间账:1MHz 的采样率意味着每个采样点之间的间隔只有1000纳秒 (ns)。
串行瓶颈:如果使用传统的单线串行方式依次读取6个通道,总共需要传输 6 16 = 96 比特。即使SPI时钟跑到50 MHz(每个比特20 ns),仅仅是数据传输就需要 96 20 = 1920 ns。这已经超过了1us的采样周期,系统必然会丢帧。
并行优势:通过6路MISO并行读取,读取6个通道16位数据所需的时间和读取1个通道完全一样,只需要16个时钟周期,即 16 * 20 = 320 ns。这样,ADC的转换时间和FPGA的数据处理就有了充足的余量。
2. 为什么由 FPGA 产生触发信号(Trigger)?
这主要是出于对时序确定性和信号纯度的考量。
确定性:如果在SoC(如运行Linux的处理器)端通过软件指令来触发每次采样,由于Linux是非实时操作系统,采样间隔会产生不可预测的抖动。
谱纯度:在1MHz这样的高频采样下,即使是几纳秒的时序抖动,也会在后续的FFT频谱分析中产生严重的相位噪声。而FPGA内部的计数器由高精度晶振直接驱动,能够产生极其均匀、精确的采样触发脉冲,保证了信号的时域完整性。
3. 为什么要用状态机来控制 CNVST 和 BUSY 信号?
这主要是为了适配高速ADC(尤其是逐次逼近型SAR ADC)的工作特性。
SAR架构适配:多数高速ADC需要一个明确的转换启动脉冲(CNVST),并在转换期间通过BUSY信号告知外部控制器“忙状态”,此时不能进行数据读取。
自动同步与低延迟:状态机可以精确控制CNVST脉冲的宽度,并自动捕获BUSY信号的下降沿(转换完成)。一旦检测到转换结束,状态机立即拉低片选(CS)并启动SPI传输,将系统延迟降到最低,确保数据能够被及时、完整地读取,避免“赶不上”下一个采样周期。
4. 使用 shift_reg 数组与数据拼接的考量
资源与逻辑清晰度:在Verilog中使用寄存器数组(如 reg [15:0] shift_reg [5:0])可以清晰地对多个通道的移位寄存器进行建模。这种写法能够很好地映射到FPGA内部的触发器(FF)资源。在ST_READ状态下,每个通道的数据在SCLK的上升沿被同步移入各自的寄存器,最终在ST_DONE状态一次性拼接输出。这保证了6个通道的采样数据在时间轴上是严格对齐的,对于多通道同步分析至关重要。
物理层提醒:由于adc_sclk运行在50MHz,属于相对较高的频率,在PCB布局时必须保证6条MISO走线的长度尽可能一致。否则,因走线延迟差异(Skew)可能导致某些通道的数据在采样时刻出现错位,引入误差。
数据格式优化:data_out输出的是96位宽数据。在实际系统中,为了与32位或64位处理器高效交互,建议在存入FIFO或发送给SoC之前,将其补齐为128位(例如拼接一个固定的32位帧头:{32'hAAAA_5555, data_out})。这样既符合现代SoC处理数据的原生宽度(如32位总线),提升传输效率,这个帧头本身也可以作为数据包的标识符。
时序约束:在高频设计中,时序约束不可或缺。必须在项目的约束文件(如Xilinx的.xdc文件)中,将adc_sclk正确定义为生成的时钟,并对adc_miso等输入信号设置合理的set_input_delay约束。只有这样,综合和布局布线工具才能优化时序路径,防止建立/保持时间违例,避免实际工作中出现随机的高频噪点或数据错误。
这个多通道SPI控制器的设计,融合了并行处理架构的思想和对高速数字电路时序的深刻理解。它不仅仅是几行Verilog代码,更是一个解决具体工程难题的完整方案。对于从事FPGA开发和高速数据采集的工程师而言,理解其背后的设计权衡至关重要。如果你有更复杂的多ADC同步或更高速率的需求,欢迎在云栈社区的技术论坛分享你的想法,与更多开发者共同探讨。