How to Connect a Serial ADC to an FPGA

Connection of Serial ADC to FPGA

The ADC (Analog to Digital Converter) can be interfaced to FPGA/ASIC in a different way depending on the output interface. This post gives an overview of the different interfaces available in ADC interfacing. On modern ADC, when the sampling rate is below 10 Msample/sec the ADCs often implement a serial interface to provide sampled data to the user. The serial interface is a little bit complex w.r.t. a parallel interface but the use of serial protocol reduces the number of wires and allows interfacing the ADC to a microprocessor like Arduino or Raspberry Pi.

In the serial interface, the serial clock provided by the device connected to the ADC is also used as ADC sampling clock.

Figure1 - Serial ADC connected to FPGA
Figure1 – Serial ADC connected to FPGA

In this post, we will see an example of how to interface the TI ADC128S022 used in the Altera DE0-nano Board

The ADC128S022 device is a low-power, eight-channel CMOS 12-bit analog-to-digital converter specified for conversion throughput rates of 50 ksps to 200 ksps. The converter is based on a successive approximation register architecture with an internal track-and-hold circuit. It can be configured to accept up to eight input signals at inputs IN0 through IN7.

The output serial data is straight binary and is compatible with several standards, such as SPI, QSPI, MICROWIRE, and many common DSP serial interfaces.

ADC Serial Protocol

The ADC serial protocol is a simple SPI protocol.

Figure2 - ADC128S022 Serial Protocol
Figure2 – ADC128S022 Serial Protocol

During the first 3 cycles of SCLK, the ADC is in track mode, acquiring the input voltage. For the next 13 SCLK cycles the conversion is accomplished and the data is clocked out. The SCLK falling edges 1 through 4 clocks out leading zeros while falling edges 5 through 16 clocks out the conversion result, MSB first. If there is more than one conversion in a frame (continuous conversion mode as in the figure), the ADC will re-enter the track mode on the falling edge of SCLK after the N*16th rising edge of SCLK and re-enter the hold/convert mode on the N*16+4th falling edge of SCLK. “N” is an integer value. For further information on SPI ADC128S022 protocol here you can find the datasheet.


ADC128S022 controller VHDL implementation

In order to convert and read from the ADC the sampled data the protocol of Figure2 shall be implemented. On DE0-nano, the board clock is 50 MHz.

The ADC128S022 can work from 0.8 MHz to 3.2 MHz. Using the board clock we can generate the maximum sampling clock as 50/16 = 3.125 MHz.

In this example, the VHDL code of the serial ADC is implemented using 5 main blocks divided into 5 VHDL processes.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity adc_serial_control is
generic(
  CLK_DIV               : integer := 100 );  -- input clock divider to generate output serial clock; o_sclk frequency = i_clk/(CLK_DIV)
port (
  i_clk                       : in  std_logic;
  i_rstb                      : in  std_logic;
  i_conv_ena                  : in  std_logic;  -- enable ADC convesion
  i_adc_ch                    : in  std_logic_vector(2 downto 0);  -- ADC channel 0-7
  o_adc_data_valid            : out std_logic;  -- conversion valid pulse
  o_adc_ch                    : out std_logic_vector(2 downto 0);  -- ADC converted channel
  o_adc_data                  : out std_logic_vector(11 downto 0); -- adc parallel data  
-- ADC serial interface
  o_sclk                      : out std_logic;
  o_ss                        : out std_logic;
  o_mosi                      : out std_logic;
  i_miso                      : in  std_logic);
end adc_serial_control;

architecture rtl of adc_serial_control is
constant C_N                     : integer := 16;

signal r_counter_clock            : integer range 0 to CLK_DIV;
signal r_sclk_rise                : std_logic;
signal r_sclk_fall                : std_logic;
signal r_counter_clock_ena        : std_logic;

signal r_counter_data             : integer range 0 to C_N-1;
signal r_tc_counter_data          : std_logic;

signal r_conversion_running       : std_logic;  -- enable serial data protocol 

signal r_miso                     : std_logic;
signal r_conv_ena                 : std_logic;  -- enable ADC convesion
signal r_adc_ch                   : std_logic_vector(2 downto 0);  -- ADC converted channel
signal r_adc_data                 : std_logic_vector(11 downto 0); -- adc parallel data  

begin

--------------------------------------------------------------------
-- FSM
p_conversion_control : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_conv_ena             <= '0';
    r_conversion_running   <= '0';
    r_counter_clock_ena    <= '0';
  elsif(rising_edge(i_clk)) then
    r_conv_ena             <= i_conv_ena;
    if(r_conv_ena='1') then
      r_conversion_running   <= '1';
    elsif(r_conv_ena='0') and (r_tc_counter_data='1') then -- terminate current conversion
      r_conversion_running   <= '0';
    end if;
    
    r_counter_clock_ena    <= r_conversion_running;  -- enable clock divider

  end if;
end process p_conversion_control;

p_counter_data : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_counter_data       <= 0;
    r_tc_counter_data    <= '0';
  elsif(rising_edge(i_clk)) then
    if(r_counter_clock_ena='1') then
      if(r_sclk_rise='1') then  -- count data @ o_sclk rising edge
        if(r_counter_data<C_N-1) then
          r_counter_data     <= r_counter_data + 1;
          r_tc_counter_data  <= '0';
        else
          r_counter_data     <= 0;
          r_tc_counter_data  <= '1';
        end if;
      else
        r_tc_counter_data  <= '0';
      end if;
    else
      r_counter_data     <= 0;
      r_tc_counter_data  <= '0';
    end if;
  end if;
end process p_counter_data;

-- Serial Input Process
p_serial_input : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_miso               <= '0';
    r_adc_ch             <= (others=>'0');
    r_adc_data           <= (others=>'0');
  elsif(rising_edge(i_clk)) then
    r_miso               <= i_miso;
    
    if(r_tc_counter_data='1') then
      r_adc_ch             <= i_adc_ch; -- strobe new
    end if;

    case r_counter_data is
      when  4  => r_adc_data(11)  <= r_miso;
      when  5  => r_adc_data(10)  <= r_miso;
      when  6  => r_adc_data( 9)  <= r_miso;
      when  7  => r_adc_data( 8)  <= r_miso;
      when  8  => r_adc_data( 7)  <= r_miso;
      when  9  => r_adc_data( 6)  <= r_miso;
      when 10  => r_adc_data( 5)  <= r_miso;
      when 11  => r_adc_data( 4)  <= r_miso;
      when 12  => r_adc_data( 3)  <= r_miso;
      when 13  => r_adc_data( 2)  <= r_miso;
      when 14  => r_adc_data( 1)  <= r_miso;
      when 15  => r_adc_data( 0)  <= r_miso;
      when others => NULL;
    end case;
  end if;
end process p_serial_input;

-- SERIAL Output process
p_serial_output : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    o_ss                 <= '1';
    o_mosi               <= '1';
    o_sclk               <= '1';
    o_adc_data_valid     <= '0';
    o_adc_ch             <= (others=>'0');
    o_adc_data           <= (others=>'0');
  elsif(rising_edge(i_clk)) then
    o_ss                 <= not r_conversion_running;

    if(r_tc_counter_data='1') then
      o_adc_ch             <= r_adc_ch; -- update current conversion
      o_adc_data           <= r_adc_data;
    end if;
    o_adc_data_valid     <= r_tc_counter_data;

    if(r_counter_clock_ena='1') then  -- sclk = '1' by default 
      if(r_sclk_rise='1') then
        o_sclk   <= '1';
      elsif(r_sclk_fall='1') then
        o_sclk   <= '0';
      end if;
    else
      o_sclk   <= '1';
    end if;
  
    if(r_sclk_fall='1') then
      case r_counter_data is
        when  2  => o_mosi <= r_adc_ch(2);
        when  3  => o_mosi <= r_adc_ch(1);
        when  4  => o_mosi <= r_adc_ch(0);
        when others => NULL;
      end case;
    end if;
  end if;
end process p_serial_output;

-- CLOCK divider
p_counter_clock : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_counter_clock            <= 0;
    r_sclk_rise                <= '0';
    r_sclk_fall                <= '0';
  elsif(rising_edge(i_clk)) then
    if(r_counter_clock_ena='1') then 
      if(r_counter_clock=(CLK_DIV/2)-1) then  -- firse edge = fall
        r_counter_clock            <= r_counter_clock + 1;
        r_sclk_rise                <= '0';
        r_sclk_fall                <= '1';
      elsif(r_counter_clock=(CLK_DIV-1)) then
        r_counter_clock            <= 0;
        r_sclk_rise                <= '1';
        r_sclk_fall                <= '0';
      else
        r_counter_clock            <= r_counter_clock + 1;
        r_sclk_rise                <= '0';
        r_sclk_fall                <= '0';
      end if;
    else
      r_counter_clock            <= 0;
      r_sclk_rise                <= '0';
      r_sclk_fall                <= '0';
    end if;
  end if;
end process p_counter_clock;

end rtl;

In the control logic process “p_conversion_control” the clock generation is enabled using an input control signal “i_conv_ena“. When the conversion is disabled, the control logic will complete the current conversion before stopping the process.

The process “p_counter_data” counts the current clock rising edge and controls the data conversion. The counter is used as a sequencer of the current conversion state.

In the process “p_serial_input” the input ADC channel is sampled before starting the conversion and provided on the serial output data line to the ADC. The “r_counter_data” internal counter will demux the input serial data into a parallel register. At the end of the current, the received “r_adc_data” will be stored in the output parallel 12-bit register “o_adc_data

The process “p_serial_output” will drive the serial clock output and serial data output to the ADC.

In the serial clock generator process ”p_counter_clock” the serial clock is generated by the division of the board clock. The clock generation is disabled if no conversion is required.


The video shows how to implement the serial ADC VHDL code controller on the DE0-nano Altera board.


Serial ADC controller VHDL code Simulation

In the simulation windows, the board clock is 50 MHz (20 ns). The Clock division factor is 16 so the generated serial clock will be 3.125 MHz (i.e.320 ns)

Figure3 - Serial ADC controller Simulation
Figure3 – Serial ADC controller Simulation

Serial ADC controller VHDL code implementation on FPGA

The DE0-nano provides 8 LEDs. In order to verify the ADC controller VHDL code, the 8 LEDs of the board are connected to the 8 ADC MSB, so we can use the led to “read” the ADC converted values.

The ADC channel is selected using the switch SW 3..1 that will select the serial ADC input channel 0 to 7. The Serial ADC conversion is controlled using SW4 that can enable/disable the ADC conversion.

Figure4 - DE0 nano Serial ADC implementation architecture
Figure4 – DE0 nano Serial ADC implementation architecture

If you appreciate this post, please help us to share it with your friend.

To contact us, please write to: surf.vhdl@gmail.com

We appreciate any of your comments, please post below:

38 thoughts to “How to Connect a Serial ADC to an FPGA”

  1. Hi
    I want to implement a spi for interface between ADC and FPGA. I read your code. but i have problem. would you please help me?
    o_ss must active after one O_sclk that ADC start to sent new data. but this code just enable o_ss one time.
    how can i active o_ss after one o_sclk?

      1. Hi! I’m kinda start my first fpga project, could you help me how to update the pinout file please? thankyou

  2. Hi
    Can you please explain what is the difference between ADC and ADC interface in figure 1.
    How can I interface AD7476 with fpga? Can you please explain the process.

    1. in this post, I deal with serial ADC.
      I mean, the interfacing between the ADC and FPGA is a serial interface. Figure 1 reports the typical serial interface

  3. Hi Surf VHDL,
    Thanks for posting this. I recently started working with an ADC (ADS1675 from TI, 24 bit Resolution and 4 MSPS) and configured it with DE10 Nano. This ADC produces serial output at up to 96MHz clock. I need to capture this output with the FPGA on DE10 nano and transfer it then to Computer. It will be very much appreciated if you bit advice on doing it. What would be the best way of doing it?

    Thanks in Advance

  4. Hello,
    First of all, thank you so much for this work.
    I would like if you could send me the test bench?

    and thank u

  5. Thank you so much or this sharing. It helps a lot in order for me to understand more about spi. Apart from that, can I have the .xdc file or .ucf file for this project? You can email me at m.ariffrosman@gmail.com. Thank you so much

  6. Hello !
    In first I want to thanks you a lot for your work and your help.

    I analyse your code and simulate it, but there is just a little problem, the first conversion doesn’t take in account the channel specified.

    The problem is that you output the channel in the end of conversion, so the first one is negliged….

    Do you have any idea to change simply the code without have more signals ?

    Anyway, thanks again for everything ! 🙂

  7. I apologize, I just don’t take time for reading correclty the datasheet of ADC to see that the channel data on DIN is for the next frame of conversion.

  8. Hello !
    I am looking for some code that uses the ADC from the DE0-Nano board and that reads data from the 8 analog input channels by multiplexing. The langage could be Verilog or VHDL or even C code.
    Any idea ?
    Thanks in advance…
    Mathieu Winger

Leave a Reply

Your email address will not be published. Required fields are marked *