Teensy 4.1 - Encoder - elapsedMicros issue

RoboGio

Member
Hi all,


I’m testing an encoder‐reading sketch on a Teensy 4.1 and running into a couple of puzzling behaviors. Any help would be greatly appreciated!


What I’m seeing:

  1. Step-count “jumps”
    • The encoder reading itself (using the Encoder library) is accurate—every physical detent advances or retreats is counted.
    • However, in my serial output I sometimes see the printed step count increase (or decrease) by more than one between consecutive prints.
  2. Spurious delta-time readings
    • I’m timing each step event with elapsedMicros() to compute the interval between encoder events.
    • Most intervals look reasonable, but every so often elapsedMicros() reports a very small value (e.g. 3 µs), which doesn’t match how fast I’m actually turning the knob.

Hardware & setup:
  • Teensy 4.1
  • GHS38 solid-shaft incremental rotary encoder (200 PPR; library reports 800 counts/rev)
  • TXS0108E 8-channel logic-level converter
  • Serial baud rate: 115200
Questions:
  1. Why does the printed step count sometimes jump by more than one?
  2. What can cause those unrealistically small elapsedMicros() intervals, appearing in my timing data?

Thanks in advance for any tips.

Serial Print snapshot:

time elapsed, Steps, smallest time elapsed:
153,-1302,22
152,-1301,22
150,-1300,22
155,-1299,22
1590,-1290,22
3,-1289,3
160,-1288,3
1883,-1278,3
3,-1277,3
175,-1276,3
1771,-1266,3
144,-1265,3
1383,-1257,3
53,-1256,3

Here attaching code:

C++:
#include <Encoder.h>

// Encoder on pins 2 & 3
Encoder myEnc(2, 3);

// Stopwatch for intervals
elapsedMicros timeSinceLastChange;

// State
long oldPosition        = -999;
unsigned long fastestDt = 0xFFFFFFFFUL;  // max unsigned long

void setup() {
  Serial.begin(115200);
  // CSV header: dt_us,position,fastest_dt_us
  Serial.println(F("dt_us,position,fastest_dt_us"));
}

void loop() {
  long newPosition = myEnc.read();
  if (newPosition != oldPosition) {
    // 1) snapshot and reset interval timer
    unsigned long dt = (unsigned long)timeSinceLastChange;
    timeSinceLastChange = 0;

    // 2) update fastest (minimum) dt
    if (dt < fastestDt) {
      fastestDt = dt;
    }

    // 3) update state
    oldPosition = newPosition;

    // 4) print: current interval, encoder count, and best-so-far
    Serial.print(dt);
    Serial.print(',');
    Serial.print(newPosition);
    Serial.print(',');
    Serial.println(fastestDt);
  }
}
 
quick guess might be taking too long to print and report the data.
By the time the IF() exits the next reading is ready so if falls behind showing missed readings?

Perhaps the prints catch up then showing the record low short interval.

Make an array of [100][3] for the reading values to print then when 100 readings are indexed - stop and print them all and reset the index and repeat.

Or - first easy step - minimize the printing and remove the last two prints ',' and fastestDt. That will speed the flow
 
Here are the results after testing with the buffer:


  1. Using the buffer initially eliminates the timing and step‐miss issues.
  2. However, once the buffer fills, prints its contents, and restarts, the next buffer cycle exhibits the same problem.

Question:
How can I ensure correct printing?


Here new code with buffer:

C++:
#include <Encoder.h>

Encoder myEnc(2, 3);

elapsedMicros timeSinceLastChange;

long oldPosition        = -999;
unsigned long fastestDt = 0xFFFFFFFFUL;  // max unsigned long

const int BUFFER_SIZE = 1000;   // Number of entries in the buffer
unsigned long dtBuffer[BUFFER_SIZE];
int bufIndex = 0;              // Next write position in the buffer

void setup() {
  Serial.begin(115200);
  Serial.println(F("dt_us,position,fastest_dt_us"));
}

void loop() {
  long newPosition = myEnc.read();
  if (newPosition != oldPosition) {
    unsigned long dt = (unsigned long)timeSinceLastChange;
    timeSinceLastChange = 0;

    if (dt < fastestDt) {
      fastestDt = dt;
    }

    oldPosition = newPosition;
    dtBuffer[bufIndex++] = fastestDt;

    if (bufIndex >= BUFFER_SIZE) {
      for (int i = 0; i < BUFFER_SIZE; i++) {
        Serial.println(dtBuffer[i]);  // print each dt_us
      }
      bufIndex = 0;  // clear buffer for new data
      Serial.println(oldPosition);
    }
  }
}
 
Several transitions on a timescale of microseconds is contact bounce surely?

While you are printing the buffer the encoder state will be changing under your feet as its interrupt driven, so you'll miss a bunch of chages probably...
 
What solution can be implemented to capture the elapsed time and direction of each individual step, and save that data to an SD card?
 
Glad the buffering showed the nature of the problem.
Given that it is expected that during the printing as noted in p#4 that the encoder continues piling up changes that are unread.

Not clear why the printing is needed? The more printing the more misses of the values. Also what the end result of the encoder processing is?S

Perhaps only print when misses or problems occur?
Log the data to a large buffer - but again when the buffer fills there will be time away from loop() and the encoder data will not be processed normally.

Logging to SD will add more overhead and loss of time in loop() as it will take some normal time and on regualr occasion take much longer times for the write to complete.

If the needed encoder attention could be done on an interrupt rather than loop() polling then ideally it would be more responsive and not miss events.
 
Just seeing: Serial baud rate: 115200

No sure how that factors in as it seems a level shifter brings the encoder to the Teensy?
 
1)Not clear why the printing is needed? This was done to check any errors and check the feasibility of logging data of each individual encoder pulse and micros to SD card.
2)Perhaps only print when misses or problems occur? True, was more to check the feasibility of saving to SD card each individual encoder pulse and with respective snapshot of time.
3)Also what the end result of the encoder processing is? We need to capture each pulse’s direction and timestamp so we can compute acceleration, velocity, and position simultaneously data that will let us train a humanoid robot to move with smooth, human-like motion.

Would it be feasible to modify the Encoder.h library so that, when the interrupt fires, it captures both the current encoder count and a micros() timestamp?
 
Would it be feasible to modify the Encoder.h library so that, when the interrupt fires, it captures both the current encoder count and a micros() timestamp?

The encoder count is updated in the ISRs of the two encoder inputs (A and B), so yes, you could modify the library to associate an ElapsedMicros timestamp with each change in count, but I don't think that's a practical way to meet your objectives. Your example data shows ~22 us between changes, so your interrupt frequency is about 50 kHz. That's not terribly high for the T4, but did you know the T4 has timers that can count quadrature encoder pulses in hardware, so it can do all of that with no CPU usage? Consider using the QuadEncoder library instead of Encoder, and reading the running count from an IntervalTimer at whatever frequency you think is necessary. I'd suggest starting with a 1-kHz IntervalTimer and experimenting with computing updates at that frequency.
 
Would this code with quadencoder.h library do the 1kHz sampling? and then add a circular buffer to write to SD.

C++:
#include "QuadEncoder.h"

const uint8_t ENCODER_MODULE = 1;      // Use hardware ENC module 1 (options 1-4)
const uint8_t PIN_A = 2;               // Encoder Phase A connected to Teensy pin 2
const uint8_t PIN_B = 3;               // Encoder Phase B connected to Teensy pin 3
const uint8_t USE_PULLUPS = 0;         // Set to 1 if your encoder needs pullups, 0 otherwise

const uint32_t SAMPLE_RATE_HZ = 1000;
const uint32_t SAMPLE_PERIOD_US = 1000000 / SAMPLE_RATE_HZ;

const int BUFFER_SIZE = 10000;         

QuadEncoder myEncoder(ENCODER_MODULE, PIN_A, PIN_B, USE_PULLUPS, 255, 255, 255); // Use 255 for unused optional pins
IntervalTimer samplingTimer;

volatile int32_t encoderPositionBuffer[BUFFER_SIZE];
volatile int bufferWriteIndex = 0;

volatile bool printBufferFlag = false;

void sampleEncoderISR() {
  // Only write if the buffer isn't full
  if (bufferWriteIndex < BUFFER_SIZE) {
    encoderPositionBuffer[bufferWriteIndex] = myEncoder.read();
    bufferWriteIndex++; // Move to the next buffer slot
    if (bufferWriteIndex >= BUFFER_SIZE) {
        printBufferFlag = true; // Signal the main loop to print
    }
  }
}

void setup() {
  myEncoder.init();
  myEncoder.write(0);
  samplingTimer.begin(sampleEncoderISR, SAMPLE_PERIOD_US);
}

void loop() {
if (printBufferFlag) {
    Serial.println("--- Buffer Full - Printing Data ---");

    for (int i = 0; i < BUFFER_SIZE; ++i) {
       int32_t position = encoderPositionBuffer[i];
       Serial.print(i);
       Serial.print(": ");
       Serial.println(position);
    }
    printBufferFlag = false;
    Serial.println("--- Buffer Printing Done ---");

}
 
During printing no Position data will be buffered.

After printing the bufferWriteIndex is not reset, so no more data will be collected.

Setting up a second buffer might be enough to allow continued logging during prints, or a large enough circular buffer of some type.
 
The encoder stuff looks good, though I haven't tried to built your sketch. Rather than write your own ring buffer, which is tricky to get right, I always recommend using the RingBuf that is built into the SdFat library. It is interrupt-safe, so you can be sure that whatever you write from the interrupt level will be read correctly from loop(). It really simplifies buffering for logging to SD files because you associate the buffer with a file, but you can also use it as generic ring buffer, with data in and data out, and printing to Serial as you are now. The SdFat example TeensySDIOLogger shows how to use RingBuf with a file, and also how to use file.isBusy() to avoid blocking when writing to SD files.

The code below is a working sketch originally by @mborgerson and modified by me to use RingBuf and isBusy(). It reads/logs data from ADC at 1 MSPS, but you can take out the ADC stuff and replace it with your encoder and IntervalTimer.

Code:
/********************************************************************
  1.0 MegaSample T4.1 ADC using ADC timer to trigger the
  ADC collection.

  This version saves histogram data for timing interval
  and ADC values
  
  MJB  10/19/20  Modified to log 12-bit samples and save as uint16_t
  JWP  02/11/25  Use SdFat RingBuf and (file).isBusy() function
********************************************************************/
#include "SdFat.h"
#include <TimeLib.h>
#include <ADC.h>
#include <RingBuf.h>

// instantiate a new ADC object
ADC *adc = new ADC(); // adc object;

// SdFS file system accepts both FAT and ExFAT cards
SdFs sd;
FsFile logFile;

#define RINGBUF_SIZE (128*1024)  // JWP - 100KB buffer for 2MBps data rate
RingBuf<FsFile,RINGBUF_SIZE> rb; // JWP - ring buffer

#define SD_CONFIG SdioConfig(FIFO_SDIO)
const char compileTime [] = "MegaSample logger Compiled on " __DATE__ " " __TIME__;
//const int admarkpin = 1;
const int wrmarkpin = 0;
const int ledpin = 13;

// WRMARKHI and WRMARKLO are used to observe SDC Write timing on oscilloscope
#define  WRMARKHI digitalWriteFast(wrmarkpin, HIGH);
#define  WRMARKLO digitalWriteFast(wrmarkpin, LOW);

#define  LEDON digitalWriteFast(ledpin, HIGH); // Also marks IRQ handler timing
#define  LEDOFF digitalWriteFast(ledpin, LOW);

// buffers for histogram data
#define TMHISTOMAX 1000  // max interval  10000 cycles  at 600MHz  = 6 microseconds
#define ADCMAX 4096  // for 12-bit samples
uint32_t tm_histobuffer[TMHISTOMAX];  // for timing intervals up to 40.96mSec
uint32_t adc_histobuffer[ADCMAX];

// Saving 12-bit data as uint16_t
#define SAMPRATE 1000000

bool verboseflag = false;  // true to show some output during sampling
bool writeflag = true;   // true to write, false for no writes to SDC

volatile uint32_t totalsamples = 0;

const uint adcpin = A9;  // my 2.5V precision reference

void setup() {
  // put your setup code here, to run once:
 
  Serial.begin(9600);
  delay(500);
  Serial.println(compileTime);
  // activate ARM cycle counter
  ARM_DEMCR |= ARM_DEMCR_TRCENA; // Assure Cycle Counter active
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;

  pinMode(wrmarkpin, OUTPUT);
  pinMode(ledpin, OUTPUT);
  pinMode(adcpin, INPUT_DISABLE);
  adc->adc0->setAveraging(1 ); // set number of averages
  adc->adc0->setResolution(12); // set bits of resolution
  adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // change the conversion speed
  adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // change the sampling speed

  if (!StartSDCard()) {
    // do fast blink forever

    do { // hang with blinking LED
      LEDON
      delay(100);
      LEDOFF
      delay(100);
    } while (1);

  }// end of  if (!StartSDCard())

  rb.begin(&logFile); // JWP - associate logFile with RingBuf 

  setSyncProvider(getTeensy3Time); // helps put time into file directory data
}



void loop() {
  // put your main code here, to run repeatedly:
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch == 'l')  LogADC();
    if (ch == 'a')  ShowADCHisto();
    if (ch == 't')  ShowTmHisto();
    if (ch == 'v')  verboseflag = !verboseflag;
    if (ch == 'w')  writeflag = !writeflag;
    if (ch == 'd')  sd.ls(LS_SIZE | LS_DATE | LS_R);
  }
}

void ShowSetup(void){
  Serial.printf("Collection rate is %lu samples/second.\n",SAMPRATE);
  Serial.print("Serial output during collection is ");
  if(verboseflag) Serial.println("ON"); else Serial.println("OFF");
  Serial.print("SDC Writes are ");
  if(writeflag) Serial.println("ON"); else Serial.println("OFF");
}

// define the number of samples to collect as 10 M Samples
#define MAXSAMPLES  10*1024l*1024l
/*****************************************************
   This is the ADC timer interrupt handler
 ******************************************************/

volatile uint32_t lastcycles;
volatile uint16_t overflows;

// This ISR runs in about 120nSec on T4.1 at 600MHz.
// It buffers the data, but SDC writes are user-selected
// timing and adc histogram values are saved in RAM
void adc0_isr()  {
  uint32_t tmdiff, thiscycles;
  uint16_t adc_val;
  LEDON;
  if (totalsamples < MAXSAMPLES) { // sample until enough collected
    thiscycles =  ARM_DWT_CYCCNT;
    tmdiff = thiscycles - lastcycles;

    lastcycles = thiscycles;
    totalsamples++;

    // Collect ADC value and update the ADC Histogram data
    adc_val = adc->adc0->readSingle();
    // Save ADC data in buffer
    rb.memcpyIn( &adc_val, sizeof(adc_val) );

    // make sure we don't write outside histogram buffers
    if (adc_val >= ADCMAX) adc_val = ADCMAX;
    if (tmdiff >= TMHISTOMAX) tmdiff = TMHISTOMAX - 1;
    // Skip the first two samples, as they often have
    // weird timing values. Update histogram data
    if (totalsamples > 2) {
      tm_histobuffer[tmdiff]++;
      adc_histobuffer[adc_val]++;
    }

#if defined(__IMXRT1062__)  // Teensy 4.0
    asm("DSB");
#endif
  }  // end of if(totalsamples < maxsamples
  LEDOFF;
}

/******************************************************
   Read MAXSAMPLES from ADC at 1 microsecond intervals
   Store the results in adcbuffer;
 *****************************************************/

void LogADC(void) {
  uint16_t lcount;
  Serial.println("Reading ADC Samples");
  totalsamples = 0;
  overflows = 0;

  memset(adc_histobuffer, 0, sizeof(adc_histobuffer));  // clear adc histogram counts
  memset(tm_histobuffer, 0, sizeof(tm_histobuffer));  // clear  timing histogram counts
  ShowSetup();
  if (!OpenLogFile()) {
    Serial.print("Did not open log file.");
  }

  adc->adc0->stopTimer();
  adc->adc0->startSingleRead(adcpin); // call this to setup everything before the Timer starts, differential is also possible
  delay(1);
  adc->adc0->readSingle();

  // now start the ADC collection timer
  adc->adc0->startTimer(SAMPRATE); //frequency in Hz
  lastcycles =  ARM_DWT_CYCCNT;
  adc->adc0->enableInterrupts(adc0_isr);

  lcount = 0;
  uint32_t nWrites = 0;
  uint32_t rbMaxUsed = 0;
  uint32_t fileMaxWriteCycles = 0;
  do {
    // track max number of bytes in RingBuf
    uint32_t n = rb.bytesUsed();
    if (n > rbMaxUsed)
      rbMaxUsed = n;

    if (rb.bytesUsed() >= 512 && !logFile.isBusy()) { // when data in buffer, write to SD card
      WRMARKHI
      if (logFile) {
        uint32_t start = ARM_DWT_CYCCNT;
        rb.writeOut( 512 );
        uint32_t cycles = ARM_DWT_CYCCNT - start;
        // track longest write time, skipping first which always takes 2x longer than others
        if (nWrites!=0 && cycles > fileMaxWriteCycles)
          fileMaxWriteCycles = cycles;
        nWrites++;
      }
      if(verboseflag && (nWrites%256)==0) {
        Serial.print("."); // mark each 128KB written
        if (lcount++ > 19) {
          Serial.println();
          lcount = 0;
        }
      }
      WRMARKLO
    }
  }  while (totalsamples < MAXSAMPLES);
  adc->adc0->stopTimer();
  if (logFile) {
    rb.sync(); // JWP - write remaining data from RingBuf to file
    logFile.truncate();  //truncate to amount actually written
    logFile.close();
  }
  Serial.printf("\nADC Read %lu samples with %u overflows\n",totalsamples,overflows);
  Serial.printf("RingBuf max usage %u bytes of %u\n", rbMaxUsed, RINGBUF_SIZE );
  Serial.printf("File max write time %u us\n", uint32_t(fileMaxWriteCycles*(1E6/F_CPU_ACTUAL)));
}


bool OpenLogFile(void) {
  uint64_t alloclength;
  if(!writeflag) return false;  // don't open file if not writing
  if (!logFile.open("Log1MS.dat",  O_RDWR | O_CREAT | O_TRUNC)) {
    return false;
  }
  alloclength = (uint64_t)200 * (uint64_t)(1024L * 1024l); //200MB

  if (!logFile.preAllocate(alloclength)) {
    Serial.println("Pre-Allocation failed.");
    return false;
  } else {
    Serial.println("Pre-Allocation succeeded.");
  }
  return true;

}


void ShowTmHisto(void) {
  uint32_t i;
  Serial.println("Timing Histogram Data in ARM Cycle Counts");
  for (i = 0; i < TMHISTOMAX; i++) {
    if (tm_histobuffer[i] > 0) {
      Serial.printf("%5lu %5lu\n", i, tm_histobuffer[i]);
    }
  }
  Serial.println();

}

void ShowADCHisto(void) {
  uint32_t i;
  Serial.println("ADC Histogram Data in  Counts");
  for (i = 0; i < ADCMAX; i++) {
    if (adc_histobuffer[i] > 0) {
      Serial.printf("%5lu %5lu\n", i, adc_histobuffer[i]);
    }
  }
  Serial.println();
}


bool StartSDCard() {
  if (!sd.cardBegin(SD_CONFIG)) {
    Serial.println("cardBegin failed");
  }
  if (!sd.volumeBegin()) {
    Serial.println("volumeBegin failed");
  }
  if (!sd.begin(SdioConfig(FIFO_SDIO))) {
    Serial.println("\nSD File initialization failed.\n");
    return false;
  } else  Serial.println("initialization done.");

  if (sd.fatType() == FAT_TYPE_EXFAT) {
    Serial.println("Type is exFAT");
  } else {
    Serial.printf("Type is FAT%d\n", int16_t(sd.fatType()));
  }
  // set date time callback function
  SdFile::dateTimeCallback(dateTime);
  return true;
}
/*****************************************************************************
   Read the Teensy RTC and return a time_t (Unix Seconds) value

 ******************************************************************************/
time_t getTeensy3Time() {
  return Teensy3Clock.get();
}

//------------------------------------------------------------------------------
/*
   User provided date time callback function.
   See SdFile::dateTimeCallback() for usage.
*/
void dateTime(uint16_t* date, uint16_t* time) {
  // use the year(), month() day() etc. functions from timelib

  // return date using FAT_DATE macro to format fields
  *date = FAT_DATE(year(), month(), day());

  // return time using FAT_TIME macro to format fields
  *time = FAT_TIME(hour(), minute(), second());
}
 
Combining the two best options mentioned—QuadEncoder.h and the built‑in RingBuf from SdFat—I put together the code below (with a bit of help from GPT). I’d appreciate your expertise on whether it’s correctly implemented.
/*********************************************************************
Teensy 4.1 – QuadEncoder logger using ARM_DWT_CYCCNT time‑stamps
• Hardware ENC1 counts edges on pins 1(A) & 0(B)
• CHANGE interrupt on Phase‑A fires only when shaft moves
• ISR saves {cycle‑count, encoder position} to a 64 kB RingBuf
• Foreground empties RingBuf to SD‑card with isBusy() back‑pressure
• Press any key to stop or after MAX_RECORDS samples
*********************************************************************/
#include "SdFat.h"
#include <RingBuf.h>
#include <TimeLib.h>
#include "QuadEncoder.h"

// ─────────────── user settings ───────────────────────────────────────────────
constexpr uint8_t ENC_INDEX = 1; // ENC1 → pins 1 (A) & 0 (B)
constexpr uint8_t ENC_PIN_A = 1; // GPIO for attachInterrupt
constexpr size_t RBUF_SIZE = 64 * 1024; // 64 kB
constexpr uint32_t MAX_RECORDS = 2UL * 1024 * 1024; // 2 M edges
constexpr uint32_t PREALLOC_MB = 64; // 64 MB file
// ─────────────────────────────────────────────────────────────────────────────

// ---------- encoder, SD, FIFO ------------------------------------------------
QuadEncoder quadEnc(ENC_INDEX);
SdFs sd;
FsFile logFile;
RingBuf<FsFile, RBUF_SIZE> rb;
#define SD_CONFIG SdioConfig(FIFO_SDIO)

// ---------- sample structure -------------------------------------------------
struct Sample { uint32_t cycles; int32_t pos; } __attribute__((packed));
static_assert(sizeof(Sample) == 8, "Sample must be 8 bytes");

// ---------- ISR bookkeeping --------------------------------------------------
volatile uint32_t nSamples = 0;
volatile uint32_t overflows = 0;
const uint8_t LED_PIN = 13;
#define LED_ON() digitalWriteFast(LED_PIN, HIGH)
#define LED_OFF() digitalWriteFast(LED_PIN, LOW)

// ---------- prototypes -------------------------------------------------------
bool initSD();
bool openLog();
void startLog();
void stopLog();
void IRAM_ATTR encEdgeISR();
// ─────────────────────────────────────────────────────────────────────────────

void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000);

pinMode(LED_PIN, OUTPUT);
quadEnc.begin();
quadEnc.clearCount();

// ── enable ARM cycle counter ───────────────────────────────────────────────
ARM_DEMCR |= ARM_DEMCR_TRCENA;
ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;

if (!initSD()) {
while (1) { LED_ON(); delay(100); LED_OFF(); delay(100); }
}

Serial.println(F("QuadEncoder + CYCCNT logger ready"));
Serial.println(F("Commands: l = log, d = dir, h = help"));
}

void loop() {
if (!Serial.available()) return;
char c = Serial.read();
switch (c) {
case 'l': startLog(); break;
case 'd': sd.ls(LS_SIZE | LS_DATE | LS_R); break;
case 'h': Serial.println(F("l‑log d‑dir h‑help")); break;
}
}

// ───────────── logging control ───────────────────────────────────────────────
void startLog() {
if (!openLog()) { Serial.println(F("File open failed")); return; }
rb.begin(&logFile);

nSamples = overflows = 0;
quadEnc.clearCount();

attachInterrupt(digitalPinToInterrupt(ENC_PIN_A), encEdgeISR, CHANGE);

Serial.println(F("\nLogging… Press any key to stop."));
while (!Serial.available() && nSamples < MAX_RECORDS) {
if (rb.bytesUsed() >= 512 && !logFile.isBusy())
rb.writeOut(512); // sector‑aligned non‑blocking flush
}
stopLog();
}

void stopLog() {
detachInterrupt(digitalPinToInterrupt(ENC_PIN_A));
rb.sync(); // flush tail of FIFO
logFile.truncate(); // trim unused pre‑allocated bytes
logFile.close();
Serial.printf("Finished – %lu samples, %u overflows\n",
nSamples, overflows);
}

// ───────────── GPIO‑edge ISR ────────────────────────────────────────────────
void IRAM_ATTR encEdgeISR() {
LED_ON();
if (nSamples < MAX_RECORDS) {
Sample s { ARM_DWT_CYCCNT, quadEnc.read() };
if (!rb.memcpyIn(&s, sizeof(s))) overflows++;
nSamples++;
}
LED_OFF();
}

// ───────────── SD helpers ───────────────────────────────────────────────────
bool initSD() {
if (!sd.begin(SD_CONFIG)) { Serial.println(F("SD init failed")); return false; }

SdFile::dateTimeCallback([](uint16_t* d, uint16_t* t) {
*d = FAT_DATE(year(), month(), day());
*t = FAT_TIME(hour(), minute(), second());
});

Serial.printf("SD: FAT%d / exFAT %s\n", int(sd.fatType()),
sd.fatType() == FAT_TYPE_EXFAT ? "(exFAT)" : "");
return true;
}

bool openLog() {
if (!logFile.open("enc_cyc.dat", O_RDWR | O_CREAT | O_TRUNC)) return false;
uint64_t bytes = uint64_t(PREALLOC_MB) * 1024ULL * 1024ULL;
if (!logFile.preAllocate(bytes)) {
Serial.println(F("pre‑allocate failed"));
return false;
}
return true;
}
 
When you post code, please use the </> button to keep the formatting. I have reposted your code below.

Have you tried to compile this code? You seem to be trying to use pin 1 to both count pulses with QuadEncoder, and also use it as a GPIO with edge interrupt to compute period. There is another active thread on the same topic. If you want to count pulses over a fixed time, use QuadEncoder. If you want to measure the time between edges, use FreqMeasureMulti.

Code:
/*********************************************************************
Teensy 4.1 – QuadEncoder logger using ARM_DWT_CYCCNT time‑stamps
• Hardware ENC1 counts edges on pins 1(A) & 0(B)
• CHANGE interrupt on Phase‑A fires only when shaft moves
• ISR saves {cycle‑count, encoder position} to a 64 kB RingBuf
• Foreground empties RingBuf to SD‑card with isBusy() back‑pressure
• Press any key to stop or after MAX_RECORDS samples
*********************************************************************/
#include "SdFat.h"
#include <RingBuf.h>
#include <TimeLib.h>
#include "QuadEncoder.h"

// -------------------------- user settings -----------------------------------
constexpr uint8_t ENC_INDEX = 1; // ENC1 → pins 1 (A) & 0 (B)
constexpr uint8_t ENC_PIN_A = 1; // GPIO for attachInterrupt
constexpr size_t RBUF_SIZE = 64 * 1024; // 64 kB
constexpr uint32_t MAX_RECORDS = 2UL * 1024 * 1024; // 2 M edges
constexpr uint32_t PREALLOC_MB = 64; // 64 MB file
// -----------------------------------------------------------------------------

// ---------- encoder, SD, FIFO ------------------------------------------------
QuadEncoder quadEnc(ENC_INDEX);
SdFs sd;
FsFile logFile;
RingBuf<FsFile, RBUF_SIZE> rb;
#define SD_CONFIG SdioConfig(FIFO_SDIO)

// ---------- sample structure -------------------------------------------------
struct Sample { uint32_t cycles; int32_t pos; } __attribute__((packed));
static_assert(sizeof(Sample) == 8, "Sample must be 8 bytes");

// ---------- ISR bookkeeping --------------------------------------------------
volatile uint32_t nSamples = 0;
volatile uint32_t overflows = 0;
const uint8_t LED_PIN = 13;
#define LED_ON() digitalWriteFast(LED_PIN, HIGH)
#define LED_OFF() digitalWriteFast(LED_PIN, LOW)

// ---------- prototypes -------------------------------------------------------
bool initSD();
bool openLog();
void startLog();
void stopLog();
void IRAM_ATTR encEdgeISR();
// -----------------------------------------------------------------------------

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000);

  pinMode(LED_PIN, OUTPUT);
  quadEnc.begin();
  quadEnc.clearCount();

  // --- enable ARM cycle counter ---------------------------------------
  ARM_DEMCR |= ARM_DEMCR_TRCENA;
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;

  if (!initSD()) {
    while (1) { LED_ON(); delay(100); LED_OFF(); delay(100); }
  }

  Serial.println(F("QuadEncoder + CYCCNT logger ready"));
  Serial.println(F("Commands: l = log, d = dir, h = help"));
}

void loop() {
  if (!Serial.available()) return;
  char c = Serial.read();
  switch (c) {
    case 'l': startLog(); break;
    case 'd': sd.ls(LS_SIZE | LS_DATE | LS_R); break;
    case 'h': Serial.println(F("l‑log d‑dir h‑help")); break;
  }
}

// ----------------------- logging control ------------------------------
void startLog() {
  if (!openLog()) { Serial.println(F("File open failed")); return; }
  rb.begin(&logFile);

  nSamples = overflows = 0;
  quadEnc.clearCount();

  attachInterrupt(digitalPinToInterrupt(ENC_PIN_A), encEdgeISR, CHANGE);

  Serial.println(F("\nLogging… Press any key to stop."));
  while (!Serial.available() && nSamples < MAX_RECORDS) {
    if (rb.bytesUsed() >= 512 && !logFile.isBusy())
      rb.writeOut(512); // sector‑aligned non‑blocking flush
  }
  stopLog();
}

void stopLog() {
  detachInterrupt(digitalPinToInterrupt(ENC_PIN_A));
  rb.sync(); // flush tail of FIFO
  logFile.truncate(); // trim unused pre‑allocated bytes
  logFile.close();
  Serial.printf("Finished – %lu samples, %u overflows\n",
  nSamples, overflows);
}

// ----------------------- GPIO‑edge ISR -----------------------------------------
void IRAM_ATTR encEdgeISR() {
  LED_ON();
  if (nSamples < MAX_RECORDS) {
    Sample s { ARM_DWT_CYCCNT, quadEnc.read() };
    if (!rb.memcpyIn(&s, sizeof(s))) overflows++;
    nSamples++;
  }
  LED_OFF();
}

// ------------------------- SD helpers ------------------------------------------
bool initSD() {
  if (!sd.begin(SD_CONFIG)) { Serial.println(F("SD init failed")); return false; }

  SdFile::dateTimeCallback([](uint16_t* d, uint16_t* t) {
  *d = FAT_DATE(year(), month(), day());
  *t = FAT_TIME(hour(), minute(), second());
  });

  Serial.printf("SD: FAT%d / exFAT %s\n", int(sd.fatType()),
  sd.fatType() == FAT_TYPE_EXFAT ? "(exFAT)" : "");
  return true;
}

bool openLog() {
  if (!logFile.open("enc_cyc.dat", O_RDWR | O_CREAT | O_TRUNC)) return false;
  uint64_t bytes = uint64_t(PREALLOC_MB) * 1024ULL * 1024ULL;
  if (!logFile.preAllocate(bytes)) {
    Serial.println(F("pre‑allocate failed"));
    return false;
  }
  return true;
}
 
Last edited:
Back
Top