Michael Hanagan
Michael Hanagan

Reputation: 33

ESP32 class member variable not updating as expected in timer callback function

I have this code:

#include <Arduino.h>

#include "esp_timer.h"

//volatile int iCounter; <-- Uncommenting this line makes iCounter update in myTestFn()

class Test {
    public:       

        Test(void) {
            esp_timer_handle_t testTimer;
            esp_timer_create_args_t timerConfig;
            timerConfig.arg = this;
            timerConfig.callback = reinterpret_cast<esp_timer_cb_t>( testFn );
            timerConfig.dispatch_method = ESP_TIMER_TASK;
            timerConfig.name = "Test_Timer";
            esp_timer_create( &timerConfig, &testTimer );
            esp_timer_start_periodic( testTimer, 1000000 );
        }
        
        static void testFn(void *arg) {
          Test *obj = (Test *)arg;
          obj->myTestFn();
        }
          
        void myTestFn(void) {
            iCounter = iCounter +1;
            Serial.printf("Test succeeded! Counter: %d Time elapsed: %lu.%03lu sec\n", iCounter, millis() / 1000, millis() % 1000);
        }
    private:

        volatile int iCounter; // <-- This does not work, iCounter does not update in myTestFn()
};

void setup() {

    Serial.begin(115200);
    while(!Serial);
    Test justATestObject;
}

void loop() {
}

I expected the iCounter variable to be indexed with each call to myTestFn(), but the value remains 1.

When I move the declaration of iCounter outside the class Test{} it does update (which seems odd to me). How do I get "Test" class member variables to update inside myTestFn()?

Upvotes: 0

Views: 65

Answers (1)

Robert L
Robert L

Reputation: 106

You almost had the problem in hand and tried to program around it. This is one of those painful impedance mismatches between the C-oriented SDK and C++ dev.

Your problem is that you don't have a class instance from the interrupt context. The interrupt callback is a C function. Your interrupt callback is C++ and because you're using class data, you need a 'this' pointer that you don't have because the interrupt doesn't have a class instance. It works when you move the counter outside the class because it's then a global and not this->iCounter (remember that the 'this' is optional in even vaguely modern C++.)

There are two ways around this that I can think of. I'm typing from memory and the syntax is finicky, so this is more of a "launchpad" answer than a "paste this into your code" answer.

  1. Register a lambda for the interrupt handler and capture [this]. The syntax is something vaguely like: timerConfig.callback = []{Test:myTestFn};
  2. Use bind() to get a pointer bundle that includes the function pointer and the [this] reference. timerConfig.callback = std::bind(&Test::myTestFn, this)

I'm pretty sure that the answer is in this approximate space, though I surely have the precise C++ spellings wrong. Key words like PMF (pointer to [static] member function), capture this, and, generally, interrupt handling in C++ should help further google-fu.

Remember, too, that data that can be touched by an interrupt handler on ESP32 needs to be marked IRAM_ATTR. If you get a (LoadProhibited). Exception was unhandled. when trying to access your instance data, you'll know you've run afoul of this. Running printf in an interrupt handler is a pretty brave thing to do.

Upvotes: 0

Related Questions