Measure Timing Advance BLDC Motor

I'm putting this in the Project Guidance since I don't know if this is a code issue, or hardware issue...

Long post warning... Code at the bottom.

I'll start with what the project is:
I am trying to make my own version of a motor analyzer. At some point I will most likely sell these to a very limited market, just a heads up for anyone that may help. There are a couple of these devices on the market already, mine obviously will have a few different features that I think are needed beyond what the current products offer.

Here are the 3 main products offered right now sorted by price. You can search them to get a better understanding of what I am trying to accomplish.
Motolyser II <--- Uses the Xmega256a3u μC
Hobbywing Tunalyzer <--- uses an STM32F401 μC and STM32F3xxx μC
SkyRC Motor Analyzer <--- Cheapest model and I can't remember the μC

For those who don't want to search for what these do...
For 1/10 RC racing we use 540 BLDC sensored motors. You can adjust the hall sensor position of the motors by twisting the endbell and this increases the "timing" of the motor. The part of the project I am working on now is measuring that timing. The conclusion I have come to is to measure the bemf zero crossing compared to the respective phase hall sensor signal. I have been working on this project for a long time because quite frankly it is above my paygrade. But I am not going to let that stop me...

So here is what I think needs to happen. Measure BEMF of a phase and compare that to the hall sensor signal. Subtract the time between the hall sensor and the BEMF and compute into degrees and that gives you the timing advance. So how do you get the BEMF? You have to compare the phase signal to a virtual neutral that combines all the phases together to get the "zero" cross point. In that end, I have wires connected to each phase, then ran through a voltage divider to be below the μC GPIO voltage limit. From there branch all the phase through a 100kΩ resistors to create the virtual neutral. This virtual neutral will be 1/2 the bus voltage, or 1/2 the voltage of the phases. The phase voltage is essentially an AC voltage that will go above and below the virtual neutral. However the phase voltage never actually goes negative to the gnd. I will post some scope pics to show what I mean.

Solving for an analog signal. The BEMF signal is analog and we are trying to compare it to a digital signal created from the latching hall effect sensor. Once you get your BEMF signal you have to run it through an analog comparator. For this you take the phase signal and input it into the non inverting input and you compare that to the virtual neutral which goes to the inverting input. If you are using a μC with an onboard comparator you may need to flip the inverting and non inverting inputs and account for that in your code. Now the second issue. The bemf and virtual neutral are riddled with PWM noise from the switching mosfets. To help with this I added low pass filters on the BEMF signals at the voltage divider and the virtual common at the 100kΩ resistors. The problem with this, is the low pass filter can affect the zero cross time if your filters are too aggressive.

Dev board/μC choice. I am using the Arduino Nano ESP32. I started my development with an Uno R3, but there are a lack of hardware interrupts on it and I don't think the 16MHz processor is fast enough. I was trying to decide between the ESP32 and the STM32F411, but I want WiFi and or BLE so I decided on the ESP32. You can use most GPIO pins on the ESP32 for hardware interrupts.

I got everything built on my bench, and it is quite the mess, lol. To start out I just measured one phase signal after the comparator to one rpm signal. The results are interesting. If the timing is below 30° on the can, which ends up being 60° actual (there is a story there), then the measurement is linear. However, once the timing gets past 60° the output is no longer linear to the marks on the can. I assume this has to do with trig and the fact that these sensors rotate, but I haven't searched for this yet. Regardless, I made a chart with a few different motors and timing positions so I could create an equation to find the timing. At this point I am pretty close to correct values but there are still discrepancies. The other products on the market
are able to capture the value a lot better, lol... Well, the cheapest one on the market isn't the best at it, but.

Now I decided I would move on a little with the testing and add the other 2 phases and rpm sensors. And this is where I start having issues. I made a class for the phase and rpm sensors. The class has all the functions in it for the calculations, the only thing I have to do is add the new class member and the hardware interrupts, then print the results. But whenever I add another class member and the applicable code for it, my board starts turning off as soon as I start the motor. So to repeat. One class member it works fine, but adding the second or third the board turns on and off repeatedly when trying to measure the inputs. The code loads fine.

Things I may try.

  1. Input pulling instead of interrupts.
  2. Learn FreeRTOS so I can leverage the second core of the ESP32. Not sure if that would fix the issue since the timer will still be the same.
  3. Try a different dev board. I have a few and don't mind buying one I don't have.

Now on to pictures, schematics, code, the fun stuff everyone wants to examine.

First up, scope pics.
Signal 1: Hall sensor
Signal 2: Comparator output
Signal 3: BEMF
Signal 4: Virtual neutral

The point of the following pics is to show some filtering is required.

Pic 1: All Low pass filters, 100% throttle (no pwm at 100%)

Pic 2: All low pass filters, 48% throttle

Pic 3: No low pass filters, 100% throttle

Pic 4: No low pass filters, 48% throttle

Pic 5: No low pass on the virtual neutral, 100% throttle

Pic 6: No low pass on the virtual neutral, 48% throttle

Fritzing: No resistors on the comparator outputs in the diagram.

Schematic: No motor or motor driver, but a cleaner look for the connection.

Bench: Well, this is messy.

And finally, the code.
Note: Only 1 class member is commented in right now.

/* 
   Written by Andrew Sarratore
   Board Arduino Nano ESP32 Nora 106
   V 1.00.00
   Date: 6/20/2024
   Test for reading zero cross and Timing
   */

//////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////



//////////////////////////////////////////////////////////////////////////////////////////////////
// DEFINES AND MACROS ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#define REV_A 2
#define PHASE_A 3
//#define REV_B 4
//#define PHASE_B 5
//#define REV_C 6
//#define PHASE_C 7
#define LPIN 13

//////////////////////////////////////////////////////////////////////////////////////////////////
// STRUCTS, TYPES, AND CLASSES ///////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Combined class for RPM and BEMF
const uint8_t N = 10;
class Pulse {
public:
  uint8_t sPin;
  uint8_t pPin;
  volatile bool rpmNewIsrFlag;
  volatile bool phaseNewIsrFlag;
  uint32_t rpmMicros;
  uint32_t prevRpmMicros;
  volatile uint32_t rpmIsrMicros;
  volatile uint32_t phaseIsrMicros;
  volatile uint32_t phaseIsrLastMicros;
  uint32_t phaseMicros;
  float freq;
  float degree;
  uint32_t rpm;
  float timing;
  float cTiming;
  uint8_t samples;
  float sumAvgTiming;
  float avgTiming[];

  Pulse(uint8_t dSPin, uint8_t dPPin, bool rpmNewIsrFlag, bool phaseNewIsrFlag);

  void begin();
  void rpmData();
};

Pulse::Pulse(uint8_t dRPin, uint8_t dPPin, bool dRpmNewIsrFlag, bool dPhaseNewIsrFlag) {
  sPin = dRPin;
  pPin = dPPin;
  rpmNewIsrFlag = dRpmNewIsrFlag;
  phaseNewIsrFlag = dPhaseNewIsrFlag;
}

void Pulse::begin() {
  pinMode(sPin, INPUT);
  pinMode(pPin, INPUT);
}

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    rpm = (freq * 60);                                      // Converts from ticks per second to ticks per minute
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {   //ISR flag us true, let's do stuff
    noInterrupts();                //Disable interrupts
    phaseMicros = phaseIsrMicros;  //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;       //Change our ISR flag, guess we wait until its true again
    interrupts();                  //Enable interrupts
    timing = (((phaseMicros - rpmMicros) * 1000) / degree);
    avgTiming[samples] = timing;
    samples++;
    if (samples >= N) {
      sumAvgTiming = 0;
      for (samples = 0; samples < N; samples++) {
        sumAvgTiming += avgTiming[samples];
      }
      sumAvgTiming = sumAvgTiming / N;
      samples = 0;
    }
    if (sumAvgTiming <= 60) {
      cTiming = sumAvgTiming - 30;
    } else {
      cTiming = (((sumAvgTiming - 60) * 2.5) + 30);
    }
  }
}

Pulse pulseA(2, 3, false, false);
//Pulse pulseB(4, 5, false, false);
//Pulse pulseC(6, 7, false, false);

//////////////////////////////////////////////////////////////////////////////////////////////////
// PROTOTYPES ////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void rpmAISR();
//void rpmBISR();
//void rpmCISR();
void phaseAISR();
//void phaseBISR();
//void phaseCISR();
void flashLED();
void showData();


//////////////////////////////////////////////////////////////////////////////////////////////////
// GLOBALS ///////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

uint32_t displayMillis = 0;
uint32_t prevDisplayMillis = 0;
uint8_t led = LOW;
const uint16_t refresh = 1000;

//////////////////////////////////////////////////////////////////////////////////////////////////
// SETUP /////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  pulseA.begin();
  //pulseB.begin();
  //pulseC.begin();
  pulseA.avgTiming[N] = {};
  //pulseB.avgTiming[N] = {};
  //pulseC.avgTiming[N] = {};
  pinMode(LPIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(REV_A), rpmAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_A), phaseAISR, RISING);
  //attachInterrupt(digitalPinToInterrupt(REV_B), rpmBISR, RISING);
  //attachInterrupt(digitalPinToInterrupt(PHASE_B), phaseBISR, RISING);
  //attachInterrupt(digitalPinToInterrupt(REV_C), rpmCISR, RISING);
  //attachInterrupt(digitalPinToInterrupt(PHASE_C), phaseCISR, RISING);
  prevDisplayMillis = millis();
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// MAIN LOOP /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void loop() {
  pulseA.rpmData();
  //pulseB.rpmData();
  //pulseC.rpmData();
  displayMillis = millis();
  if (displayMillis >= prevDisplayMillis + refresh) {
    prevDisplayMillis = millis();
    flashLED();
    showData();
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void rpmAISR() {
  pulseA.rpmIsrMicros = micros();
  pulseA.rpmNewIsrFlag = true;
}

void phaseAISR() {
  pulseA.phaseIsrMicros = micros();
  pulseA.phaseNewIsrFlag = true;
}

/*
void rpmBISR() {
  pulseB.rpmIsrMicros = micros();
  pulseB.rpmNewIsrFlag = true;
}

void phaseBISR() {
  pulseB.phaseIsrMicros = micros();
  pulseB.phaseNewIsrFlag = true;
}
*/

/*
void rpmCISR() {
  pulseC.rpmIsrMicros = micros();
  pulseC.rpmNewIsrFlag = true;
}

void phaseCISR() {
  pulseC.phaseIsrMicros = micros();
  pulseC.phaseNewIsrFlag = true;
}
*/

void flashLED() {
  if (led == LOW) {
    led = HIGH;
  } else {
    led = LOW;
  }
  digitalWrite(LPIN, led);
}


void showData() {
  int indexA;
  Serial.println("Phase A Array");
  for (indexA = 0; indexA < N; indexA++) {
    Serial.println(pulseA.avgTiming[indexA]);
  }

/*
  int indexB;
  Serial.println("Phase B Array");
  for (indexB = 0; indexB < N; indexB++) {
    Serial.println(pulseB.avgTiming[indexB]);
  }
*/

/*
  int indexC;
  Serial.println("Phase C Array");
  for (indexC = 0; indexC < N; indexC++) {
    Serial.println(pulseC.avgTiming[indexC]);
  }
*/
  Serial.println();
  Serial.println("PHASE A");
  Serial.print("  FREQ  ");
  Serial.print(pulseA.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseA.rpm);
  Serial.print("   AVG Timing  ");
  Serial.print(pulseA.sumAvgTiming);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseA.cTiming, 2);
  Serial.print("*");
  Serial.println();

/*
  Serial.println("PHASE B");
  Serial.print("  FREQ  ");
  Serial.print(pulseB.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseB.rpm);
  Serial.print("   AVG Timing  ");
  Serial.print(pulseB.sumAvgTiming);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseB.cTiming, 2);
  Serial.print("*");
  Serial.println();
*/

/*
  Serial.println("PHASE C");
  Serial.print("  FREQ  ");
  Serial.print(pulseC.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseC.rpm);
  Serial.print("   AVG Timing  ");
  Serial.print(pulseC.sumAvgTiming);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseC.cTiming, 2);
  Serial.print("*");
  Serial.println();
  */
}

If you have made it this far, thanks for looking. I am open to suggestions on the code or even my approach to this. Maybe there is an easier way to measure the zero cross than what I am doing.

I'd simplify the averaging with an EWMA:

 ewmaTiming += (timing - ewmaTiming)/10;

This wouldn't require the array for averaging timings, and it would give more responsive results.

I will give this a try. I've been meaning to start figuring out EWMA.

Update to hardware issue:

I've tried a few different dev boards now.
Arduino DUE - Code uploads but as soon as I start the motor it freezes up.
Arduino Giga R1 WiFi - Code uploads, nothing freezes, but the interrupts are happening too fast for the board to keep up. Get bad results.
Arduino R4 - Same as the Giga R1
STM32F411 Black Pill - Code uploads, haven't tested functionality yet.
I am thinking about getting a Teensy 4.1.

A bit here about EWMA

1 Like

Did you budget out the time? How fast do you expect interrupts and how much time between interrupts will you have for processing?

Interesting project.

  • I've compiled your project on an esp32 devkit1 end it 'works' with 1 and 2 instances of the class ( had only to redefine pins from 2-5 to 16-19 ), so I'd say your code is ok, maybe you could try using different pins? Or what if you enable only pulseB? ( as I have no input connected no interrupt is called, does your project crashes even with input connected? )
  • only in the first case/image you posted you have a 'clean' comparator output, in all other cases the phaseXISR will be called 'multiple times' ( too much times ) and also calculation won't be correct, so I think you need to 'stay always' in this case
  • the filtering on phaseX input has an rc of roughly 63uS which should be accounted ( I think ) and no... I don't understand the calculations of cTimings
    if (sumAvgTiming <= 60) {
      cTiming = sumAvgTiming - 30;
    } else {
      cTiming = (((sumAvgTiming - 60) * 2.5) + 30);
    }

( I think you did this to have results similar to the other commercial circuits, if this is the case I think we are not doing things 'super correctly' or we could call this a first approximation result )

  • another question, show also the 'phase X' input signal, and why using the 'virtual ground' ( which is quite noisy in my opinion ) as a reference signal, and not a fixed reference signal, is this because the 'phase X' varies in amplitude with motor/rpm? If this is the case we could 'calculate' and regenerate it using the d/a

Doesn't seem to work with floats... Which is weird because arduino.cc says it should. Changing timing from a float to a uint32_t fixes the issue, but I do lose some resolution, maybe... I can use the original value from the millis() though and apply the EWMA to that, then convert to float. I think that will work

I did not actually do the math to budget the time, I need to do that. However, I did start with a function generator to measure a know phase offset. The fastest any motor would turn should be about 70,000 RPM, rounding up I can say a max of 1200Hz. The quickest time should be a 30° offset. To take account for 3 sets of interrupts, the μC needs to keep up with interrupts at 3600Hz @30° would be an interval of about 23µs. But I figured I might need some room for margin so I speed up the function generator to 12kHz @30° offset, which is about 7µs and and the Uno R4 keeps up fine.

14:55:59.284 -> PHASE A
14:55:59.284 ->   FREQ  11904.00 Hz  RPM  714240   Timing   30*   AVG Timing  30.00*   cTiming   -0.00*

Here is the scope output.

This is with the Uno R4. The ESP32 was able to get to around 2µs before it started getting incorrect readings.

The project compiles with all the class instances on my ESP32. The problem is as soon as I add inputs the dev board starts turning off an on. I have tried with and without serial data, I though maybe the serial interrupts were causing conflicts.

Yes, the rc filter does have a time response on the signal, I am still testing different values, but I need to get a prototype on a pcb to really test as the breadboard is going to change the response as well.

And yes, I am good with calling cTiming a "first approximation result". I need to understand why the change after 60° isn't linear anymore. And just as an fyi, 30° offset is actually 0° as far as we are concerned. There is a 30° offset "built in" to the system to start. I'll state this another way. 0° can timing reads as a 30° offset on the scope.

Yes, the amplitude of the virtual neutral changes with the amplitude of the phase signal. It will always be 1/2 the voltage of the phase signal. And yes it alternates due to commutation just like the phase signal and is riddled with pmw at partial throttle. That is the reason I added another low pass filter to that signal. Yes, creating a straight DC signal that is always 1/2 the voltage of the phase signal would be ideal.

Thank you. I think I understand the concept now. This can be implemented easily with @DaveX one line code or with an Alpha variable instead of the /10 that DaveX used. Also I changed the /10 to /100.

I'd think it should work with floats. And with uint32_t math, be careful that negatives aren't rolling over to MAX_UINT. If speed is a issue, you might consider maintaining the input and filtered in original units and doing the more expensive conversions at when you are doing the output.

To manage resolution using integers, you could scale them by 100 or 2^N, etc., and get selectable resolution higher than a 6-7 digit float can manage.

I chose the /10 because it matched the 1/10 weight of your initial N=10 batch windowing filter:

Some nice things about EWMA is it takes 1/Nth the memory, gives results with N-times the frequency, and is pretty quick. Also, compared to the batch-averaged result, the EWMA also weights the most recent observation more heavily than the T_{i-N} observation. And its speed/smoothing/time constant is adjustable by changing alpha rather than an array allocation.

I would think as well, but with timing as a float it returns "nan". But regardless, I just averaged the millis() difference, which will always be positive. Below is the new class function.

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    rpm = (freq * 60);                                      // Converts from ticks per second to ticks per minute
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {   //ISR flag is true, let's do stuff
    noInterrupts();                //Disable interrupts
    phaseMicros = phaseIsrMicros;  //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;       //Change our ISR flag, guess we wait until its true again
    interrupts();                  //Enable interrupts
    ewmaPhaseTime += (((phaseMicros - rpmMicros) - ewmaPhaseTime)/100); // Exponential Weighted Moving Average
    timing = ((ewmaPhaseTime * 1000) / degree);
    
    if (timing <= 60) {
      cTiming = timing - 30;
    } else {
      cTiming = (((timing - 60) * 2.5) + 30);
    }
  }
}

I am definitely going to use the EWMA with this. I don't have to wait for the array to fill up and I can use a higher number of samples as well, again because I don't have to wait in the code for the array to fill up.

Ok... With the R4, I looked at the data sheet for the μC, cross referenced with the pinout of the R4 and found suitable pins that were on different interrupt masks. These pins end up being, D1, D2, D8, A1, A2, A4. Note, with the R4, not all pins can be an interrupt pin. Here are the results:

Next step, go back to the ESP32 and figure out which pins to use...

And the new code:

/* 
   Written by Andrew Sarratore
   Board Arduino Uno R4 Minima
   V 1.00.00
   Date: 6/20/2024
   Test for reading zero cross and Timing
   V 1.00.10
   Date: 7/28/2027
   Change average from sample array to EWMA - DaveX - ewmaTiming += (timing - ewmaTiming)/10;
   Change pins for the interrupts - D1, D2, D8, A1, A2, A4
   */

//////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#include <Arduino.h>

//////////////////////////////////////////////////////////////////////////////////////////////////
// DEFINES AND MACROS ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#define REV_A 1
#define PHASE_A 2
#define REV_B 8
#define PHASE_B A4
#define REV_C A2
#define PHASE_C A1
#define LPIN 13

//////////////////////////////////////////////////////////////////////////////////////////////////
// STRUCTS, TYPES, AND CLASSES ///////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Combined class for RPM and BEMF
//const uint8_t N = 10;
//const float ALPHA = 0.1;
class Pulse {
public:
  uint8_t sPin;
  uint8_t pPin;
  volatile bool rpmNewIsrFlag;
  volatile bool phaseNewIsrFlag;
  uint32_t rpmMicros;
  uint32_t prevRpmMicros;
  volatile uint32_t rpmIsrMicros;
  volatile uint32_t phaseIsrMicros;
  volatile uint32_t phaseIsrLastMicros;
  uint32_t phaseMicros;
  float freq;
  float degree;
  uint32_t rpm;
  float timing = 0;
  float cTiming;
  float ewmaPhaseTime;

  Pulse(uint8_t dSPin, uint8_t dPPin, bool rpmNewIsrFlag, bool phaseNewIsrFlag);

  void begin();
  void rpmData();
};

Pulse::Pulse(uint8_t dRPin, uint8_t dPPin, bool dRpmNewIsrFlag, bool dPhaseNewIsrFlag) {
  sPin = dRPin;
  pPin = dPPin;
  rpmNewIsrFlag = dRpmNewIsrFlag;
  phaseNewIsrFlag = dPhaseNewIsrFlag;
}

void Pulse::begin() {
  pinMode(sPin, INPUT);
  pinMode(pPin, INPUT);
}

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    rpm = (freq * 60);                                      // Converts from ticks per second to ticks per minute
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {   //ISR flag is true, let's do stuff
    noInterrupts();                //Disable interrupts
    phaseMicros = phaseIsrMicros;  //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;       //Change our ISR flag, guess we wait until its true again
    interrupts();                  //Enable interrupts
    ewmaPhaseTime += (((phaseMicros - rpmMicros) - ewmaPhaseTime)/10);
    timing = ((ewmaPhaseTime * 1000) / degree);
    
    if (timing <= 60) {
      cTiming = timing - 30;
    } else {
      cTiming = (((timing - 60) * 2.5) + 30);
    }
  }
}

Pulse pulseA(1, 2, false, false);
Pulse pulseB(8, A4, false, false);
Pulse pulseC(A2, A1, false, false);

//////////////////////////////////////////////////////////////////////////////////////////////////
// PROTOTYPES ////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void rpmAISR();
void rpmBISR();
void rpmCISR();
void phaseAISR();
void phaseBISR();
void phaseCISR();
void flashLED();
void showData();


//////////////////////////////////////////////////////////////////////////////////////////////////
// GLOBALS ///////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

uint32_t displayMillis = 0;
uint32_t prevDisplayMillis = 0;
uint8_t led = LOW;
const uint16_t refresh = 1000;

//////////////////////////////////////////////////////////////////////////////////////////////////
// SETUP /////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  pulseA.begin();
  pulseB.begin();
  pulseC.begin();
  pinMode(LPIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(REV_A), rpmAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_A), phaseAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_B), rpmBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_B), phaseBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_C), rpmCISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_C), phaseCISR, RISING);
  prevDisplayMillis = millis();
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// MAIN LOOP /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void loop() {
  pulseA.rpmData();
  pulseB.rpmData();
  pulseC.rpmData();
  displayMillis = millis();
  if (displayMillis >= prevDisplayMillis + refresh) {
    prevDisplayMillis = millis();
    flashLED();
    showData();
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Phase A and Sensor A
void rpmAISR() {
  pulseA.rpmIsrMicros = micros();
  pulseA.rpmNewIsrFlag = true;
}
void phaseAISR() {
  pulseA.phaseIsrMicros = micros();
  pulseA.phaseNewIsrFlag = true;
}

//Phase B and Sensor B
void rpmBISR() {
  pulseB.rpmIsrMicros = micros();
  pulseB.rpmNewIsrFlag = true;
}
void phaseBISR() {
  pulseB.phaseIsrMicros = micros();
  pulseB.phaseNewIsrFlag = true;
}

// Phase C and Sensor C
void rpmCISR() {
  pulseC.rpmIsrMicros = micros();
  pulseC.rpmNewIsrFlag = true;
}
void phaseCISR() {
  pulseC.phaseIsrMicros = micros();
  pulseC.phaseNewIsrFlag = true;
}


// Flash onboard LED to verify program takes
void flashLED() {
  if (led == LOW) {
    led = HIGH;
  } else {
    led = LOW;
  }
  digitalWrite(LPIN, led);
}

// Serial Output
void showData() {
  Serial.println();
  Serial.println("PHASE A");
  Serial.print("  FREQ  ");
  Serial.print(pulseA.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseA.rpm);
  Serial.print("   Timing   ");
  Serial.print(pulseA.timing);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseA.cTiming, 2);
  Serial.print("*");
  Serial.println();

  Serial.println("PHASE B");
  Serial.print("  FREQ  ");
  Serial.print(pulseB.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseB.rpm);
  Serial.print("   Timing   ");
  Serial.print(pulseB.timing);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseB.cTiming, 2);
  Serial.print("*");
  Serial.println();

  Serial.println("PHASE C");
  Serial.print("  FREQ  ");
  Serial.print(pulseC.freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(pulseC.rpm);
  Serial.print("   Timing   ");
  Serial.print(pulseC.timing);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(pulseC.cTiming, 2);
  Serial.print("*");
  Serial.println();
}

And moving the serial data to a class function.

/* 
   Written by Andrew Sarratore
   Board Arduino Uno R4 Minima
   V 1.00.00
   Date: 6/20/2024
   Test for reading zero cross and Timing
   V 1.00.10
   Date: 7/28/2027
   Change average from sample array to EWMA - DaveX - ewmaTiming += (timing - ewmaTiming)/10;
   Change pins for the interrupts - D1, D2, D8, A1, A2, A4
   */

//////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#include <Arduino.h>

//////////////////////////////////////////////////////////////////////////////////////////////////
// DEFINES AND MACROS ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#define REV_A 1
#define PHASE_A 2
#define REV_B 8
#define PHASE_B A4
#define REV_C A2
#define PHASE_C A1
#define LPIN 13

//////////////////////////////////////////////////////////////////////////////////////////////////
// STRUCTS, TYPES, AND CLASSES ///////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Combined class for RPM and BEMF
class Pulse {
public:
  uint8_t sPin;
  uint8_t pPin;
  volatile bool rpmNewIsrFlag;
  volatile bool phaseNewIsrFlag;
  uint32_t rpmMicros;
  uint32_t prevRpmMicros;
  volatile uint32_t rpmIsrMicros;
  volatile uint32_t phaseIsrMicros;
  volatile uint32_t phaseIsrLastMicros;
  uint32_t phaseMicros;
  float freq;
  float degree;
  uint32_t rpm;
  float timing = 0;
  float cTiming;
  float ewmaPhaseTime;

  Pulse(uint8_t dSPin, uint8_t dPPin, bool rpmNewIsrFlag, bool phaseNewIsrFlag);

  void begin();
  void rpmData();
  void showData();
};

Pulse::Pulse(uint8_t dRPin, uint8_t dPPin, bool dRpmNewIsrFlag, bool dPhaseNewIsrFlag) {
  sPin = dRPin;
  pPin = dPPin;
  rpmNewIsrFlag = dRpmNewIsrFlag;
  phaseNewIsrFlag = dPhaseNewIsrFlag;
}

void Pulse::begin() {
  pinMode(sPin, INPUT);
  pinMode(pPin, INPUT);
}

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    rpm = (freq * 60);                                      // Converts from ticks per second to ticks per minute
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {   //ISR flag is true, let's do stuff
    noInterrupts();                //Disable interrupts
    phaseMicros = phaseIsrMicros;  //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;       //Change our ISR flag, guess we wait until its true again
    interrupts();                  //Enable interrupts
    ewmaPhaseTime += (((phaseMicros - rpmMicros) - ewmaPhaseTime)/10); // Exponetial Weighted Moving Average
    timing = ((ewmaPhaseTime * 1000) / degree); // Timing of the motor - Will be actual offset
    
    if (timing <= 60) {  
      cTiming = timing - 30; // Account for can timing offset of 30*
    } else {
      cTiming = (((timing - 60) * 2.5) + 30); // Offset is not linear past 60* actual timing
    }
  }
}

void Pulse::showData() {
  Serial.println();
  Serial.println("PHASE");
  Serial.print("  FREQ  ");
  Serial.print(freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(rpm);
  Serial.print("   Timing   ");
  Serial.print(timing);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(cTiming, 2);
  Serial.print("*");
  Serial.println();
}
Pulse pulseA(1, 2, false, false);
Pulse pulseB(8, A4, false, false);
Pulse pulseC(A2, A1, false, false);

//////////////////////////////////////////////////////////////////////////////////////////////////
// PROTOTYPES ////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void rpmAISR();
void rpmBISR();
void rpmCISR();
void phaseAISR();
void phaseBISR();
void phaseCISR();
void flashLED();

//////////////////////////////////////////////////////////////////////////////////////////////////
// GLOBALS ///////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

uint32_t displayMillis = 0;
uint32_t prevDisplayMillis = 0;
uint8_t led = LOW;
const uint16_t refresh = 1000;

//////////////////////////////////////////////////////////////////////////////////////////////////
// SETUP /////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  pulseA.begin();
  pulseB.begin();
  pulseC.begin();
  pinMode(LPIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(REV_A), rpmAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_A), phaseAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_B), rpmBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_B), phaseBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_C), rpmCISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_C), phaseCISR, RISING);
  prevDisplayMillis = millis();
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// MAIN LOOP /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void loop() {
  pulseA.rpmData();
  pulseB.rpmData();
  pulseC.rpmData();
  displayMillis = millis();
  if (displayMillis >= prevDisplayMillis + refresh) {
    prevDisplayMillis = millis();
    flashLED();
    pulseA.showData();
    pulseB.showData();
    pulseC.showData();
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Phase A and Sensor A
void rpmAISR() {
  pulseA.rpmIsrMicros = micros();
  pulseA.rpmNewIsrFlag = true;
}
void phaseAISR() {
  pulseA.phaseIsrMicros = micros();
  pulseA.phaseNewIsrFlag = true;
}

//Phase B and Sensor B
void rpmBISR() {
  pulseB.rpmIsrMicros = micros();
  pulseB.rpmNewIsrFlag = true;
}
void phaseBISR() {
  pulseB.phaseIsrMicros = micros();
  pulseB.phaseNewIsrFlag = true;
}

// Phase C and Sensor C
void rpmCISR() {
  pulseC.rpmIsrMicros = micros();
  pulseC.rpmNewIsrFlag = true;
}
void phaseCISR() {
  pulseC.phaseIsrMicros = micros();
  pulseC.phaseNewIsrFlag = true;
}


// Flash onboard LED to verify program takes
void flashLED() {
  if (led == LOW) {
    led = HIGH;
  } else {
    led = LOW;
  }
  digitalWrite(LPIN, led);
}

The only problem here is I don't know how to make "Phase" show as "PhaseA", "PhaseB", "PhaseC"

in the constructor pass the 'phase X' string

That makes sense, I'll fix that.

Also good news! Using the same pins I am using on the R4 works on the Nano ESP32.

Now, there is so much more to put into this... I need to measure the rotor asymmetry, for that I will need to measure every other rpm rising edge, divide by 1/2 and compare to every rpm rising edge.

But the real difficult part is going to be building my own motor controller. That circuit will need to have a 7.4V and a 3.2V regulated output and be able to handle 50A continuous current.

Got the constructor updated.

// Combined class for RPM and BEMF
class Pulse {
public:
  uint8_t sPin;
  uint8_t pPin;
  volatile bool rpmNewIsrFlag;
  volatile bool phaseNewIsrFlag;
  uint32_t rpmMicros;
  uint32_t prevRpmMicros;
  volatile uint32_t rpmIsrMicros;
  volatile uint32_t phaseIsrMicros;
  volatile uint32_t phaseIsrLastMicros;
  uint32_t phaseMicros;
  float freq;
  float degree;
  uint32_t rpm;
  float timing = 0;
  float cTiming;
  float ewmaPhaseTime;
  const char *phase;

  Pulse(uint8_t dSPin, uint8_t dPPin, bool rpmNewIsrFlag, bool phaseNewIsrFlag, const char *phaseX);

  void begin();
  void rpmData();
  void showData();
};

Pulse::Pulse(uint8_t dRPin, uint8_t dPPin, bool dRpmNewIsrFlag, bool dPhaseNewIsrFlag, const char *phaseX) {
  sPin = dRPin;
  pPin = dPPin;
  rpmNewIsrFlag = dRpmNewIsrFlag;
  phaseNewIsrFlag = dPhaseNewIsrFlag;
  phase = phaseX;
}

void Pulse::begin() {
  pinMode(sPin, INPUT);
  pinMode(pPin, INPUT);
}

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    rpm = (freq * 60);                                      // Converts from ticks per second to ticks per minute
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {                                          //ISR flag is true, let's do stuff
    noInterrupts();                                                       //Disable interrupts
    phaseMicros = phaseIsrMicros;                                         //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;                                              //Change our ISR flag, guess we wait until its true again
    interrupts();                                                         //Enable interrupts
    ewmaPhaseTime += (((phaseMicros - rpmMicros) - ewmaPhaseTime) / 10);  // Exponetial Weighted Moving Average
    timing = ((ewmaPhaseTime * 1000) / degree);                           // Timing of the motor - Will be actual offset

    if (timing <= 60) {
      cTiming = timing - 30;  // Account for can timing offset of 30*
    } else {
      cTiming = (((timing - 60) * 2.5) + 30);  // Offset is not linear past 60* actual timing
    }
  }
  if (freq < 35) {
    freq = 0;
  }
}

void Pulse::showData() {
  Serial.println();
  Serial.println(phase);
  Serial.print("  FREQ  ");
  Serial.print(freq);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(rpm);
  Serial.print("   Timing   ");
  Serial.print(timing);
  Serial.print("*");
  Serial.print("   cTiming   ");
  Serial.print(cTiming, 2);
  Serial.print("*");
  Serial.println();
}
Pulse pulseA(1, 2, false, false, "PhaseA");
Pulse pulseB(8, A4, false, false, "PhaseB");
Pulse pulseC(A2, A1, false, false, "PhaseC");

I think for the float, perhaps ewmaXXXX is initially uninitialized, and that persists through the interations. Or if the observation somehow calculates to a NAN, it will persist in a float. Integers don't have NANs, so a NAN won't persist.

Even if the interval between millis() and a past timestamp is always positive, if you have a sub-average observation, the second half of this calculation will be negative, causing problems for unsigned integers:

Here's some code demonstrating the persistance of NAN in floats, and averaging negative unsigned longs:

// Change the NAN to 0 to avoid NAN polluting the ewmaFloat:
float ewmaFloat = NAN, obsFloat = 0;
uint32_t ewmaUL = 0, obsUL = 0;
int32_t ewmaL = 0, obsL = 0;
uint32_t ewmaULQ8 = 0, obsULQ8 = 0;
int32_t ewmaLQ8 = 0, obsLQ8 = 0;

uint16_t divisor = 64;

uint32_t now, last = 0;
const uint32_t interval = 200;

void setup() {
  Serial.begin(115200);
}

void loop() {
  now = millis();
  if (now - last >= interval ) {
    last += interval;
    obsUL = random(2000);
    obsULQ8 = obsUL * 8;
    obsFloat = (obsUL) ;
    obsL = obsUL;
    obsLQ8 = obsL*8; 

    ewmaFloat += (obsFloat - ewmaFloat) / divisor;
    ewmaUL += (obsUL - ewmaUL) / divisor;
    ewmaULQ8 += (obsULQ8 - ewmaULQ8) / divisor;

    ewmaL += (obsL - ewmaL) / divisor;
    ewmaLQ8 += (obsLQ8 - ewmaLQ8) / divisor;

    Serial.print(" ");
    //Serial.print(now);
    Serial.print(" ");
    Serial.print(obsUL);
    Serial.print(" ");
    Serial.print(ewmaFloat);
    Serial.print(" ");
    Serial.print(ewmaUL/1.0);
    Serial.print(" ");
    Serial.print(ewmaULQ8/8.0);
    Serial.print(" ");
    Serial.print(ewmaL);
    Serial.print(" ");
    Serial.print(ewmaLQ8/8.0);
    Serial.println();
  }
}

gives:

  807 nan 12.00 12.50 12 12.50
  1249 nan 31.00 31.75 31 31.75
  73 nan 31.00 32.38 31 32.38
  1658 nan 56.00 57.75 56 57.75
  930 nan 69.00 71.37 69 71.37
  1272 nan 87.00 90.12 87 90.12
  1544 nan 109.00 112.75 109 112.75
  878 nan 121.00 124.62 121 124.62
  1923 nan 149.00 152.63 149 152.63
  1709 nan 173.00 176.88 173 176.88
  440 nan 177.00 180.88 177 180.88
  165 nan 67109040.00 8388789.00 177 180.75
  492 nan 133169328.00 16646329.00 181 185.50
  1042 nan 198197440.00 24774854.00 194 198.88
  1987 nan 262209504.00 32776386.00 222 226.75
  503 nan 325221344.00 40652872.00 226 231.00
  327 nan 387248640.00 48406284.00 227 232.50
  1729 nan 448306752.00 56038568.00 250 255.88

I am going to have to study the code you posted a bit more to understand, and I don't quite follow "sub-average observation".

But, now I am stuck on the rotor asymmetry. From my calculations I can get both a percentage and degrees, but when I use the EWMA equation I get "nan" again for the percentage. Even though the EWMA are based on floats and fabs() of the floats. So this should always be positive. When I get out my calculator and do the calculations I don't have issues.

Here is the function:

// Rotor Asymmetry
void asymmetry() {
  if (rotorNewIsrFlag == true) {
    prevPrevRotorMicros = prevRotorMicros;
    prevRotorMicros = rotorMicros;
    noInterrupts();
    rotorMicros = rotorIsrMicros;
    rotorNewIsrFlag = false;
    interrupts();
    sideA = rotorMicros - prevRotorMicros;
    sideB = prevRotorMicros - prevPrevRotorMicros;
    asymP = fabs(1 - (sideA / sideB)) * 100;
    asymD = fabs(((((sideA - sideB) / 1000000) / (1 / pulseA.freq)) * 360) / 2);
    ewmaAsymP += ((asymP - ewmaAsymP) / 50);
    ewmaAsymD += ((asymD - ewmaAsymD) / 50);
   }
}

And the full code now.

/* 
   Written by Andrew Sarratore
   Board Arduino Uno R4 Minima
   V 1.00.00
   Date: 6/20/2024
   Test for reading zero cross and Timing
   V 1.00.10
   Date: 7/28/2027
   Change average from sample array to EWMA - DaveX - ewmaTiming += (timing - ewmaTiming)/10;
   Change pins for the interrupts - D1, D2, D8, A1, A2, A4
   Moved Serial Data to the class
   Add Rotor Asymmetry
   */

//////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#include <Arduino.h>
#include <math.h>

//////////////////////////////////////////////////////////////////////////////////////////////////
// DEFINES AND MACROS ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

#define REV_A 1
#define PHASE_A 2
#define REV_B 8
#define PHASE_B A4
#define REV_C A2
#define PHASE_C A1
#define ROTOR 4
#define LPIN 13

//////////////////////////////////////////////////////////////////////////////////////////////////
// STRUCTS, TYPES, AND CLASSES ///////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Combined class for RPM and BEMF
class Pulse {
public:
  uint8_t sPin;
  uint8_t pPin;
  volatile bool rpmNewIsrFlag;
  volatile bool phaseNewIsrFlag;
  uint32_t rpmMicros;
  uint32_t prevRpmMicros;
  volatile uint32_t rpmIsrMicros;
  volatile uint32_t phaseIsrMicros;
  uint32_t phaseIsrLastMicros;
  uint32_t phaseMicros;
  float freq;
  float degree;
  float timing = 0;
  float cTiming;
  float ewmaPhaseTime;
  uint16_t ewmaRPM;
  const char *phase;

  Pulse(uint8_t dSPin, uint8_t dPPin, bool rpmNewIsrFlag, bool phaseNewIsrFlag, const char *phaseX);

  void begin();
  void rpmData();
  void showData();
};

Pulse::Pulse(uint8_t dRPin, uint8_t dPPin, bool dRpmNewIsrFlag, bool dPhaseNewIsrFlag, const char *phaseX) {
  sPin = dRPin;
  pPin = dPPin;
  rpmNewIsrFlag = dRpmNewIsrFlag;
  phaseNewIsrFlag = dPhaseNewIsrFlag;
  phase = phaseX;
}

void Pulse::begin() {
  pinMode(sPin, INPUT);
  pinMode(pPin, INPUT);
}

void Pulse::rpmData() {
  if (rpmNewIsrFlag == true) {                              // ISR flag is true, let's do stuff
    prevRpmMicros = rpmMicros;                              // Save previous rpmMicros
    noInterrupts();                                         // Turn off interrupts
    rpmMicros = rpmIsrMicros;                               // set rpmMicros to the ISR interrupt time
    rpmNewIsrFlag = false;                                  // ISR flag is false, wait until next time
    interrupts();                                           // Enable interrupts
    freq = (1000000 / (rpmMicros - prevRpmMicros));         // Account for micro seconds
    ewmaRPM += (((freq * 60) - ewmaRPM) / 50);              // Converts from ticks per second to ticks per minute with EWMA
    degree = (((rpmMicros - prevRpmMicros) * 1000) / 360);  // Converts frequency into 360 equal degrees
  }
  if (phaseNewIsrFlag == true) {                                          //ISR flag is true, let's do stuff
    noInterrupts();                                                       //Disable interrupts
    phaseMicros = phaseIsrMicros;                                         //Set phaseMicros to the ISR event time
    phaseNewIsrFlag = false;                                              //Change our ISR flag, guess we wait until its true again
    interrupts();                                                         //Enable interrupts
    ewmaPhaseTime += (((phaseMicros - rpmMicros) - ewmaPhaseTime) / 50);  // Exponetial Weighted Moving Average
    timing = ((ewmaPhaseTime * 1000) / degree);                           // Timing of the motor - Will be actual offset

    if (timing <= 60) {
      cTiming = timing - 30;  // Account for can timing offset of 30*
    } else {
      cTiming = (((timing - 60) * 2.5) + 30);  // Offset is not linear past 60* actual timing
    }
  }
  if (freq < 35) {
    freq = 0;
    ewmaRPM = 0;
    timing = 0;
    cTiming = 0;
  }
}

void Pulse::showData() {
  Serial.println();
  Serial.println(phase);
  Serial.print("  FREQ  ");
  Serial.print(freq, 1);
  Serial.print(" Hz");
  Serial.print("  RPM  ");
  Serial.print(ewmaRPM);
  Serial.print("   Timing   ");
  Serial.print(timing, 1);
  Serial.print("°");
  Serial.print("   cTiming   ");
  Serial.print(cTiming, 1);
  Serial.print("°");
  Serial.println();
}
Pulse pulseA(1, 2, false, false, "PhaseA");
Pulse pulseB(8, A4, false, false, "PhaseB");
Pulse pulseC(A2, A1, false, false, "PhaseC");

//////////////////////////////////////////////////////////////////////////////////////////////////
// PROTOTYPES ////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void rpmAISR();
void rpmBISR();
void rpmCISR();
void phaseAISR();
void phaseBISR();
void phaseCISR();
void flashLED();
void asymmetryISR();
void asymmetry();

//////////////////////////////////////////////////////////////////////////////////////////////////
// GLOBALS ///////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

uint32_t displayMillis = 0;
uint32_t prevDisplayMillis = 0;
uint8_t led = LOW;
const uint16_t refresh = 1000;
volatile uint32_t rotorIsrMicros;
volatile bool rotorNewIsrFlag = false;
uint32_t rotorMicros = 0;
uint32_t prevRotorMicros = 0;
uint32_t prevPrevRotorMicros = 0;
float sideA = 0;
float sideB = 0;
float asymD = 0;
float asymP = 0;
float ewmaAsymP = 0;
float ewmaAsymD = 0;


//////////////////////////////////////////////////////////////////////////////////////////////////
// SETUP /////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  pulseA.begin();
  pulseB.begin();
  pulseC.begin();
  pinMode(ROTOR, INPUT);
  pinMode(LPIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(REV_A), rpmAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_A), phaseAISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_B), rpmBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_B), phaseBISR, RISING);
  attachInterrupt(digitalPinToInterrupt(REV_C), rpmCISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PHASE_C), phaseCISR, RISING);
  attachInterrupt(digitalPinToInterrupt(ROTOR), asymmetryISR, CHANGE);
  prevDisplayMillis = millis();
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// MAIN LOOP /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

void loop() {
  pulseA.rpmData();
  pulseB.rpmData();
  pulseC.rpmData();
  asymmetry();
  displayMillis = millis();
  if (displayMillis >= prevDisplayMillis + refresh) {
    prevDisplayMillis = millis();
    flashLED();
    pulseA.showData();
    pulseB.showData();
    pulseC.showData();
    Serial.println();
    Serial.println("Asymmetry");
    Serial.print("   Degrees   ");
    Serial.print(ewmaAsymD, 4);
    Serial.print("°");
    Serial.print("   Percentage   ");
    Serial.print(ewmaAsymP);
    Serial.print("%");
  }
}

//////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS /////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// Phase A and Sensor A
void rpmAISR() {
  pulseA.rpmIsrMicros = micros();
  pulseA.rpmNewIsrFlag = true;
}
void phaseAISR() {
  pulseA.phaseIsrMicros = micros();
  pulseA.phaseNewIsrFlag = true;
}

//Phase B and Sensor B
void rpmBISR() {
  pulseB.rpmIsrMicros = micros();
  pulseB.rpmNewIsrFlag = true;
}
void phaseBISR() {
  pulseB.phaseIsrMicros = micros();
  pulseB.phaseNewIsrFlag = true;
}

// Phase C and Sensor C
void rpmCISR() {
  pulseC.rpmIsrMicros = micros();
  pulseC.rpmNewIsrFlag = true;
}
void phaseCISR() {
  pulseC.phaseIsrMicros = micros();
  pulseC.phaseNewIsrFlag = true;
}

// Rotor Asymmetry ISR
void asymmetryISR() {
  rotorIsrMicros = micros();
  rotorNewIsrFlag = true;
}

// Rotor Asymmetry
void asymmetry() {
  if (rotorNewIsrFlag == true) {
    prevPrevRotorMicros = prevRotorMicros;
    prevRotorMicros = rotorMicros;
    noInterrupts();
    rotorMicros = rotorIsrMicros;
    rotorNewIsrFlag = false;
    interrupts();
    sideA = rotorMicros - prevRotorMicros;
    sideB = prevRotorMicros - prevPrevRotorMicros;
    asymP = fabs(1 - (sideA / sideB)) * 100;
    asymD = fabs(((((sideA - sideB) / 1000000) / (1 / pulseA.freq)) * 360) / 2);
    ewmaAsymP += ((asymP - ewmaAsymP) / 50);
    ewmaAsymD += ((asymD - ewmaAsymD) / 50);
   }
}

// Flash onboard LED to verify program takes
void flashLED() {
  if (led == LOW) {
    led = HIGH;
  } else {
    led = LOW;
  }
  digitalWrite(LPIN, led);
}

Also an update to the ESP32 constantly rebooting. I have found that if you do a calculation for a float but with values that are not a float, the ESP32 doesn’t like it. When I cast the non float values to a float in the calculation it works like it should. So you can either make the other values floats or cast them as floats in your calculations.

If you use a sensible ratio that is a factor of two and work in long int with numbers that are a decent size integer math works fine.
Why would you need it to be (A + 8B) / 9 when (A+7B) / 8 would work just as well?

1 Like