Arduino millis Timer

the 1st thing Doug Comer discusses in Operating System Design: the XINU Approach is timers. XINU Is Not UNIX

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.

timers are in important feature of an OS

Thank you very much for the book you introduced and the information you shared.

Post updated.

I played with using Wokwi first time, this works on Wokwi with Arduino Uno and button wired between pin 7 and GND:

// GFS_button  3/5/25
// this is NOT beginner code
// written for Uno Rev 3, uses port register read and mask of board pin 7

const byte buttonPin = 7;  // PIND bit 7
byte  pin7history = 0xFF;

void setup()
{
  Serial.begin( 115200 );
  Serial.println( F( "\n\nButton\n " ));
  
  pinMode( buttonPin, INPUT_PULLUP );
}


void button()
{
  static byte  prevMillis = 0xFF;

  if ( prevMillis != (byte) millis())
  {
    pin7history = pin7history << 1;
    pin7history += (( PIND & 0x80 ) ? 1 : 0 );
    prevMillis = (byte) millis();
  }
}

void loop()
{
  button();

  switch ( pin7history )
  {
    case 0x7F :
    Serial.print( F( "button release " ));
    Serial.println( millis());
    pin7history = 0xFF;
    break;

    case 0x80 :
    Serial.print( F( "button press " ));
    Serial.println( millis());
    pin7history = 0;
    break;
  }
}

I might go for a multiple buttons and matrix buttons version. I have those in older versions that don't use port registaer reads.

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;
  }
}