Categories
algorithm Arduino software

Interrupts and C++ Class Instances

When you need to process an interrupt, there is nothing else you can do but process it. Most Arduino examples show how to do this in a ‘C’ context. Processing interrupts in a C++ class is a lot less trivial, as class scoping rules and constructs get in the way, and specific arrangements need to be taken to ensure data is processed correctly.

What is an Interrupt?

An interrupt is a response by the microprocessor to an event that needs attention from the software. Interrupt processing hardware alerts the processor to temporarily halt the current flow of code execution as soon as possible to process the event in a timely manner.

The processor responds by suspending software execution, saving its current state, and running some interrupt handler code (also called an interrupt service routine, ISR). Once the ISR is completed, the processor resumes normal activities by restoring the previously saved current state.

Interrupts are commonly used by hardware devices to indicate electronic or physical state changes that require real-time attention.

Processing Interrupts

To process an interrupt, an application first needs to tell the processor to attach a piece of code (the ISR) to a specific interrupt. The Arduino C++ extensions provide a number of functions that allow user applications to do this relatively easily.

const uint8_t SWITCH_PIN = 2;

volatile uint16_t switchCount;

void switchChanged(void)
{
  switchCount++;
}

void setup(void)
{
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switchChanged, CHANGE);
}

void loop(void)
{
  // do whatever needs to be done
}

The small application above simply counts the number of times digital pin SWITCH_PIN changes state. The ISR switchChanged() is used to increment a global counter on every change. The processor knows to call this ISR because we have used attachInterrupt() to connect the ISR with the interrupt.

The volatile keyword is used to tell the code compiler that the variable is shared between ‘normal’ code and the ISR. This stops any unintended data caching from corrupting the variable.

ISR in a C++ Class

It seems a relatively straightforward task to move the ISR into a class.

class cCounter
{
public:
  cCounter(uint8_t pinInt) : pinInt(pinInt) {}

  ~cCounter(void)
  {
    detachInterrupt(digitalPinToInterrupt(pinInt));
  }

  bool begin(void)
  {
    int8_t irq = digitalPinToInterrupt(pinInt);
  
    if (irq != NOT_AN_INTERRUPT)
    {
      pinMode(pinInt, INPUT);
      attachInterrupt(irq, globalISR, CHANGE);
      reset();
    }
    
    return(irq != NOT_AN_INTERRUPT);
  }

  inline void reset(void) { count = 0; }

private:
  // Define the class variables
  uint8_t pinInt;            // The interrupt pin used

  static volatile uint16_t count; // Encoder interrupt counter
  static void globalISR(void);
};

// Interrupt handling declarations required outside the class
volatile uint16_t cCounter::count;
void cCounter::globalISR(void) { cCounter::count++; }

The ISR needs to be declared as a static function as the address of this function needs to be determined at compile time for the attachInterrupt() to work properly (ie, it cannot be associated with an arbitrarily created instance of the class). This scoping requirement then cascades to the count variable, as the ISR needs to know its address.

We now have a problem. As the ISR is global there is only ever one instance of this ISR and count variable shared by all objects created from cCounter. So if we create 2 objects (as in the code below) expecting them to count different interrupts, they will update the same counter and not function as expected.

cCounter c1(2); 
cCounter c2(3);

void setup(void)
{
  Serial.begin(9600);
  if (!c1.begin()) Serial.println("Can't start c1");
  if (!c2.begin()) Serial.println("Can't start c2");
}

void loop(void)
{
  // do whatever
}

Handling ISR in many objects

There is a solution to this dilemma. Each object can be identified by its instance handler (the this-> pointer that is implicit to every C++ object), so if we could somehow know which handle relates to which globalISR() then the correct instance’s count can be updated.

We can exploit the ‘global’ nature of static class data by creating a table of instances that can be used by the ISR. However, each ISR must know which instance entry to use and this needs to be hardcoded in the ISR (see the code at the end of this article). For example, the table entry for instance [3] in the table will be used by globalISR3(). In other words, the association is hard coded in the ISR but flexible in the class.

Additionally, if we expect objects to be created and destroyed, then some form of global ISR management is needed to make sure that we don’t run out of globalISRx() functions. A static simple bitfield (ISRUsed) is defined to record, one ISR per bit, which ISRs have been allocated. The code uses a uint8_t which allows up to 8 globalISRx() to be managed. Using the same method, larger integers with more bits would allow for more concurrent objects.

Whilst this creates some complication, each instance of the class can now have its own globalISRx() and still manage to run instance related code:

  • begin() looks for an available ISR in the bitfield. If one is found, the instance handle is saved in the correct array entry, the associated ISR is found using a look up table and attached to the interrupt.
  • When an interrupt occurs, the globalISRx() uses the instance handle from the global myInstance array to call the instanceISR() which will update the correct counter.
  • When the object is destroyed, the interrupt is detached and the instance bitfield updated to allow reuse of the globalISRx().

By using the object instance as the ‘glue’ between the global and instance related parts of interrupt handling, the code below demonstrates an architecture independent (portable), scalable and sustainable solution to the problem of managing interrupts in the Arduino ecosystem.


#define MAX_ISR 8

class cCounter
{
public:
  cCounter(uint8_t pinInt) : pinInt(pinInt) {}

  ~cCounter(void)
  {
    detachInterrupt(digitalPinToInterrupt(pinInt));
    ISRUsed &= ~_BV(myISRId);   // free up the ISR slot for someone else
  }

  bool begin(void)
  {
    int8_t irq = digitalPinToInterrupt(pinInt);
  
    if (irq != NOT_AN_INTERRUPT)
    {
      pinMode(pinInt, INPUT);
  
      // assign ourselves a ISR ID ...
      myISRId = UINT8_MAX;
      for (uint8_t i = 0; i < MAX_ISR; i++)
      {
        if (!(ISRUsed & _BV(i)))    // found a free ISR Id?
        {
          myISRId = i;                 // remember who this instance is
          myInstance[myISRId] = this; // record this instance
          ISRUsed |= _BV(myISRId);    // lock this in the allocations table
          break;
        }
      }
      // ... and attach corresponding ISR callback from the lookup table
      {
        static void((*ISRfunc[MAX_ISR])(void)) =
        {
          globalISR0, globalISR1, globalISR2, globalISR3,
          globalISR4, globalISR5, globalISR6, globalISR7,
        };
  
        if (myISRId != UINT8_MAX)
          attachInterrupt(irq, ISRfunc[myISRId], CHANGE);
        else
          irq = NOT_AN_INTERRUPT;
      }
      reset();
    }
    return(irq != NOT_AN_INTERRUPT);
  }

  inline void reset(void) { count = 0; }

private:
  // Define the class variables
  uint8_t pinInt;            // The interrupt pin used
  uint8_t myISRId;           // This is my instance ISR Id for myInstance[x] and encoderISRx
  volatile uint16_t count; // Encoder interrupt counter

  static uint8_t ISRUsed;        // Keep track of which ISRs are used (global bit field)
  static cCounter* myInstance[]; // Callback instance for the ISR to reach instanceISR()

  void instanceISR(void) { count++; }   // Instance ISR handler called from static ISR globalISRx

  // declare all the [MAX_ISR] encoder ISRs
  static void globalISR0(void);
  static void globalISR1(void);
  static void globalISR2(void);
  static void globalISR3(void);
  static void globalISR4(void);
  static void globalISR5(void);
  static void globalISR6(void);
  static void globalISR7(void);
};

// Interrupt handling declarations required outside the class
uint8_t cCounter::ISRUsed = 0;           // allocation table for the globalISRx()
cCounter* cCounter::myInstance[MAX_ISR]; // callback instance handle for the ISR

// ISR for each myISRId
void cCounter::globalISR0(void) { cCounter::myInstance[0]->instanceISR(); }
void cCounter::globalISR1(void) { cCounter::myInstance[1]->instanceISR(); }
void cCounter::globalISR2(void) { cCounter::myInstance[2]->instanceISR(); }
void cCounter::globalISR3(void) { cCounter::myInstance[3]->instanceISR(); }
void cCounter::globalISR4(void) { cCounter::myInstance[4]->instanceISR(); }
void cCounter::globalISR5(void) { cCounter::myInstance[5]->instanceISR(); }
void cCounter::globalISR6(void) { cCounter::myInstance[6]->instanceISR(); }
void cCounter::globalISR7(void) { cCounter::myInstance[7]->instanceISR(); }

cCounter c1(2); 
cCounter c2(3);

void setup(void)
{
  Serial.begin(57600);
  
  if (!c1.begin()) Serial.println("Can't start c1");
  if (!c2.begin()) Serial.println("Can't start c2");
}

void loop(void)
{
  // do whatever
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s