Teensy 4.0 is amazing: 500,000 interrupts per second

Status
Not open for further replies.

tjrob

Member
Background:
I built an ADC box for a scientific instrument that has to read four channels of an ADS1256 ADC and write their values to the USB Serial device, 1,000 times per second. My initial code uses polling in the usual setup()/loop(), and works 99.999% of the time. But if you plug in a USB thumb drive while it is running, it gets a data overrun, because the USB bus is busy elsewhere for several milliseconds. There have been other unexplained data overruns. Interestingly, the first implementation used a Raspberry Pi 3B+, but its WiFi put too much noise into the ADC, and its serial and Bluetooth don't have enough bandwidth; I found the Teensy 4.0 and have never looked back -- the only disadvantage is I need a 50-foot USB cable in the lab.

So I have to use interrupts, with a large buffer between reading the ADC at interrupt level and writing the Serial device at base level. I have considerable experience with this sort of software, and decided to first measure how fast the Teensy 4.0 could handle interrupts, as I expected to require 5,000 interrupts per second. I wrote TestIntervalTimer to measure how fast the interrupts of IntervalTimer could go. The interrupt routine simply puts micros() into a circular buffer and checks for data overrun; the base level checks the intervals between them and prints lines at 1 kHz. I was amazed that it can go at 500,000 interrupts per second; it ran overnight without error. At that rate the interrupt routine has good margin, but the base level can barely keep up. The code uses std::atomic rather than turning interrupts off to avoid race conditions.

The large available memory means that in the real application, the buffer[] used in this test will be large enough for 20 seconds of USB or host delay, much longer than required.

One surprise is that four calls to Serial.print() plus Serial.println() runs faster than the corresponding snprintf() and Serial.println().

Code:
// TestIntervalTimer - simple test of IntervalTimer.
/* 
 *  Interrupt level is as simple as possible, just putting micros() into 
 *  buffer[] and checking for data overrun.
 *  
 *  Base level pulls entries from buffer[], checks the interval between 
 *  entries, and prints to USB at 1 kHz.
 *  
 *  Rather than disabling interrupts, uses std::atomic to avoid race conditions.
 *  On Teensy 4.0 it runs reliably at 500,000 interrupts per second (Interval=2).
 *  
 *  Occasionally the base is delayed up to ~ 80 milliseconds; interrupt is not.
 */

#include <atomic>

const int Interval=2;      // microseconds between interrupts
const int BUFSIZE=100000;  // 4 bytes per entry

// circular buffer
uint32_t buffer[BUFSIZE];
volatile std::atomic<int32_t> put(0);
volatile std::atomic<int32_t> get(0);

IntervalTimer myTimer;

void setup() {
  Serial.begin(115200);
  delay(1000); // Serial cannot start immediately

  if(!put.is_lock_free()) {
    Serial.println("std::atomic<int32_t> is not lock free!");
    return;
  }
  
  myTimer.priority(1); // very high priority interrupt
  myTimer.begin(timerFunction, Interval);
}

// timerFunction - put micros() into buffer[] and check for overrun
void timerFunction() {
  register uint32_t now = micros();
  register int p = put.load();
  buffer[p] = now;
  if(++p >= BUFSIZE) p = 0;
  if(p == get.load()) {
    Serial.println("*** DATA OVERRUN ***");
    myTimer.end();
  }
  put.store(p);
}

void loop() {
  static uint32_t prev=0;
  static int maxData=0, nProcessed=0;
  if(nProcessed == 0) prev = micros() - Interval;
  
  int p = put.load();
  int g = get.load();

  // handle one data entry
  if(g != p) {
    // calculate # data entries
    int k = p - g;
    if(k < 0) k = BUFSIZE-g + p;
    if(maxData < k) maxData = k;

    uint32_t v = buffer[g];
    ++nProcessed;

    // print first 1000, all errors, and at 1 kHz
    if(nProcessed < 1000 || v-prev != Interval || nProcessed%(1000/Interval) == 0) {
#ifdef SNPRINTF  // keeps up only for Interval>=4
      char line[128];
      snprintf(line,sizeof(line),"%d %d %lu",nProcessed,maxData,v-prev);
      Serial.println(line);
#else            // barely keeps up for Interval=2
      Serial.print(nProcessed);
      Serial.print(" ");
      Serial.print(maxData);
      Serial.print(" ");
      Serial.println(v-prev);
#endif // SNPRINTF
    }

    prev = v;
    if(++g >= BUFSIZE) g = 0;
    get.store(g);
  }
  
  // short timeout to let interrupt get ahead
  //if(nProcessed == 100000) delay(20);
  
  // long timeout to force data overrun
  //if(nProcessed == 500000) delay(500);
}
 
What clock speed was that at? If at 600MHz that's upto 2400 instruction issues (assuming dual issue fully utilized),
That no longer appears particularly surprizing given there's no OS overhead to deal with.

BTW you are calling a Serial function in the ISR, which may lock up I believe.
 
This is the default Teensy 4.0 setup using teensyduino. So I guess it is 600 MHz.

The Serial function is called in the ISR only upon data overrun, when it is stopping. No big deal if it locks up. :)
 
Status
Not open for further replies.
Back
Top