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.
- Input pulling instead of interrupts.
- 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.
- 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.