Одновременное чтение кнопки?

loop

В настоящее время я работаю над проектом, который требует одновременной проверки состояния двух кнопок. Каждой кнопке ВЫСОКОЕ состояние назначается один цикл if. Вот основная концепция:

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

Проблема в том, что проверка состояния кнопки таким образом требует, чтобы цикл S1_State _должен быть прочитан до цикла S2_State_, что дает игроку 1 преимущество. Хотя я не уверен, насколько ярко выражена эта проблема на практике, она определенно была заметна, когда я впервые пробовал этот проект с помощью кода Python на Pi с помощью аналогичных средств.

Можно ли одновременно проверять условия обоих циклов if? Если нет, есть ли другие способы обойти эту проблему?

, 👍4

Обсуждение

прочитайте свой код построчно..... представьте, что обе кнопки нажаты одновременно ....... это ожидаемое поведение?, @jsotola


5 ответов


Лучший ответ:

11

Есть очень простое решение вашей проблемы, и оно называется "прерывания".

Процессы внутри метода loop выполняются синхронно, поэтому всегда одно чтение будет выполнено перед вторым, и всегда одна проверка будет выполнена перед второй (даже если они находятся внутри одного if заявление)

К счастью, в ATmega есть способ имитации асинхронного поведения. Это называется "внешнее прерывание". Вы можете настроить ATmega внутри Arduino, чтобы остановить обработку метода loop и вместо этого немедленно запускать другой метод всякий раз, когда изменяется напряжение на одном из контактов. Вы «прерываете» выполнение loop, отсюда и название «interrupt». В Arduino Uno это можно сделать (только) на контактах 2 и 3.

https://www.arduino.cc/reference/en/language/ функции/внешние прерывания/присоединить прерывание/

Я сделал небольшую модель в Tinkercad

Arduino с двумя кнопками

Каждый раз, когда вы нажимаете левую кнопку, PIN2 Arduino замыкается на землю. Всякий раз, когда вы нажимаете правую кнопку, PIN3 Arduino замыкается на землю.

А теперь мой исходный код

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()
{
    //здесь ничего
}

А теперь немного пояснений. В setup мы сначала устанавливаем PIN2 и PIN3 как «вход с подтягивающим резистором». В нашем дизайне это означает, что когда кнопка не нажата, на этих контактах будет 5 В, но как только кнопки нажаты, напряжение падает до 0 В. Затем мы убеждаемся, что прерывания включены (они включены по умолчанию, но мне нравится показывать всем, кто читает код, что я буду их использовать), а затем делаем некоторые attachInterrupt магия. Мы делаем так, чтобы «падающий фронт» (изменение с 5 В на 0 В) на PIN2 и PIN3 немедленно запускал методы Player1ButtonPress и Player2ButtonPress.

Когда я играл с кнопками, они работали отлично.

И несколько слов о возможном читерстве. Удерживание кнопки нажатой не создает «падающий фронт» (напряжение на контакте постоянно равно 0), поэтому если вы добавите только некоторые глобальные переменные с информацией о том, когда игра начинается и кто выиграл. Затем добавьте некоторые условия в два метода, отвечающие за прерывания. Затем вы можете использовать метод loop, чтобы запустить игру и показать результат.

,

Дает ли обработка прерывания преимущество одному из игроков? Что происходит в случае ничьей?, @Craig

@Craig Чем больше я думаю об этом, тем больше я думаю, что все еще будет крошечное смещение, потому что ATmega будет анализировать значения контактов с частотой 16 МГц, поэтому все еще возможно, что произойдет «ничья», и одна кнопка будет выбрана процессор быть "победителем" неправильно. Но на этот раз решение займет всего 1 такт (6 наносекунд ). digitalRead занимает около 50 тактов, а нам нужно два из них. Таким образом, мы получили в 100 раз большее разрешение, и я думаю, что для «игры» этого достаточно, чтобы рискнуть событием. В электронике нет ничего идеального., @Filip Franik

Вы не можете подключить кнопку к прерыванию таким образом, вы должны принять меры предосторожности против дребезга: http://ohm.bu.edu/~pbohn/__Engineering_Reference/debouncing.pdf, @PimV

Условие победы в игре достигается при первом спадающем фронте. Отказ от дребезга не требуется, так как каждый последующий «отскок» будет считаться нажатием кнопки после окончания игры. Устранение дребезга имеет смысл только в том случае, если системе приходится ждать последующего ввода., @Filip Franik


3

Я думаю, вы хотите, чтобы через случайное время загорелся светодиод «Старт», и тот, кто первым нажмет кнопку, побеждает.

В вашем скетче есть некоторые проблемы:

  • Кажется, даже если кнопка нажимается слишком рано, она все равно учитывается. Вы должны проверить этот «обман». Поэтому сразу после того, как загорится светодиод «Пуск», проверьте, не нажаты ли какие-либо кнопки. Если это так, этот игрок проигрывает.
  • Предполагая, что игроки честны, вы проверяете их кнопки, которые, вероятно, в первую итерацию не будут нажаты (это будет немедленная скорость реакции). Однако, чем вы ждете 5 секунд; вероятно, оба игрока нажали свои кнопки, и вы сначала проверяете кнопку первого игрока. Поможет, если вы измените задержку на 1 мс, а то и вовсе уберете.

Непроверенное предложение:

int playerXWon = 0; // Еще ни один игрок не выиграл, 1 = игрок 1 выиграл, 2 = игрок 2 выиграл, 3 = ничья

void loop() 
{ 
   // Задержка на случайное время.
   P1_Time = random(2000, 5000);
   delay(P1_Time);

   CheatCheck();

   if (playerXWon == 0)
   {
      // Начинать
      digitalWrite(P1_Pin, HIGH); 

      checkFirstPress(); 
   }
}

void CheatCheck()
{
   // Проверяем, не нажал ли игрок свою кнопку слишком рано.
   // (Кстати, не имеет значения, если игрок нажмет и отпустит кнопку до того, как загорится светодиод.
   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() 
{
   // Выигрывает тот, кто нажмет первым.
   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); // Ошибка программирования
       break;
   }

   playerXWon = player;

   digitalWrite(P1_Pin, LOW);
}
,

О читерстве будет позже, я знаю об этой проблеме и добавлю позже. Я хотел сначала заняться этим, так как это важнее, и, в отличие от предотвращения мошенничества, я не знаю, как это сделать., @oh double-you oh

Я не уверен, насколько этот подход изменит ситуацию, поскольку, скорее всего, все равно потребуется, чтобы одна кнопка читалась за другой в любом цикле, в котором происходят эти 5 секунд. Не могли бы вы уточнить, что вы подразумеваете под «изменить задержку»?, @oh double-you oh

На самом деле, лучше убрать задержку. Когда он удален, кнопки проверяются много раз за мс, поэтому тот, кто нажмет кнопку первым, выиграет. Теперь он проверяется только каждые 5 секунд, и, вероятно, оба игрока легко нажимают кнопку в течение этого времени., @Michel Keijzers

Я также разделил большую функцию на более мелкие функции., @Michel Keijzers

Здесь много вещей, которые я еще не изучил, например функции переключения и использование циклов void в качестве функций, спасибо за указатели., @oh double-you oh

switch - это оператор if для более простой проверки нескольких значений (вместо if, else if, else..). Функция void — это просто взять фрагмент кода и поместить его отдельно. Это делает ваши программы более понятными и удобными в сопровождении. Также вы можете повторно использовать эти фрагменты, не копируя их каждый раз., @Michel Keijzers


12

Вместо использования функции digitalRead() высокого уровня доступ к регистрам порта низкого уровня. См. эту документацию.

Регистр порта состоит из одного байта, и каждый бит представляет собой один из цифровых входов Arduino.

Сделайте что-то вроде этого:

pin_status = PIND; //Входной порт D (контакты 0-7)
button_1 = bitRead(pin_status, 2); // цифровой контакт 2
button_2 = bitRead(pin_status, 3); // цифровой контакт 3

Значения считываются одновременно, поэтому возможно совпадение.

,

3

Я не был уверен, стоит ли публиковать это как ответ, поскольку в основном это еще несколько соображений по трем существующим ответам (для справки, это ответ Мишеля Кейзерса - с использованием digitalRead -, ответ Крейга - сразу прочитать переменную PIND - , Филипа Франика - использовать прерывания), но получилось довольно длинно и в один комментарий все это не уместилось.

То, что написали вы и другие респонденты, теоретически верно. Проверка кнопки игрока 1 перед игроком 2 дает преимущество игроку 1. Чего вам не хватает, так это... Сколько преимуществ?

Длительность механических действий (таких как нажатие кнопки) составляет около 50 мс*.

Поскольку я предполагаю, что ваше приложение предназначено для проверки времени реакции, время реакции человека составляет около 250 мс.

Кроме того, механические кнопки имеют "дребезги". Типичное время устранения дребезга находится в диапазоне 20-100 мс. Этого можно избежать, просто проверив первое ребро.

Игрок получает преимущество, если он признается победителем, даже если кнопка была нажата одновременно.


_* Я пытался найти источник для этого, но мне не удалось получить некоторые данные. Я пробовал с онлайн-секундомером и примерно одновременно нажимал две клавиши пробела (ноутбук и USB-клавиатура), получая результаты около 75 мс. Это не реальное значение, поэтому, если у кого-то есть измеренные значения или оценки, не стесняйтесь комментировать


Теперь предположим, что вы правильно закодировали свою программу, чтобы проверить совпадения и избежать задержек, поскольку они сильно повлияют на следующие измерения.

В случае digitalRead вы выполняете следующую серию действий:

  1. Чтение состояния контакта 1 – 3,6 мкс.
  2. Чтение состояния контакта 2 – 3,6 мкс.
  3. Проверить статусы и решить, кто выиграл - несколько десятков инструкций (около 1us)
  4. Цикл — пара инструкций (0,5 мкс)

60 инструкций взяты из этой темы (3,6 мкс = 57,8 инструкций на частоте 16 МГц); что касается других, это грубое измерение. Давайте предположим, что функция на самом деле производит выборку вывода в самом конце функции.

Теперь, поскольку вы проверяете ничью, если обе кнопки будут нажаты во время фаз 3, 4 или 1, вы получите ничью (поскольку обе кнопки будут считаться нажатыми). Если обе кнопки будут нажаты во время фазы 2, кнопка 2 будет помечена как нажатая, а кнопка 1 — нет, что дает преимущество этому игроку. Время, когда это происходит, составляет около 3,6 мкс. Таким образом, вы даете игроку с баттоном 2 преимущество в 3,6 мкс.

В случае PIND вы считываете кнопки точно в одно и то же время. Таким образом, преимущество равно 0.

В случае прерывания, когда прерывания EXT0 и EXT1 запускаются одновременно, выполняется EXT0, так как это было раньше в ISR. Следовательно, игрок, нажимающий кнопку на EXT0, имеет преимущество в один цикл проверки прерывания. Я признаю, что не знаю, как часто входы проверяются на прерывание EXT, но я предполагаю, что проверка выполняется каждый такт (следовательно, преимущество составляет 62,5 нс).


А теперь суммируем преимущества

  • цифровое чтение: 3,6 мкс
  • Вывод: 0
  • прерывание: 62,5 нс

Решение для меня довольно очевидное, и оно... Плевать! Даже в худшем случае вы на четыре порядка быстрее своего феномена. Вы даете одному игроку преимущество в 4 мс в игре 250 мс (0,0016% преимущества). Я почти уверен, что механические различия между двумя кнопками (возможно, одна из них немного жестче, или имеет немного больший размер, или имеет немного более низкий контакт) влияют на чтение гораздо больше, чем это.

В конце концов, с такой настройкой вы сможете получить точность, не превышающую несколько десятков миллисекунд. С другими настройками измерения (возможно, оптическими) вы можете снизить время до 1 мс. Добавление 4us преимущества одному игроку не повлияет на результат.

Постарайтесь сосредоточиться на остальной части программы, а для этой проблемы используйте любое решение, которое вы считаете более понятным.

ПРИМЕЧАНИЕ. Это основано на предположении, что разработана надлежащая программа с проверкой совпадений, без задержек и т. д. Программа, как вы написали в вопросе, не в порядке с этой точки зрения (как сейчас, она останется в цикле навсегда; если вы измените Sx_State на digitalRead, это станет примерно справедливым - что-то вроде 4us преимущества для первого игрока и 3,8us преимущества для второго игрока)

,

1

Здесь я должен согласиться с frarugi87. Несколько микросекунд, затраченных digitalRead() совершенно не важны для вашего приложения. Но позвольте мне добавить, что даже если бы ваши игроки были сверхлюдьми, способными время реакции менее миллисекунды, вы не даете преимущества ни одному из них, если вы попеременно отметите кнопку 1, затем кнопку 2, затем кнопку 1 и так далее. Причина в том, что в малое время окно между кнопкой чтения 1 и кнопкой чтения 2, вы даете преимущество для игрока 2: он выиграет, если будут нажаты обе кнопки одновременно. В следующем временном окне (между проверкой кнопка 2 и кнопка 1) такое же преимущество игрок 1.

В результате в игре присутствует незначительное количество случайностей: если и то, и другое кнопки нажимаются одновременно, результат вроде бы случайный. Но опять же, шансы на это настолько ничтожны, что в этом нет никакого смысла беспокоиться об этом.

Вот как я бы определил победителя. Я думаю, что этот код ясно что мы попеременно проверяем кнопку 1, затем кнопку 2, затем кнопка 1...

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