The Problem
One limitation of using Arduino core and libraries is the collision that may appear when using some libraries and explicitly the collision of using interrupts (ISR). This could occur when a library uses an interrupt service routine inside its source code, and the developer later may need to use the same ISR for another reason which will lead to a compilation error because of defining the same ISR twice: (.text+0x0): multiple definition of `__vector_xFor example: if SoftwareSerial, the built-in Arduino core library, is going to be included in the main sketch, then any of the three available ISRs for PCINT (Pin Change Interrupt) can’t be reused later (in Atmega328 for instance). These are used for detecting the change of input logical level (rise or fall) and a good amount of applications need both; the SoftwareSerial and PCINT ISRs as well. There are many other examples for losing access to very common and used ISRs like timers ISRs.
In this micro-blog, we’re going to discuss a suggested solution to keep the spirit of simplicity which Arduino SDK provides without losing every ISR a library use. This can be applied to the built-in libraries and core files and also to the future developed third-party libraries.
Suggested Solution: Handler (Hook) Functions
It wouldn’t be an elegant way to define the ISR by developer in the main application as the library must be a one-stop solution. The idea is to add a way to extend the ISR code later by adding a handler (A.K.A hook or call-back) function. This can be done by adding hook or handler functions available to be attached to any defined function by developer in the main code.Let’s have an example library: a demo library for Arduino UNO that defines an ISR for TIMER1_COMPA_vect to make interrupt every 1 second using Timer1. We will define an ISR and add something to it to make it extendable later.
This will be done by using what’s called a function pointer in C. It’s a pointer that points to a function instead of a variable. To know more about function pointers in C, read Geeksforgeeks handy introduction.
To implement this in a demo library:
1- Define a function pointer in the header
static void (*__timer1Hook)(void);
2- Add a method in the library class to assign the function defined in the developer main code to the previous variable.
void Timer1::timer1Hook(void (*function)(void)) { __timer1Hook = function; }
3- Add a call to the function pointer in the ISR.
ISR (TIMER1_COMPA_vect) { Serial.println("TIMER1 ISR"); __timer1Hook(); }
Complete Demo Example
demoLib.h
#ifndef demoLib_h #define demoLib_h #if (ARDUINO >= 100) #include "Arduino.h" #else #include "WProgram.h" #endif static void (*__timer1Hook)(void); class Timer1 { public: void start(); void timer1Hook(void (*function)(void)); private: void t1Init(void); }; extern Timer1 t1; #endif
demoLib.cpp
#include"demoLib.h" Timer1 t1; void Timer1::start() { this->t1Init(); } void Timer1::timer1Hook(void (*function)(void)) { __timer1Hook = function; } void Timer1::t1Init() { TCCR1B = (1<<WGM12) |(1<<CS12)|(1<<CS10) ; TCCR1A = 0; TCNT1 = 0; OCR1A = 15625; // T/C match it evey 1000ms TIMSK1 = (1<<OCIE1A) | (1<<ICIE1) ; } ISR (TIMER1_COMPA_vect) { Serial.println("TIMER1 ISR"); __timer1Hook(); }
Application.ino
#include "demoLib.h" void timer1extend() { Serial.println("Extended"); } void setup() { // put your setup code here, to run once: Serial.begin(9600); t1.start(); t1.timer1Hook(timer1extend); } void loop() { // put your main code here, to run repeatedly: }
Real Examples
- Wire library shipped with Arduino core use this trick to enable the developer to define his/her own on receive/request handlers for I2C bus. See: Wire.onReceive(handler) & Wire.onRequest(handler).
- Some third-party libraries like Timer library which implements a basic scheduler. The basic idea behind this library is also to define function pointers and process them inside the timer ISR and decide which handler(task) should be executed in each interrupt.