Arduino Trick: How To Share Interrupt Service Routines(ISR) between Libraries and Application
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:For 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.
Another option to implement such a hook is a “weak function”. An example is yield() in https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/hooks.c where yield is defined as:
void yield(void) __attribute__ ((weak, alias(“__empty”)));
If an application define yield, the applications yield() will be used instead of __empty(). The decision is made by the compiler at compile time and no extra variable to store the callback is needed.
Thanks for the input Jens.
You’re right, but sometimes you may need a special function for each interrupt according to the context. Another concern, yield function is called by the delay function in Arduino core (wiring.c).
I was imprecise. “yield” is just an example for a weak function used in arduino.
The idea is to use a weak function for __timer1Hook instead of a function pointer.
demoLib.cpp:
void timer1Hook(void) __attribute__ ((weak, alias(“__empty”)));
ISR (TIMER1_COMPA_vect)
{
Serial.println(“TIMER1 ISR”);
timer1Hook();
}
And an application could “register” a hook just by defining timer1Hook:
Application.ino:
…
void timer1Hook(void) {
// will be called (if defined) from TIMER1_COMPA_vect without a registration like “t1.timer1Hook(timer1extend);”
Serial.println(“Extended”);
}
…
It’s very clear now. Thanks for the tip.
One problem with this is that calling a function from an ISR on AVR is significantly more expensive that putting the code directly into the ISR (even without redirectability.) For example, given the C program:
volatile int a;
extern void incrementb();
ISR(INT0_vect) {
a++;
}
ISR(INT1_vect) {
incrementb();
}
The INT0 is ISR is about 20 instructions, while the INT1 ISR is about 35 instructions (and hasn’t even done the increment.)
This is because of the AVR ABI, which says about a dozen registers need no be saved by a called function. When an ISR happens, if the ISR KNOWS it doesn’t use those registers at all, it doesn’t need to save/restore them, either. But as soon as it calls some other function, it has to assume that that function MIGHT change one of those registers, which means an extra dozen push and pop instructions 🙁
Aside from that, you’ve also described how the arduino implements “attachInterrupt()”, and that code might be used as an example as well.
100% true. Thansks for your input.
Many aspects of Arduino waste the MCU resources especially functions related to GPIO. That’s why I avoid talking about optimization when I talk about Arduino APIs =).
Really? Are you kidding me? Are you talking about high performance in the Arduino platform? LOL. For what kind of applications using Arduino 15 machine cycles “is more expensive”, 4k processing? Come’on.
Both solutions, callbacks and with the week isr, are nice. Why didn’t Arduino’s designers takes this need into consideration? For any embedded system a tick is a must.
Hi Yahya Tawil, my name is Luis. I’ve been trying adapt your explanation in my own issue, but I’ve had some problems with this. part of my project consist on just run over timer 1 up to overflow and detect this event, but seems does not work becuase I have always the same time regardless if I change the prescaler. For now I have my code in a plain text in arduinoide, but final idea is will carry to a library. I hope you or an advanced follower from you could help with this, this is my code:
/*clase*/
static void (*__timer1Hook)(void);
class Timer1
{
public:
void start();
void timer1Hook(void (*function)(void));
private:
void t1Init(void);
};
extern Timer1 t1;
/*cpp*/
Timer1 t1;
void Timer1::start()
{
this->t1Init();
}
void Timer1::timer1Hook(void (*function)(void))
{
__timer1Hook = function;
}
void Timer1::t1Init()
{
TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10);
TCCR1A = 0;
TCNT1 = 0;
//OCR1A = 15625; // T/C match it evey 1000ms
TIMSK1 |= (1 << TOIE1); // enable timer overflow interrupt
sei();
}
ISR (TIMER1_OVF_vect)
{
Serial.println("TIMER1 ISR");
__timer1Hook();
}
/*ino*/
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:
}
That works but always with same frequency even whether TCCR1B |= (0 << CS12) | (1 << CS11) | (1 << CS10).
Thanks in advance ans congratulations for your articles they're very useful and practical.
Hello Luis
Thanks for your comment and support!
Regards your issue, the first question: do you use any Servo or Software Serial libraries that may re-initialize the Timer 1 later?
Your issue seems only related to T1 configuration not to the hook trick.
Hi, Yahya!
Thanks for your reply.
I only use Timer1, I do not have any another library or accesory that alters timer1. My problem is that only works with 64 prescaler and I would like with 8 prescaler TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10), but it does not.
Regards,
Luis
I see … After reviewing the code. I think this little nasty ‘or’ operation which made troubles for your test!
just try to remove it:
TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10); =>
TCCR1B =0;
TCCR1B = (0 << CS12) | (1 << CS11) | (0 << CS10); I think there is an old value of TCCR1B assigned in Arduino core. I hope this works for you. I tested with my Arduino. Best
Perfect!! thank you very much. I tested it and it is functioning even tough with 256 prescaler. I guess that TCCR1B comes with default config and requires be clearing first.
Again congratulations for this excellent blog.