4

I'm currently working on a project that requires that I simultanously check the state of two buttons. Each button HIGH state is assigned to one if loop. Here's the basic concept:

void loop() {
  S1_State = digitalRead(S1_Pin);
  S2_State = digitalRead(S2_Pin);
  ButtonPRESSED = false;

  P1_Time = random(2000, 5000);
  delay(P1_Time);
  digitalWrite(P1_Pin, HIGH);

  while(ButtonPRESSED == false) {
    if (S1_State == HIGH) {
       Serial.print("Player 1 wins");
       Serial.print("");
       digitalWrite(P1_Pin, LOW);
       ButtonPRESSED = true;  
       delay(5000); } 
    if (S2_State == HIGH) {
       Serial.print("Player 2 wins");
       Serial.print("");
       digitalWrite(P1_Pin, LOW);
       ButtonPRESSED = true;  
       delay(5000); }

The problem is that checking for the button states this way requires that the S1_State loop _has to be read before the S2_State loop_, thus giving player 1 an advantage. Although I'm not sure how pronounced this issue is in practice, it certainly was noticable when I was first trying this project out through python code on a Pi through similar means.

Is it possible check the conditions to both if loops simultanously? If not, are there other ways to circumvent this problem?

  • read through your code line by line ..... imagine that both the buttons are pressed at the same time ....... is that the expected behavior? – jsotola Jan 14 at 23:46
11

There is a very easy solution to your problem, and it's called "interrupts".

The processes inside loop method are executed synchronously, so always one read will be done before second one, and always one check will be done before a second one (even if they are inside one if statement)

Fortunately there is a way of simulating asynchronous behavior in ATmega. It's called "external interrupt". You can setup ATmega inside Arduino to stop processing loop method and instead immediately run a different method whenever a voltage on one of the pins change. You are "interrupting" execution of loop hence the name "interrupt". In Arduino Uno this can be done (only) on pins 2 and 3.

https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

I made a little model i Tinkercad

Arduino with two push buttons

Whenever you push left button PIN2 of the Arduino gets shorted to ground. Whenever you push right button PIN3 of the Arduino gets shorted to ground.

And now my source code

void setup()
{
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);

  interrupts();
  attachInterrupt(digitalPinToInterrupt(2), Player1ButtonPress, FALLING);
  attachInterrupt(digitalPinToInterrupt(3), Player2ButtonPress, FALLING);

  Serial.begin(9600);
}

void Player1ButtonPress() {
  Serial.println("Player 1 wins");
}

void Player2ButtonPress() {
  Serial.println("Player 2 wins");
}

void loop()
{
    //nothing here
}

And now some explanation. In setup we first setup PIN2 and PIN3 as "input with pullup resistor". In our design this means that when button is not pressed those pins will have 5V on them, but as soon as the buttons are pressed voltages go down to 0V. Then we make sure that interrupts are turned on (they are on by default, but I like to show everyone who is reading the code that I will be using them) and then we do some attachInterrupt magic. We make that a "falling edge" (change from 5V to 0V) on PIN2 and PIN3 will immediately run methods Player1ButtonPress and Player2ButtonPress.

When I was playing with buttons they worked perfectly.

enter image description here

And a word about possible cheating. Keeping the button pressed does not create a "falling edge" (voltage on a pin is constantly 0) so if you only add some global variables with info when game starts and who won. Then add some conditions in the two methods responsible for interrupts. You can then use the loop method to start the game, and show result.

  • Does the interrupt handling give either player an advantage? What happens in the event of a tie? – Craig Jan 14 at 16:54
  • @Craig The more I think about it the more I think that there still will be a tiny bias, because ATmega will analyze values of pins with 16MHz frequency so there still is possible that a "tie" will happen and one button will be chosen by processor to be a "winner" incorrectly. But this time the decision will take only 1 clock cycle (6 nano seconds). digitalRead take about 50 clock cycles, and we need two of them. So we gained 100 times more resolution and I think it's enough for a "game" to risk suck event. Nothing is perfect in electronics. – Filip Franik Jan 14 at 17:20
  • You can't connect a button to an interrupt this way, you have to take debouncing precautions: ohm.bu.edu/~pbohn/__Engineering_Reference/debouncing.pdf – PimV Jan 19 at 12:57
  • The game win condition is achieved on first falling edge. Debouncing is unnecessary since every subsequent "bounce" will be counted as a button press after game has ended. Debouncing makes sense only if system has to wait for subsequent input. – Filip Franik Jan 19 at 18:39
12

Instead of using the high level digitalRead() function access the low level port registers. See this documentation.

A port register is one byte and each bit represents one of the digital inputs on the arduino.

Do something like this:

pin_status = PIND; //Input port D (pins 0-7)
button_1 = bitRead(pin_status, 2); // digital pin 2
button_2 = bitRead(pin_status, 3); // digital pin 3

The values are read simultaneously so it is possible to have a tie.

3

I think you want after a random time, a 'Start' LED to be lit, and whoever presses his/her button first wins.

There are some problems in your sketch:

  • It seems even when a button is pressed to early, the button is still taken into account. You should check for this 'cheating'. So check directly after the 'start' LED is lit, that no buttons are pressed. If so, that player loses.
  • Assuming the players are honest, you check for their buttons, which probably the first iteration it will not be pressed (that would be an immediate reaction speed). However, than you wait 5 seconds; probably both players pressed their buttons, and you check the button of the first player first. It will help, if you change the delay to 1 ms, or even remove it completely.

Untested proposal:

int playerXWon = 0; // No player won yet, 1 = player 1 wins, 2 = player 2 wins, 3 = draw

void loop() 
{ 
   // Delay for a random time.
   P1_Time = random(2000, 5000);
   delay(P1_Time);

   CheatCheck();

   if (playerXWon == 0)
   {
      // Start
      digitalWrite(P1_Pin, HIGH); 

      checkFirstPress(); 
   }
}

void CheatCheck()
{
   // Check if a player did not press its button too early. 
   // (Btw, it does not matter if the player presses and release its button before the LED goes on.
   S1_State = digitalRead(S1_Pin);
   S2_State = digitalRead(S2_Pin);

   if (S1_State == HIGH)
   {
      else if (S2_State == HIGH) 
      {
         PlayerWin(3);
      }
      else 
      {
         PlayerWin(2);
      }
   }
   else 
   {
      if (S2_State == HIGH) 
      {
         PlayerWin(1);
      }
   }
}

void checkFirstPress() 
{
   // Whoever presses first wins.
   while (playerXWon == 0) 
   {
      S1_State = digitalRead(S1_Pin);
      S2_State = digitalRead(S2_Pin);  

      if (S1_State == HIGH) 
      {
         if (S2_State == HIGH) 
         {
            PlayerWin(3);
         }
         else if (S1_State == HIGH) 
         {
            PlayerWin(1);
         }
      } 
      else if (S2_State == HIGH)
      {
         PlayerWin(2);
      }
   }
}

void playerWins(int player)
{
   switch (player) 
   {
   case 1:
      Serial.print("Player 1 wins\n");
      break;

   case 2:
      Serial.print("Player 2 wins\n");
      break;

   case 3:
      Serial.print("Both players win\n");
      break;

    default:
       assert(NULL); // Programming error
       break;
   }

   playerXWon = player;

   digitalWrite(P1_Pin, LOW);
}
  • The cheating pevention will come later, I'm aware of that problem and will add it later. I wanted to tackle this first as it's more important and unlike the cheating prevention I don't know how to do it. – oh double-you oh Jan 14 at 13:18
  • I'm not sure how much that approach would change things as it would most likely still require one button to be read after the other in whatever loop those 5 seconds take place in. Could you clarify what you mean by "change the delay"? – oh double-you oh Jan 14 at 13:23
  • Actually, it's better to remove the delay. When it's removed, many times per ms the buttons are checked, so whoever presses the button first will win. Now it is only checked every 5 seconds, and probably both players press the button within this time easily. – Michel Keijzers Jan 14 at 13:30
  • I also split the big function in smaller functions. – Michel Keijzers Jan 14 at 13:58
  • Lot's of stuff here I haven't learned yet like switch functions and using void loops as functions, thanks for the pointers. – oh double-you oh Jan 14 at 15:10
3

I was not sure about posting this as an answer, since it is basically some more considerations on the three existing answers (for reference, they are Michel Keijzers's one - using digitalRead -, Craig's one - read the PIND variable at once -, Filip Franik's one - use interrupts), but it became quite long and so a single comment could not fit all of this.

What you and the other answerers wrote is in theory correct. Checking the button of player 1 before player 2 gives an advantage to player one. What you lack is... How much advantage?

Mechanical actions (such as the pressing of a button) have a time duration that is around 50ms*.

Since I assume your application is a reaction time tester, human reaction times are around 250ms.

Moreover mechanical buttons have "bounces". Typical debounce times are in the range 20-100ms. You can avoid this by just checking the first edge.

A player gets an advantage if he is awarded as winner even if the button was pressed at the same time.


_* I tried to find a source for this, but I was not able to get some data. I tried with an online stopwatch and pressing the two spacebars (notebook and USB keyboard) roughly at the same time, obtaining results around 75ms. This is not a real value, so if someone has some measured values or estimations feel free to comment


Now, let's assume you properly coded your program so that you check for ties and avoid delays, since these will greatly affect the following measurements.

In the digitalRead case you are executing this series of actions:

  1. Read status of pin 1 - 3.6us
  2. Read status of pin 2 - 3.6us
  3. Check the statuses and decide who won - some tens of instructions (about 1us)
  4. Loop - a couple instructions (0.5us)

The 60 instructions comes from this thread (3.6us = 57.8 instructions @16MHz); as for the others it's a rough measurement. Let's assume that the function actually samples the pin at the very end of the function.

Now, since you are checking for ties, if both buttons get pressed during phases 3, 4 or 1 you will get a tie (since both buttons will be read as pressed). If both buttons get pressed during phase 2, button 2 will be marked as pressed and button 1 not, thus giving advantage to that player. The time when this happens is about 3.6us long. So you are giving a 3.6us advantage to button 2 player.

In the PIND case you are reading the buttons exactly at the same time. The advantage is thus 0.

In the interrupt case when both EXT0 and EXT1 interrupts are triggered at the same time EXT0 gets executed, since it is before in the ISR. Consequently the player pressing the button on EXT0 has one interrupt checking cycle of advantage. I admit I do not know how often the inputs are checked for the EXT interrupt, but I assume it performs a check every clock cycle (consequently the advantage is 62.5ns).


Now, let's sum up the advantage

  • digitalRead: 3.6us
  • PIND: 0
  • interrupt: 62.5ns

The solution is pretty obvious to me, and it is... Don't care! Even in the worst case scenario you are four order of magnitude faster than your phenomenon. You are giving one player a 4us advantage in a 250ms game (0.0016% of advantage). I'm pretty sure that the mechanical differences between the two buttons (maybe one is a bit stiffer, or has a slightly higher size, or has a slightly lower contact) affect the reading much more than this.

In the end, with this kind of setup you will reasonably be able to have an accuracy that is at most some tens of milliseconds. With other measurement setups (maybe optically based) you can go as low as 1ms. Adding 4us of advantage to one player will not influence the result.

Try to focus on the rest of the program, and for this problem use whichever solution you find more understandable.

NOTE: This is based on the assumption that a proper program is developed, with tie checks and no delays and so on. The program as you wrote in the question is not ok from this point of view (as it is now it will stay in the loop forever; if you change Sx_State to digitalRead it will become roughly fair - something like 4us of advantage to player one and 3.8us of advantage to player two)

2

I have to concur with frarugi87 here. The few microseconds taken by digitalRead() are completely inconsequential for your application. But let me add that, even if your players were super-humans capable of sub-millisecond reaction times, you are not giving an advantage to any of them if you alternatively check button 1, then button 2, then button 1, and so on. The reason is that, in the small time window between reading button 1 and reading button 2, you give an advantage to player 2: he will win if both buttons are pressed simultaneously. In the following time window (between checking button 2 and button 1) the same advantage goes to player 1.

The net result is that the game has a tiny amount of randomness: if both buttons are pressed simultaneously, the result is seemingly random. But again, the chances of this happening are so tiny that it makes no sense to worry about it.

Here is how I would determine the winner. I think this code makes clear that we are alternatively checking button 1, then button 2, then button 1...

int find_winner()
{
    for (;;) {
        if (digitalRead(S1_Pin) == HIGH)
            return 1;
        if (digitalRead(S2_Pin) == HIGH)
            return 2;
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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