I am working on a project that requires a number of I/O pins for various purposes. I chose the ATTiny88 because it has plenty of GPIO pins, and is relatively inexpensive.

Here is the updated code per timemage, with many thanks!

#include <Arduino.h>
#include <TM1637Display.h>
#include "PinChangeInterrupt.h"

int counter = 0;
bool Update = false;
int currentStatePhase_B;
int lastStatePhase_B;

// Shutdown Output
#define SW 11

// Rotary Encoder Inputs
#define Phase_A 13
#define Phase_B 12
#define DIO 3
#define CLK 4

TM1637Display display(CLK, DIO);

void setup() {

  // Set encoder pins as inputs
  pinMode(SW, OUTPUT);

    // Read the initial state of Phase_B
    lastStatePhase_B = digitalRead(Phase_B);
    // Call updateEncoder() when any high/low changed seen
    // on interrupt 0 (pin 2), or interrupt 1 (pin 3)
    attachPCINT(digitalPinToPCINT(Phase_A), updateEncoder, CHANGE);
    attachPCINT(digitalPinToPCINT(Phase_B), updateEncoder, CHANGE);


void loop() {
    if (Update){
        // Show decimal numbers with/without leading zeros
    display.showNumberDec(counter, false);
    // Expect: ___0
        Update = false;

void updateEncoder(){
  delay (4);
    // Read the current state of Phase_B
    currentStatePhase_B = digitalRead(Phase_B);

    // If last and current state of Phase_B are different, then pulse occurred
    // React to only 1 state change to avoid double count
    if (currentStatePhase_B != lastStatePhase_B  && currentStatePhase_B == 1){

        // If the Phase_A state is different than the Phase_B state then
        // the encoder is rotating CCW so decrement
        if (digitalRead(Phase_A) != currentStatePhase_B) {
            counter --;
        } else {
            // Encoder is rotating CW so increment
            counter ++;

    if (counter > 1023){
      counter = 1023;
    if (counter < 0){
      counter = 0;

    // Remember last CLK state
    lastStatePhase_B = currentStatePhase_B;
    Update = true;

It is now capturing most, but not by any means all, interrupts, but the direction is highly unreliable. More often than not, it counts up regardless of which way the encoder is turned,and often counts down when the encoder is turned clockwise.

Here is what seems to be a good reference: ATtiny88 8-bit Microcontroller

  • 1
    forget about your code for now ... make sure that the interrupt actually works ... write a simple program that has one ISR (interrupt service routine) ... the ISR triggers when a pushbutton is pressed, and alternates the status of an LED at every press
    – jsotola
    Feb 23, 2023 at 0:56
  • 1
    you're overthinking the encoder program ... trigger on one channel only ... the state of the other channel indicates the direction of rotation
    – jsotola
    Feb 23, 2023 at 0:58
  • 4
    Here is what seems to be the best reference ... no, the manufacturer's website is the source of the best reference not some 3rd party site ... look at page 53 in ww1.microchip.com/downloads/en/DeviceDoc/doc8008.pdf
    – jsotola
    Feb 23, 2023 at 3:26
  • more info onlinedocs.microchip.com/pr/…
    – jsotola
    Feb 23, 2023 at 3:48
  • 1
    2′. Since updateEncoder() only cares about the rising edges of Phase_B (i.e. when it changes from 0 to 1), there is no point in calling it on the falling edges. Attach the interrupt on RISING events and remove the test on currentStatePhase_B and lastStatePhase_B (and remove those variables also). Feb 26, 2023 at 13:01

1 Answer 1


Interrupt types

One source says there are only two interrupt pins, ...

There are two "external interrupt" pins; that is the term you'll find in the chip's datasheet. This is the term that refers to the kind of pins that attachInterupt uses. These are the ones in the pinout labeled INT0,INT1,INTn. The "external" term may be confusing for a couple of reasons. Lots of external things cause interrupts, but this name is applied to when you're not talking about events tied to particular peripherals, like USART (serial peripheral) receiving a character. The special function of "external interrupt" is dedicated to dispatching interrupts based on signals on these INT0/INT1/INT# pins. There's one interrupt vector for each of these signals (two in your case). As you've probably seen in the documentation they can be configured to trigger on rising or falling edges, both edges, and low levels. So, they're right about that to the extent that they're talking about the "external interrupt" pins.

The "external" name is also confusing because of what else you've found:

Another source says there are quite a few Change Interrupt pins

This is also true. These are the "PCINT#" signal names listed in the chip pinout. These "pin change interrupts" are newer; not new, but newer. There exist(ed) AVR that had/have "external interrupts" but no "pin change interrupts". I'd like to think that if they were created at the same time they wouldn't have simply called the former ones "external", because these are no less external although they're less dedicated. There's only a single ISR/interrupt for each group of 8 PCINT pins. And unlike the others they only naturally detect that a pin has changed and nothing about the nature of that change (rising vs falling). But a lot of AVR support them and those that do have a significant number of them. In the case of your ATTiny88, all of the GPIO (Arduino "digital") pins are capable of dispatching a pin change interrupt. However, the Arduino AVR core doesn't provide a function for using them.

Modifying NicoHood's PinChangeInterrupt library

One of the people that used to be in my circle was NicoHood, who created this PinChangeInterrupt library. I have a foggy memory of helping with bits of it.

What he did was model it after the normal attachInterrupt function. So, you can use it almost as though you have the RISING/FALLING/CHANGE "external interrupt" capability on all of your PCINT# pins; in your case all of your pins. Underneath it is tracking the state of each port pin to determine what has changed and in what direction. In other words, using an interrupt that fires given a change on any of 8 pins together with variables to synthesize per-pin callbacks for not just change but also specifically rising and falling if you want. This is not perfect it is possible to lose short lived events and it takes longer to dispatch your handler function. But it's probably fine for what you're doing.

It does not support your chip out of the box. However you can modify to support the ATTiny88; partially if you're not picky. You should find it in the library manager as "PinChangeInterrupt" by NicoHood, currently at version 1.2.9. If you install that it should show up under your sketchbook/libraries directory. You will need to edit the file at path:


That's your local copy of this file.

Your ATTiny88's handling of PCINT is going to be nearly the same as the ATMega328P that's in the UNO. Or rather nearly a superset of it. The chips are closely related in a way. However, you have extra PCINT capable pins on the ATTiny88 that aren't available on the ATMega328P. So you can crudely hack in a line into this section to read:

#if defined(__AVR_ATmega328__) || defined(__AVR_ATmega328A__) || defined(__AVR_ATmega328PA__) || defined(__AVR_ATmega328P__) || defined(__AVR_ATmega328PB__) \
   || defined(__AVR_ATmega168__) || defined(__AVR_ATmega168A__) || defined(__AVR_ATmega168PA__) || defined(__AVR_ATmega168P__) || defined(__AVR_ATmega168PB__) \
   || defined(__AVR_ATmega88__) || defined(__AVR_ATmega88A__) || defined(__AVR_ATmega88PA__) || defined(__AVR_ATmega88P__) || defined(__AVR_ATmega88PB__) \
   || defined(__AVR_ATtiny88__) \
   || defined(__AVR_ATmega48__) || defined(__AVR_ATmega48A__) || defined(__AVR_ATmega48PA__) || defined(__AVR_ATmega48P__) || defined(__AVR_ATmega48PB__)

The addition of that second to last line above should make the library treat the ATTiny88 you have as though it were an ATMega328P. This compiles under ATTinyCore. I am not set up to test it on a real ATTiny88. But, I expect it will work if you try some examples.

Having done that, the library should be work on any of the pins that carry a PCINT0 through PCINT23 label. If you want it to work with PCINT24-PCINT27, that can be done, but requires more work. But given what you've said you probably don't need one of those four and they're not available on the DIP package which I'm guessing you have anyway.

Updating your code

You would to #include "PinChangeInterrupt.h" at the top of your code, and then your current code:

    attachInterrupt(digitalPinToInterrupt(Phase_A), updateEncoder, CHANGE);
    attachInterrupt(digitalPinToInterrupt(Phase_B), updateEncoder, CHANGE);

then becomes:

    attachPCINT(digitalPinToPCINT(Phase_A), updateEncoder, CHANGE);
    attachPCINT(digitalPinToPCINT(Phase_B), updateEncoder, CHANGE);

With luck, that should do it provided Phase_A and Phase_B under your core (e.g. ATTinyCore) map to GPIO/Arduino "digital" pins that carry a PCINT# signal where the number less than 24.

As I said, I have no way of testing it on real hardware right now. But I expect it will work. If not, it is the sort of direction you need to go in.

Doing PCINT yourself

You can also take a more direct approach with reading the datasheet and sorting out how to do what the library does for you, only manually. One of the examples NicoHood provided with the library is not really an example of using the library but rather an explanation as to what the library does in the form of code. Notice it doesn't actually include the library; it would operate stand alone. So if you want to understand the guts yourself by following the ATtiny88 datasheet, that's a place to start.

One reason you might want to do it yourself anyway is if your encoders are being operated quickly, to the point where you can't afford some of the extra effective dispatch time imposed by what the PinChangeInterrupt library is doing in sorting out which pin changed and how. If you handle PCINT yourself, you can make a point of putting your two encoder pins in completely different groups of 8 such that there is only one source of pin change interrupt for each ISR and so there is no need to check which pin caused the interrupt and to route that to another function.

  • 1
    +1, very nice answer. Just a small correction: “they can be configured to trigger on rising or falling edges or at high and low levels”. They can be configured to trigger on change (like PCINT), rise, fall or low, but not on high level. Feb 24, 2023 at 8:27
  • You're right. Brain on autopilot I guess. Will update it either on next natural edit or with in a couple of days if I have no other cause.
    – timemage
    Feb 24, 2023 at 12:44
  • Well, I guess I'm going to integrate your change now, because they haven't given an indication on a change and this is likely going to by my last post.
    – timemage
    Feb 24, 2023 at 21:25
  • I am sorry, I don't quite follow the conversation between the two of you. Feb 26, 2023 at 5:42
  • Meanwhile, the code is close to working. At least the interrupts are now occurring. They frequently get missed, however, and they are counting erratically. Instead of counting up when the encoder turns clockwise and down when it is turned counter-clockwise, more often than not iot counts up regardless of which way the encoder is turned, and occasionally it counts down regardless oif which way the encoder is turned. I am unsure what is causing this behavior. Feb 26, 2023 at 5:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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