Урок 6. Обработка дребезга контактов кнопки. Интерфейс связи между программными блоками.

Arduino UNO R3

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

Предыдущий урок     Список уроков     Следующий урок

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

 

Программа управления светодиодом.

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

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

Т.е. программа должна отлавливать фронт кнопки или перепад состояния сигнала. Есть два фронта сигнала:

  • с высокого состояния в низкое ( --_ );
  • с низкого состояния в высокое (_--).

Нажатие кнопки будет соответствовать перепаду сигнала из высокого в низкое. Это событие мы и должны выделить.

/*  Программа sketch_6_1 урока 6
 *  На каждое нажатие кнопки инвертирует состояние светодиода
 *  Работает неправильно из-за дребезга контактов */
  
#define LED_PIN 13     // номер вывода светодиода равен 13
#define BUTTON_PIN 12  // номер вывода кнопки равен 12

boolean buttonState;      // состояние кнопки
boolean buttonPrevState;  // предыдущее состояние кнопки
boolean ledState;         // состояние светодиода

void setup() {
  pinMode(LED_PIN, OUTPUT);        // определяем вывод 13 (светодиод) как выход
  pinMode(BUTTON_PIN, INPUT_PULLUP);  // определяем вывод 12 (кнопка) как вход
}

void loop() {

  buttonState= digitalRead(BUTTON_PIN); // записываем состояние кнопки в переменную buttonState
 
  if ( (buttonPrevState == HIGH) && (buttonState == LOW) ) {
   
    // предыдущее состояние кнопки - отжата, а текущее - нажата
    ledState= ! ledState;             // инверсия состояния светодиода
    digitalWrite(LED_PIN, ledState);  // запись состояния светодиода из переменной нв выход   
  }
 
  buttonPrevState= buttonState;         // предыдущее состояние кнопки = текущему
}

Выделение события происходит в конструкции

if ( (buttonPrevState == HIGH) && (buttonState == LOW) )

&& это логическое И, производимое над условными выражениями. В результате его применения будет выработано условие истинно, если оба выражения истинны. В данном случае блок оператора if будет выполнен, если одновременно предыдущее состояние кнопки = HIGH и текущее состояние кнопки = LOW. Т.е. если кнопка была отжата в предыдущей проверке, а  сейчас нажата.

По этому условию инвертируется состояние светодиода.

В принципе, не сложная программа. Но загрузите ее в плату Ардуино и проверьте, как она работает.

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

 

Дребезг контактов кнопки.

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

Дребезг контактов

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

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

Надежная программа должна обязательно обрабатывать сигналы с  кнопок и механических датчиков.

Как бороться с дребезгом контактов?

Существует несколько способов обработки дребезга контактов. Описанный ниже – один из самых надежных. Еще один способ я опишу в следующих уроках.

Так вот. Решение достаточно очевидно. Надо не реагировать на частое переключение сигнала. Не может кнопка быть нажата на время, например,  0,0001 сек. Значит это дребезг. Надо дождаться, когда в течение определенного времени состояние кнопки будет стабильным, и только тогда принимать решение. А на частые переключения сигнала не реагировать.

 

Программа устранения дребезга контактов кнопки.

Давайте напишем такую программу. Чтобы быть ближе к практическому программированию, выделим в программе блоки и постараемся правильно оформить интерфейс между ними.

Существуют готовые функции обработки сигналов с кнопок. Вызываешь функцию с номером вывода в качестве аргумента, и она возвращает состояние вывода, “очищенное” от дребезга. Но операция устранения дребезга занимает значительное время, не меньше длительности переходного процесса, как правило, 10 мс. И все это время программа будет ждать результата работы функции. А кнопок может быть несколько. Да и программе, кроме чтения состояния кнопок, еще что-то надо делать, и не все процессы можно надолго прерывать.

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

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

Программа состоит из двух основных блоков в бесконечном цикле loop():

  • блок обработки сигнала кнопки;
  • блок управления светодиодом.

Что нам может потребоваться в результате обработки кнопки. Какие переменные создавать. Как правило, необходимы только два признака:

  • Один показывает  текущее состояние кнопки (нажата или отжата).
  • Второй сообщает о том, что кнопка была нажата (был перепад сигнала с высокого уровня в низкий).

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

boolean flagPress= false;    // признак кнопка в нажатом состоянии
boolean flagClick= false;    // признак нажатия кнопки (фронт)

Блок обработки сигнала кнопки мы вызываем с периодом 2 мс. Для отсчета времени устойчивого состояния сигнала необходим счетчик состояния кнопки и константа, задающее это время.

byte  buttonCount= 0;        // счетчик подтверждений состояния кнопки  
#define TIME_BUTTON 12       // время устойчивого состояния кнопки (* 2 мс)

Функции программного блока заключаются в том, что он вырабатывает признаки:

  • flagPress= true, если кнопка нажата;
  • flagPress= false, если кнопка отжата;
  • flagClick= true, если было событие – кнопка была нажата. Этот признак должен сбрасываться после обработки события. В блоке обработки он может только устанавливаться.

Если вызывать такой блок регулярно с периодом 2 мс, то признаки будут соответствовать текущему состоянию кнопки. Их могут использовать другие блоки программы в любом месте. Получается параллельное выполнение задач.

Вот скетч программы, построенной по такому принципу.

/*  Программа sketch_6_2 урока 6
 *  На каждое нажатие кнопки инвертирует состояние светодиода */

#define LED_PIN 13     // номер вывода светодиода равен 13
#define BUTTON_PIN 12  // номер вывода кнопки равен 12

// переменные и константы для обработки сигнала кнопки
boolean flagPress= false;    // признак кнопка в нажатом состоянии
boolean flagClick= false;    // признак нажатия кнопки (фронт)
byte  buttonCount= 0;        // счетчик подтверждений состояния кнопки  
#define TIME_BUTTON 12       // время устойчивого состояния кнопки (* 2 мс) 

boolean ledState;            // переменная состояния светодиода
  
void setup() {
  pinMode(LED_PIN, OUTPUT);        // определяем вывод 13 (светодиод) как выход
  pinMode(BUTTON_PIN, INPUT_PULLUP);  // определяем вывод 12 (кнопка) как вход
}

// бесконечный цикл с периодом 2 мс
void loop() {

/* блок обработки сигнала кнопки
 * при нажатой кнопке flagPress= true
 * при отжатой кнопке flagPress= false
 * при нажатии на кнопку flagClick= true */

  if ( flagPress == (! digitalRead(BUTTON_PIN)) ) {
     // признак flagPress = текущему состоянию кнопки
     // (инверсия т.к. активное состояние кнопки LOW)
     // т.е. состояние кнопки осталось прежним
     buttonCount= 0;  // сброс счетчика подтверждений состояния кнопки
  }
  else {
     // признак flagPress не = текущему состоянию кнопки
     // состояние кнопки изменилось
     buttonCount++;   // +1 к счетчику состояния кнопки

     if ( buttonCount >= TIME_BUTTON ) {
      // состояние кнопки не мянялось в течение заданного времени
      // состояние кнопки стало устойчивым
      flagPress= ! flagPress; // инверсия признака состояния
buttonCount= 0;  // сброс счетчика подтверждений состояния кнопки

      if ( flagPress == true ) flagClick= true; // признак фронта кнопки на нажатие     
     }   
  }

 
  // блок управления светодиодом
  if ( flagClick == true ) {
    // было нажатие кнопки
    flagClick= false;       // сброс признака фронта кнопки
    ledState= ! ledState;   // инверсия состояние светодиода
    digitalWrite(LED_PIN, ledState);  // вывод состояния светодиода   
  }

  delay(2);  // задержка на 2 мс
}

В блоке обработки состояния кнопки происходит следующее:

  • Если текущее состояние кнопки соответствует признаку flagPress, то ничего не делается. Только сбрасывается счетчик подтверждения состояния.
  • Если состояния кнопки и признака различны, то счетчик начинает считать время подтверждения. Любой возврат к равенству состояния кнопки и признака сбрасывает счетчик.
  • Если состояние сигнала устойчивое в течение количества циклов, определяемого константой TIME_BUTTON, то признак состояния кнопки flagPress инвертируется.
  • При этом формируется признак фронта сигнала flagClick, если кнопка стала нажатой. На отжатие кнопки этот признак не вырабатывается.

Блок управления светодиодом, думаю, пояснять не надо.

Блоки включены в бесконечный цикл с периодом 2 мс. Это утверждение не совсем корректно, но давайте пока его не обсуждать. Главное, что блок обработки сигнала кнопки вызывается регулярно с периодом примерно 2 мс.

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

В константе TIME_BUTTON задано время устойчивого состояния сигнала кнопки – 24 мс. Я рекомендую выбирать это время в пределах 20-30 мс.

Для проверки алгоритма обработки сигнала увеличьте значение константы TIME_BUTTON до 250 (время 500 мс). Увидите, что при быстром нажатии (менее 0,5 сек) светодиод не меняет своего состояния. Т.е. алгоритм подтверждения устойчивого состояния работает правильно.

 

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

Предыдущий урок     Список уроков     Следующий урок

Один комментарий на «Урок 6. Обработка дребезга контактов кнопки. Интерфейс связи между программными блоками.»

  1. начинаю изучать программирование контроллеров, этот материал мне подходит, а вопросы для уточнения можно задавать?

  2. Здравствуйте,а как изменить программу,чтобы светодиод в состоянии HIGH мигал?

  3. в первом примере buttonPrevState не инициализирован явно, но уже используется. Поправьте 🙂

    • По второму примеру.
      Все-таки использование функции millis() из встроенного в IDE примера debounce, выглядит привлекательнее «тупой» задержки delay().

      • В принципе Вы правы, но при объявлении переменные инициализируются нулями.
        Что касается «тупой задержки delay()» из второго примера. Задержка функцией delay используется для простой, понятной реализации периодического вызова программного блока. Другие способы еще не были представлены в уроках. Начиная с урока 10 подобные блоки вызываются в обработчике прерывания по таймеру.

  4. Эдуард,
    Большое вам спасибо за то, что вы делаете! Мало где информация подается под таким правильным «методическим» углом: не только изучить железо и как копировать решения на нем, но и выработать общие подходы к программированию МК, научиться самому выбирать и рассчитывать компоненты. Надеюсь у вас еще много есть чем поделиться! Можно бы потом из этого и книгу собрать было.

  5. Только вникаю в азы программирования, поэтому вопросы могут показаться смешными))..
    Разве в строке программы
    #define TIME_BUTTON 12
    не происходит присвоение константе TIME_BUTTON вывода под номером 12, ведь строка
    #define BUTTON_PIN 12
    та же самая, только имя константы другое

    • Строка #define TIME_BUTTON 12 означает, что везде где в тексте программы встретится имя TIME_BUTTON компилятор тупо заменит его на символы 12. А применять его к номеру вывода или к времени это решаете Вы. Просто так совпало что и номер вывода 12 и константа времени 12. В предыдущем уроке #define описывается подробно.

      • да да, я понял, перечитал 3 раза и врубился, был невнимателен, TIME_BUTTON это счетчик количества времени включенного состояния, спасибо

  6. И еще вопрос, а в блоке управления светодиодом разве не нужно вместе с flagClick= false, также обнулить переменную flagPress?
    flagPress = false
    Иначе выходить должно, что при отжатии кнопки светодиод снова переключится ?

    • Нет. Почитайте внимательно выдержку из урока:
      Функции программного блока заключаются в том, что он вырабатывает признаки:
      •flagPress= true, если кнопка нажата;
      •flagPress= false, если кнопка отжата;
      •flagClick= true, если было событие – кнопка была нажата. Этот признак должен сбрасываться после обработки события. В блоке обработки он может только устанавливаться.

      flagPress показывает текущее состояние, а flagClick — признак того, что было событие. Он запоминает его.

      • Спасибо, Эдуард, я проверил еще раз и увидел строку
        if ( flagPress == true ) flagClick= true

      • Будет ли правильным вот такой код для избежания дребезжания ?

        // мигаем диодом от кнопки.
        //Включение и выключение по отдельному нажатию

        #define PIN_DIOD 13
        #define PIN_KNOPKA 2
        boolean last_button = LOW;// предыдущее состояние
        boolean Now = LOW; // значение для светодиода
        boolean now_button = LOW; // текущее состояние

        // настройка, предустановка
        void setup()
        {
        pinMode (PIN_DIOD, OUTPUT);
        pinMode (PIN_KNOPKA, INPUT);
        }

        // бесконечный цикл
        void loop()

        {
        now_button = digitalRead(PIN_KNOPKA); // читаем состояние кнопки

        if (last_button == LOW && now_button == HIGH) // если кнопка была в лоу и стала в хай
        {
        Now = ! Now; // инвертируем значение на светодиоде

        }
        last_button=digitalRead(PIN_KNOPKA); // читаем стостояние кнопки
        delay(2);
        digitalWrite(PIN_DIOD, Now);//записываем значение в порт диода
        }

        • Думаю, правильно работать не будет. Программа выловит первый же фронт и сработает на него.

  7. Здравствуйте! Не понятно откуда берется значение переменной ledState в блоке упр. светодиодом «ledState= ! ledState; // инверсия состояние светодиода». Ведь она только объявлена как глобальная переменная и нигде в программе не установлено ее начальное значение.

    • Здравствуйте!
      Во первых, при инициализации переменных их значение равно 0.
      Во вторых, в данной программе нам не важно начальное состояние светодиода. При каждом нажатии кнопки он должен менять свое состояние.

      • 1)Все равно не понял, почему каким-то переменным задаётся нулевое значение, а каким -то нет? И тем более не понятно почему если не задаем никакого значения то по умолчанию переменным присваивается нулевое значение, зачем тогда им задавать «принудительно» нулевое значение (false)? Или есть какие-то правила?

        • Формально Вы правы. Но, как правило, все компиляторы инициализируют не заданные переменные 0. Некоторые переменные не надо инициализировать по алгоритму. Задавайте при инициализации. Это дисциплинирует, хуже не будет.

  8. 2)Если я правильно понял то 2-й пример работает сл.образом: после включения LED не горит; при нажатии на кнопку через 24 мс LED загорается; при отжатии LED продолжает гореть; при повторном нажатии через 24 мс LED гаснет и т.д. Это так?

    • В принципе все правильно. Светодиод меняет свое состояние на каждое нажатие кнопки. Именно нажатие, не отжатие. А то, что состояние меняется через 24 мс, не столь важно. Это время на обработку сигнала кнопки. Не думаю, что его в реальной программе надо учитывать.

  9. 3)Почему в выражении нет фигурных скобок: if ( flagPress == true ) flagClick= true; или при построении простого выражения они не нужны?

  10. Здравствуйте!
    Если в вашем цикле loop мы его слегка изменим, станет ли программа работать не правильно?

    void loop() {
    if ( flagPress == (! digitalRead(BUTTON_PIN)) ) {
    buttonCount= 0;
    }

    else {
    buttonCount++;

    if ( buttonCount >= TIME_BUTTON ) {
    buttonCount= 0;
    flagClick= true;
    }
    }

    if ( flagClick == true ) {
    flagClick= false;
    ledState= ! ledState;
    digitalWrite(LED_PIN, ledState);
    }

    delay(2);
    }

    а именно избавимся от строчки flagPress= ! flagPress; и не станем выполнять условие if ( flagPress == true ) нужное для присвения flagClick= true;. Выполним присвоение flagClick= true после того как выполнится условие buttonCount >= TIME_BUTTON.

    • Я боюсь ошибиться. Думаю, тогда flagClick будет вырабатываться и на нажатие, и на отжатие кнопки.

      • Извиняюсь, поторопился и ошибся. Да программа будет работать не правильно, но мне кажется, что ошибка будет выглядеть так: после того как кнопка нажата, а дребезг устранен сработает flagClick= true и произойдет инвертация значения светодиода. Однако в связи с тем что пока палец находится на кнопке условие flagPress == (! digitalRead(BUTTON_PIN) будет продолжать отправлять нас на ветку else, тем самым каждую последующую итерацию цикла loop, будет увеличиваться счетчик buttonCount. И пока палец на кнопке, каждые 12 итераций цикла loop с задержкой по 2 мс каждый (т е каждые 24 мс), будет также происходить инвертация значения светодиода. Таким образом пока кнопка нажата светодиод будет моргать, до тех пор пока палец не отпустит кнопку и светодиод не сохранит своего текущего значения, которое может быть как включенным, так и выключенным. Большое спасибо, за интересную статью и ответ.

  11. Встречал много реализаций гашения дребезга, но Ваш код самый краткий и элегантный !
    Спасибо за примеры!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *