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.