I'm trying to use pin change interrupts to detect pressed buttons. Until now I have never worked with these kind of interrupts and there are some problems, so I want to make sure if this is the correct usage.

If I got the datasheet right the following things must be done to use a pin change interrupt:

  1. Set which PINs you want to control in the PCMSK register
  2. Enable the PINs register for pin change interrupt control (PCICR)
  3. Enable interrupts
  4. Use the corresponding interrupt vector

Project: Simple Moodlamp, Colors controlled via 4 Buttons.


  • Atmega168A-PU
  • 4 mini push button switches
  • MOSFETS to control my 3 Watt RGB LED

Here is the code I'm using which is not working as expected:

#include <avr/io.h>
#include <stdint.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define BUTTON1 (1<<PC5) 
#define BUTTON2 (1<<PC4) 
#define BUTTON3 (1<<PC3) 
#define BUTTON4 (1<<PC2) 

#define GREEN   (1<<PB1) 
#define BLUE    (1<<PB2) 
#define RED     (1<<PB3) 

void init() {

        // enable LED
        DDRB |= GREEN;
        DDRB |= BLUE;
        DDRB |= RED;

        // button pullups
        PORTC |= BUTTON1;
        PORTC |= BUTTON2;
        PORTC |= BUTTON3;
        PORTC |= BUTTON4;

        // pin change interrupts for buttons
        PCMSK1 |= PCINT13;
        PCMSK1 |= PCINT12;
        PCMSK1 |= PCINT11;
        PCMSK1 |= PCINT10;

        // enable pin change for buttons
        PCICR |= PCIE2;



ISR(PCINT2_vect) {

                PORTB = BLUE;

void ledTest() {

                PORTB ^= RED;
                PORTB ^= RED;
                PORTB ^= RED;
                PORTB ^= RED;

                PORTB ^= BLUE;
                PORTB ^= BLUE;
                PORTB ^= BLUE;
                PORTB ^= BLUE;

                PORTB ^= GREEN;
                PORTB ^= GREEN;
                PORTB ^= GREEN;
                PORTB ^= GREEN;

int main() {


        PORTB |= GREEN;

        while(1) {

Note: The buttons should be debounced. Since I'm trying to this step by step and it shouldn't mater for turning on the LED, I ignored it here.

Question: Is the way I'm trying to use the interrupts correct?

Problems with my setup:

  • Buttons1-3 are totally ignored.
  • Button4 is triggering a reset of the atmega

Things I checked:

  • Buttons are in no way connected to the reset PIN
  • Buttons are properly connected to GND if pressed
  • Buttons are not connected to GND if not pressed
  • Buttons work nicely if I utilize them without an interrupt, e.g.:

    if(!(PINC & BUTTON4)) { PORTB ^= BLUE; }

  • 16MHZ external crystal / internal crystal
  • Any errors in the routing
  • I'm using a 100nF capacitator between PWR and GND on the atmega
  • VCC(7), GND(8), GND(22), AVCC(20) are connected (since I don't need AREF, its not connected)
  • \$\begingroup\$ You need the PCIE1 flag (not PCIE2) and PCINT1_vect (not PCINT2) \$\endgroup\$ Sep 10 '13 at 2:23
  • \$\begingroup\$ Why PCIE1? I'm using the C Register, so if I count it would be A(PCIE0), B(PCIE1), C(PCIE2)? Anyway, I tried it with PCIE1 nad PCINT1_vect and there is no reaction if I press the buttons. \$\endgroup\$
    – echox
    Sep 11 '13 at 20:53
  • 1
    \$\begingroup\$ It can be a bit risky to assume ortogonality in such assignments. In this particular case, you would be nearly correct, except that the ATmega168 doesn’t have a port A. In any case, I went by the datasheet and pinout. Another tip-off was that you were using PCIE2, but setting bits in PCMSK1; that can’t possibly be right (Unfortunately, I don’t know why your revised sketch still isn’t working). \$\endgroup\$ Sep 12 '13 at 0:31
  • \$\begingroup\$ Thanks, I also understand that the combination of debugging software which depends on self build hardware is not that easy ;-) \$\endgroup\$
    – echox
    Sep 12 '13 at 9:44

Pin change interrupts are usually not a good way to detect button actions. This is because mechanical buttons bounce, and you will get lots of meaningless interrupts, and then you still have to do debouncing anyway.

A better way is to have a periodic interrupt, like every 1 ms (1 kHz rate). That's a long time on most processors, so the fraction of time spent in the interrupt will be small. Simply sample the button state every interrupt. Declare a new button state if you have seen the new state 50 ms in a row. 50 ms is longer than most buttons bounce, but is still short enough so that humans won't notice or care about the lag.

Note that this way you can also handle multiple buttons in the same periodic 1 ms interrupt. All you need is one counter for each button.

More on debounce time:

Occasionally, as in this case, someone says 50 ms is too long a debounce time. This is not true for ordinary buttons pressed by humans. It might be a issue perhaps in very timing-critical applications like a stopwatch, but so far I haven't run into one. I did test on this in the early 1980s, and lots of other people have too.

It is true that typical pushbutton bounce time is around 10 ms, with just about all settling by 25 ms. The limiting factor on debounce time is human perception. 50 ms is a bit shorter than where people start to notice a delay when they aren't looking for it. Even then, it takes a much longer time for it to be annoying. It can be possible in some cases for a human to detect a difference between 50 ms and 0 ms delay if they are specifically looking for it, but that is quite different from pushing a button and seeing something happen and not thinking about the delay.

50 ms is therefore a good debounce time because the delay is below the perception limit in ordinary applications, way below the annoyance limit, and well above the bounce time of most switches. I have found switches that did bounce for nearly that long, so you might as well push to the perception limit since there is nothing to loose.

I have done many products with firmware-debounced buttons using 50 ms debounce time. Not once did a customer mention even noticing a delay. They all accepted the buttons as working fine without issue.

  • 1
    \$\begingroup\$ 50ms might be too long for some cases (usually, 10-20ms is the limit of human perception, and that should be enough for debouncing), but the method described here is the way to go. \$\endgroup\$ Sep 9 '13 at 22:45
  • 1
    \$\begingroup\$ @Laszlo: No, 50 ms is not too long for the ordinary case. See addition to my answer. \$\endgroup\$ Sep 10 '13 at 12:35
  • \$\begingroup\$ I tried 50ms which works fine for me :-) I'm still curious why the pin change interrupt isn't working (beside the bouncing stuff), but this works :-) Thanks. \$\endgroup\$
    – echox
    Sep 11 '13 at 20:54

Pin change interrupts are a better way to debounce than polling. The interrupt usually goes through some logic such as a D-Flip Flop, or D-Latch. Although this is true, it is harder to implement this debounce routine with higher level compilers. Once the interrupt occurs the interrupt flag is not cleared and the interrupt enable is cleared until a delay has occurred. Once the delay has occured the state of the pin is checked and if it is still in the given state that triggered the interrupt the state of the button is changed and the interrupt flag is cleared and the interrupt enable is set. If not in the state that caused the initiate, the interrupt enable is set and the state stays the same. This frees up the processor for other tasks. Periodic interrupts waste time in the program.


"Pin change interrupts are usually not a good way to detect button actions."

Wrong. PC INT is the best option. If you use polling to check the state of a button, nothing will be performed most of the time. You waste much precious CPU time. PC INT allows actions to be performed only on request.

"This is because mechanical buttons bounce, and you will get lots of meaningless interrupts, and then you still have to do debouncing anyway."

Correct about bouncing. Yet, you should NEVER debounce a button/switch inside of an interrupt routine (same reason : waste of CPU time). ISRs are meant to be really short and efficient, code-wise. Just use hardware debouncing. Keep your software clean !

Hardware debouncing is more convenient, see here / RC debouncing + Schmitt trigger for reference. I've used it for countless times with PC INT, it never failed.

So yes, you can (and should) use PC INT to get a button state. But you also have to use proper hardware debouncing.

  • 2
    \$\begingroup\$ Software debouncing is a valid approach, and most of the time the little extra CPU overhead is irrelevant. Saying that you should usually debounce in hardware is questionable at best. Saying you have to use hardware debouncing in all cases is just flat out wrong. \$\endgroup\$ Jan 14 '17 at 19:56
  • \$\begingroup\$ In most applications the controller is running idle most of the time anyway, running the main loop. Also, CPU time required to perform an IO state check and potentially increment a variable is minimal. Implementing simple debouncing in software comes nearly for "free", hardware costs money. And don't laugh at a few cents, assembly cost money as well and if you run medium to high volumes of a product its not negligible. Its true that ISR time should be held short, but that is hardly an argument in this case. Its probably more critical if the PC INT ISR fires 50 times in a row due to bouncing. \$\endgroup\$
    – Rev
    Jan 14 '17 at 19:56
  • \$\begingroup\$ @Nelson, 'waste of CPU time' matters in some application and not in many others. You should qualify your answer for situation where CPU time is critical. \$\endgroup\$ Jan 14 '17 at 20:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.