20、AD/DA数据采集:ADC接口时序(SPI/并行)、DAC波形发生、数字示波器原型设计

这一章,我们来啃一块硬骨头——AD/DA数据采集。说实话,很多初学者一听到“高速采集”就头大,觉得那是高手才碰的东西。其实不然。你只要把ADC的接口时序搞明白,把DAC的波形发生逻辑理清楚,一个简易的数字示波器原型就能搭起来。我在项目中做过好几版采集系统,从低速的工业传感器到高速的射频采样,踩过的坑不少。今天我把这些经验掰开揉碎了讲给你听。

20.1 ADC接口时序:SPI与并行

ADC的接口,说白了就两大类:串行和并行。串行里最常见的是SPI,并行则多见于高速场合。咱们一个一个说。

20.1.1 SPI接口ADC

SPI接口的ADC,优点是引脚少,缺点是速度慢。一般用在几十K到几M采样率的场景。比如我常用的AD7689,16位精度,500kSPS,SPI接口,非常适合做精密测量。

SPI的时序其实不复杂,但有个细节容易翻车——数据是在SCK的哪个边沿采样的。不同的ADC,CPOL和CPHA可能不一样。我建议你养成一个习惯:拿到芯片手册,先看时序图,确认是“上升沿采样”还是“下降沿采样”。

核心要点:SPI模式下,ADC通常工作在“模式0”(CPOL=0, CPHA=0)或“模式3”(CPOL=1, CPHA=1)。前者在上升沿采样,后者在下降沿采样。

下面是一个典型的SPI ADC读取时序代码(Verilog):

// SPI ADC读取模块(以AD7689为例)
module spi_adc_read (
    input  wire        clk,          // 系统时钟,比如50MHz
    input  wire        rst_n,        // 复位
    input  wire        adc_start,    // 启动转换
    output reg  [15:0] adc_data,     // 读取到的16位数据
    output reg         adc_done,     // 转换完成标志
    // SPI接口
    output reg         sclk,         // SPI时钟
    output reg         cs_n,         // 片选(低有效)
    output reg         mosi,         // 主机输出,从机输入
    input  wire        miso          // 主机输入,从机输出
);

// 状态机定义
localparam IDLE   = 3'd0;
localparam START  = 3'd1;
localparam SHIFT  = 3'd2;
localparam DONE   = 3'd3;

reg [2:0] state, next_state;
reg [4:0] bit_cnt;      // 位计数器,0~15
reg [15:0] shift_reg;   // 移位寄存器

// 状态机主逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
        sclk <= 0;
        cs_n <= 1;
        mosi <= 0;
        adc_done <= 0;
        adc_data <= 16'd0;
    end else begin
        case (state)
            IDLE: begin
                cs_n <= 1;
                sclk <= 0;
                adc_done <= 0;
                if (adc_start) begin
                    cs_n <= 0;          // 拉低片选
                    state <= START;
                end
            end
            START: begin
                // 发送配置命令(这里简化,只读通道0)
                mosi <= 1'b0;           // 配置位
                bit_cnt <= 5'd0;
                state <= SHIFT;
            end
            SHIFT: begin
                if (bit_cnt < 5'd16) begin
                    // 产生SCK时钟
                    sclk <= ~sclk;
                    if (sclk) begin      // 下降沿采样MISO
                        shift_reg <= {shift_reg[14:0], miso};
                        bit_cnt <= bit_cnt + 1'b1;
                    end
                end else begin
                    state <= DONE;
                end
            end
            DONE: begin
                cs_n <= 1;              // 释放片选
                adc_data <= shift_reg;  // 锁存数据
                adc_done <= 1;          // 完成标志
                state <= IDLE;
            end
        endcase
    end
end

endmodule
我的经验:SPI时钟频率不要超过ADC手册规定的最大值。我曾经为了追求速度,把SCK提到10MHz,结果AD7689读出来的数据偶尔跳变。后来降到5MHz,稳如老狗。记住,稳定比速度重要。

20.1.2 并行接口ADC

并行ADC,比如AD9280、AD9226这些,采样率动辄几十M到上百M。接口也简单——数据线直接输出,外加一个时钟信号。但简单归简单,时序约束反而更严格。

并行ADC的时序,核心就两个参数:建立时间(tSU)和保持时间(tH)。数据在时钟沿之后,必须稳定一段时间才能被正确采样。我在做一款100M采样率的采集卡时,就因为PCB走线长了那么几厘米,导致高速数据出错。后来加了等长走线和源同步时钟,问题才解决。

注意:并行ADC的数据总线,在FPGA端一定要用IDDR或ILOGIC原语来捕获。直接用普通寄存器去采,时序大概率会出问题。尤其是采样率超过50M的时候。

下面是一个并行ADC的读取示例(使用IDDR原语):

// 并行ADC数据捕获(使用IDDR)
module parallel_adc_capture (
    input  wire        clk_adc,      // ADC输出时钟(比如50MHz)
    input  wire [11:0] adc_data,     // ADC数据总线(12位)
    output reg  [11:0] data_out,     // 同步后的数据
    output reg         data_valid    // 数据有效标志
);

// 使用IDDR原语捕获双沿数据(如果ADC是DDR模式)
wire [11:0] data_rise, data_fall;

genvar i;
generate
    for (i = 0; i < 12; i = i + 1) begin : gen_iddr
        IDDR #(
            .DDR_CLK_EDGE("SAME_EDGE_PIPELINED")
        ) u_iddr (
            .Q1(data_rise[i]),
            .Q2(data_fall[i]),
            .C(clk_adc),
            .CE(1'b1),
            .D(adc_data[i]),
            .R(1'b0),
            .S(1'b0)
        );
    end
endgenerate

// 这里以上升沿数据为例(单沿模式)
always @(posedge clk_adc) begin
    data_out  <= data_rise;
    data_valid <= 1'b1;
end

endmodule

20.2 DAC波形发生

说完了ADC,咱们聊聊DAC。DAC的作用正好反过来——把数字信号变成模拟信号。波形发生,说白了就是让DAC按照你想要的形状输出电压。

常见的波形有正弦波、三角波、方波。实现方法其实都一样:查表法。你事先算好一个周期的波形数据,存到ROM里,然后按一定速率循环读取,DAC就输出对应的波形了。

20.2.1 正弦波发生器

正弦波是最常用的。我一般用MATLAB或者Python算好1024个点的正弦值,存成coe文件,然后例化一个ROM。下面是一个简单的实现:

// 正弦波发生器(查表法)
module sine_wave_gen (
    input  wire        clk,          // 系统时钟
    input  wire        rst_n,        // 复位
    input  wire [31:0] freq_word,    // 频率控制字
    output reg  [11:0] dac_data      // 输出到DAC的数据(12位)
);

// 相位累加器
reg [31:0] phase_acc;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        phase_acc <= 32'd0;
    end else begin
        phase_acc <= phase_acc + freq_word;
    end
end

// 取高10位作为ROM地址(1024点)
wire [9:0] rom_addr;
assign rom_addr = phase_acc[31:22];

// 例化正弦波ROM
sine_rom u_sine_rom (
    .clka(clk),
    .addra(rom_addr),
    .douta(dac_data)
);

endmodule
频率怎么算?输出频率 = (freq_word * 系统时钟频率) / 2^32。比如系统时钟50MHz,你想输出1kHz正弦波,freq_word = (1000 * 2^32) / 50_000_000 ≈ 85899。这个值就是你的频率控制字。

20.2.2 三角波和方波

三角波更简单,不需要ROM。你用一个计数器,先递增到最大值,再递减到0,循环往复就行。方波就更简单了——输出高电平,延时,输出低电平,延时。

不过要注意,DAC的输出有建立时间。你切换数据太快,模拟输出可能跟不上。我遇到过一个问题:方波频率设到1MHz,结果DAC输出变成了三角波。后来查手册,发现DAC的建立时间是1us,1MHz的周期才1us,根本来不及建立。所以,DAC的更新速率要匹配它的建立时间。

20.3 数字示波器原型设计

好了,ADC和DAC都讲完了。现在把它们组合起来,就是一个简易的数字示波器原型。说白了,就是:ADC采集信号 → FPGA处理 → 显示到屏幕(或通过DAC输出)。

我当年做第一个示波器原型时,结构大概是这样的:

  1. 前端调理电路:信号进来,先经过运放做阻抗匹配和电平搬移,把信号调整到ADC的输入范围(比如0~2V)。
  2. ADC采样:用并行ADC(比如AD9280,8位,32M采样率)把模拟信号变成数字信号。
  3. 触发控制:这是示波器的灵魂。你需要一个触发模块,检测信号是否满足触发条件(比如上升沿超过阈值)。只有触发到了,才开始存储数据。
  4. 存储与显示:把触发后的数据存到FIFO或BRAM里,然后通过VGA或HDMI显示到屏幕上。如果只是原型,也可以用串口发到PC上显示。
触发模块的核心逻辑:比较当前采样值和上一个采样值。如果上一个值低于阈值,当前值高于阈值,就认为检测到了上升沿触发。然后产生一个触发信号,启动数据存储。

下面是一个简单的触发检测模块:

// 上升沿触发检测
module trigger_detect (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  adc_data,     // ADC采样数据
    input  wire [7:0]  trigger_level,// 触发电平
    output reg         trigger_out   // 触发信号
);

reg [7:0] data_dly;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_dly <= 8'd0;
        trigger_out <= 1'b0;
    end else begin
        data_dly <= adc_data;  // 延迟一拍
        // 上升沿触发条件:上一拍低于阈值,当前拍高于阈值
        if (data_dly < trigger_level && adc_data >= trigger_level) begin
            trigger_out <= 1'b1;
        end else begin
            trigger_out <= 1'b0;
        end
    end
end

endmodule
避坑指南:我曾经在触发模块上栽过跟头。触发信号只持续一个时钟周期,但数据存储需要多个周期。结果就是,触发到了,但数据没存全。解决办法是:触发信号产生后,用一个计数器控制存储窗口,比如触发后连续存1024个点。这样波形就能完整显示了。

20.4 本章小结

这一章内容不少,但核心就三件事:ADC怎么读、DAC怎么写、示波器怎么搭。SPI接口的ADC,注意时序模式;并行接口的ADC,注意建立保持时间。DAC波形发生,查表法最实用。数字示波器原型,触发模块是关键。

嗯,说实话,这些内容我当年也是反复调试才搞明白的。尤其是并行ADC的高速捕获,第一次做的时候,波形全是毛刺,查了两天才发现是PCB走线问题。所以,如果你第一次做没成功,别灰心。多看看时序图,多量量信号,问题总能找到。

下一章,我们会讲更高级的DDS信号发生器。到时候,波形发生这块会玩出更多花样。咱们下章见。