Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 13 of 13

Thread: Teensy 3.5 - Instrumentation Amp for Single-Ended Analog Recording

  1. #1
    Junior Member
    Join Date
    Apr 2019
    Posts
    8

    Teensy 3.5 - Instrumentation Amp for Single-Ended Analog Recording

    Hello Everybody,

    I'm trying to assemble a two-stage instrumentation amplifier for differential analog signals to be recorded single-endedly on the teensy 3.5
    My colleague Avner Wallach has created this circuit design for this purpose (the resistors and capacitors for filtering are just a suggestion):
    Click image for larger version. 

Name:	two stage instrumental amp wallach.jpg 
Views:	32 
Size:	80.4 KB 
ID:	22649

    These are the core components:
    1. AD8225: first stage differential amplification
    2. OP2177: second stage OpAmp
    3. OP1177: level shifter (raises GND to midpoint between VSS and VPP)
    4. Passive RC highpass filter on input stage
    5. Sallen-Key lowpass filter on output stage


    For recording the signals on the Teensy 3.5, I use a sketch that uses DMA to save incoming analog data to a buffer which is then flushed regularly to the SD card:
    Code:
    //*************************************************************************
    // Two channel EOD logger programm for testing purposes
    // Barebone: no RTC
    //*************************************************************************
    
    #include <ADC.h>              //library for easier ADC implementation
    #include <DMAChannel.h>       //library for easier DMA implementation
    #include <array>              //use C++ array structs
    #include <SdFat.h>            // work with SD card
    
    
    /*----------------------------------------------------------------*/
    const uint32_t pdbfreq = 100000;  // sampling speed [Hz] max ~300MHz - actually the trigger frequency of the programmable delay block
    uint32_t duration = 10;           // duration of each measure-cycle [s]
    String Name = "Log";              // file name prefix
    unsigned long debug_start;
    /*----------------------------------------------------------------*/
    
    /*DECLARATIONS adc and pins---------------------------------------*/
    ADC adc;            // used to declare the adc.### object
    
    /*PINS------------------------------------------------------------*/
    const uint8_t adc_pin0 = A2;                    // A2 is connected to ADC0
    const uint8_t adc_pin1 = A22;                   // A22 is connected to ADC1
    /*----------------------------------------------------------------*/
    
    /*Buffer declarations----------------------------------------------*/
    std::array<volatile uint16_t, (uint32_t)64 * 512> buffer __attribute__ ((aligned (16 * 1024)));      // size of buffer is limited due to the Teensy's program memory
    std::array<volatile uint16_t, (uint32_t)64 * 512> buffer1 __attribute__ ((aligned (16 * 1024)));
    
    uint32_t      BUF_DIM       = 32768/2;                                          //size of buffer that holds data from ADC
    uint32_t      FILE_SIZE     = 0;                                                //initial variables for filesize and pointer to...
    uint32_t      last          = 0;     
    uint32_t      last1         = 0;     
    uint32_t      bytes         = 0;
    float         preceil       = 0;
    float         scale         = 0;
    /*----------------------------------------------------------------*/
    
    /*DECLARATIONS dma and tcd----------------------------------------*/
    DMAChannel dma;                                 // used to declare the dma.### object for the first channel
    DMAChannel dma1;                                // used to declare the dma.### object for the second channel
    
    typeof(*dma.TCD)  tcd_mem[4] __attribute__ ((aligned (32))) ;   // alignment of four 32-byte blocks; needed for four different TCDs
    typeof(*dma1.TCD)  tcd1_mem[4] __attribute__ ((aligned (32))) ;
    /*----------------------------------------------------------------*/
    
    
    /*DECLARATIONS microSD card files---------------------------------*/
    SdFatSdioEX sd;                                  // used to declare the sd.### object (Sdfat);
    
    uint16_t fileNr = 0;                             // after a given duration a new file is created; fileNr is an index used for the filename
    uint16_t fileNr1 = 0;
    
    File file;                                       // file object for logging data
    File file1;                                      // file object for logging data
    /*----------------------------------------------------------------*/
    
    
    // function creates new files for data logging
    /*----------------------------------------------------------------*/
    void filestuff() {
      fileNr++;
      String filename = Name + "_dma0_" + fileNr + ".bin";
      char fname[30];
      filename.toCharArray(fname, 30);
      Serial.print("filename: ");
      Serial.println(filename);
    
      if (!file.open(fname, O_RDWR | O_CREAT)) {
        sd.errorHalt("open dma0 file failed");
      }
    }
    
    void filestuff1() {
      fileNr1++;
      String filename1 = Name + "_dma1_" + fileNr1 + ".bin";
      char fname1[30];
      filename1.toCharArray(fname1, 30);
      Serial.print("filename: ");
      Serial.println(filename1);
      if (!file1.open(fname1, O_RDWR | O_CREAT)) {
        sd.errorHalt("open dma1 file failed");
      }
    }
    
    void setup() 
    {
      /*Serial monitor--------------------------------------------------*/
      debug_start = millis();
      Serial.begin(115200);
      while ((millis() - debug_start) <= 5000);
      //while (!Serial && ((millis() - debug_start) <= 5000));
      Serial.println("Begin Setup\n");
      /*----------------------------------------------------------------*/
    
      /*FileSetup-------------------------------------------------------*/
      String filename = Name + "_dma0_" + fileNr + ".bin";                        // create filenames
      char fname[30];
      filename.toCharArray(fname, 30);
    
      String filename1 = Name + "_dma1_" + fileNr + ".bin";
      char fname1[30];
      filename1.toCharArray(fname1, 30);
    
      Serial.println(filename);
      Serial.println(filename1);
           
      if (!  sd.begin()) {                                                        // start sdio interface to SD card
        sd.initErrorHalt("SdFatSdio   begin() failed");
      }    sd.chvol();
      if (!file.open(fname, O_RDWR | O_CREAT)) {                                  // create SD card files
        sd.errorHalt("open dma0 failed");
      }
      if (!file1.open(fname1, O_RDWR | O_CREAT)) {
        sd.errorHalt("open dma1 failed");
      }
    
      delay(100);
      /*----------------------------------------------------------------*/
    
      /*DurationSetup---------------------------------------------------*/
      bytes = ((duration*1000000)/(1000000/pdbfreq))* 2; 
      preceil = bytes/BUF_DIM;
      scale = ceil(preceil);                                                    // round up preceil value
      FILE_SIZE = (scale+2) * BUF_DIM;                                          // after writing FILE_SIZE uint16_t values to a file a new file is created
      /*----------------------------------------------------------------*/
    
      /*Mode Setup------------------------------------------------------*/
      pinMode(13, OUTPUT);                                                     // built-in LED is at PIN 13 in Teensy 3.5
      pinMode(adc_pin0, INPUT);                                                // configure as analog input pins
      pinMode(adc_pin1, INPUT);
      /*----------------------------------------------------------------*/
    
      /*ADC Setup-------------------------------------------------------*/
      adc.startSingleRead(adc_pin0, ADC_0);                                    // start ADC conversion
      adc.startSingleRead(adc_pin1, ADC_1);
    
      adc.setAveraging       (                              1  );              // ADC configuration
      adc.setResolution      (                           16, 0  );
      adc.setConversionSpeed ( ADC_CONVERSION_SPEED::HIGH_SPEED);
      adc.setSamplingSpeed   ( ADC_SAMPLING_SPEED::HIGH_SPEED  );
    
      adc.setAveraging (1, ADC_1);
      adc.setResolution (16, ADC_1);
      adc.setConversionSpeed ( ADC_CONVERSION_SPEED::HIGH_SPEED, ADC_1);
      adc.setSamplingSpeed   ( ADC_SAMPLING_SPEED::HIGH_SPEED, ADC_1  );
    
      adc.setReference(ADC_REFERENCE::REF_3V3, ADC_0);                         // set analog reference
      adc.setReference(ADC_REFERENCE::REF_3V3, ADC_1);
      /*----------------------------------------------------------------*/
    
      /* DMA ----------------------------------------------------------*/
      dma.source                 (           ADC0_RA);                         // source is the ADC result register
      dma.transferSize           (                 2);                         // set 2, one uint16_t value are two bytes
      dma.triggerAtHardwareEvent (DMAMUX_SOURCE_ADC0);                         // DMAMUX alignes source to DMA channel
    
      dma1.source                 (           ADC1_RA);
      dma1.transferSize           (                 2);
      dma1.triggerAtHardwareEvent (DMAMUX_SOURCE_ADC1);
      /*----------------------------------------------------------------*/
      
      /*TCD-------------------------------------------------------------*/
    
      // configure TCD for first dma
      dma.TCD->CITER    =           16 * 512;
      dma.TCD->BITER    =           16 * 512;
      dma.TCD->DOFF     =                  2;                                  // set 2, one uint16_t value are two bytes
      dma.TCD->CSR      =               0x10;
    
      dma.TCD->DADDR        = (volatile void*) &buffer [ 0 * 512]  ;
      dma.TCD->DLASTSGA     = (   int32_t    ) &tcd_mem[       1]  ;           // points to a 32-byte block that is loaded into the TCD memory of the DMA after major loop completion
      memcpy ( &tcd_mem[0], dma.TCD , 32 ) ;                                   // 32-byte block is transferred to &tcd_mem[0]
    
      dma.TCD->DADDR        = (volatile void*) &buffer [16 * 512]  ;
      dma.TCD->DLASTSGA     = (   int32_t    ) &tcd_mem[       2]  ;
      memcpy ( &tcd_mem[1], dma.TCD , 32 ) ;
    
      dma.TCD->DADDR        = (volatile void*) &buffer [32 * 512]  ;
      dma.TCD->DLASTSGA     = (   int32_t    ) &tcd_mem[       3]  ;
      memcpy ( &tcd_mem[2], dma.TCD , 32 ) ;
    
      dma.TCD->DADDR        = (volatile void*) &buffer [48 * 512]  ;
      dma.TCD->DLASTSGA     = (   int32_t    ) &tcd_mem[       0]  ;
      memcpy ( &tcd_mem[3], dma.TCD , 32 )  ;
    
      memcpy ( dma.TCD ,  &tcd_mem[0], 32 ) ;                                  // 16-byte block that is transferred into the TCD memory of the DMA
    
      // equal configuration for TCD of  second dma1
    
      dma1.TCD->CITER    =           16 * 512;
      dma1.TCD->BITER    =           16 * 512;
      dma1.TCD->DOFF     =                  2;
      dma1.TCD->CSR      =               0x10;
    
      dma1.TCD->DADDR        = (volatile void*) &buffer1 [ 0 * 512]    ;
      dma1.TCD->DLASTSGA     = (   int32_t    ) &tcd1_mem[       1]    ;
      memcpy ( &tcd1_mem[0], dma1.TCD , 32 ) ;
    
      dma1.TCD->DADDR        = (volatile void*) &buffer1 [16 * 512]    ;
      dma1.TCD->DLASTSGA     = (   int32_t    ) &tcd1_mem[       2]    ;
      memcpy ( &tcd1_mem[1], dma1.TCD , 32 ) ;
    
      dma1.TCD->DADDR        = (volatile void*) &buffer1 [32 * 512]    ;
      dma1.TCD->DLASTSGA     = (   int32_t    ) &tcd1_mem[       3]    ;
      memcpy ( &tcd1_mem[2], dma1.TCD , 32 ) ;
    
      dma1.TCD->DADDR        = (volatile void*) &buffer1 [48 * 512]    ;
      dma1.TCD->DLASTSGA     = (   int32_t    ) &tcd1_mem[       0]    ;
      memcpy ( &tcd1_mem[3], dma1.TCD , 32 )  ;
    
      memcpy ( dma1.TCD ,  &tcd1_mem[0], 32 ) ;
      /*----------------------------------------------------------------*/
    
      /*Start DMA and ADC-----------------------------------------------*/
      dma.enable();                                                             // enable DMA
      dma1.enable();
    
      adc.enableDMA(ADC_0);                                                     // connect DMA and ADC
      adc.enableDMA(ADC_1);
    
      adc.adc0->stopPDB();                                                      // start PDB conversion trigger
      adc.adc0->startPDB(pdbfreq);
    
      adc.adc1->stopPDB();
      adc.adc1->startPDB(pdbfreq);
    
      NVIC_DISABLE_IRQ(IRQ_PDB);                                               // we don't want or need the PDB interrupt
    
      adc.adc0->printError();                                                  // print ADC configuration errors
      adc.adc1->printError();
      /*----------------------------------------------------------------*/
    
    
      /*Debug-----------------------------------------------------------*/
      Serial.println(BUF_DIM);
      Serial.println(FILE_SIZE);
      Serial.print("bytes: ");
      Serial.println(bytes);
      Serial.println((uint32_t)&buffer[ 0], HEX);                               // debug: print memory location of buffer
      Serial.println((uint32_t)&buffer[ 16 * 512], HEX);
      Serial.println((uint32_t)&buffer[ 32 * 512], HEX);
      Serial.println((uint32_t)&buffer[ 48 * 512], HEX);
      Serial.println((uint32_t)&buffer1[ 0], HEX);
      Serial.println((uint32_t)&buffer1[ 16 * 512], HEX);
      Serial.println((uint32_t)&buffer1[ 32 * 512], HEX);
      Serial.println((uint32_t)&buffer1[ 48 * 512], HEX);
      Serial.println("----------------------------------");
      /*----------------------------------------------------------------*/
    
      /*Signal end of Setup method--------------------------------------*/
      for (int i = 0; i < 5; i++){                                             // visual feedback, blink 5 times if the setup was completed
        digitalWrite(13, HIGH);
        delay(300);
        digitalWrite(13, LOW);
        delay(300);
      }
    }
    
    
    void loop() {  
      while ( ((64*1024-1) & ( (int)dma.TCD->DADDR - last )) > BUF_DIM ){  
        if (BUF_DIM != (uint32_t)file.write( (char*)&buffer[((last/2)&(32*1024-1))], BUF_DIM) ){ 
          sd.errorHalt("write dma0 failed");    
          }
        last += BUF_DIM ;  
        
        if (BUF_DIM != (uint32_t)file1.write( (char*)&buffer1[((last1/2)&(32*1024-1))], BUF_DIM) ){ 
          sd.errorHalt("write dma1 failed");
          }
        last1 += BUF_DIM ;
      } 
      /*----------------------------------------------------------------*/
      if ( last >= FILE_SIZE ) {                                              // check if end of file is reached
        file.close();
        last = 0;                                                             // reset last
        filestuff();                                                          // create new files for data logging
      }
      if ( last1 >= FILE_SIZE ) {                                              // check if end of file is reached
        file1.close();
        last1 = 0;                                                             // reset last
        filestuff1();                                                          // create new files for data logging
        
        // blink LED to signal end of recording
        digitalWrite(13, HIGH);
        delay(5000);
        digitalWrite(13, LOW);
      }
      /*----------------------------------------------------------------*/
    }
    While the amp circuit performs well when I read its output with an oscilloscope, I haven't been successful in recording the signal on the teensy.
    We tried the following configuration:
    1. Power supply: 4.5V (VPP = 4.5V, VSS = 0V, GND = 2.25V)
    2. Input signal: 1kHz sine wave, ~ 100mV peak-to-peak amplitude
    3. Amplifier settings: 5x gain, 300Hz highpass and 20kHz lowpass filter on ADC0; 10x gain, 300Hz highpass and 50kHz lowpass filter on ADC1
    4. Teensy was powered by VPP (on VIN) and VSS (on AGND)
    5. Recording settings: 100 kS/s, 3.3V internal reference


    The results are completely shifted sine waves that are clipping/jumping from the minimum to the maximum:
    ADC0:
    Click image for larger version. 

Name:	dma0.png 
Views:	17 
Size:	45.6 KB 
ID:	22652
    ADC1:
    Click image for larger version. 

Name:	dma1.png 
Views:	13 
Size:	50.0 KB 
ID:	22653

    I guess, I must have made a mistake in wiring or shifting the input level but at the moment, I can't find where I'm going wrong here. Can anybody help me out?
    One thing that I noticed is that I should maybe connect the level-shifted GND (2.25V) to the Teensy, to set the analog zero? Only, if I e.g. connect it to the Teensy GND, I get weird results:
    ADC0:
    Click image for larger version. 

Name:	dma0.png 
Views:	13 
Size:	50.9 KB 
ID:	22654
    ADC1:
    Click image for larger version. 

Name:	dma1.png 
Views:	12 
Size:	52.5 KB 
ID:	22655

    If I feed one input channel to the differential input for ADC0 (A11) and use the 2.25V GND as second input (A10) and use a teensy sketch for differential recordings, I measure an okay sine-wave with occasional clippings in the negative phase:
    Click image for larger version. 

Name:	diff_clipping.png 
Views:	14 
Size:	49.4 KB 
ID:	22656
    Obviously, this defeats the purpose of having a differential amplifier at all... Does anybody have a suggestion?

  2. #2
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    840
    Your first graph has a scale that goes negative. But in single ended mode, the output of the ADC is unsigned. Mix up signed and unsigned and you get strange jumps at 32767.

  3. #3
    When I look for a data sheet on the AD8225 IA, what I find looks nothing like your schematic. The AD8225 is a single device in an 8 pin package with fixed gain and no Rg.

    What are you really using?

  4. #4
    Junior Member
    Join Date
    Apr 2019
    Posts
    8
    Quote Originally Posted by jonr View Post
    Your first graph has a scale that goes negative. But in single ended mode, the output of the ADC is unsigned. Mix up signed and unsigned and you get strange jumps at 32767.
    AH!! Damn, you're right, I used a python script that I originally wrote for the differential output *facepalm*
    Changed datatype to unsigned integer and get this beautiful sine-wave
    ADC0:
    Click image for larger version. 

Name:	uint_dma0.png 
Views:	13 
Size:	47.9 KB 
ID:	22659

    ADC1:
    Click image for larger version. 

Name:	uint_dma1.png 
Views:	12 
Size:	46.7 KB 
ID:	22660

    Seems like either my input is not a true 1kHz wave or my sampling frequency is off - I'll look into that

    Quote Originally Posted by UhClem View Post
    When I look for a data sheet on the AD8225 IA, what I find looks nothing like your schematic. The AD8225 is a single device in an 8 pin package with fixed gain and no Rg.

    What are you really using?
    Sorry, typo, it's the AD8224. We're currently looking for a different first stage though because the minimum operating voltage of the AD8224 is about 4.2V and we'd like to make the whole circuit battery-compatible. So if anybody knows a comparable 3.3V alternative, I'm open for suggestions (but will look around myself as well).

    Thank you!!

    *edit: can't edit my initial post to correct the model number to AD8224 - hope everybody who's interested sees this here

  5. #5
    Senior Member
    Join Date
    Apr 2014
    Location
    Cheltenham, UK
    Posts
    128
    One of these might solve your battery problem. Battery powered by 18650 cell, outputs 5v and 3.3v. Use the 5v and power the teensy as well, or 3.3v to teensy and 5v to ADC.
    Click image for larger version. 

Name:	Battery charger and holder.JPG 
Views:	14 
Size:	42.8 KB 
ID:	22664

  6. #6
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    840
    You could also add an inverting charge pump - the AD8224 will run on 3.3V and -3.3V.

  7. #7
    Senior Member
    Join Date
    Jul 2020
    Posts
    714
    A couple of things -

    adding a resistor of a few k between the opamp output and Teensy pin will protect the teensy pin should it
    not be powered up when the opamp is.

    The virtual ground circuit places a heavy capacitive load on the OP1177R, probably causing it to oscillate. Opamps
    aren't designed to drive capacitive loads.

    Virtual grounds don't need heavy decoupling like this, just have decoupling between Vpp and Vss (where it matters
    for an opamp), and add a capacitor to the resistor divider to short out all the Johnson noise going into the OP1177R
    so its not putting that onto the virtual ground. I think you have Vss and digital ground at the same potential,
    so add the capacitor between Vss and the resistor divider node.

    If you feed virtual ground (via a resistor) to another analog pin on the teensy you can compensate for the
    virtual ground voltage when taking measurements.

    Oh, there's no need for a precision opamp to derive virtual ground, but good bandwidth and output current
    are useful to keep the virtual ground impedance nice and low across the frequencies of interest.

  8. #8
    Junior Member
    Join Date
    Apr 2019
    Posts
    8
    Quote Originally Posted by BriComp View Post
    One of these might solve your battery problem. Battery powered by 18650 cell, outputs 5v and 3.3v. Use the 5v and power the teensy as well, or 3.3v to teensy and 5v to ADC.
    Click image for larger version. 

Name:	Battery charger and holder.JPG 
Views:	14 
Size:	42.8 KB 
ID:	22664
    Quote Originally Posted by jonr View Post
    You could also add an inverting charge pump - the AD8224 will run on 3.3V and -3.3V.
    Thank you for those suggestions! I believe that a lower voltage first stage might be the easier solution but in case this proves difficult, these are great fallbacks!
    About the inverting charge pump - would that affect the way we shift the signal for single ended recording (i.e. could we work with a virtual GND of 1.65V and power the AD8224 with +-3.3V)?

    Quote Originally Posted by MarkT View Post
    A couple of things -

    adding a resistor of a few k between the opamp output and Teensy pin will protect the teensy pin should it
    not be powered up when the opamp is.

    The virtual ground circuit places a heavy capacitive load on the OP1177R, probably causing it to oscillate. Opamps
    aren't designed to drive capacitive loads.

    Virtual grounds don't need heavy decoupling like this, just have decoupling between Vpp and Vss (where it matters
    for an opamp), and add a capacitor to the resistor divider to short out all the Johnson noise going into the OP1177R
    so its not putting that onto the virtual ground. I think you have Vss and digital ground at the same potential,
    so add the capacitor between Vss and the resistor divider node.

    If you feed virtual ground (via a resistor) to another analog pin on the teensy you can compensate for the
    virtual ground voltage when taking measurements.

    Oh, there's no need for a precision opamp to derive virtual ground, but good bandwidth and output current
    are useful to keep the virtual ground impedance nice and low across the frequencies of interest.
    That's a good tip for protecting the teensy - especially if we'd employ separate power sources which might lead to the teensy being unpowered while the amp circuit is running.

    Do I read you correctly in that you're suggesting to change the virt. GND circuit from this (original):
    Click image for larger version. 

Name:	virtual ground circuit.png 
Views:	10 
Size:	8.6 KB 
ID:	22673

    To this (?):
    Click image for larger version. 

Name:	virtual ground circuit_decoupled.png 
Views:	10 
Size:	14.3 KB 
ID:	22675
    Last edited by StefanM; 11-28-2020 at 01:06 PM.

  9. #9
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    840
    > a virtual GND of 1.65V and power the AD8224 with +-3.3V)?

    You would want the AD8224 to operate without a virtual ground. And the 2nd stage needs power too. Probably not worth the changes, I'd go with one of your other options.

  10. #10
    Senior Member
    Join Date
    Jul 2020
    Posts
    714
    Quote Originally Posted by StefanM View Post
    Do I read you correctly in that you're suggesting to change the virt. GND circuit from this (original):
    Click image for larger version. 

Name:	virtual ground circuit.png 
Views:	10 
Size:	8.6 KB 
ID:	22673

    To this (?):
    Click image for larger version. 

Name:	virtual ground circuit_decoupled.png 
Views:	10 
Size:	14.3 KB 
ID:	22675
    No. That would pull the opamp input to the rail. Capacitor is _across_ the lower resistor to short out noise voltage.

  11. #11
    Junior Member
    Join Date
    Apr 2019
    Posts
    8
    aye, true, the capacitor would render the voltage divider useless the way I drew it. Rather like this then (sorry, I'm taking small steps here):
    Click image for larger version. 

Name:	virtual ground circuit_decoupled.png 
Views:	9 
Size:	10.0 KB 
ID:	22694

    I tried reading up more about suggested capacity for such a capacitor but I guess that varies a lot with the application and voltage invovled. With a voltage of 4.5V between VSS and VPP, would you say that 1uF is sufficient?

  12. #12
    Senior Member
    Join Date
    May 2015
    Location
    USA
    Posts
    840
    Here is some very general advice. Learn the basics of LTSpice and use it, even for simple circuits. Never perfect, but it's a great way to learn things like "what would happen if I injected a little noise here".

  13. #13
    Senior Member
    Join Date
    Jul 2014
    Posts
    3,066
    Quote Originally Posted by jonr View Post
    Learn the basics of LTSpice and use it, even for simple circuits.
    Especially fir simple circuits, and try to get realistic models for your components (i.e. not idealistic OPamps)

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •