DEV Community

Hedy
Hedy

Posted on

ADC acquisition method in FPGA

Implementing Analog-to-Digital Converter (ADC) acquisition is a fundamental and common task in FPGA design. The method depends heavily on the type of ADC interface.

Here’s a breakdown of the most common ADC types and how to acquire data from them using an FPGA.

1. Serial Interface ADCs (SPI / Microwire)
This is one of the most common types for lower-speed, lower-channel-count applications. Examples: ADCs from Analog Devices (AD7xxx series), Texas Instruments (ADS7xxx series).

Key Signals:

  • SCLK (Serial Clock): Generated by the FPGA (SPI controller master).
  • CS_n (Chip Select): Active-low signal generated by the FPGA to initiate a conversion and frame the data transfer.
  • SDATA (Serial Data/MISO): The data line from the ADC to the FPGA. Data is shifted out MSB-first or LSB-first on each SCLK edge.
  • Sometimes: DIN (MOSI): For configuring the ADC's internal registers.

Acquisition Method (Finite State Machine - FSM):
The FPGA implements an SPI master controller, typically with a state machine.

Example Verilog FSM for a 16-bit SPI ADC:

verilog

module spi_adc_controller (
    input wire clk,         // FPGA system clock (e.g., 100 MHz)
    input wire reset_n,
    input wire start_conv,  // Signal to start a new conversion
    output reg cs_n,        // To ADC
    output reg sclk,        // To ADC
    input wire sdata,       // From ADC
    output reg [15:0] data, // Parallel output data
    output reg data_valid   // Pulses high when 'data' is valid
);

// States for the FSM
localparam [1:0] STATE_IDLE  = 2'b00;
localparam [1:0] STATE_CONV  = 2'b01; // Conversion time (CS_n low to first SCLK)
localparam [1:0] STATE_READ  = 2'b10; // Shifting in data

reg [1:0] current_state, next_state;
reg [4:0] bit_counter; // Counts 16 bits (0 to 15)
reg [15:0] data_reg;   // Shift register to accumulate serial data
reg [7:0] clk_divider; // Counter to generate slower SCLK
reg sclk_enable;       // Enable signal for SCLK generation

// State Register
always @(posedge clk or negedge reset_n) begin
    if (!reset_n)
        current_state <= STATE_IDLE;
    else
        current_state <= next_state;
end

// Next State Logic
always @(*) begin
    next_state = current_state;
    case (current_state)
        STATE_IDLE: if (start_conv) next_state = STATE_CONV;
        STATE_CONV: if (clk_divider == 8'd50) next_state = STATE_READ; // Wait for conversion time
        STATE_READ: if (bit_counter == 5'd16) next_state = STATE_IDLE; // Done reading 16 bits
    endcase
end

// Output Logic & Counters
always @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        cs_n <= 1'b1;
        sclk <= 1'b0;
        clk_divider <= 8'b0;
        bit_counter <= 5'b0;
        data_reg <= 16'b0;
        data_valid <= 1'b0;
        sclk_enable <= 1'b0;
    end else begin
        data_valid <= 1'b0; // Default value
        case (current_state)
            STATE_IDLE: begin
                cs_n <= 1'b1;
                sclk <= 1'b0;
                sclk_enable <= 1'b0;
                bit_counter <= 5'b0;
            end

            STATE_CONV: begin
                cs_n <= 1'b0; // Pull CS_n low to start conversion
                // Wait for a period to meet ADC T_CONV spec
                if (clk_divider < 8'd50) 
                    clk_divider <= clk_divider + 1;
                else begin
                    clk_divider <= 8'b0;
                    sclk_enable <= 1'b1; // Enable SCLK for the read phase
                end
            end

            STATE_READ: begin
                // Generate SCLK by dividing the system clock
                if (sclk_enable) begin
                    if (clk_divider < 8'd25) begin
                        clk_divider <= clk_divider + 1;
                    end else begin
                        clk_divider <= 8'b0;
                        sclk <= ~sclk; // Toggle SCLK

                        // On the falling edge of SCLK (ADC data is stable), sample the data
                        if (sclk == 1'b1) begin 
                            data_reg <= {data_reg[14:0], sdata}; // Shift left
                            bit_counter <= bit_counter + 1;
                            if (bit_counter == 5'd15) begin
                                // On the last bit, latch the data and signal it's valid
                                data <= {data_reg[14:0], sdata};
                                data_valid <= 1'b1;
                                sclk_enable <= 1'b0; // Stop SCLK
                            end
                        end
                    end
                end
            end
        endcase
    end
end
endmodule
Enter fullscreen mode Exit fullscreen mode

2. Parallel Interface ADCs (Wide-Bus)
Used for high-speed, high-resolution applications. Examples: AD92xx, AD96xx series.

Key Signals:

  • D[0:N-1]: Parallel data bus.
  • DCO (Data Clock Output): A clock output by the ADC that is synchronous to the data. The FPGA uses this to capture the data.
  • FCO (Frame Clock Output): Signals the start of a data frame (often aligns with the first or last sample).

Acquisition Method (Double Data Rate - DDR):
The challenge is that the data rate is very high. The DCO is often DDR (Double Data Rate), meaning data is valid on both its rising and falling edges.

FPGA Primitives Used:

  • IDDR (Input Double Data Rate): A primitive inside the FPGA's I/O logic (IOB). It captures data on both clock edges and presents it as two parallel words on the FPGA's internal clock rate.
  • IDELAY: Used to finely adjust the timing of the input data to ensure it meets the setup/hold requirements of the DCO.

Simplified Flow:

  1. The DCO from the ADC is routed to a global clock buffer (BUFG/BUFIO) inside the FPGA.
  2. The parallel data lines (D[0:N-1]) are routed to IDDR primitives, clocked by the DCO.
  3. The FCO is similarly captured and used to frame the parallel data from the IDDRs.
  4. The two parallel words from the IDDR are combined into a single data stream at the internal clock rate.

Example Snippet (Conceptual - using Xilinx IDDR):

verilog

// This is a conceptual example. Actual implementation uses vendor-specific primitives.
wire dco;   // From ADC
wire [7:0] adc_din; // 8-bit data bus from ADC
reg [15:0] captured_data;

// For each bit in the bus, an IDDR is used.
IDDR #(
    .DDR_CLK_EDGE("OPPOSITE_EDGE"),
    .SRTYPE("SYNC")
) IDDR_inst [7:0] (
    .Q1(rise_data[7:0]), // Data on rising edge of DCO
    .Q2(fall_data[7:0]), // Data on falling edge of DCO
    .C(dco),             // DDR clock from ADC
    .CE(1'b1),
    .D(adc_din[7:0]),    // From ADC pin
    .R(1'b0),
    .S(1'b0)
);

// Now, combine the two edges into one word at half the DCO rate.
always @(posedge fpga_sys_clk) begin
    captured_data <= {rise_data, fall_data}; // Form a 16-bit word
    data_valid <= 1'b1; // This is valid every cycle of fpga_sys_clk
end
Enter fullscreen mode Exit fullscreen mode

3. Delta-Sigma (ΔΣ) ADCs (e.g., Audio CODECs)
These are often serial interface ADCs but with a specific continuous data stream.

Key Signals:

  • BCLK (Bit Clock): Continuous serial clock.
  • LRCLK (Left/Right Clock): Word select clock (High = Left channel, Low = Right channel).
  • DIN/DOUT: Serial data, typically transmitted MSB-first on one BCLK edge and captured on the opposite edge.

Acquisition Method (Shift Register):
The FPGA synchronizes to the LRCLK to know which channel is being received and uses the BCLK to shift in the data.

verilog

always @(posedge bclk) begin
    // Shift data in on each BCLK pulse
    audio_shift_reg <= {audio_shift_reg[22:0], sdata};

    // When LRCLK changes, the shift register is full
    if (lrclk != lrclk_prev) begin
        lrclk_prev <= lrclk;
        if (lrclk == 1'b1) 
            left_channel_data <= audio_shift_reg;
        else
            right_channel_data <= audio_shift_reg;
        // Reset the shift register for the next word
        audio_shift_reg <= 24'b0;
    end
end
Enter fullscreen mode Exit fullscreen mode

General Workflow for Any ADC Interface

  1. Read the Datasheet Meticulously: Understand the timing diagrams, setup/hold times (t_SU, t_HD), and protocol.
  2. Choose the Acquisition Method: Based on the interface (SPI, Parallel, LVDS, etc.).
  3. Design a State Machine (FSM): For SPI and other controlled protocols, an FSM is almost always the correct solution.
  4. Generate Precise Timings: Use counters clocked by the system clock to generate CS_n pulse widths, SCLK frequencies, and meet t_CONV requirements.
  5. Synchronize Asynchronous Signals: If an ADC signal (like a Data Ready interrupt) is not synchronous to the FPGA's clock, use a synchronizer chain (two flip-flops) to prevent metastability.
verilog

reg [1:0] sync_reg;
always @(posedge clk) sync_reg <= {sync_reg[0], adc_drdy};
wire adc_drdy_sync = sync_reg[1]; // Now synchronized
Enter fullscreen mode Exit fullscreen mode
  1. Simulate Thoroughly: Write a testbench that models the ADC's behavior. This is crucial for verifying your controller's state machine and timing.
  2. Constrain Your Design: In your FPGA toolchain (Vivado/Quartus), provide timing constraints for the ADC clocks to ensure the physical implementation meets timing.

Top comments (0)