So, I have arrived to something that seems to be doing what I was trying to achieve: a class with basic functionality for measuring the velocity of up to four motors using their encoders, with interrupts calling class member functions as callbacks without needing any code outside of the class to assign them.
Tested with VS Code + PIO and Arduino Core 2.x. A word of advice: my programming skills in C++ are rather rudimentary, so please take this code as a functional example rather than a best-practice reference. It seems to work for my use case, but there may be better or safer ways to implement certain parts. Use at your own risk and feel free to suggest improvements!
main.cpp
#include <Arduino.h>
#include <Motor.h>
Motor motA = Motor(9, 10, 11, 12, 20.4, 11); // JGA25-371 motors seem to have encoders with 11 pulses/rev
Motor motB = Motor(17, 16, 18, 8, 20.4, 11);
void setup() {
Serial.begin(115200);
motA.begin(1, 50.0, 0.02); // #timer [0-3], sample frequency [Hz], time constant [s] (first order filter)
motB.begin(2, 50.0, 0.02);
}
void loop() {
motA.setPWM(60, 1);
motB.setPWM(60, 1);
while(millis()<5000) {
if (motA.getTimerTriggered()) {
Serial.printf( "Time = %04.3f s, omA = %+05.2f rpm, omB = %+03.2f rpm\n", (float)millis()/1000, motA.getOm_rev_m(), motB.getOm_rev_m());
motA.resetTimerTriggered();
}
}
motA.stop();
motB.stop();
while(1){}
}
Motor.h
#ifndef __MOTOR_H__
#define __MOTOR_H__
#include <Arduino.h>
#include "FunctionalInterrupt.h"
class Motor {
private:
uint8_t pinIN1, pinIN2;
uint8_t pinEncA, pinEncB;
long nPulses;
int nPulsesSinceLastSample; // Counter of encoder pulses
int nPulsesPerEncoderRev;
float nPulses2Rev, nPulses2Phi, phi2nPulses; // Relationship between number of puleses to angle in revs and radians
float iRatio; // Gear ratio
float phi; // Angular position
float om; // Angular velocity
void cbkEncA();
float sampleFreq;
float sampleTime;
float timeConstant;
float alpha;
bool timerTriggered;
hw_timer_t* timer = nullptr;
static constexpr int MAX_TIMERS = 4;
static Motor* instances[MAX_TIMERS];
static void IRAM_ATTR onTimer0();
static void IRAM_ATTR onTimer1();
static void IRAM_ATTR onTimer2();
static void IRAM_ATTR onTimer3();
public:
Motor();
Motor(uint8_t, uint8_t, uint8_t, uint8_t, float, uint8_t);
void begin(uint8_t, float, float);
void stop();
~Motor();
void IRAM_ATTR cbkTimer();
void setPWM(uint8_t, int8_t);
void setPWM(float);
long getNPulses();
bool getTimerTriggered();
void resetTimerTriggered();
float getPhi();
float getPhi_rev();
float getOm();
float getOm_rev_s();
float getOm_rev_m();
};
#endif
Motor.cpp
#include <Motor.h>
Motor* Motor::instances[MAX_TIMERS] = {nullptr};
Motor::Motor() {}
Motor::Motor(uint8_t pinIN1, uint8_t pinIN2, uint8_t pinEncA, uint8_t pinEncB, float iRatio, uint8_t nPulsesPerEncoderRev) {
this->pinIN1 = pinIN1;
this->pinIN2 = pinIN2;
this->pinEncA = pinEncA;
this->pinEncB = pinEncB;
pinMode(pinIN1, OUTPUT);
pinMode(pinIN2, OUTPUT);
pinMode(pinEncA, INPUT_PULLUP);
pinMode(pinEncB, INPUT_PULLUP);
this->nPulsesPerEncoderRev = nPulsesPerEncoderRev;
this->iRatio = iRatio;
this->nPulses2Rev = nPulsesPerEncoderRev * iRatio;
this->nPulses2Phi = nPulses2Rev/(2*PI);
this->phi2nPulses = (2*PI)/nPulses2Rev; // Just to change a division into a callback into a multiplication, hoping the coprocessor can cope with it faster
nPulses = 0;
phi = 0;
om = 0;
this->timerTriggered = false;
}
Motor::~Motor() {
}
void Motor::begin(uint8_t timerIndex, float sampleFreq, float timeConstant) { // Timer# [0-3], Sample frequency in Hz, low pass filter / moving average time cosntant
this->sampleFreq = sampleFreq;
this->sampleTime = 1/sampleFreq;
this->alpha = sampleTime / (sampleTime + timeConstant);
if (timerIndex >= MAX_TIMERS) return;
// Calculate prescale and tick number pro timer activation for the desired frequency
uint16_t prescaler = 2; // Two seems to be the minimum accepted value by the ESP32-S3
const uint32_t APB_CLK = 80000000;
uint64_t ticks = APB_CLK / (sampleFreq*prescaler);
while (ticks > 4294967295ULL && prescaler < 65535) {
prescaler++;
ticks = APB_CLK / (sampleFreq * prescaler);
}
instances[timerIndex] = this;
timer = timerBegin(timerIndex, prescaler, true);
switch (timerIndex) {
case 0: timerAttachInterrupt(timer, onTimer0, false); break;
case 1: timerAttachInterrupt(timer, onTimer1, false); break;
case 2: timerAttachInterrupt(timer, onTimer2, false); break;
case 3: timerAttachInterrupt(timer, onTimer3, false); break;
}
timerAlarmWrite(timer, ticks, true);
timerAlarmEnable(timer);
auto isrPulse = [this]() {
this->cbkEncA();
};
attachInterrupt(digitalPinToInterrupt(pinEncA), isrPulse, RISING);
}
void Motor::stop() {
if (timer) {
timerAlarmDisable(timer);
timerDetachInterrupt(timer);
timerEnd(timer);
timer = nullptr;
}
this->setPWM(0, 0);
detachInterrupt(digitalPinToInterrupt(pinEncA));
for (int i=0; i<MAX_TIMERS; i++) {
if (instances[i] == this) {
instances[i] = nullptr;
break;
}
}
timerTriggered = false;
nPulsesSinceLastSample = 0;
}
void Motor::setPWM(uint8_t pwm, int8_t direction) {
if (direction > 0) {
analogWrite(this->pinIN1, pwm);
analogWrite(this->pinIN2, 0);
} else if (direction < 0) {
analogWrite(this->pinIN1, 0);
analogWrite(this->pinIN2, pwm);
} else {
analogWrite(this->pinIN1, 0);
analogWrite(this->pinIN2, 0);
}
}
void Motor::setPWM(float pwm) {
uint8_t pwm_u8 = (uint8_t)abs(pwm);
if (pwm > 0) {
this->setPWM(pwm_u8, 1);
} else if (pwm < 0) {
this->setPWM(pwm_u8, -1);
} else {
this->setPWM(pwm_u8, 0);
}
}
void IRAM_ATTR Motor::cbkEncA() {
if (analogRead(pinEncB) > 0) {
this->nPulses++;
this->nPulsesSinceLastSample++;
} else {
this->nPulses--;
this->nPulsesSinceLastSample--;
}
}
void IRAM_ATTR Motor::onTimer0() { if (instances[0]) instances[0]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer1() { if (instances[1]) instances[1]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer2() { if (instances[2]) instances[2]->cbkTimer(); }
void IRAM_ATTR Motor::onTimer3() { if (instances[3]) instances[3]->cbkTimer(); }
void Motor::cbkTimer() {
this->om = (1-alpha)*this->om + alpha*(float)this->nPulsesSinceLastSample * this->sampleFreq * this->phi2nPulses ; // Rot. velocity in rad/s
this->nPulsesSinceLastSample = 0;
this->timerTriggered = true;
}
long Motor::getNPulses() {
return this->nPulses;
}
bool Motor::getTimerTriggered() {
return this->timerTriggered;
}
void Motor::resetTimerTriggered() {
this->timerTriggered = false;
}
float Motor::getPhi() {
return this->nPulses / nPulses2Phi;
}
float Motor::getPhi_rev() {
return this->nPulses / nPulses2Rev;
}
float Motor::getOm() {
return this->om;
}
float Motor::getOm_rev_s() {
return this->om / (2*PI);
}
float Motor::getOm_rev_m() {
return this->om / (2*PI) * 60;
}