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
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);
attachPCINT(digitalPinToPCINT(Phase_A), updateEncoder, CHANGE);
attachPCINT(digitalPinToPCINT(Phase_B), updateEncoder, CHANGE);
With luck, that should do it provided
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.
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
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
RISINGevents and remove the test on
lastStatePhase_B(and remove those variables also).