找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2214

积分

0

好友

345

主题
发表于 6 小时前 | 查看: 2| 回复: 0

在高速数据采集应用中,时间往往是最大的挑战。当采样率要求达到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同步或更高速率的需求,欢迎在云栈社区的技术论坛分享你的想法,与更多开发者共同探讨。




上一篇:深入解析Etcd:Kubernetes首选的高可用键值存储与实战入门
下一篇:EROFS NTFS XFS 三大文件系统内核开发近期重磅进展:内存去重、新实现与自愈能力
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-7 20:29 , Processed in 0.292468 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表