Thursday, April 11, 2013

The Problem ( and Solutions ) With Arduino Interrupts

If you have ever tried to port an Arduino project that uses interrupts from one board type to another, you have probably experienced frustration, this includes the sample code throughout RCArduino.


The following post examines the differences in interrupts between the popular boards, the Arduino UNO, Micro, Mega, Mini and Leonardo. Based on this information we can modify sketches to run on the full range of 8-bit Arduinos.

The 8-bitArduino boards are based on one of three related chips -

ATMega328 - UNO and Mini
ATMega32u4 - Leonardo and Micro
ATMega2560 - Mega

Each of these chips supports two types of interrupts -

1) External Interrupts
These are flexible easy to use interrupts which can be triggered by rising, falling or changing signals. The disadvantage is that there are a limited number available on each chip type.

If we want to access more interrupts we need to look at the next type -

2) Pin Change Interrupts
The underlying chip in your Arduino supports a second type of interrupt however these interrupts are not directly supported by Arduino and need to be accessed through an additional library.

Differences In External Interrupts

The external interrupts are associated with specific digital pins on each chip type, the following table taken from the attachInterrupt reference page lists the available external interrupts and the associated Arduino pin on each chip -


Boardint.0int.1int.2int.3int.4int.5
Uno, Ethernet,
Mini
23
Mega25602321201918
Leonardo, Micro3201


The Arduino team have hidden some of the differences between the ATMega328 and ATM2560 so that attaching INT0 using the attachInterrupt function will attach an interrupt to digital pin 2 on both chips even though on the Mega digital pin2 is actually INT4.

The same logic has not been carried across to the ATMega32u4 Based leonardo. Notice how int0 and int1 are actually reversed on the Leonardo, this will be a major trap for people who are porting code from the UNO.


Does the Leonardo have four external interrupts ?
While the Leonardo appears to have 4 external interrupts, int2 and int3 are attached to digital pins 0 and 1 which are almost always reserved for serial input/output. So yes there are four interrupts, but two of them are only available by disabling serial functionality.

Differences In Pin Change Interrupts
On the Arduino UNO, pin change interrupts can be used to enable interrupts on any of the Arduino PINs to give access to a total of 19 interrupts (13 digital pins and 6 Analog pins).

I initially assumed that this was also possible on the Mega, Micro and Leonardo as well. It isn't.

Pin change interrupts are supported on the following Leonardo/Micro pins - 8,9,10 and 11.

Pin change interrupts are supported on Arduino Mega pins 10,11,12,13,14,15 and analog pins 6 to 15

Interrupts and RCArduino
These differences between the Arduino platforms will have been responsible for some of the difficulty that users have had in porting RCArduino code to Minis, Micros, Leonardos and Megas.

The good news is that now we have a full understanding of the inconsistencies between the different devices there should be no problem in modifying the sample sketches to run on them.

If your having trouble with a sample sketch, get in touch, in the meantime I will be updating some of the sketches to work across multiple boards.

Stay tuned

Duane B

22 comments:

  1. Could I get some advice on changing this to work on the micro?

    void interruptSetup(){

    // Initializes Timer1 to throw an interrupt every 2mS.
    TCCR1A = 0x00; // DISABLE OUTPUTS AND PWM ON DIGITAL PINS 9 & 10
    TCCR1B = 0x11; // GO INTO 'PHASE AND FREQUENCY CORRECT' MODE, NO PRESCALER
    TCCR1C = 0x00; // DON'T FORCE COMPARE
    TIMSK1 = 0x01; // ENABLE OVERFLOW INTERRUPT (TOIE1)
    ICR1 = 16000; // TRIGGER TIMER INTERRUPT EVERY 2mS
    sei(); // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED
    }
    ISR(TIMER1_COMPA_vect){ // triggered when Timer1 counts to 124
    cli(); // disable interrupts while we do this
    Signal = analogRead(pulsePin); // read the Pulse Sensor
    sampleCounter += 2; // keep track of the time in mS with this variable
    int N = sampleCounter - lastBeatTime; // monitor the time since the last beat to avoid noise

    ReplyDelete
  2. Hi,
    I cannot help based on the level of information provided, but rather than post more information here, try the Arduino forum where a larger number of people will be in a position to help.

    Duane B

    ReplyDelete
  3. Pin change interrupts supported on Arduino Mega are pins 10,11,12,13,14,15, 50,51, 52, 53 and analog pins 6 to 15

    ReplyDelete
  4. Hi, I am running a Mega.... pretty bare at the moment. I think I am current with all the libraries. Had things working on an UNO. Backed down to the MultiChannels sketch for reading and driving 3 servos.
    Got it working on the UNO fine
    On the MEGA, I have read working by using the interupt supported pins mentioned above.
    It refuses to write. I have tried the pens mentioned above, and most of the other I/O pins... Nothing. Scope shows a flat line. (Serial print confirms the read when I enable that).
    Pins are writing fine when I load an alternate sketch with the Servos.h library which is NOT present with the other sketch.
    Any Ideas? any tests to run??

    D

    ReplyDelete
    Replies
    1. Me again with the Mega.... I am not very experienced with this (but am with RC)
      Seems the writeMicroseconds portion of the library is not working?? No pin outputs... with my limited understanding, I tried to play with TCNT0 instead of 1, and Timer 3, not 1... but no luck with the combination writing to the port...

      D

      Delete
  5. Hi,
    I assume that your using code from one of the other posts on the blog, can you repeat your first comment on the post which contains the code you are using, its difficult to help without knowing which code you are using.

    Duane.

    ReplyDelete
    Replies
    1. I am using the code included as an example in the ZIP download 'sketch'... straight up....

      I am using inputs in the list above that are working (and known to work.)
      I am using any output I can think of and none work.

      // Assign your channel in pins
      #define THROTTLE_IN_PIN 10
      #define STEERING_IN_PIN 11
      #define AUX_IN_PIN 51

      // Assign your channel out pins
      #define THROTTLE_OUT_PIN 12
      #define STEERING_OUT_PIN 9
      #define AUX_OUT_PIN 52

      I have added various combinations of these statements in the sketch in an attempt to get it working.



      #define __USE_PORT_JK
      // #define NO_PORTA_PINCHANGES // ??
      #define NO_PORTC_PINCHANGES
      #define NO_PORTD_PINCHANGES

      // #define AVR_ATmega2560 1
      // #define MORE_SERVOS_PLEASE 1

      Best Regards,
      D

      Delete
    2. https://github.com/scottjgibson/RCArduinoFastLib
      Sketch is in the ZIP here.
      Worked on Uno,
      Reads but no write/pulse out on MEGA
      Tried a lot of things.

      D

      Delete
  6. Hi,
    The issue you are having is that there are two functions within the library that map Arduino UNO pins to the underlying ATMega328 Ports and pin numbers. This function does not currently support the Mega, however its easy to add mega support. The two functions are -

    // map an arduino pin number to the ATMega PORT that holds the pin
    volatile uint8_t* CRCArduinoFastServos::getPortFromPin(uint8_t sPin)
    {
    volatile uint8_t* pPort = RC_CHANNELS_NOPORT;

    if(sPin <= 7)
    {
    pPort = &PORTD;
    }
    else if(sPin <= 13)
    {
    pPort = &PORTB;
    }
    else if(sPin <= A5) // analog input pin 5
    {
    pPort = &PORTC;
    }

    return pPort;
    }

    // map an Arduino pin to the bit number in the relevant ATMega port
    uint8_t CRCArduinoFastServos::getPortPinMaskFromPin(uint8_t sPin)
    {
    uint8_t sPortPinMask = RC_CHANNELS_NOPIN;

    if(sPin <= A5)
    {
    if(sPin <= 7)
    {
    sPortPinMask = (1 << sPin);
    }
    else if(sPin <= 13)
    {
    sPin -= 8;
    sPortPinMask = (1 << sPin);
    }
    else if(sPin <= A5)
    {
    sPin -= A0;
    sPortPinMask = (1 << sPin);
    }
    }

    return sPortPinMask;
    }

    If you choose pin numbers that are higher than 20, you could hack this function to work for UNO pins < 20 and Mega pins > 20. Try this and at some point in the future I will add Mega support using a cleaner approach - using the pins > 20 approach will get you going until then.

    Duane B

    ReplyDelete
  7. You can find the mapping from Arduino Mega Pins to ATMega ports and pins here -

    http://arduino.cc/en/Hacking/PinMapping2560

    as an example, pin 21 is PD0 = PORTD bit 0
    pin 51 is PB2 = PORTB bit 2

    Duane B

    ReplyDelete
  8. Well, learned some things... then got in over my head.... The version below won't compile... i had something similar that would compile.... I was getting some interaction with the Rx, and some pulse output... but it was not at all right... at like 15.2hz and 65.6ms (long) period most of the time... the square wave was usually about 50% at 32.8ms, but sometimes if input changed it would drop down to a very short 0.48ms. one wave seemed to pwm a bit with input changes... others would 'switch... long or short'.
    The code below (notes) may give you some head start on the pin mapping... and which pins I think are good candidates for use (**). Anyway, I think I will start over and try again from fresh libraries as I can't seem to find my errors... seems I am still a ways away from getting ouputs.... Thanks for the help! D
    (Ultimately, I want 6 or more inputs and 6 or more outputs... and am using pins 20 and 21 for I2C 6 axis.) hence I am thinking... can I use some of the A8 to A15 range as inputs? and maybe 10 to 13 (or 15) and if needed someting in the 50-53 range as outputs.... I think my next attempt may focus more narrowly on these peices...

    // Easy to optimise this, but lets keep it readable instead, its short enough.
    // Mega do not use ports C and D, Use J and K (and B), Check E, F and H?
    // I am using 20, 21 to interface with 6050 6DOF I2C
    volatile uint8_t* CRCArduinoFastServos::getPortFromPin(uint8_t sPin)
    {
    volatile uint8_t* pPort = RC_CHANNELS_NOPORT;

    if(sPin <= 9)
    {
    pPort = &PORTD; // 2,3,5 are Port E
    }
    else if(sPin <= 13)
    {
    pPort = &PORTB; // 10,11,12,13 are port B ** was else if
    }
    else if(sPin <= 15)
    {
    pPort = &PORTJ; // 14 and 15 use J **
    }
    else if(sPin <= 53) // all legal between 14 and 56
    {
    pPort = &PORTB; // 50-53 are port B **
    }
    else if(sPin <= A7)
    {
    pPort = &PORTC; // A0 -A7 appear to be Port F (not C)
    }
    else if(sPin <= A15)
    {
    pPort = &PORTK; // A8-A15 are port K **
    }
    return pPort;
    }

    // Easy to optimise this, but lets keep it readable instead, its short enough.
    uint8_t CRCArduinoFastServos::getPortPinMaskFromPin(uint8_t sPin)
    {
    uint8_t sPortPinMask = RC_CHANNELS_NOPIN;

    if(sPin <= A15)
    {
    if(sPin <= 9)
    {
    sPortPinMask = (1 << sPin); // don't use for now
    }
    else if(sPin <= 13)
    {
    sPin -= 6;
    sPortPinMask = (1 << sPin); // for 10,11,12,13 **
    }
    else if(sPin <= 15)
    {
    sPin = 15 - sPin;
    sPortPinMask = (1 << sPin); // for 14 is J1 , 15 is J0 **
    }
    else if(sPin <= 53)
    {
    sPin = 53 - sPin;
    sPortPinMask = (1 << sPin); // for 50-53 only hack **
    }
    else if(sPin <= A7)
    {
    sPin -= A0;
    sPortPinMask = (1 << sPin); // Do not try yet
    }
    else if(sPin <= A15)
    {
    sPin -= A8; //or sPin -=A8 or 8 for A8 to A15 **
    sPortPinMask = (1 << sPin);
    }
    }
    return sPortPinMask;
    }

    ReplyDelete
  9. OK... I have something that is getting me some functionality... I won't clog up this board with it yet as it is kind of a mess right now... but If I can clean up what I have as best I can, I will post the relevent sections.... (I had changed to TCNT0 at some point, and not fully reverted :( ). Of course it will be kind of MEGA Only and I will do the best I can to filter the relevent pins that I feel can work..... D

    ReplyDelete
  10. A brief update... you did put me on the trail of a Hack to the files... I expect they are mega now and somewhat broke for an UNO, others, etc.
    What I am getting now confirmed... is I am Reading on pins A8 to A15
    I am writing Servo Pulses on D6to13 and D50to53 Think A8 to 15 can write also (confirmed A14&15 just now)
    I think most of the ones I am writing to are also able to read.
    Tried Pretty hard to get D14 and 15 (J) writing but failed.... Seems the rest are working which is good (This seems trivial now),
    The 'hack code currently is below FWIW...
    Any of the pins above that should be avoided for any obvious timer conflicts? I am using the 6050 'imu' on D20,21
    Thanks, D

    // Easy to optimise this, but lets keep it readable instead, its short enough.
    // Mega do not use ports C and D, Use J and K (and B), Check E, F and H?
    // I am using 20, 21 to interface with 6050 6DOF I2C
    volatile uint8_t* CRCArduinoFastServos::getPortFromPin(uint8_t sPin)
    {
    volatile uint8_t* pPort = RC_CHANNELS_NOPORT;

    if(sPin >= 6 && sPin <= 9)
    {
    pPort = &PORTH; // 2,3,5 are Port E Not working
    }
    else if(sPin <= 13)
    {
    pPort = &PORTB; // 10,11,12,13 are port B ** was else if
    }
    else if(sPin <= 15)
    {
    pPort = &PORTJ; // 14 and 15 use J ** Not Working
    }
    else if(sPin >= 50 && sPin <= 53)
    {
    pPort = &PORTB; // 50-53 are port B **
    }
    // else if(sPin <= A7)
    // {
    // pPort = &PORTC; // A0 -A7 appear to be Port F (not C)
    // }
    else if(sPin <= A15) //was sPin >= A8 && sPin <= A15
    {
    pPort = &PORTK; // A8-A15 are port K **
    }

    return pPort;
    }

    // Easy to optimise this, but lets keep it readable instead, its short enough.
    uint8_t CRCArduinoFastServos::getPortPinMaskFromPin(uint8_t sPin)
    {
    uint8_t sPortPinMask = RC_CHANNELS_NOPIN;


    if(sPin <= A15)
    {
    if(sPin >= 6 && sPin <= 9)
    {
    sPin -= 3;
    sPortPinMask = (1 << sPin); // don't use for now
    }
    else if(sPin >=10 && sPin <= 13)
    {
    sPin -= 6;
    sPortPinMask = (1 << sPin); // for 10,11,12,13 **Works
    }
    else if(sPin <= 15)
    {
    // sPin -= (15-sPin);
    // sPortPinMask = (1 << sPin); // for 14 is J1 , 15 is J0 **No
    }
    else if(sPin <= 53) //was sPin >= 50 && sPin <= 53
    {
    sPin = (53-sPin);
    sPortPinMask = (1 << sPin); // for 50-53 only hack **Works
    }
    // else if(sPin <= A7)
    // {
    // sPin -= A0;
    // sPortPinMask = (1 << sPin); // Do not try yet
    // }
    else if(sPin <= A15)
    {
    sPin -= A8; //or sPin -=A8 or 8 for A8 to A15 **NO
    sPortPinMask = (1 << sPin);
    }
    }
    return sPortPinMask;
    }


    ReplyDelete
  11. Hi, Good that you have enough pins working for your project, I know the code isn't pretty, but once you have enough outputs move on and enjoy building the rest of your project, I will eventually come back and build Mega/UNO support by detecting the board at compile time. Good job on the quick and dirty solution though.

    Duane.

    ReplyDelete
  12. Hi Duane,

    You said "While the Leonardo appears to have 4 external interrupts... but two of them are only available by disabling serial functionality." Can you explain how to disable serial functionality please? I thought that if my sketch did not contain "Serial1.begin()" or any other references to "Serial1", then interrupts 2 and 3 would work, but they don't. I have tried using "Serial1.end()" but no joy.

    Thanks,

    Paul

    ReplyDelete
  13. Hi again Duane, a correction for your Arduino Interrputs blog entry: "Pin change interrupts are supported on the following Leonardo/Micro pins - 8,9,10 and 11". Unfortunately a Leonardo has pin 11, but a micro does not.

    ReplyDelete
  14. Pin change interrupts are supported on Arduino Mega pins:-
    10,11,12, 50,51,52,53, A7,A8,A11,A14,A15

    ReplyDelete
  15. On a Micro can i still use pins 2 and 3 as regular interrupts if i'm doing the pin change interrupt thing? I'm using pins 8-10 now. Maybe i'm misunderstanding that PaulRB says pin 11 doesn't work. It's works for me.
    Thx.

    ReplyDelete
  16. Hi Duane, I've been following your posts and working on my own stuff to process RC signals. I was able to get a nano to do what I wanted, but that's just using 2 signals - so I'm looking at a micro.

    So from your post I see I can attach interrupts to pins 0,1,2,3 - but then you say "Pin change interrupts are supported on the following Leonardo/Micro pins - 8,9,10 and 11."

    So what's the deal here? Can I attachinterrupt on CHANGE to all 8 pins of a micro? 8-11? RISING/FALLING on 0,1,2,3 only?

    Thanks - really helpful site for this!

    ReplyDelete
  17. The 8bit Arduinos support two types of interrupt 1) External Interrupts and 2) Pin Change Interrupts. External interrupts are the ones you are already familiar with that can be accessed using the attachInterrupt function.

    Pin Change interrupts are a little more involved to use and a little more limited however there is a library PinChangeInt which overcomes these two issues. There is a bit about the library here - http://rcarduino.blogspot.ae/2012/03/need-more-interrupts-to-read-more.html

    When you have enough interrupts for your project using the external interrupts it makes sense to use them however when you need more, you will need to get familiar with pin change interrupts and the PinChangeInt library.

    If you use the library, the Nano you already have will give you a lot more interrupts (upto 19) than a Micro can.

    Duane B

    ReplyDelete
  18. Thanks DuaneB, sorry I didn't respond earlier. I'll try the pin change library and give it a go as well. I've got both a micro and nano.

    I may be playing a balancing act with the available pins, since I want to output lateral-g's on an LCD (tilt sensor + spi) and maybe have a few other sensors on my crawler.

    ReplyDelete