Accessing class members inside an interrupt handler

Viewed 750

I have a UART class that has instances of Fifo class (just a rough idea as of now), but if I do that, and have an interrupt handler in a source file, there wouldn't be a way for me to access a Fifo instance or any other Uart members.

// uart.hpp
class Uart {
      Fifo fifoRx;
      // ...
};

// uart.cpp
void UART_IRQ_Handler(void) 
{
   // have no way to access the Uart class
}

In C, I would have a Uart struct in the header, and perhaps define the Fifo instance as static in a source file and access it inside the IRQ handler, but not quite sure of going around this in C++ while maintaining encapsulation.

// uart.c
static Fifo fifoRx;

void UART_IRQ_Handler(void) 
{
   // fifoRx is accessible
}

Perhaps if you want to access the Uart class itself, one way would be to set the static pointer to the instance inside the UartInit(Uart *pUart) function

// uart.c
static Uart *psUart;

void UART_IRQ_Handler(void) {
    // access UART through psUart 
}

void UartInit(Uart *pUart) {
    psUart = pUart;
    // ...
}

But how would you go about doing something similar in modern C++ while keeping encapsulation intact?

5 Answers

I've faced the same problem and I've solved it using a somewhat dirty and complicated design. My solution is dependent on some hardware support, so it may not be applicable on your platform. My solution is developed on STM32F1 & STM32F4 uCs, which use Cortex M3 & M4 cores respectively. It should probably work on M0 too. Also, my solution uses some C++17 features, but it can be converted to C++11 and above with a little bit modification.

It's apparent that some hardware related classes, like the UART class you mentioned, are closely coupled with interrupt handlers. The uC can have multiple instances of these classes. This also requires that you need multiple instances of ISRs. Because they can't have arguments, and mapping multiple interrupt vectors to same function may cause problems if they have to determine the source of the interrupt.

Another problem about the ISRs is that their names are predetermined by the vendor supplied vector table code, sometimes written in assembly. As each vector has a hard-coded name, it's not possible to write a generalized code without some macro dark magic.

First, the ISR naming problem needs to be solved. This is the part that depends on hardware support. Cortex M3 & M4 cores gives you the ability to point different vector tables located on a different address. If the vector table is copied from FLASH to RAM, then it will possible to register any function with void ISR_func(void) signature as an ISR during runtime. M0 cores have limited vector table relocation capabilities but they still offer a way to move it to the start of RAM.

I think the details of moving the ISR vector table from FLASH to RAM is a little bit off topic for this question, but let's assume the following function is available, which resisters a function as the ISR of a given vector:

void registerISR(IRQn_Type IRQn, void (*isr)());

We can assume that the maximum number of class instances are known, as the number of UART hardware modules is known. We need the following components:

A static member array of Uart pointers, each pointing to an instance of Uart.

static constexpr unsigned MaxUarts {3};
inline static Uart* uartList[MaxUarts] {nullptr};

A function template of the ISR. These will be static member functions but in the end each instance will have its own dedicated ISR:

template <unsigned uartIndex>
void Uart::uartIsr()
{
    auto instance = uartList[uartIndex];
    instance->doSometing();
}

An static member array of function pointers, which points to all the possible instances of ISRs. This wastes some flash space due to code duplication, but shouldn't be much problem if the ISRs are small.

using isrPointer = void (*)();
inline static const isrPointer uartHandlers[MaxUarts] {
    &uartIsr<0>,
    &uartIsr<1>,
    &uartIsr<2>
}

Finally, the ctor of the Uart needs to register itself to the static array of Uart instances and it must also register the ISR to the vector table

Uart::Uart(/* Arguments, possibly the instance no */) {
    instanceNo = instanceNoAssigner++; // One can use a static counter
    uartList[instanceNo] = this;
    registerISR(this->getIrqNo(), uartHandlers[instanceNo])
}

So the Uart class header should look like this:

class Uart {
public:
    Uart(/* Arguments */);
private:
    inline static uint8_t instanceNoAssigner {0};
    
    void doSomething(); // Just a place holder
    IRQn_Type getIrqNo(); // Implementation depends on HW

    template <unsigned uartIndex>
        static void uartIsr();

    static constexpr unsigned MaxUarts {3};
    inline static Uart* uartList[MaxUarts] {nullptr};

    using isrPointer = void (*)();
    inline static const isrPointer uartHandlers[MaxUarts] {
        &uartIsr<0>,
        &uartIsr<1>,
        &uartIsr<2>
    }
}

template <unsigned uartIndex>
Uart::uartIsr()
{
    auto instance = uartList[uartIndex];
    instance->doSometing();
}

It has been a long post. I hope I didn't make too many errors as I ported it from one of my real-wold projects.

Update: Example vector table relocation and ISR registration code for STM32F407.

Some modifications are needed in linker script file. Vendor supplied linker script already has the .isr_vector section (obviously). I just added start & end markers.

/* The startup code into "ROM" Rom type memory */
.isr_vector :
{
  . = ALIGN(4);
  _svector = .;
  KEEP(*(.isr_vector)) /* Startup code */
  . = ALIGN(4);
  _evector = .;
} >ROM

I also created a new section at the beginning of RAM. For Cortex M3 & M4, vector table can be anywhere (with some alignment requirements). But for Cortex M0, the start of the RAM is the only alternative location, as they lack VTOR register.

/* Relocated ISR Vector Table */
.isr_vector_ram 0x20000000 :
{
  . = ALIGN(4);
  KEEP(*(.isr_vector_ram))
  . = ALIGN(4);
} >RAM

And finally, these are the two functions I use for relocation (called once during initialization) & registration:

extern uint32_t _svector;
static constexpr size_t VectorTableSize {106}; // words
uint32_t __attribute__((section(".isr_vector_ram"))) vectorTable[VectorTableSize];

void relocateVectorTable()
{
    // Copy IRQ vector table to RAM @ 0x20'000'000
    uint32_t *ptr = &_svector;
    for (size_t i = 0; i < VectorTableSize; ++i) {
        vectorTable[i] = ptr[i];
    }

    // Switch to new table
    __disable_irq();
    SCB->VTOR = 0x20'000'000ULL;
    __DSB();
    __enable_irq();
}

void registerISR(IRQn_Type IRQn, void (*isr)())
{
    uint32_t absAdr = reinterpret_cast<uint32_t>(isr);
    __disable_irq();
    vectorTable[IRQn + 16] = absAdr;
    __DSB();
    __enable_irq();
}

Somehow, GCC is clever enough to automatically make the least significant bit of raw function address 1, to indicate Thumb instructions.

You could try one of the following mechanism

  1. Introduce a public static member Fifo fifoRx
    // uart.h
    class Uart {
        public:
          static Fifo fifoRx;
          // ...
    
    };
    // Initialize static member of Uart Class
    Fifo Uart::fifoRx = {0};
    
    // uart.cpp
    void UART_IRQ_Handler(void) 
    {
       // directly access static member
       Uart::fifoRx.<memeber_access>
    }
  1. Option 2. Introduce a private static member and use a static getter/setter member functions to access/modify the fifoRx
    class Uart {
          static Fifo fifoRx;
          // ...
         public:
          // Add the getter, setter static functions here
          //eg: static <data_type> get_fifo_front(), 
          //eg: static add_element_to_fifo(<data_type> data)
          //eg: static remove_element_from_fifo_front()
             
    };
    
    // uart.cpp
    void UART_IRQ_Handler(void) 
    {
       // access using static member function 
       //Uart::get_fifo_front()
    }
    
    // Initialize static member of Uart Class
    Fifo Uart::fifoRx = {0};

Your C version and C++ version are not really comparable. In C you could have a Uart structure containing a Fifo structure - that would be comparable. There is no reason why what you are doing in C won't work in C++ (i.e. your C code is also valid C++ code).

The real question you should ask is why have you introduced the Uart class if you did not need such a thing in your C code. The answer to that rather determines an acceptable C++ solution.

Either way if your C solution is acceptable, the same solution can be applied to the C++:

static Uart uart1 ;

void UART1_IRQ_Handler(void) 
{
   ...
   uart1.fifoRx.put( UART1_DR ) ;
   ...
}  

However, you are then likely to have duplicate ISR code for each UART you support. For performance that might be desirable, but a more object oriented approach would be to have the ISR body as part of the class:

class Uart 
{
    public:
      Uart( UART uart ) : m_uart(uart) { ... } ;
      void isr()
      { 
        ...
          m_fifoRx.put( m_uart.DR ) ;
        ...
      }

    private:
      Fifo m_fifoRx;
      UART m_uart ;
};

static Uart uart1( UART1 ) ;
static Uart uart2( UART2 ) ;
void UART1_IRQ_Handler(void) 
{
    uart1.isr() ;
}

void UART2_IRQ_Handler(void) 
{
    uart2.isr() ;
}

In that way you only maintain the one ISR handling code and it applies to all supported UARTS, which is presumably the purpose of doing it this way?

You can make a static variable inside the class and add interrupt routines as a friend members.

your uart.hpp would look like:

#define NUM_UART 3
enum class uartCh {uart1=0, uart2, uart3};
extern "C" void UART1_IRQHandler ();
extern "C" void UART2_IRQHandler ();
extern "C" void UART3_IRQHandler ();

class uart
{
public:
  static uart* meUart[NUM_UART];

  void initInterrupt(const uartCh a_ch) 
  {
    /*do your initialization*/
    int chmem = static_cast<int>(a_ch);
    if (chmem >= NUM_UART) return;
    meUart[chmem] = this;
  }
  void myUARTHandler()
  {
  /*do something with your struct*/
  }
  
  friend void UART1_IRQHandler();
  friend void UART2_IRQHandler();
  friend void UART3_IRQHandler();
}

because you have static array of ptrs (static uart*), you need to allocate them in the cpp file. So your main.cpp is:

uart *uart::meUart[NUM_UART]{nullptr};
constexpr int UART1 = static_cast<int>(uartCh::uart1);
constexpr int UART2 = static_cast<int>(uartCh::uart2);
constexpr int UART3 = static_cast<int>(uartCh::uart3);
int main ()
{
  uart myUartClass;
  myUartClass.initInterrupt(uartCh::uart1);
}


extern "C" void UART1_IRQHandler ()
{
  uart::meUart[UART1]->myUARTHandler();
}

extern "C" void UART2_IRQHandler ()
{
  uart::meUart[UART2]->myUARTHandler();
}

you can also make a template out of that and avoid sending enum parameter to initialization function, but you get the idea.

You could declare the interrupt handler as a friend function so the handler could access private members of the class.

// uart.h
class Uart {
      Fifo fifoRx;
      friend void UART_IRQ_Handler(void) 
      // ...
};
// uart.cpp
void UART_IRQ_Handler(void) 
{
   // have no way to access the Uart class
}
Related