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 on the different interfaces available in ADC interfacing. On modern ADC, when the sampling rate is below the 10 Msample/sec the ADCs often implement a serial interface to provides sampled data to the user. The serial interface is 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 Rapsberry Pi.

In the serial interface, the serial clock provided by the device connected to the ADC is used also 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 the 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 clock out leading zeros while falling edges 5 through 16 clock 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 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 division of 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 appreciated this post, please help us to share it with your friend.

If you need to contact us, please write to: surf.vhdl@gmail.com

 

We appreciate any of your comment, please post below:

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

Leave a Reply

Your email address will not be published.