Прерывание при нажатии кнопки + устранение дребезга

Я хочу, чтобы функция прерывания выполнялась при каждом нажатии кнопки. Кнопка подключена к контакту 2 и GND. Таким образом, контакт переключается в состояние LOW всякий раз, когда нажимается кнопка. В дополнение к этому следует использовать надлежащее устранение дребезга. В качестве доказательства концепции функция прерывания должна просто переключать BUILTIN_LED всякий раз, когда нажимается кнопка.

Я пробовал много разных подходов, но не могу заставить их работать.

Это моя последняя итерация:

bool led_status = LOW; // текущее состояние выходного вывода
int buttonState; // текущее чтение с входного вывода
int lastButtonState = HIGH;  //предыдущее чтение с входного вывода

// следующие переменные являются беззнаковыми длинными, потому что время, измеренное в
// миллисекунды, быстро станет большим числом, чем может быть сохранено в int.
unsigned long lastDebounceTime = 0;  // последний раз, когда выходной пин был переключен
unsigned long debounceDelay = 50;    // время устранения дребезга; увеличить, если выход мерцает

int button_switch = 2; // вывод внешнего прерывания
bool initialisation_complete = false; // запретить любые прерывания до завершения инициализации



// ISR для обработки триггеров прерывания, возникающих от соответствующего кнопочного переключателя
// проверяем, не нажали ли вы кнопку только что
// (т.е. входной сигнал изменился с НИЗКОГО на ВЫСОКИЙ), и вы ждали достаточно долго
// с момента последнего нажатия, чтобы игнорировать любой шум:
void button_interrupt_handler()
{
  //статический длинный int elapse_timer;
  
  if (initialisation_complete == true) //можно запустить ISR только после завершения инициализации arduino
  {
    // новое прерывание, так что ладно, начнем новый процесс чтения кнопки -
    // теперь нужно дождаться отпускания кнопки и истечения периода устранения дребезга
    // это будет сделано в функции button_read

    int reading = digitalRead(button_switch);
    if ( reading != lastButtonState) // Если переключатель изменился из-за шума или нажатия:
    {
      lastDebounceTime = millis(); // сброс таймера устранения дребезга
    }

    // каким бы ни было чтение, оно было там дольше, чем устранение дребезга
    // задержка, так что примите это как фактическое текущее состояние:
    if ( (millis() - lastDebounceTime) > debounceDelay)
    {
      if (reading != buttonState) // если состояние кнопки изменилось:
      {
        buttonState = reading;
        if (buttonState == HIGH) // переключать светодиод только в том случае, если новое состояние кнопки — HIGH
        {
          led_status = !led_status;
        }
      }
    }
    digitalWrite(LED_BUILTIN, led_status); // устанавливаем светодиод
    lastButtonState = reading; // сохранить чтение. В следующий раз в цикле это будет lastButtonState:
  }
} // конец button_interrupt_handler


void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(button_switch, INPUT_PULLUP); // нет разрешения = изменить на INPUT_PULLUP
  attachInterrupt(digitalPinToInterrupt(button_switch), button_interrupt_handler, FALLING); //no res = изменить на ПАДЕНИЕ
  digitalWrite(LED_BUILTIN, led_status);
  initialisation_complete = true; // открыть обработку прерываний для бизнеса
}

void loop()
{
  //ничего не делать
}

Я не уверен, что делаю неправильно или почему это не работает.

EDIT: когда я нажимаю кнопку, вообще ничего не происходит.

, 👍0

Обсуждение

Я бы, наверное, выбрал более простой подход. Если ISR запущен, примите это и переключите светодиод, а затем установите таймер, как вы уже сделали (lastDebounceTime). Используйте таймер, чтобы последующие вызовы ISR не имели никакого эффекта. Когда таймер истекает (через 50 мс), ISR снова ведет себя нормально. Прерывание, однако, обычно не лучший способ обработки кнопки, и обычно достаточно просто опрашивать ее каждые X мс., @6v6gt

@jsotola Спасибо! Вы правы, я отредактировал вопрос., @user1584421


3 ответа


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

2

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

Это можно попробовать и в симуляторе: https://wokwi.com/projects/351021820324872788


const uint8_t ledPin = 13 ;
const uint8_t button_switch = 2;  // the button is wired between this pin and ground (LOW side)
const unsigned long debounceDelay = 100 ;    // the debounce time; increase if the output flickers

enum ButtonState  { PENDING_PRESS, IN_PRESS, PENDING_RELEASE} ;
ButtonState buttonState = ButtonState::PENDING_PRESS ;
uint32_t stateEnteredAtMs = millis() ;


void setup() {
  Serial.begin(115200);
  pinMode( ledPin, OUTPUT) ;
  pinMode( button_switch, INPUT_PULLUP) ;
  
}

void loop() {
  uint32_t ms = millis() ;
  static uint32_t lastButtonCheckAtMs = 0 ;

  if( ms - lastButtonCheckAtMs > 10 ) {

    switch ( buttonState ) {
 
     case ButtonState::PENDING_PRESS : {
        if ( digitalRead(button_switch)  == LOW ) {
          buttonState = ButtonState::IN_PRESS ;
          stateEnteredAtMs = ms ;
        }
        break ;
     }

     case ButtonState::IN_PRESS : {
        if ( digitalRead(button_switch) == HIGH )  {  
          if ( ms - stateEnteredAtMs < debounceDelay ) {
            stateEnteredAtMs = ms ;
            buttonState = ButtonState::PENDING_PRESS ;
          } 
          else {
            stateEnteredAtMs = ms ;
            buttonState = ButtonState::PENDING_RELEASE ;
            digitalWrite( ledPin, ! digitalRead(ledPin)) ;  // toggle led
          }
        }
        break ;
     }

     case ButtonState::PENDING_RELEASE : {
        if ( ms - stateEnteredAtMs > debounceDelay ) {
            stateEnteredAtMs = ms ;
            buttonState = ButtonState::PENDING_PRESS ;
          } 
        break ;
     }
    }
    lastButtonCheckAtMs = ms ;
  }
}

,

Спасибо! Работает ли этот код, когда нажатие кнопки приводит к LOW (это означает отсутствие сопротивления) или когда нажатие кнопки приводит к HIGH (это означает, что кнопка и резистор)?, @user1584421

@user1584421 user1584421 Кнопка подключена между землей и контактом Arduino. Это означает, что когда кнопка нажата, она воспринимается функцией digitalRead() как НИЗКАЯ. Я добавил ссылку на симулятор. Там тоже должно быть понятно, как он распаян. Надеюсь, я сделал это понятным., @6v6gt

Спасибо. Я не нажимал на эту ссылку раньше. Есть странное поведение, когда кнопка нажимается два раза одновременно. Это не регистрируется как два клика. Должно пройти какое-то время, прежде чем можно будет зарегистрировать второй щелчок. Кроме того, могу ли я взять код loop() и просто поместить его во внешнее прерывание? Я хотел бы, чтобы это было внутри функции прерывания., @user1584421

@user1584421 user1584421 Это не странное поведение, это фильтрация потенциальных отказов. Вы можете настроить параметр debounceDelay в скетче. Вы не можете запустить этот скетч в ISR, потому что вы не можете задержать ISR в ожидании таких событий, как отпускание кнопки и т. д. Однако, если вам нужно решение на основе ISR, попробуйте здесь, в симуляторе: https://wokwi.com /проекты/351221603927851607, @6v6gt

Хммм.. Спасибо, но вы можете себе представить, как можно выполнить нажатие кнопки внутри ISR? Не имеет значения, подключена ли кнопка к резистору или нет (вывод HIGH/LOW)., @user1584421

@ user1584421 Попробуйте код здесь https://wokwi.com/projects/351221603927851607 и скажите, почему он не соответствует вашим требованиям. Нажатие кнопки вызывает вход в ISR. Оказавшись внутри ISR, он не может ждать, пока активируются кнопки. Тем не менее, он гарантирует, что на него не повлияют отказы, блокируя нежелательные импульсы кнопки в течение настраиваемого периода в миллисекундах после первоначального обнаружения нажатия кнопки., @6v6gt

Спасибо! Я попробовал код на реальном оборудовании, и где бы я ни нажимал кнопку, светодиод кратковременно мигал, а затем выключался. Он не остается включенным/выключенным. На самом деле, светодиод был просто экспериментом, в моем практическом приложении я хочу построить конечный автомат. Возможно, переключаться между состояниями ENUM. Еще одно наблюдение, которое я должен отметить, связано с этой строкой last_entryAtMs = millis(); Переменная last_entryAtMs является статической. Разве там не должна быть защита от переполнения?, @user1584421

Точка переполнения недействительна, так как она инициализируется нулем при каждом вызове прерывания. К сожалению, я не могу редактировать комментарий сейчас., @user1584421

@user1584421 user1584421 Симулятор работает медленнее, чем в реальном времени, поэтому некоторые тайминги, возможно, придется увеличить для удобства использования, например: debounceDelay = 300 ; . Статические переменные инициализируются только один раз, НЕ при каждом проходе. Беззнаковое целое просто переворачивается при переполнении. millis() обновляется каждые ок. 50 дней, так что здесь не должно быть больших проблем., @6v6gt

Большое спасибо! Это сработало! Впрочем, не исключено, что мое творение будет стоять еще много-много дней. Поэтому необходимо позаботиться о переливе. Где и как именно это должно произойти?, @user1584421


1

Практически при любом запуске ISR состояние кнопки будет считываться как НИЗКОЕ, потому что ISR запускается падающим фронтом. Время от заднего фронта до показания очень короткое, около микросекунд, поэтому изменение уровня маловероятно.

Как уже предлагают другие, вы можете сделать ISR "глухим" в течение некоторого времени после принятого заднего фронта. Вы можете сделать это внутри ISR. Подход заключается в том, чтобы принимать последующие триггеры только в том случае, если они достаточно длинны после первого.

Имейте в виду, что некоторые кнопки подпрыгивают, даже если их отпустить. В таких случаях вы можете столкнуться с триггером.

В любом случае не основывайте решение на чтении состояния кнопки после ее спада.


Самое простое решение для чтения кнопок — циклический таймер с периодом в несколько миллисекунд. Возьмите образец каждые 50 мс или около того и определите изменение кнопки в программном обеспечении. Вам даже не нужно добавлять код устранения дребезга, если период больше, чем время дребезга вашей кнопки.

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

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

,

Спасибо. Если я не основываю чтение кнопки после ее заднего края, это означает, что я должен использовать резистор? Или я могу добиться этого без него?, @user1584421

Нет, конечно нужны определенные уровни на входе. С простой кнопкой SPST вам нужен резистор. Как еще цифровая логика могла бы обнаружить спадающий фронт? Однако вы можете использовать внутренний резистор, если он предусмотрен в устройстве., @the busybee


0

Взгляните на эту строку в своем коде — самое первое, что делается в ISR,

целое чтение = digitalRead(button_switch);

Я думаю, что причина того, что ваша программа «ничего не делает», заключается в том, что вы читаете состояние кнопки внутри ISR, и, поскольку вы запускаете прерывание по заднему фронту, вы ВСЕГДА будете видеть 0 в качестве состояния кнопки в момент эта точка. Он никогда не изменится, если вы прочитаете его один раз в начале ISR, он всегда будет =0, если только вы не останетесь в ISR и не дождетесь изменения, что является очень, очень плохой идеей.

Все остальные ваши сравнения и обнаружения терпят неудачу, потому что в ISR считывание кнопки всегда будет одним и тем же и = 0, потому что именно так вы попали в ISR. Что касается вашей программы, кнопка никогда не нажималась и не нажималась.

Это правда, в зависимости от того, как вы настроите кнопку; единственное изменение состояния будет обнаружено из-за инициализации, но это не главное.

Вы можете проверить это самостоятельно, переместив все свои переменные в volatile globals, а затем запустив в своем цикле следующее:

void loop()
{
Serial.print("lastButtonState ");
Serial.println(lastButtonState);
Serial.print("buttonState ");
Serial.println(buttonState);
Serial.print("reading ");
Serial.println(reading);
}

Возможно, у вас есть и другие проблемы, но эта мне запомнилась.

Я бы посоветовал вам взглянуть на этот проект и посмотреть, как он это делает — https://create.arduino.cc/projecthub/ronbentley1/button-switch-using-an-external-interrupt-7879df

Он считывает значение переключателя в прерывании [я не понимаю, зачем ему проверять значение, поскольку в его случае оно всегда будет ВЫСОКИМ]. Отличие в том, что он использует ISR только как триггер. Остальная часть чтения коммутатора для обнаружения изменений состояния выполняется вне ISR.

Кроме того, как только происходит прерывание, устанавливается статус «сработает», и после этого ISR просто прекращает работу, пока статус не изменится на «не срабатывает». и это делается вне ISR. В сопроводительной статье объясняется, что он делает в коде и почему.

Вы ничего не делаете [после setup()] за пределами ISR, и я думаю, что вам следует переосмыслить этот подход.

Я протестировал его программу, и она отлично работает. Отмечу, что функционально он определяет "прессу" как нажатие и отпускание кнопки (с соответствующим устранением дребезга). Таким образом, вы не увидите светодиодный переключатель, пока не нажмете и не отпустите кнопку. Это не всегда так, как мне бы хотелось, но это и не главное.

Еще два момента...

Следите за тем, чтобы ISR был как можно короче. У меня нет твердых доказательств, но, правильно это или нет, ваше выглядит длиннее, чем мне было бы удобно.

Наконец, я думаю, вам понравится эта замечательная ветка по millis() внутри ISR. Использование millis() и micros() внутри процедуры прерывания

Конечно, вы НЕ используете задержки внутри ISR, и нет ничего плохого в использовании millis() внутри ISR, но вы должны знать об этом поведении.

Это мои пять копеек - надеюсь, это поможет.

,