Перейти к содержимому

Обработка кнопок на ESP32 и борьба с дребезгом контактов

Добрый день, уважаемые читатели!

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

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

Ранее я уже рассказывал, как работать с GPIO-портами на ESP32 на ESP-IDF (да по сути на платформе Arduino должно быть то же самое):

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

Кнопка (или выключатель) является простейшим электрическим устройством, которая выполняет очень простую функцию: замыкает и размыкает контакт. Кнопки бывают нескольких типов:

  • нормально разомкнутые – в исходном состоянии (кнопка не нажата) контакты кнопки не замкнуты, при нажатии на неё – замыкаются
  • нормально замкнутые – в исходном состоянии (кнопка не нажата) контакты кнопки замкнуты, при нажатии на неё – размыкаются
  • переключающие – в исходном состоянии (кнопка не нажата) замкнута одна группа контактов, при нажатии на неё – другая

и еще:

  • без фиксации (их еще называют тактовыми или с самосбросом) – в этом случае состояние контактов автоматически восстановиться после её отпускания (пружиной)
  • с фиксацией (по сути это выключатель) – для размыкания контактов придется ещё раз нажать на кнопку или произвести другое действие.

Для микроконтроллера всё это “без разницы” – ему главное уловить изменения напряжения на его входе. А для этого нам придется “подтянуть” один из выводов кнопки к питанию или “земле”, в зависимости от того, куда вы подключили второй контакт кнопки.

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

 


Подключаем кнопку к GPIO – немного электроники

Допустим, нам требуется подключить кнопку и реализовать какую-то обработку нажатий на неё. Если вам нужно несколько кнопок – просто придется повторить тоже самое несколько раз. Существует два варианта подключения кнопок – с подтяжкой к питанию, и с подтяжкой к “земле”:

  • В первом случае на GPIO постоянно будет присутствовать высокий уровень, то есть “1”, а при замыкании контактов – низкий уровень, то есть “0”.
  • Во втором случае наоборот, на GPIO постоянно будет присутствовать высокий уровень, то есть “0”, а при замыкании контактов – низкий уровень, то есть “1”.

Условно будем называть уровень, который соответствует нажатой кнопке (замкнутым контактам) – активным уровнем. То есть в первом случае активный уровень – “0”, во втором – “1”.

Но ESP32 во многих случаях позволяет немного упростить схему и сэкономить на резисторах, если задействовать встроенную в чип подтяжку. Как это сделать, описано здесь:  работа с портами ввода-вывода GPIO. Если ваша кнопки или кнопки находятся на относительно коротких проводах внутри корпуса, то её будет вполне достаточно, и схемы можно упростить так:

 


Какие GPIO можно использовать для подключения кнопок

Все ли GPIO ESP32 использовать для подключения кнопок и переключателей? Да практически все, что не заняты “системными” функциями. Но с некоторыми оговорками.

Рассмотрим это на примере ESP32 классической линейки. На сериях ESP32-S2, ESP32-S3, ESP32-С2, ESP32-С3, ESP32-С6 и т.д. выводы могут быть другие.

  • На подавляющем большинстве GPIO вы можете подключать свои кнопки как угодно – хоть с активной единицей, хоть с активным нулем, хоть со встроенной подтяжкой, хоть с внешней. На схеме ниже они помечены ярко зеленым цветом.
  • Некоторые выводы не имеют встроенной подтяжки (GPI 34-39). Вы можете использовать их только на вход, и с внешней подтяжкой резисторами. В остальном они не имеют ограничений.
  • Как известно, любая ESP имеет специальные выводы, которые при запуске чипа определяют режимы её работы, и которые называются Strapping Pins. Использовать их в проектах можно, но осторожно – при запуске микроконтроллера из логические уровни должны строго соответствовать тому, что запланировал производитель.
    • Если GPIO при запуске должен быть подтянут к питанию при старте, то ваша кнопка не должна быть помехой этому – она должна быть только с активным нулем. Эти выводы помечены желтым цветом фона.
    • Если GPIO при запуске должен быть подтянут к земле при старте, то и в этом случае ваша кнопка не должна быть помехой этому – она должна быть только с активной единицей. Эти выводы помечены розовым цветом фона.

Вот такая получилась у меня схема:

На сериях ESP32-S2, ESP32-S3, ESP32-С2, ESP32-С3, ESP32-С6 и т.д. выводы могут быть другие!

 


Дребезг контактов и борьба с ним

Кнопки мы подключили. Но сразу скажу – работать они так будут, но плохо. Все дело в дребезге контактов. Дело в том, что идеальных кнопок не существует, и кнопка всегда имеет подвижный контакт. А подвижный контакт при перемещении и переключении будет едва заметно вибрировать. Ну это для вас не заметно, а для микроконтроллера – ой как заметно! И весьма мешает в работе. На осциллограмме это выглядит так:

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

Существует несколько вариантов решения этой проблемы:

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

Иногда проще и “выгоднее” применить программную фильтрацию, иногда – аппаратную (особенно когда “кнопка” далеко от МК и возможны сторонние помехи). 

 


Аппаратные способы борьбы с дребезгом контактов

Для начала давайте рассмотрим “технически сложные”, то есть аппаратный способы борьбы с этой напастью.

.

Борьба с дребезгом контактов с помощью RC-фильтров

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

Очень часто можно встретить схему, где дополнительный конденсатор включен параллельно контактам кнопки – но так делать однозначно не стоит, работать это будет плохо. Мало того, что конденсатор мгновенно разрядится через контакты кнопки; так еще и контакты кнопки могут со временем “подгореть”, если емкость не маленькая.

Гораздо лучше добавить ещё один резистор, через который будет разряжаться этот самый конденсатор. Ну а заряжаться – уже через два последовательных резистора:

Здесь мы уже не можем избавиться от верхнего по схеме резистора подтяжки (задействовав встроенный в ESP). Номиналы резисторов и конденсаторов в данной схеме пока выбраны весьма условно (в интернете много похожих схем, кстати) – по хорошему их нужно обязательно рассчитать.

Для указанных на схеме элементов полная разрядка конденсатора будет происходить за время примерно 1 мс, зарядка – немного дольше. Если исходить из того факта, что дребезг контактов может длиться 10 мс и дольше, то этого явно не достаточно. Становиться понятно почему часто встречаются отзывы, что данная схема работает плохо. Нужно либо в 10 раз увеличивать номинал конденсатора, либо – резисторов, либо и то и другое. Причем электролитические конденсаторы в данной схеме лучше не применять – они плохо работают с короткими иглообразными импульсами.

Исходя из переходных процессов длительностью около 10мс схему можно переделать так:

:

Вот теперь приемлемо

Здесь мы одновременно увеличили и емкость конденсатора, и резисторы подтяжки. В этом случае постоянная времени RC-цепи составляет уже 15~20 мс, что, в принципе, удовлетворяет нашим условиям. Конечно, колебания на GPIO полностью не исчезнут, но будут заметно сглажены. Заодно эта схема будет довольно хорошо фильтровать короткие помехи, если ваша кнопка находится на значительном удалении от микроконтроллера.

Иногда рекомендуют добавить дополнительный резистор между конденсатором фильтра и входом контроллера – это уменьшит влияние последнего на RC-фильтр. Но, учитывая то, что входное сопротивление GPIO у современных микроконтроллеров очень высокое, вряд ли в этом есть какая-то особая необходимость, так что последний резистор с чистой совестью можно исключить.

Увы, но на практике на “плохих” с окислившимися контактами кнопках переходные процессы могут быть и существенно дольше 20 мс… Но тогда вас спасут … триггеры.

.

 

Борьба с дребезгом контактов с помощью триггеров и одновибраторов

Существует довольно много способов подавить дребезг (а иногда заодно и помехи) с помощью “активной” электроники. Под словом активной здесь понимается применение транзисторов, логических элементов, компараторов и прочих микросхем.

Самый популярный способ – использовать RS-триггер. Но потребуется логическая микросхема И-НЕ и переключающая кнопка. Есть и готовые микросхемы триггеров, вполне можно использовать и их. Ниже на рисунке представлены несколько вариантов схем:

Это один из самых надежных способов подавить дребезг контактов. Помню, в конце 1980-х как-то собирал схему программируемого переключателя гирлянд на К155РУ2 (это было ОЗУ 64 бит), и там как раз использовалась подобная схема для гашения дребезга контактов кнопок программирования.

Иногда применяют также схема на основе ждущих одновибраторов (найдите разницу между 2 и 3):

Но ввиду сложности и необходимости применения дополнительных элементов, в любительской DIY-практике, такие схемы – скорее редкость, чем правило.

 

Борьба с электромагнитными помехами на длинных проводах

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

Для защиты от коротких всплесков напряжения можно применить TVS-диод (или супрессор). Модифицируем нашу схему:

Поскольку низковольтные супрессоры обладают довольно высокими токами утечки, придется снизить номинал подтягивающего резистора, иначе ток утечки через TVS-диод может запросто “смоделировать” нажатие кнопки. По этой же причине его нельзя ставить параллельно конденсатору фильтра.

Если вам предстоит подключить к МК довольно удаленную кнопку или “концевик”, которые большую часть времени находятся в разомкнутом состоянии, то для ещё большей надежности можно применить оптрон, например по одной из примерно таких схем:

В данном случае в качестве примера схема спроектирована без гальванической развязки (общая “земля” и общая шина питания), хотя на оптроне можно сделать схему и с развязкой. В случае применения оптронов устанавливать дополнительные RC-цепочки нет особого смысла, ибо светодиод сам по себе достаточно хороший фильтр от высокочастотных помех. Но вот защитить оптрон TVS-диодом не помещает в любом случае. Резистор я выбрал исходя из тока ~5 мА на оптрон. Питание на светодиоды можно подать и от +5В и от +12В – но тогда вам необходимо выбрать другой номинал для токоограничительного резистора.

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

Переходим к программным способам.

 


Программные способы борьбы с дребезгом контактов

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

В первую очередь хотелось бы отметить то, что лично я практически всегда использую для обработки сигналов с кнопок прерывания. Так как периодический опрос состояния входа – это сильно не оптимальный метод в большинстве случаев. Но бывают и исключения – в одном проекте я сознательно отказался от использования прерываний в пользу периодического опроса состояния GPIO, когда мне нужно было знать состояние входа с заданной периодичностью (вместо “как можно быстрее” для обычных кнопок) и дабы синхронизировать чтение с основным потоком задачи.

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

Метод вертикальных счетчиков

Метод вертикальных счетчиков – применяется в случае периодического опроса состояний входа, например в основном цикле Arduino-кода или по таймеру. Суть метода состоит в том, что мы периодически считываем состояние входа, и если кнопка нажата – увеличиваем счетчик, если ловим bounce-помеху – сбрасываем его до 0. По достижении заданного порога можно выдавать сигнал на выход. Таким образом пока идут импульсы переходного процесса, счетчик(и) будут неизбежно периодически сбрасываться. 

Метод вертикальных счетчиков особенно удобен, если необходимо обрабатывать сразу несколько входов, например энкодеров. Он математически “красив” и оптимален в машинном коде. Очень хорошо описан GDI, мне понравилось, рекомендую почитать: embedders.org/blog/gdi/debouncing.html.

Но… сам я этот алгоритм не использую, так как его сложно “прикрутить” к прерываниям (хотя и можно, наверное – я не задавался такой целью).

 

Метод отложенного чтения

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

Суть метода проста как два пальца и заключается в следующем:

  1. Получив первый сигнал прерывания и(или) прочитав состояние входа и обнаружив его изменение, мы на время блокируем прерывания по данному входу (если вы используете их) и не выполняем никаких повторных чтений состояний по нему. Время этой задержки должно быть выбрано так, чтобы заведомо “пропустить” все переходные процессы и, с другой стороны, не вносить заметных задержек в реакцию системы на нажатие кнопки. Задержка может формироваться любым удобным вам способом – я обычно использую программные таймеры, их куча.
  2. Затем, когда время задержки истекло, просто читаем состояние входа самым обычным способом и активируем прерывания заново (если они используются, конечно).

Давайте рассмотрим это чуть более подробно.

1. Вначале нам необходимо настроить порт на работу на вход:

gpio_reset_pin(_gpio_num);
gpio_set_direction(_gpio_num, GPIO_MODE_INPUT);
if (int_pull_enabled) {
  if (_active_level) {
    gpio_set_pull_mode(_gpio_num, GPIO_PULLDOWN_ONLY);
  } else {
    gpio_set_pull_mode(_gpio_num, GPIO_PULLUP_ONLY);
  };
} else {
  gpio_set_pull_mode(_gpio_num, GPIO_FLOATING);
};

2. Затем создаем таймер debounce:

if ((_debounce_time > 0) && !(_timer)) {
  esp_timer_create_args_t tmr_cfg;
  tmr_cfg.arg = this;
  tmr_cfg.callback = debounceTimeout;
  tmr_cfg.dispatch_method = ESP_TIMER_TASK;
  tmr_cfg.name = "debounce";
  tmr_cfg.skip_unhandled_events = false;
  esp_timer_create(&tmr_cfg, &_timer);
};

3. После этого необходимо настроить и разрешить прерывания:

gpio_set_intr_type(_gpio_num, GPIO_INTR_ANYEDGE);
gpio_isr_handler_add(_gpio_num, gpioIsrHandler, this);
gpio_intr_enable(_gpio_num);

Всё, кнопка готова к работе.

4. При изменении состояния входа срабатывает прерывание и вызывается его обработчик gpioIsrHandler:

gpio_intr_disable(_gpio_num);
esp_timer_start_once(_timer, _debounce_time);

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

5. Когда таймер debounce отработал – читаем состояние входа:

uint8_t newState;
gpio_get_level(_gpio_num) == _active_level ? newState = 1 : newState = 0;
if (newState <> lastState) {
  // Произошло изменение состояния, реагируем на это
};

И не забываем восстановить прерывания…

gpio_intr_enable(_gpio_num);

 

Этот метод и реализован в моей библиотеки reGPIO

 


Библиотека reGpio

Ссылка: github.com/kotyara12/reGpio

Библиотека отслеживает состояние входа GPIO с помощью прерываний. Реализована она на C++ в виде класса. Таким образом вы можете создать на каждую кнопку или выключатель свой экземпляр класса и обрабатывать их отдельно. Как я уже и написал выше, используется метод отложенного чтения состояния порта. Для отслеживания переключения входов используются прерывания, для формирования временных задержек – программные таймеры.

Возможности библиотеки на текущий момент:

  • Возможна работа с любыми доступными GPIO, любыми активными уровнями, с внешней или внутренней подтяжкой.
  • Автоматическая настройка встроенных GPIO и прерываний – вам больше не нужно заботится об этом для ваших кнопок
  • Возможна как работа через прерывания, так и без прерываний (например по внешнему таймеру или в цикле)
  • Настраиваемое пользователем время debounce – интервала
  • При изменении состояния входа класс может оповестить остальные задачи программы тремя способами:
    • через системную очередь событий
    • через установку флагов в группе событий
    • через функцию обратного вызова
  • Три вида оповещений:
    • изменение состояния
    • короткое нажатие (фиксируется после нажатия и последующего отпускания кнопки)
    • длинное нажатие (фиксируется после нажатия и последующего отпускания кнопки)
  • Есть возможность временной блокировки входов по каким-то внешним причинам.

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

1. Объявляем экземпляр класса, например так:

static reGPIO btnMode(
  16,      // GPIO 16
  0,       // Низкий активный уровень, вывод по умолчанию подтянут к 3,3в, кнопка подтягивает его к земле
  true,    // Используем встроенную подтяжку, экономим на резисторе
  true,    // Используем прерывания
  25000,   // Время debounce задержки - 25000 микросекунд или 25 миллисекунд
  nullptr  // Нет callback-а для реакции
);

2. Настроим GPIO и прерывания:

btnMode.iniGpio();

3. В качестве примера обрабатываем сообщения из очереди событий:

void sensorsGpioEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  if (event_data) {
    if (lcdBacklightIsactive()) {
      gpio_data_t* data = (gpio_data_t*)event_data;
      if (event_id == RE_GPIO_BUTTON) {
        rlog_w("BTN", "Button pressed: %d", data->pin);
        if (data->pin == CONFIG_GPIO_BUTTON_MODE) {
          // Кнопка "режим", короткое нажатие
        };
      } else if (event_id == RE_GPIO_LONG_BUTTON) {
        rlog_w("BTN", "Button long pressed: %d", data->pin);
        if (data->pin == CONFIG_GPIO_BUTTON_MODE) {
          // Кнопка "режим", длинное нажатие
        };
      };      
    };
  };
}

Через очередь событий “прилетает” следующая структура:

/**
 * GPIO pin and logic level details :: 32 bit
 * */
typedef struct {
  uint8_t bus;      // I2C bus +1 for expanders, 0 for internal ports
  uint8_t address;  // I2C address for expanders, 0 for internal ports
  uint8_t pin;      // Chip pin number
  uint8_t value;    // Logic level: 0 or 1
} gpio_data_t;

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

 

Но во многих случаях для оповещения задачи удобнее использовать установку бит в группе событий – так как обработчики событий из очереди выполняются в “сторонней” задаче. Сделать это просто через вызов метода setEventGroup():

btnMode.setEventGroup(_sensorsFlags, FLG_BUTTON_PRESSED | FLG_BUTTON_MODE, FLG_BUTTON_PRESSED | FLG_BUTTON_MODE | FLG_BUTTON_LONG);

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

 


Вот в общем то и всё, о чем я хотел сегодня вам рассказать. Разрешите откланяться, ваш Александр aka kotyara12. До следующих встреч на сайте и на telegram-канале! 

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 7 оценок ]

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

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