Комментарии 43
С использованием прерываний сделать асинхронную обработку кнопок с подавлением дребезга и детекцией типа нажатия - хорошо, но не рокет-сайенс. В конце концов в любой uC системе так или иначе приходится это делать - не на Си, так на Ассемблере.
А вот в оригинальном Мониторе (читай - BIOS‘е) Радио-86РК обработка клавиатуры (втч неблокирующая) была сделана вообще без прерываний, и при этом поддерживался чисто программный (!) звук подтверждения нажатия клавиш, антидребезг, и ускоренный повтор при долгом нажатии. Никаких аппаратных средств измерения времени (кроме программных циклов) в нем тоже не было. И опрос матрицы тоже был чисто программный. Если разобрать, как это было сделано тогда - окажется любопытно.
Я думал об этом. Но, боюсь, что без предварительной подготовки такой материал переварить будет сложно. Поэтому я начал писать начиная с базовых принципов.
Если разобрать, как это было сделано тогда - окажется любопытно.
Ну разве что ради любопытства :)
Позволю себе несколько замечаний.
Зачем хранить инициализирующие параметры кнопок в структуре в оперативной памяти? Ее как правило не так уж много, а изменения этих параметров в процессе работы программы вроде бы не предполагается.
Структура текущего состояния кнопок мне кажется излишне большой. Ниже приведу свой вариант с пояснениями.
"В обработчике прерывания, вместо вывода в терминал сообщений о нажатии кнопки, вы можете разместить полезный код, который будет реагировать на нажатие кнопки." - ну зачем же учить начинающих плохому? В прерывании код должен быть как можно более коротким. В данном случае - только оценка текущего состояния кнопок и все, никаких реакций на их нажатие, тем более никаких медленных блокирующих выводов в UART. Это очень, очень вредная рекомендация.
Размер структуры текущего состояния кнопки можно сделать более компактным - это опять же сократит использование оперативки, как и размещение инициализирующих данных в константах или дифайнах. Вот пример:
enum
{
KB_FREE, // отсутствие событий у кнопки
KB_WORKED, // последнее событие кнопки было обработано
KB_PREPRESSED, // есть сигнал нажатия, идет выжидание антибребезга
KB_SPRESSED, // подтверждено нажатие кнопки после антидребезга
KB_LPRESSED, // кнопка остается нажатой длительное время
KB_SRELEASED, // кнопка отпущена после короткого времени нажатия
KB_LRELEASED, // кнопка отпущена после долгого нажатия
} KBD_STATES;
typedef struct
{
KBD_STATES state;
uint8_t time;
} KEYBOARD;
Структура имеет размер всего 5 байтов, и этого достаточно для обработки короткого и длинного нажатий, а так же отпускания после короткого и длинного нажатий.
Алгоритм обработки каждой кнопки в цикле в прерывании таймера таков:
for (uint8_t kb = 0; kb < keys_count; kb++)
{
switch (Keys[kb].state) // проверяем текущий статус кнопки
{
// если событие кнопки было обработано и сейчас кнопка отпущена,
// то возвращаем ее статус в свободный
case KB_WORKED:
if (!KeyRead(kb)) // если на входе нет сигнала нажатой кнопки
{
Keys[0].state = KB_FREE;
}
break;
// если кнопка в свободном состоянии, или была отпущена после
// любого нажатия, и в данный момент есть сигнал ее нажатия,
// то начинаем обработку антидребезга
case KB_FREE:
case KB_SRELEASED:
case KB_LRELEASED:
if (KeyRead(kb))
{
Keys[kb].state = KB_PREPRESSED;
Keys[kb].time = 0; // обнуляем счетчик времени
}
break;
// если кнопка в состоянии обработки антидребезга и есть сигнал
// нажатия - инкрементируем счетчик и проверяем не достиг ли он времени
// проверки антидребезга, если достиг, то присваиваем кнопке статус
// короткого нажатия.
// Если сигнала нажатия нет, значит проверка антидребезга не прошла,
// обнуляем счетчик времени и возвращаем статус кнопки в свободный.
case KB_PREPRESSED:
if (KeyRead(kb))
{
if (Keys[kb].time > 6) // время обработки антидребезга вышло
Keys[kb].state = KB_SPRESSED;
else
Keys[kb].time++;
}
else
{
Keys[kb].state = KB_FREE;
Keys[kb].time = 0;
}
break;
// если кнопка в состоянии короткого нажатия и есть сигнал нажатия -
// инкрементируем счетчик времени и проверяем не достиг ли он времени
// долгого нажатия, если достиг, то переводим статус кнопки в долгое
// нажатие.
// Если нет сигнала нажатия - значит кнопка была отпущена, ставим
// ей статус "отпущена после короткого нажатия".
case KB_SPRESSED:
if (KeyRead(kb))
{
if (Keys[kb].time > 150)
{
Keys[kb].state = KB_LPRESSED;
}
else
Keys[kb].time++;
}
else
{
Keys[kb].state = KB_SRELEASED;
Keys[kb].time = 0;
}
break;
// если кнопка в состоянии долгого нажатия и есть сигнал нажатия -
// инкрементируем счетчик времени (это просто для легкой организации
// автоповтора нажатий).
// Если нет сигнала нажатия - значит кнопка была отпущена, ставим
// ей статус "отпущена после долгого нажатия".
case KB_LPRESSED:
if (!KeyRead(kb))
{
Keys[kb].state = KB_LRELEASED;
Keys[kb].time = 0;
}
else
Keys[kb].time++;
break;
}
Этот код позволяет достаточно легко организовать и автоповтор нажатия с заданным интервалом - просто проверять значение счетчика времени при статусе кнопки KB_LPRESSED и если он достиг определенного значения - обнулять его и выполнять действия по очередному "нажатию" автоповтора.
Все реакции на события кнопок необходимо размещать в основной программе, а ни в коем случае не в прерывании. Просто в основном цикле (или где это нужно) проверяется текущий статус нужной кнопки и если он не "свободен" (KB_FREE) и не "обработан" (KB_WORKED), то идем выполнять полезный код, связанный с событием кнопки, после чего выставляем кнопке статус "обработан" (KB_WORKED). Этот статус сам сменится на "свободен" при ближайшем очередном вызове прерывания таймера с обработкой состояний кнопок.
По счетчику времени: нет нужды считать в нем миллисекунды или любые другие реальные временные величины. Достаточно считать вызовы прерывания таймера - они все равно следуют с определенным промежутком времени. Поэтому достаточно 1-байтового счетчика, который посчитает, скажем, 5 вызовов (с периодом 20 миллисекунд) вместо 100 миллисекунд для антидребезга. Или 50 вызовов вместо 1000 миллисекунд для долгого нажатия.
Видимо, я не достаточно точно выразил мысль про "полезный код" в прерывании.
Как будут обмениваться данными обработчик прерывания и фоновая программа, решать вам. Может быть это будут глобальные переменные, а может быть специальные функции с реализацией атомарного доступа или еще что-то покруче.
Я и имел ввиду, что надо передать данные о нажатии кнопки в фоновую программу.
Когда пишешь код, всегда надо искать компромиссы. Или производительность, или удобство. Когда речь идет о программировании Arduino, то о производительности речь не идет. Концепция предполагает удобство программирования без оглядки на ресурсы контроллера.
О недостатках и достоинствах моего примера я написал в заключении в статье. Код ориентирован на те случаи, когда кнопок немного.
Если говорить об использовании ресурсов микроконтроллера, просто посмотрите, как реализованы некоторые библиотеки Arduino. И почитайте статьи по программированию Arduino в интернете. Это дикая смесь ООП и обычного функционального программирования в стиле Си.
Была идея продолжить статью, и выполнить оптимизацию кода. Но объем материала, который посвящен простой кнопке, мог бы стать пугающе большим. Я, все таки, рассчитываю на новичков в данном вопросе. Не хотелось бы отпугивать потенциальную аудиторию материалом, который трудно переварить за один раз.
Я и имел ввиду, что надо передать данные о нажатии кнопки в фоновую программу.
Честно говоря, из текста это совершенно не очевидно, по крайней мере для меня :)
Концепция предполагает удобство программирования без оглядки на ресурсы контроллера.
Речь ведь не о производительности, а о базовых принципах программирования для микроконтроллера. В прерываниях должен быть код, выполняющийся за минимально возможное время, что в 8-битной Ардуине на 8 МГц, что в 32-битном ARM-е на 400 МГц :)
просто посмотрите, как реализованы некоторые библиотеки Arduino. И почитайте статьи по программированию Arduino в интернете.
Это отдельный вид искусства антипрограммирования, согласен. Значит тем более не нужно прививать начинающим эти антипаттерны :)
Вы смотрели, как реализована обработка прерываний в hal-драйвере? Там перекладных прилично. 20 лет назад надо было оптимизировать код для экономии ресурсов. Сейчас это не сильно актуально.
Смотрел и щупал, поэтому не использую его в своих проектах. HAL уже вплотную приблизился к ардуино-стилю :)
Оптимизация всегда будет актуальной для нормальных разработчиков, но повторюсь - речь не об оптимизации, а о правильном программировании. Если Вы в прерывании UART начнете обрабатывать пришедшие данные, а в прерывании таймера выполнять код по нажатию кнопок, то рискуете запросто нарваться на ситуацию когда контроллер не будет вылазить из прерываний даже если у него вагон ресурсов.
Давайте разделять цели и задачи. То, о чем Вы говорите, для меня понятно, можете не продолжать. Я начинал программировать ещё до того, как ардуино появилась. Статья нацелена на тех, кто только начал. USART в прерывание для данного случая - это хороший приём для того, чтобы увидеть работоспособность кода с прерываниями.
Есть разные подходы к программированию. Для ардуино он свой. Если был совсем плохой, то не пережил бы первый десяток.
В «A Guide to Debouncing» описан прикольный алгоритм:
// Service routine called by a timer interrupt
bool_t DebounceSwitch2()
{
static uint16_t State = 0; // Current debounce status
State = (State << 1) | !RawKeyPressed() | 0xe000;
if (State == 0xf000)
return TRUE;
return FALSE;
}
Зависит от частоты таймера.
Если таймер часто вызывается то дребезг пролезет.
О, вот это круто! ФНЧ на программном сдвиговом регистре ;)
Какой в этом смысл? Для 8-ми битного контроллера сдвинуть 16-ти битную переменную и дважды наложить маску, ни чуть не проще, чем счетчик увеличить на единицу и проверку выполнить.
Не очень понял с каким вариантом идёт сравнение. Если использовать счетчик нажатий, то надо сравнить что его значение совпадает со значением 12 тиков назад. Выглядит сложнее чем один инкремент и проверка.
Раз в 300мс вызываем функцию которая проверяет нажата ли кнопка, если нажата ставим флаг нажатия, если нет -то флаг снимаем. Этим мы сразу убираем дребезг. В программе тот кому надо проверяет флаг нажатия,поле обработки сбрасывает его. 10 строк кода. Очень удобно и позволяет мгновенно изменить управление хоть на ИК-приемник. Он будет также поднимать флаги.
На мой взгляд, 300мс это недостаточно часто, чтобы оперативно реагировать на нажатия кнопок. Так можно и на пропускать чего-нибудь.
Это же кнопка не от ядерной ракеты, кнопки выполняют сервисные функции настройки и управления, если вам нужно что то на самом деле мгновенно отслеживать, это вешается на прерывание. Я во всех своих проектах использую подобный способ опроса кнопок. Ну и опять же я сторонник так скажем писания детерминированного кода. Функция вернет управление через гарантированное время. Любая. В микроконтроллерах это очень важно.
Прерывания это конечно хорошо, но я обычно неблокирующие "ПЛК" таймеры применяю в том числе для фильтрации дребезга кнопок.
После того, как функция "TimerHandler" связана с прерыванием таймера, можно перенести в нее обработку кнопки из функции "loop()". Из функции "loop()" весь код удалю.
...
В обработчике прерывания, вместо вывода в терминал сообщений о нажатии кнопки, вы можете разместить полезный код, который будет реагировать на нажатие кнопки. А в функции "loop()" реализовать основную логику вашей программы.
"Инженеру старой школы" за размещение подобных частей программы в прерываниях таймера дали бы линейкой по рукам очень больно. Потом бы выгнали из класса и позвали родителей, чтобы те потом его дома ещё высекли.
Сансей, спасибо за Ваш комментарий! Мне как раз не хватало публичной порки)))
Хотелось бы выслушать Вашу точку зрения о том, как же надо использовать прерывания, чтоб по рукам не били?
возможно имеется в виду, что в прерывании взводим флаг (или меняем переменную состояния для объекта, если объект глобальный), который уже потом в цикле (или в другом важно месте акромя обработчик прерывания) опрашивается и принимается решение о дальнейших действиях.
Это тоже не самый лучший вариант. Если фоновая программа в цикле опрашивает состояния какого-то глобального флага, который когда-то может быть установлен в прерывании, то чем это принципиально отличается от синхронной обработки без прерываний?!
Менять из прерывания переменную-состояние для фоновой программы может быть опасно с точки зрения стабильности. Как Вы себе это представляете? В фоновой программе работает какой-то процесс, и вдруг в прерывании меняется его состояние... сомнительно тоже.
Мне кажется, что отреагировать на кнопку в прерывании обработкой каких-то данных - это не самая плохая идея. А вот потом уже подготовленные данные могут быть приняты и обработаны в фоновой программе. И на основе этих данных можно и флаги менять, и состояния переключать.
Видимо кто-то из комментаторов застрял в конце прошлого века. И до сих пор считает машинные циклы в подпрограммах. Современные контроллеры имеют достаточную производительность, чтобы не только флаги устанавливать в прерываниях. Для примера, посмотрите хотя бы как реализован HAL-драйвер в прерываниях таймеров у STM32.
Если фоновая программа в цикле опрашивает состояния какого-то глобального флага, который когда-то может быть установлен в прерывании, то чем это принципиально отличается от синхронной обработки без прерываний?!
цикл не крутится непрерывно, он "спит" и ждем событий. Любого. В том же STM (за не ARM контроллеры не скажу, не имел опыта), например, можно сделать что-то вроде:
...
for(;;) {
__WFI(); // по любому прерыванию
if( button.isPressed()){
doAnyThing();
}
}
в таком случае у вас обработка вне прерывания, но максимально близко к нему. Понятно, что наличие чего-то долго выполняющегося ломает данную систему, но тогда стоит посмотреть в сторону ОСРВ.
В фоновой программе работает какой-то процесс, и вдруг в прерывании меняется его состояние... сомнительно тоже.
для этого придумали мьютексы и машину состояний, которая оперативно отработает изменение состояния.
Мне кажется, что отреагировать на кнопку в прерывании обработкой каких-то данных - это не самая плохая идея. А вот потом уже подготовленные данные могут быть приняты и обработаны в фоновой программе. И на основе этих данных можно и флаги менять, и состояния переключать.
зависит от вида реакции. одно дело просто зажечь лампочку и/или зафиксировать время нажатия, другое запустить какой-нибудь долгий процесс. например, по нажатию на кнопку в зависимости от текущего состояния батарейки (опросить батарейку или менеджер питания), состояния прибора (опросить входы) нужно принять то или иное решение. В таком случае, в прерывании кнопки мы находимся гораздо дольше, что не всегда полезно.
Опять же, а если у вас таких прерываний не одно, а десять? Приоритетами замучаешься разруливать.
Видимо кто-то из комментаторов застрял в конце прошлого века. И до сих пор считает машинные циклы в подпрограммах.
Отвечу за себя исходя из личного опыта.
NRF52840 работаем со стеком BLE. Проброс событий из стека (закрытая либа) в основное приложение выполнен через программно генерирующий модуль событий (SWI EGU). Для BLE важны тайминги, реакция на события должна быть мгновенной, любой длительный процесс в обработчике приводит к краху стека. Да такты не считаем (пишем на С++), но обращаем внимание на это.Один из вариантов, проброс текущего ID события стека в высокоприоритетную задачу, где в кратчайшее время производится анализ события.
На stm32l0 (не самый быстрый контроллер на М0+ ядре) делал простой планировщик (с блекджеком, но без куртизанок) на машине состояний в обработчике прерываний systick. в полне себе работоспособное решение получилось. в прерывании находился примерно 150 - 200 мкс при частоте 1кГц. вполне допустимо, но и система была простой.
Современные контроллеры имеют достаточную производительность, чтобы не только флаги устанавливать в прерываниях.
То есть можно забить на оптимальный код? а если скорости и памяти не хватает, возьми МК пожирнее?
Всему есть предел, писать на ассемблере чтоб получить на 500, ну пусть на 1000 байт меньше кода тоже не стоит. Но и сидеть в обработчике прерывания больше 100 мкс порою слишком расточительно.
Для примера, посмотрите хотя бы как реализован HAL-драйвер в прерываниях таймеров у STM32
HAL от STM32 это весьма спорная штука, его небезосновательно ругают за плохую производительность. На багрепорты они реагируют порою никак. Из крайнего раза (в начале этого года), исправляли с коллегой функцию установки пина в 0. вместо использования регистра GPIOx->BSRR (который они в своем же Reference Manual рекомендуют для этих целей) почему-то использовали GPIOx->ODR. на первый взгляд разница небольшая, но для синхронного переключения нескольких пинов оказалось критичным.
Статья написана для новичков и ориентирована на ардуино. Ардуино все таки не та платформа, где борятся за оптимизацию кода. Вот и я на это упор не делал.
Напомню, статья про кнопку. Материал достаточно ёмкий, и в нем нет места для разъяснений работы с прерываниями.
Как будут обмениваться данными обработчик прерывания и фоновая программа, решать вам. Может быть это будут глобальные переменные, а может быть специальные функции с реализацией атомарного доступа или еще что-то попокруче.
В тексте я упомянул о том, что есть разные варианты обмена. Но акцент статьи всетаки не об этом.
Все ваши доводы конечно правильные, но не про ардуино. Ардуино это первый шаг для погружения в тему. Каждый должен с чего-то начинать. Ну нельзя же сразу вываливать на голову весь объем информации. Уже в данном объёме текст статьи получился сложным для прочтения за один раз. А вы предлогаете сразу раскачать статью до энциклопедии.
Ну нельзя же сразу вываливать на голову весь объем информации. Уже в данном объёме текст статьи получился сложным для прочтения за один раз.
за Ваши статьи спасибо большое, труд не маленький проделан. Прочел обе)
А вы предлогаете сразу раскачать статью до энциклопедии.
не совсем, это как бы варианты для последующих статей.
Ардуино все таки не та платформа, где борятся за оптимизацию кода.
ардуино это хорошая платформа для популяризации программирования микроконтроллеров с очень низким порогом входа и большой свободой выбора (наборы библиотек в огромном количестве, аппаратные модули и т.д). И вот тут уже может возникнуть проблема, когда пользователь подключает несколько неоптимизированных модулей и не понимает почему условная метеостанция не работает.
Я не говорю что надо "упарыватся" в оптимизацию, но важно показать возможные слабые места при совмещении с другим кодом.
Все ваши доводы конечно правильные, но не про ардуино
просто мнение, я могу в чем-то и ошибаться. Вопрос не в платформе как таковой, а в подходе к написанию кода (в том числе и для микроконтроллеров). Вы ведь лучше меня понимаете, что "arduino style" как плашка низкосортного кода появилась из некоторых библиотек. При этом есть и хорошо написанные модули, я вон например у себя на армах использую ArduinoJson. Из коробки прекрасно завелось, документация хорошая.
Напомню, статья про кнопку. Материал достаточно ёмкий, и в нем нет места для разъяснений работы с прерываниями.
Поэтому, по моему мнению, важно обозначать возможные проблемные места. Да можно прям в прерывании сделать некое действие, но вызывать блокирующий обмен по I2C все же не стоит. А вот почему, я Вам расскажу в следующей статье, где пойдет речь об обработчиках прерываний. Ведь расскажите?
В тексте я упомянул о том, что есть разные варианты обмена. Но акцент статьи всетаки не об этом.
Значит цикл не окончен)
Вы же понимаете, авторы - это народ очень ранимый.
При написании статьи, я не был уверен, будет ли это кому-то интересно. И уверенности не было, буду писать продолжение или нет. А пустых обещаний давать не люблю. Хотя, конечно, было лучше сослаться на какой-то готовый материал по теме или на будущий. Учту это для следующих текстов.
После такого комментария я наверное отодвину текущую статью по схемотехнике, и все таки сяду писать про прерывания))
Еще раз повторюсь, что с Вашими доводами я полностью согласен.
"Вы ведь лучше меня понимаете, что "arduino style" как плашка низкосортного кода появилась из некоторых библиотек." - Именно по этому пишу про Arduino. Про STM32 и прочие МК хватает хороших статей.
Про STM32 и прочие МК хватает хороших статей.
хорошего много не бывает.
Вы же понимаете, авторы - это народ очень ранимый.
поэтому я только по комментариям ;)
p.s. диаграммы в ручном режиме подсвечивали красным или софт есть удобный?
Мне кажется, что отреагировать на кнопку в прерывании обработкой каких-то данных - это не самая плохая идея.
Угу. По нажатию кнопки отправляем по RS-485 запрос датчику, дожидаемся от него ответ, обрабатываем полученные данные, выводим результат на экран. И за это время пропускаем пять следующих прерываний. Отличный план, что тут может пойти не так? :)
В фоновой программе работает какой-то процесс, и вдруг в прерывании меняется его состояние... сомнительно тоже.
Поэтому в прерывании меняется состояние не процесса, а флага, который фоновая (хотя строго говоря это как раз не фоновая, а активная) программа проверит когда у нее будет для этого возможность.
посмотрите хотя бы как реализован HAL-драйвер в прерываниях таймеров у STM32
Я Вам уже писал: HAL от ST - это далеко не лучший пример для подражания.
Нууу... Это Вы уже в крайности совсем подались! Зачем же так перегибать?
Давайте остановимся на том, что нужно подбирать решения, подходящие для каждого конкретного случая. Зачем же каждый раз стрелять по воробьям из пушки? Я встречаю очень много хороших программистов, которые на столько заигрались с кодом, что строят конечные автоматы даже там, где этого и не нужно.
"Активной программой" я привык считать ту, которая выполняется в текущий момент. А "фоновой задачей" ту, которая выполняется по сбросу и не связана с другими векторами. Может быть я заблуждаюсь. Но так меня учили, ни чего не могу с собой поделать. Привык к этим понятиям: фоновая программа и супер-цикл)))
Когда микроконтроллер работает на частоте от 100мГц и имеет разрядность 32 бита, сложно однозначно сказать, что для него хорошо, а что плохо.
Зависит от того, насколько вообще критична своевременная реакция на нажатие кнопок и, судя по допустимым временам удержания, не особо то. К предыдущей Вашей статье в комментариях уже приводилась в качестве примера библиотека АлексГайвера. На мой взгляд, она несколько переусложнена и переполнена функциональностью, но подход там верный: никогда не блокироваться, определять состояния и переходы между состояниями по событиям и их таймингам. Никаких задержек в прерываниях, надо что-то сообщить из прерывания в основную программу -- флаги, очереди. Основная программа должна предусматривать приём "неожиданной" (на самом деле ожидаемой) информации. Иногда может потребоваться модификатор volatile, чтобы компилятор знал о характере переменной и не делал предположений о её кешировании в регистре.
Функция обработки клавиатуры, рассмотренная именно в этой моей статье, не имеет ни каких вложенных задержек времени. Ощущение, что некоторые комментаторы вообще не пытаются читать текст перед тем, как делают какие-то выводы.
Обмен между прерыванием и фоновой программой вообще не рассматривается в статье. Я написал про кнопку. Текста достаточно не мало вышло. Напихать туда еще и про это надо? Тогда вместо статьи должна получиться энциклопедия.
Написано много чего и на разные темы. И на хабре врядли можно встретить что-то оригинальное на 100%. Такой материал наверное стоит искать в научных журналах. Я веду свой блог. В нем пишу о своем опыте. Привожу свою точку зрения. И, может быть, это будет для кого-то более понятно, чем статьи Гайвера, или других авторов. А может быть и нет.
У меня для Вас есть простое задание (своим детям давал): сделайте моргание двумя светодиодами одновременно-равномерно (а не так как некоторые пытались решить через поочерёдное моргание), первым 5 раз в секунду, второй 3 раза в секунду, по нажатию кнопки пусть второй моргает 7 раз в секунду, после повторного нажатия снова 3.
Подвох в следующем: они берут стандартный пример блинк, в котором моргание через делей, и пытаются его масштабировать на два светодиода. У Вас похожая ситуация с кнопками.
Простите, это Вы мне задания предлагает? Давайте условимся так: если Вы соизволите полностью ВНИМАТЕЛЬНО прочесть мою статью, ознакомиться со всеми примерами кода и поясняющими рисунками, то я выполню Ваше задание, использую библиотеку для обработки кнопки из этой статьи.
Если Вы посмотрите пояснение к алгоритму обработки кнопки, то должны будете понять, что этот способ обработки как раз хорошо подойдет для решения Вашей задачи.
P.S. Если Вы сходу накидываете вашим детям такие задачи, не давая ни какой предварительной подготовки, то я им сочувствую.
Если Вы посмотрите пояснение к алгоритму обработки кнопки ...
Спустя 9 дней я уже не помню конкретное содержание Вашей статьи, помню только изумившее меня предложение про прерывание, а перечитывать это снова ж таки реальный труд. Я ж предложил простое задание с двумя светодиодами и одной кнопкой. Это обучающее демонстрационное задание. И, получается, я же сам и должен его релизовать? Я способен это сделать и без чтения Вашей статьи.
Значит эта статья не Вам адресована. Но рассмотренный в ней алгоритм отлично подходит для вашей задачи. Если Вы сами способны свою задачу решить, то даже при белом прочтении статьи, должны были это понять.
Хотя бы просто потрудитесь посмотреть два алгоритма в предыдущем командами. Блок "делаем что-то полезное" - это и есть мигание вашими светодиодиками.
Могу предположить, что подобное "отношение" к прерываниям у Вас сложилось ещё от ассемблерных программ для чего-то типа Pic или msc51.
Как всегда подробно и по полочкам. Благодарю. С наступающим!!!
Неблокирующая обработка тактовой кнопки для Arduino. Как использовать прерывание таймера «в два клика» в стиле ардуино