The Atmega2560 has a total of 24 pins associated to the PinChange interrupt. You cannot change that. It is an internal property of the microcontroller.
As you already have seen yourself, you need to use extra chips to extent the available IO pins. There are different options to consider:
- IO-Port-Expanders (available for I2C or SPI) (for example the PCF8574. Don't know, if that is the best, just the port expander, that I also used myself)
- Shift-In (for inputs) or Shift-Out (for outputs) registers (often used are the 74HC595 (Shift-out register) and the 75HC165 (Shift-In register). I often used this tutorial as reference)
As you don't want to poll these chips all the time, I suggest, that you use one, which provides an additional interrupt pin. That pin will change its value, when one of the input changes. You could then connect this pin to one of the interrupt pins of the Atmega2560. When the interrupt triggers, you can read the states of the input pins.
But being connected via I2C, I suspect delays resulting in unequal treatment of player scores.
Indeed that wouldn't be good. So you should divide the pins up by their purpose, not by their association to a player. For example all end stop inputs, or all outputs for the stepper motors. Then you need to access, what timing requirements each group has. Then you can take the group with the lowest requirements and check, if and which chip would be able to handle the data fast enough. For example the "finish LED". You didn't specify, what exactly it does, but I guess, that it is not really time critical. So you could handle all finish LEDs via a single shift-out register.
Or take the inputs for the holes: I guess here you have some sensor, that triggers, when a ball passes it. Check how long the sensor get's triggered. Most likely (depending on the type of sensor) this depends on the speed of the ball. Take a speed, that you want to consider maximum (for example the pitching speed of a good baseball player, as these are really high), and calculate, how long the resulting pulse will be. The chip, that you choose, might be fast enough for this. So you could handle all ball hole inputs over extra chips (meaning chips providing extra input chips, like a Shift-In register or a port expander).
In case there's no need for the PCINT requirement, which pins can be used as an alternative?
Every pin capable of digital IO. That's nearly every pin on the Arduino Mega. Though that means, that you cannot use interrupts for them and you have to poll then constantly. That might be ok for you. It really depends on your exact timing requirements.
I thought polling was a "avoid where possible"-area?!
Yes and no. First you asked for the possibility of using other pins, so I mentioned that above. Though doing that means using no PinChange interrupt. Generally polling is bad, when
- you spend too much time on it, leaving no time for other functions of the code, or
- if the events, that you are polling, are too short for your polling code.
That can happen in the cases, that
- you use blocking code to poll (like waiting with a
while loop for a button press), which blocks all other functions from executing.
- your code needs a long enough event to register it (for example the signal from your hole sensors might be very short. If your polling interval is bigger than that, you will loose some events)
If you stay inside of the confinements and use non-blocking code, there is nothing bad with polling.
Let's take the often used Bounce2 library for Arduino. It helps you with reading buttons and handles the debouncing for you. It does not use any interrupts. You are expected to call
Bounce.update() often enough to catch all button presses, which is typically a few ms at max. But since the Arduino Mega runs on 16MHz, it is capable of way faster speeds than the few ms. Calling
Bounce.update() often enough is easy, when you don't write code, that stays a long time to execute. So no delays, except for maybe very short ones. Instead you can use the coding style of the
BlinkWithoutDelay example, that comes with the Arduino IDE. It uses the
millis() function as a time base to check repeatedly, if it is time to do something, and only do so, if it really is time for it. Much like you are baking a pizza by putting it into the oven and then regularly watch at your clock, if it is time to get the pizza out of the oven. You don't want to sit in front of the oven waiting for pizza. The analog of doing that is writing blocking code with
So all in all about polling vs interrupts: It depends on your requirements. Fast response times or short events often need interrupts, though you need to cope with limited usable pin numbers and different interrupts (which is a bit more complex to code). Polling - when done right - can give you big freedom and rather easy handling, while saving interrupt sources for more important functions. Though it poses bigger limits on response times and the duration of events. Also it depends strictly on the rest of your code.
Note, that in the above, terms like "time critical", "fast response times" and such are more meant for the region of a low number of microseconds. Timings in the millisecond range can easily be done by using the mentioned non-blocking coding style (either with
millis(), or for greater resolution and accuracy at low numbers of milliseconds
micros()). The principle is always the same and there are tons of tutorials about it on the web and even many questions and answers on this site. So you can google for this, to get a more detailed tutorial.