He uses a single hardware timer and supports multiple timed event in a chain where each event has a delta from the previous event.
as each event is processed the hardware timer is reset to the delta. multiple events may be invoked at the same time if a delta is zero. An event is invoke with a function pointer.
Going from one button to 6 buttons on the same port, tested on Wokwi:
// GFS_port_buttons 3/6/25
// this is NOT beginner code
// written for Uno Rev 3, uses port register read and mask of board pins 2 through 7
const byte buttonPin[ 6 ] = { 2, 3, 4, 5, 6, 7 }; // PIND bits 2, 3, 4, 5, 6, 7
const byte pinMask[ 6 ] = { 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
byte pinHistory[ 6 ];
byte pinIndex = 0;
void setup()
{
Serial.begin( 115200 );
Serial.println( F( "\n\nButton\n " ));
for ( byte i = 0; i < 6; i++ )
{
pinMode( buttonPin[ i ], INPUT_PULLUP );
pinHistory[ i ] = 0xFF;
}
}
void button()
{
static byte prevMillis = 0xFF;
if ( prevMillis != (byte) millis())
{
for ( byte i = 0; i < 6; i++ )
{
pinHistory[ i ] = pinHistory[ i ] << 1;
pinHistory[ i ] += (( PIND & pinMask[ i ] ) ? 1 : 0 );
}
prevMillis = (byte) millis();
}
}
void loop()
{
button();
switch ( pinHistory[ pinIndex ] )
{
case 0x7F :
Serial.print( F( "button " ));
Serial.print( pinIndex + 1 );
Serial.print( F( " release " ));
Serial.println( millis());
pinHistory[ pinIndex ] = 0xFF;
break;
case 0x80 :
Serial.print( F( "button " ));
Serial.print( pinIndex + 1 );
Serial.print( F( " press " ));
Serial.println( millis());
pinHistory[ pinIndex ] = 0;
break;
}
if (pinIndex < 5 )
{
pinIndex++;
}
else
{
pinIndex = 0;
}
}
You can see that this is an extension of the single button case. But what if the button pins are not all on the same port? The arrays would be different, the PINx registers would differ but that's about all. The point being that the coder needs to be comfortable in the details.
I will wait for a reply before making another example, matrix buttons.
Here is a version using Arduino digitalRead() more suited for many buttons, you can ask what or why for details.
It reads all the button histories on millis() change BUT only reads 1 button per void loop() execution. This allows for different tasks to read any button at any time however once read that detects a transition the history should be changed to 0 if press detected (history==0x80) or 0xFF if release detected (history==0x7F) to prevent the transition from triggering action over and over. Note that history == 0 (button held down) and history = 255 (button stays up) do not need changing as they are steady states.
These buttons can be on any pins.
In the case of matrixed buttons, different code should be used to read the matrix. A matrix is good for loads of buttons but harder to wire and needs 1 diode per button.
// GFS_buttons 4/9/25
// this is ~beginner code
// written for Uno Rev 3
const byte buttons = 6; // should be a small number... 20 to 30 or fewer to avoid void buttons() blocking
const byte buttonPin[ buttons ] = { 2, 3, 4, 5, 6, 7 };
byte pinHistory[ buttons ];
void setup()
{
Serial.begin( 115200 );
Serial.println( F( "\n\nButton\n " ));
for ( byte i = 0; i < buttons; i++ )
{
pinMode( buttonPin[ i ], INPUT_PULLUP );
pinHistory[ i ] = 0xFF;
}
}
void button( byte i )
{
pinHistory[ i ] = pinHistory[ i ] << 1;
pinHistory[ i ] += ( digitalRead( buttonPin[ i ] ) ? 1 : 0 );
}
void loop()
{
static byte pinIndex = 0;
static byte prevMillis = 0xFF;
if ( prevMillis != (byte) millis())
{
button( pinIndex );
if (pinIndex < buttons - 1 )
{
pinIndex++;
}
else
{
pinIndex = 0;
prevMillis = (byte) millis();
}
}
switch ( pinHistory[ pinIndex ] ) // button[ pinIndex ] returns pin history
{
case 0x7F :
Serial.print( F( "button " ));
Serial.print( pinIndex + 1 );
Serial.print( F( " release " ));
Serial.println( millis());
pinHistory[ pinIndex ] = 0xFF;
break;
case 0x80 :
Serial.print( F( "button " ));
Serial.print( pinIndex + 1 );
Serial.print( F( " press " ));
Serial.println( millis());
pinHistory[ pinIndex ] = 0;
break;
}
}