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

Thread: Analog read on Teensy 4.0 slower (compared to Teensy 3.6)

  1. #1
    Junior Member
    Join Date
    Sep 2019
    Posts
    4

    Analog read on Teensy 4.0 slower (compared to Teensy 3.6)

    Hello,

    This is my first post here... please be patient with me.

    I'm working on a research project that makes use of the Teensy 3.6 to implement a dynamic clamp system, see Dynamic Clamp for details. In short, the dynamic implementation reads an adc value (from the experimental setup), performs a calculation, and outputs an adc value back to the experimental setup. One of these so-called cycles takes roughly 10 Ás to complete on the Teensy 3.6, which is regarded as real-time for our purpose.

    However, when switching the same code to the Teensy 4.0, the cycle time increases to around 20 Ás. I've identified the analogRead() as the rate-limiting step: it took 8 Ás on the Teensy 3.6 and it takes around 20 Ás on the Teensy 4.0 to return a value from the ADC. The relevant ADC code for the Teensy 4.0 is posted below, the full code for the Teensy 3.6 is available here.

    Code:
    // variables
    int adcs = 0;                   // raw value from analog in
    
    // constants
    const int adcPin = A0;           // analog in pin identifier
    
    /* setup, run once */
    void setup(){
    
        // configure Teensy pins
        analogReadResolution(12);   // (0-4095)
    
     }
    
    /* loop, run continuously */
     void loop() {
    
        // read membrane potential (slow, ~20.0Ás)
        adcs = float(analogRead(adcPin));
    
    }
    In short:
    Why is analogRead() returning slower (8 Ás vs. 20 Ás) on the Teensy 4.0 compared to the Teensy 3.6, given the same resolution?

    Best regards,
    Christian


    P.S.: I'm currently using Arduino 1.8.9 with the Teensyduino 1.47 on Windows Microsoft Windows [Version 10.0.17763.678].

  2. #2
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    9,735
    The analogRead() code of course is different to fit the 1062's MCU design. Also the MCU at 600 MHz has a different view of analog process on the higher speed silicon involved. There is possibly room for refinement - but generally suspect it is optimized for the steps needed to return the analog value.

    Could the version of the code used to determine the timing be provided?

  3. #3
    Senior Member
    Join Date
    Oct 2012
    Location
    Portland OR
    Posts
    676
    I'm not sure, but it may be the default on Teensy 4 to do 4 averages for each call to analogRead(). You can set this with analogReadAveraging(x) where I assume x=1 (single read, no averaging) would be faster than x=4.
    I think this is the core code for Teensy 4: https://github.com/PaulStoffregen/co...ensy4/analog.c

    The code says:
    Code:
    #define MAX_ADC_CLOCK 20000000
    // 8 bit conversion (17 clocks) plus 8 clocks for input settling
    // 10 bit conversion (17 clocks) plus 20 clocks for input settling
    // 12 bit conversion (25 clocks) plus 24 clocks for input settling
    which taken at face value, means the ADC clock can be as high as 20 MHz, and a 12 bit conversion at that speed should take 25+24=49 clocks or 2.4 microseconds. Lower bit resolution can go faster, with averaging it's slower, of course there is always a speed tradeoff with noise and accuracy.

  4. #4
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    9,735
    Quote Originally Posted by JBeale View Post
    I'm not sure, but it may be the default on Teensy 4 to do 4 averages for each call to analogRead(). You can set this with analogReadAveraging(x) where I assume x=1 (single read, no averaging) would be faster than x=4.
    I think this is the core code for Teensy 4: https://github.com/PaulStoffregen/co...ensy4/analog.c

    ...
    Good note - I was just looking at that file for some reason and skipped over that
    static uint8_t analog_num_average = 4;

    Though I see the same line in Teensy3 code :: https://github.com/PaulStoffregen/co...3/analog.c#L37

  5. #5
    Senior Member
    Join Date
    Oct 2012
    Location
    Portland OR
    Posts
    676

    T4 ADC speed tests

    Running this code on a T4, it reports 174825 samples per second as shown with 12 bit resolution and analogReadAveraging(1). That works out to 5.72 microseconds per sample. It is also calculating standard deviation and peak-to-peak noise but I don't think the math is a significant time factor. If you get 20 usec at 12 bit resolution on T4, that strongly suggests you have the 4 averages setting.
    Paul's code comments suggest much faster times are possible but I don't know if the ADC clock is at 20 MHz by default, or if there are other parameters to tweak like the ADC sampling time (which was adjustable independently of the ADC clock on the T3.2 device using the Pedvide ADC library, which has not yet been ported to T4).

    Changing below code to analogReadAveraging(4); I get 47755 samples/sec or 20.9 microseconds.
    Change to 10 bit resolution with no averaging, it's 203252 samples/sec or 4.92 microseconds.
    8 bit resolution, no averaging gives you 303582 samples/sec or 3.29 microseconds.

    If you want to get the best performance at high ADC sample rates, I think you will also need a very low impedance driving the ADC input. I think some opamps can do better at this than others.

    Code:
    // Analog input test for Teensy 3/4    Oct 4 2012 - Feb 2019 J.Beale
    
    // Setup: https://picasaweb.google.com/109928236040342205185/Electronics#5795546092126071650
    
    #define VREF (3.292)         // ADC reference voltage (= power supply)
    #define VINPUT (1.328)       // ADC input voltage from NiCd AA battery
    #define ADCMAX (4095)        // maximum possible reading from ADC
    #define EXPECTED (ADCMAX*(VINPUT/VREF))     // expected ADC reading
    #define SAMPLES (50000)      // how many samples to combine for pp, std.dev statistics
    
    const int analogInPin = A0;  // Analog input is AIN0 (Teensy3 pin 14, next to LED)
    const int LED1 = 13;         // output LED connected on Arduino digital pin 13
    
    int sensorValue = 0;        // value read from the ADC input
    long oldT;
    
    void setup() {    // ==============================================================
          pinMode(LED1,OUTPUT);       // enable digital output for turning on LED indicator
          // analogReference(EXTERNAL);  // set analog reference to internal ref
          analogReadRes(12);          // Teensy 3.0: set ADC resolution to this many bits
          analogReadAveraging(1);    // average this many readings
         
          Serial.begin(115200);       // baud rate is ignored with Teensy USB ACM i/o
          digitalWrite(LED1,HIGH);   delay(1000);   // LED on for 1 second
          digitalWrite(LED1,LOW);    delay(3000);   // wait in case serial monitor still opening
         
          Serial.println("# Teensy ADC test start: ");
    } // ==== end setup() ===========
    
    void loop() {  // ================================================================ 
         
          long datSum = 0;  // reset our accumulated sum of input values to zero
          int sMax = 0;
          int sMin = 65535;
          long n;            // count of how many readings so far
          double x,mean,delta,sumsq,m2,variance,stdev;  // to calculate standard deviation
         
          oldT = millis();   // record start time in milliseconds
    
          sumsq = 0; // initialize running squared sum of differences
          n = 0;     // have not made any ADC readings yet
          mean = 0; // start off with running mean at zero
          m2 = 0;
         
          for (int i=0;i<SAMPLES;i++) {
            x = analogRead(analogInPin);
            datSum += x;
            if (x > sMax) sMax = x;
            if (x < sMin) sMin = x;
                  // from http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
            n++;
            delta = x - mean;
            mean += delta/n;
            m2 += (delta * (x - mean));
          } 
          variance = m2/(n-1);  // (n-1):Sample Variance  (n): Population Variance
          stdev = sqrt(variance);  // Calculate standard deviation
    
          Serial.print("# Samples/sec: ");
          long durT = millis() - oldT;
          float datAvg = (1.0*datSum)/n;
          Serial.print((1000.0*n/durT),2);
    
          Serial.print(" Avg: ");     Serial.print(datAvg,2);
          Serial.print(" Offset: ");  Serial.print(datAvg - EXPECTED,2);
          Serial.print(" P-P noise: ");  Serial.print(sMax-sMin);
          Serial.print(" St.Dev: ");  Serial.print(stdev,3);
          Serial.print(" T(deg.C): "); Serial.print(tempmonGetTemp(),1);
          Serial.println();
    } // end main()  =====================================================
    Last edited by JBeale; 09-21-2019 at 05:29 AM.

  6. #6
    Junior Member
    Join Date
    Sep 2019
    Posts
    4
    Quote Originally Posted by defragster View Post
    Could the version of the code used to determine the timing be provided?
    Sure, this is the full code (just the code from the Teensy 3.6 with adjustments to the input/output pins). I didn't thoroughly benchmark the ADC conversion: Instead, I just monitored the cycly times by serial communication using pyClamp before and after commenting out the first line in the loop.
    Code:
    // imports
    
    #include <math.h>               // enable floating point unit functions
    
    
    // variables
    
    bool report = false;            // report values live
    int adcs = 0;                   // raw value from analog in [1]
    int count = 0;                  // cycle counter for benchmarking
    int dacs = 0;                   // raw value from analog out [1]
    float pamps = 0.0;              // current output [pA]
    float msecs = 0.0;              // time past since last cycle (dt) [ms]
    float mvolts = 0.0;             // membrane potential [mV]
    
    /* extend this array to add calibration parameters,
       use decrementing negative indices to address elements */
    float calibras[] =  {50.0,      // Amplifier input gain (AMP_i) [mV/mV]
                         400.0,     // Amplifier output gain (AMP_o) [pA/V]
                         5.5,       // ADC input slope (ADC_m) [mV/1]
                         -11500.0,  // ADC input intercept (ADC_n) [mV]
                         750.0,     // DAC output slope (DAC_m) [1/pA]
                         2000.0,    // DAC output intercept (DAC_n) [0-4095]
                         0.0};      // Voltage offset (VLT_d) [mV]
    
    /* extend this array to add conductance parameters,
       use incrementing postive indices to address elements */
    float conducts[] =  {0.0,       // G_Shunt [nS]
                         0.0,       // G_H [nS]
                         0.0,       // G_Na [nS]
                         0.0,       // OU1_m [nS]
                         0.0,       // OU1_D [nS^2/ms]
                         0.0,       // OU2_m [nS]
                         0.0,       // OU2_D [nS^2/ms]
                         0.0};      // G_EPSC [nS]
    
    /* extend this array to add values for live reports */
    float values[] =    {0.0,       // mvolts [mV]
                         0.0,       // pamps [pA]
                         0.0};      // msecs [Ás]
    
    elapsedMicros stepTime = 0;     // individual cycle time [Ás]
    elapsedMillis cmdTime = 0;      // time since last command check [ms]
    elapsedMillis ttlTime = 0;      // time since last TTL trigger [ms]
    
    /* serial command strings are identified by their specific format:
    
        <\r> idx <\t> val <\n>
        
        index:  int
                {-inf, ..., -1} = calibras[+inf-1, ..., +0]
                {0}             = execute a command
                {+1, ..., +inf} = conducts[ 0, ..., inf-1]
        value:  float
                the value written into the specified array and position or
                the command to be executed from a switch...case structure
        
        Examples:
    
        <\r> -2 <\t> 2000.0 <\n>    changes the second parameter (Amp_o) to 2000.0
        <\r>  0 <\t>    2.0 <\n>    toggles live reports on or off
        <\r>  1 <\t>   10.0 <\n>    changes the first conductance (G_Shunt) to 10.0 */
    
    /* custom struct for serial communication */
    typedef struct serialCmd {
        int len = 0;
        int idx = 0;
        int sep = 0;
        float val = 0.0;
        String str = "";
    } command;
    command cmd;                    // serial command string struct
    
    // constants
    const int adcPin = A0;           // analog in pin identifier
    const int cmdIntvl = 20;        // command interval [ms]
    const int dacPin = 0;         // analog out pin identifier
    const int ttlPin = 1;           // trigger pin identifier
    const int lenCals = sizeof(calibras)/sizeof(calibras[0]);
    const int lenCons = sizeof(conducts)/sizeof(conducts[0]);
    const int lenVals = sizeof(values)/sizeof(values[0]);
    
    
    // functions
    
    /* clear serial COM port buffers */
    void clearBuffers() {
        Serial.flush();                     // clear write buffer
        while (Serial.available() > 0)      // clear read buffer
            Serial.read();
    }
    
    /* interprets a command string */
    void interpretString(command cmd) {
        cmd.sep = cmd.str.indexOf('\t');
        if (cmd.str.startsWith('\r') && cmd.sep != -1) {  // check format
            cmd.len = cmd.str.length();
            cmd.idx = cmd.str.substring(1,cmd.sep).toInt();
            if (cmd.idx < 0) {          // negative index, update parameters
                cmd.idx = -1 * cmd.idx - 1;
                cmd.val = cmd.str.substring(cmd.sep + 1, cmd.len).toFloat();
                setParameter(cmd.idx, cmd.val);
            } else if (cmd.idx > 0) {   // positive index, update conductances
                cmd.idx = cmd.idx - 1;
                cmd.val = cmd.str.substring(cmd.sep + 1, cmd.len).toFloat();
                setConductance(cmd.idx, cmd.val);
            } else {                    // zero index, execute command instead
                cmd.val = cmd.str.substring(cmd.sep + 1, cmd.len).toInt();
                runCommand(cmd.val);
            }
        }
    }
    
    /* receives a string from the serial COM port */
    void readString() {
        cmd.str = Serial.readStringUntil('\n') + '\n';  // avoid timeout
    }
    
    /* runs predefined commands */
    void runCommand(int exec) {
        static bool troper = false;  // remember previous live report state
        troper = report;
        if (report)     // temporarily turn off live reports
            report = false;
        switch (exec) {
            case 0:     // echo test, nothing to do
                break;
            case 1:     // send calibration parameters and conductance values
                writeString(newArray(2));
                break;
            case 2:     // toggle live reports on or off
                report = invertBoolean(troper);
                break;
            default:    // every other (undefined) command
                break;
        }
        if (exec != 2)  // return to previous live report state
            report = troper;
    }
    
    
    /* updates conductance values */
    void setConductance(int idx, float val) {
        if (idx < lenCons)
            conducts[idx] = val;
    }
    
    /* updates parameter values */
    void setParameter(int idx, float val) {
        if (idx < lenCals)
            calibras[idx] = val;
    }
    
    /* sends a string to the serial COM port */
    void writeString(String str) {
        Serial.print(str);
    }
    
    /* returns an inverted boolean value */
    bool invertBoolean(bool boo) {
        if (boo) {
            boo = false;
        } else {
            boo = true;
        }
        return boo;
    }
    
    /* serial transmission strings are identified by their specific order:
        
        <\r> vals[0] <\t> vals[1] (<\t> vals[2] ... <\t> vals[inf] ) <\n>
        
        values: float[]
        
        Examples:
    
        <\r> 1.23 <\t> 1337 <\n>    reports two values, which are identified
                                    by their order in the transmission string */
    
    /* assembles calibration parameters and conductance values for transmission */
    String newArray(int len) {
        String valstr = "";  // initialize before concatenation
        valstr = "";
        for (int i = lenCals-1; i >= 0; i--) {
            int j = -1 * (i + 1);
            values[0] = j;
            values[1] = calibras[i];
            valstr += newString(values, len);
        }
        for (int k = 0; k < lenCons; k++) {
            int l = k + 1;
            values[0] = l;
            values[1] = conducts[k];
            valstr += newString(values, len);
        }
        return valstr;
    }
    
    /*creates a new command/report string from an array of values */
    String newString(float vals[], int len) {
        static const String cr = '\r';
        static const String tb = '\t';
        static const String lf = '\n';
        static String str;
        str = cr;
        for (int i = 0; i < len; i++ ) {
            if (i > 0)
                str += tb;
            str += vals[i];
        }
        str += lf;
        return str;
    }
    
    
    /* setup, run once */
    void setup(){
    
        // configure serial COM port
        Serial.begin(115200);       // [b/s]
        Serial.setTimeout(1);       // [ms]
        clearBuffers();
    
        // configure Teensy pins
        analogReadResolution(12);   // (0-4095)
        analogWriteResolution(12);  // (0-4095)
        pinMode(ttlPin, INPUT);     // set to high-impedance state
    
        // pre-calculate lookup tables
        GenerateGaussianNumbers();  // Gaussian number pool for use by the OU processes
        GenerateSodiumLUT();        // sodium activation/inactivation
        GenerateHcnLUT();           // HCN activation
    
     }
    
    
    /* loop, run continuously */
     void loop() {
    
        // read membrane potential (slow, ~8.0Ás)
        adcs = float(analogRead(adcPin));
        mvolts = calibras[2] / calibras[0] * adcs + calibras[3] / calibras[0] + calibras[6];
    
        // calculate cycle interval
        msecs = 0.001 * float(stepTime);
        stepTime = 0;
    
        // reset current
        pamps = 0.0;
    
        // add Shunting current (fast, < 0.5Ás)
        if (conducts[0] > 0) {
            pamps += Shunting(mvolts);
        }
    
        // add HCN current (fast, < 0.5Ás)
        pamps += HCN(mvolts, conducts[1]);  // reset current after initial run
        
        // add Sodium current (fast, < 0.5Ás)
        if (conducts[2] > 0) {
            pamps += Sodium(mvolts);
        }
        
        // add OrnsteinUhlenbeck current (medium, ~1.5Ás)
        if (conducts[3] > 0 || conducts[5] > 0) {
            pamps += OrnsteinUhlenbeck(mvolts);
        }
        
        // add EPSC current (fast, ~0.5Ás)
        if (conducts[7] > 0) {
            if ((digitalReadFast(ttlPin) == HIGH) && (ttlTime > 2)) {  // check for TTL signal
                UpdateEpscTrain();
                ttlTime = 0;
            }
            pamps += EPSC(mvolts);
        }
    
        // write calculated current (fast, ~0.5Ás)
        dacs = int(calibras[4] / calibras[1] * pamps + calibras[5]);    // with capacitance 1.0 pF
        dacs = constrain(dacs, 0, 4095);    // keep constrain function simple
        analogWrite(dacPin, dacs);
    
        // update commands & report values
        if (cmdTime > cmdIntvl) {
            if (Serial.available() > 4) {   // read buffer not empty
                readString();
                writeString(cmd.str);
                interpretString(cmd);
            }
            if (report && Serial.availableForWrite() > 63) {  // write buffer empty
                values[0] = mvolts;
                values[1] = pamps;
                values[2] = 1000.0 * msecs;
                writeString(newString(values, lenVals));
            }
            cmdTime = 0;
        }
    
    }

  7. #7
    Junior Member
    Join Date
    Sep 2019
    Posts
    4
    Thank you both @defragster and @JBeale for giving me some pointers to investigate!

    I'll have to do the testing with the Teensy 4.0 fully replacing the Teensy 3.6 in the dynamic clamp setup. -
    I wouldn't want to lose any resolution on the ADC, but a reduction of signal-to-noise ratio might be acceptable.

    Might be a week or so until I get to do this...

  8. #8
    Junior Member
    Join Date
    Sep 2019
    Posts
    4
    Quote Originally Posted by JBeale View Post
    Running this code on a T4, it reports 174825 samples per second as shown with 12 bit resolution and analogReadAveraging(1). That works out to 5.72 microseconds per sample. It is also calculating standard deviation and peak-to-peak noise but I don't think the math is a significant time factor. If you get 20 usec at 12 bit resolution on T4, that strongly suggests you have the 4 averages setting.
    Paul's code comments suggest much faster times are possible but I don't know if the ADC clock is at 20 MHz by default, or if there are other parameters to tweak like the ADC sampling time (which was adjustable independently of the ADC clock on the T3.2 device using the Pedvide ADC library, which has not yet been ported to T4).

    Changing below code to analogReadAveraging(4); I get 47755 samples/sec or 20.9 microseconds.
    Change to 10 bit resolution with no averaging, it's 203252 samples/sec or 4.92 microseconds.
    8 bit resolution, no averaging gives you 303582 samples/sec or 3.29 microseconds.

    If you want to get the best performance at high ADC sample rates, I think you will also need a very low impedance driving the ADC input. I think some opamps can do better at this than others.

    Code:
    // Analog input test for Teensy 3/4    Oct 4 2012 - Feb 2019 J.Beale
    
    // Setup: https://picasaweb.google.com/109928236040342205185/Electronics#5795546092126071650
    
    #define VREF (3.292)         // ADC reference voltage (= power supply)
    #define VINPUT (1.328)       // ADC input voltage from NiCd AA battery
    #define ADCMAX (4095)        // maximum possible reading from ADC
    #define EXPECTED (ADCMAX*(VINPUT/VREF))     // expected ADC reading
    #define SAMPLES (50000)      // how many samples to combine for pp, std.dev statistics
    
    const int analogInPin = A0;  // Analog input is AIN0 (Teensy3 pin 14, next to LED)
    const int LED1 = 13;         // output LED connected on Arduino digital pin 13
    
    int sensorValue = 0;        // value read from the ADC input
    long oldT;
    
    void setup() {    // ==============================================================
          pinMode(LED1,OUTPUT);       // enable digital output for turning on LED indicator
          // analogReference(EXTERNAL);  // set analog reference to internal ref
          analogReadRes(12);          // Teensy 3.0: set ADC resolution to this many bits
          analogReadAveraging(1);    // average this many readings
         
          Serial.begin(115200);       // baud rate is ignored with Teensy USB ACM i/o
          digitalWrite(LED1,HIGH);   delay(1000);   // LED on for 1 second
          digitalWrite(LED1,LOW);    delay(3000);   // wait in case serial monitor still opening
         
          Serial.println("# Teensy ADC test start: ");
    } // ==== end setup() ===========
    
    void loop() {  // ================================================================ 
         
          long datSum = 0;  // reset our accumulated sum of input values to zero
          int sMax = 0;
          int sMin = 65535;
          long n;            // count of how many readings so far
          double x,mean,delta,sumsq,m2,variance,stdev;  // to calculate standard deviation
         
          oldT = millis();   // record start time in milliseconds
    
          sumsq = 0; // initialize running squared sum of differences
          n = 0;     // have not made any ADC readings yet
          mean = 0; // start off with running mean at zero
          m2 = 0;
         
          for (int i=0;i<SAMPLES;i++) {
            x = analogRead(analogInPin);
            datSum += x;
            if (x > sMax) sMax = x;
            if (x < sMin) sMin = x;
                  // from http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
            n++;
            delta = x - mean;
            mean += delta/n;
            m2 += (delta * (x - mean));
          } 
          variance = m2/(n-1);  // (n-1):Sample Variance  (n): Population Variance
          stdev = sqrt(variance);  // Calculate standard deviation
    
          Serial.print("# Samples/sec: ");
          long durT = millis() - oldT;
          float datAvg = (1.0*datSum)/n;
          Serial.print((1000.0*n/durT),2);
    
          Serial.print(" Avg: ");     Serial.print(datAvg,2);
          Serial.print(" Offset: ");  Serial.print(datAvg - EXPECTED,2);
          Serial.print(" P-P noise: ");  Serial.print(sMax-sMin);
          Serial.print(" St.Dev: ");  Serial.print(stdev,3);
          Serial.print(" T(deg.C): "); Serial.print(tempmonGetTemp(),1);
          Serial.println();
    } // end main()  =====================================================
    Hi JBeale,

    I've run your code on the Teensy 4.0 with different settings for analogReadAveraging(n) at 12-bit resolution and got some interesting results:

    Click image for larger version. 

Name:	adc_t4.png 
Views:	15 
Size:	48.6 KB 
ID:	17760

    There's discrete time steps for n = 0-3; 4-7; 8-15; 16-31; and 32-x that match parameter overrides in the definition of analogReadAveraging.

    With Teensy 3.6 the steps are n = 0-4; 5-8; 9-16; and 32 and the parameters overrides are, again, defined in analogReadAveraging.
    To my surprise, your code returned average times of 1.0 ms [sic!] instead of Ás for different averaging settings (0-32) on the Teensy 3.6.
    A minor code change was necessary to make the code run though:

    Code:
    //Serial.print(" T(deg.C): "); Serial.print(tempmonGetTemp(),1);
    Can you reproduce that on a Teensy 3.6?

Posting Permissions

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