Hi all,
While I may be new to this group and fairly new to Arduino I have over 20 years of experience working in embedded environments with a variety of operating systems and embedded platforms. When I went to work on my own rotary encoder library for an Arduino project I'm working on I came across a glaring limitation in the Arduino interrupt service routine implementation.
In just about every platform I've worked on except MS DOS, it is possible to register a user-defined pointer along with an ISR function pointer. This is extremely useful, especially in cases where there may be one ISR that is shared between multiple devices and it is especially useful in the C++ case.
With the current Arduino implementation with attachInterrupt it requires a different function pointer for every single GPIO that's used in order to tell which GPIO caused the interrupt. While this is nice for the simple case and for beginners, it causes many problems for more complex cases.
A perfect example of the problem is immediately visible with the Encoder library. That library jumps through great hoops in order to support more than one instance of a rotary encoder, requiring a large number of ISR functions, one per GPIO pin used. On top of this, it requires static variables and all sorts of hacks in order to do this. This can all be greatly simplified if some parameters are passed to the ISR.
In my case I happened to be working with the ESP32, but this is not specific to any particular Arduino platform. What I propose to do is to add another function, attachInterrupt2, which would allow a user-defined pointer to be passed to the ISR. This would allow the class this pointer, for example, to be passed to the ISR. This would make having multiple instantiations of a class that uses interrupts simple, without requiring multiple static ISR functions.
In my case, I implemented the function as:
void attachInterrupt2(uint8_t pin, void (*)(int pin, int value, void *data),
int mode, const void *data);
Now that I think about it it might make sense to rename pin to interrupt, though pin is the name used in esp32-hal-gpio.h. The value field is the value of the particular GPIO pin, but I think this can be removed from what I am proposing. The most important change is the data field. This makes it possible to make an ISR routine truly generic within a class. A user-defined data structure would be automatically passed to the ISR function or it could even be the class this pointer.
When I rewrote my own Encoder class, I was able to eliminate all of the ugly hacks and kludges that were implemented due to the current limited ISR implementation. The code is a lot cleaner and smaller. The only additional memory usage is that instead of having just an array of function pointers for the ISR handlers there is now an array of function pointers and void pointers.
My RotaryEncoder class looks like this compared to the existing Encoder class:
class RotaryEncoder
{
struct RotaryEncoderIsrData {
RotaryEncoder *encoder;
uint8_t bit;
};
int32_t position;
uint8_t pins[2];
uint8_t state;
bool pullup;
RotaryEncoderIsrData isrData[2];
public:
RotaryEncoder(uint8_t p1, uint8_t p2, bool pullup = true)
{
position = 0;
state = 0;
pins[0] = p1;
pins[1] = p2;
isrData[0].encoder = this;
isrData[1].encoder = this;
isrData[0].bit = 0;
isrData[1].bit = 1;
this->pullup = pullup;
}
void attach()
{
pinMode(pins[0], pullup ? INPUT_PULLUP : INPUT);
pinMode(pins[1], pullup ? INPUT_PULLUP : INPUT);
if (!pullup) {
digitalWrite(pins[0], HIGH);
digitalWrite(pins[1], HIGH);
}
noInterrupts();
delayMicroseconds(2000);
position = 0;
state = 0;
if (digitalRead(pins[0]))
state |= 1;
if (digitalRead(pins[1]))
state |= 2;
attachInterrupt2(digitalPinToInterrupt(pins[0]), isr, CHANGE, &isrData[0]);
attachInterrupt2(digitalPinToInterrupt(pins[1]), isr, CHANGE, &isrData[1]);
interrupts();
}
int32_t read()
{
int32_t pos;
noInterrupts();
pos = position;
interrupts();
return pos;
}
void write(int32_t pos)
{
noInterrupts();
position = pos;
interrupts();
}
private:
void update(int bit, int value)
{
uint8_t newState;
static const int8_t change[16] = { 0, 1, -1, 2, -1, 0, -2, 1, 1, -2, 0, -1, 2, -1, 1, 0 };
if (value)
newState = state | (1 << bit);
else
newState = state & ~(1 << bit);
position = position + change[state | (newState << 2)];
state = newState;
}
static void isr(int pin, int value, void *data)
{
struct RotaryEncoderIsrData *iDat = (struct RotaryEncoderIsrData *)data;
iDat->encoder->update(iDat->bit, value);
}
};
RotaryEncoder knob(27, 12);
While I don't have all the fancy optimization of the other class, it is much cleaner since only a single ISR function is needed to handle any number of class instantiations and GPIO pins. Compare this to Encoder.h. In the Encoder class there is a public array of data passed to the update function and there are up to 59 instantiations of the ISR function!!! This gets really ugly because of the lack of a user-defined pointer passed to the ISR function. Granted, I don't have all of the optimizations of the Encoder library for the AVR but then again with the ESP32 I have a lot more horsepower (and the Encoder library doesn't work on the ESP32 anyway). I'm not complaining about the Encoder library either because it it stuck working within the limited constraints that the current Arduino attachInterrupt function provides.
-Aaron