Как стать автором
Обновить

Комментарии 37

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

Чтобы избежать подобного коротыша опрашивающие выходы подключают через токоограничительные резисторы (обычно 1 кОм достаточно). Да и тратить целое прерывание на столбик это расточительство, лучше уж диоды напаять.

Может оказаться, что потребуется отловить нажатие кнопки внутри цикла, выполняющегося внутри процедуры loop. Указанный выше способ не позволит это сделать, так как кнопки не будут опрошены, пока не завершится этот внутренний цикл.

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

Плюсую. Всегда полезно перед изобретением велосипеда посмотреть на уже существующие решения. Zx spectrum и его клоны, например.

Точно. А еще есть решение от Motorola, у них в этой схеме было бы 5 проводов, а не 8. Вообще, N+1, где N - размерность матрицы.

Но более одной клавиши не нажимать (а зачем).

Обычно любую клавишу вместе с любой не нажимают. А специальные клавиши можно и на отдельные пины подключить.

Если вы делаете, например, гемпад или что-то игровое - то во всю нажимают любые кнопки с любыми.

Да и тратить целое прерывание на столбик это расточительство, лучше уж диоды напаять.

Прерывания и диоды никак не связаны. В классическом варианте с опросом тоже должны использоваться диоды. Резисторы 1 кОм? Они же не дадут уровню опуститься до Low.
100 Ом может быть?
Предложено было вместо активных выходов для формирования высокого уровня на строках использовать вход с подтяжкой.
По экономии прерываний, да, на младших платах столько может и не быть.

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

Вот внутри Loop такой код. Как из него по опросу выйти, чтобы цикл не тормозить? Без прерывания не вижу как.

kbd=0; // по прерыванию kbd=1
For (i=0; i<100500;i++)
{
if (kbd>0) break;
AnalogVal0[i] = analogRead(A0);
}

Я бы всячески избегал наличия длинных блокирующих циклов внутри main loop.

for (i=0; i<1005;i++)
{
  do_some_read(i);
  kbd = do_some_keyboard_function();
  if (kbd>0) break;
}

Без прерываний, например.

Спрошу в ветке про тормоза: а насколько уместно вставлять большие задержки (целых 50 мс) в основном цикле (ну почти) для устранения дребезга? Может, как-то на таймере можно отсчитать?

И вообще, приветствуются ли в принципе конструкции типа delay(xx)?

От условий зависит. Если есть таймер - почему бы и не использовать. Если устраивает наличие delay() - почему бы и не использовать.

Дребезг в дешёвых клавиатурах обычно единицы (ну десять) миллисекунд, так что 50мс, по-моему, избыточно. А про уместность - зависит от того, что надо выполнять параллельно с чтением клавиатуры и можно ли это остановить на 50мс.

На таймере тоже можно сделать, но, скорее всего, ценой оперативной памяти под счётчик тиков.

На таймере тоже можно сделать, но, скорее всего, ценой оперативной памяти под счётчик тиков.

Не понял, зачем считать тики отдельно?

И вообще, честно говоря, до сих пор не до конца понимаю: зачем что-то делать на ардуино (AVR), если есть те же Stm, в которых за меньшую цену больше памяти и флеша, куча таймеров, куча EXTI и прочего? Единственный аргумент, который нашел - это наличие невероятного количества готовых библиотек.

Если мы говорим про ардуино - то комьюнити у атмег сильно больше и произвольно взятая библиотека точно будет работать с атмегой (ну, если не сказано иное), но не факт, что с STM32. Например, в ардуино-ядре STM32 до сих пор несовместимая реализация API для USB-HID, поэтому всякие геймпады, написанные для Atmega32u4, не запускаются на STM32, и надо с нуля писать свой код, глубоко погрузившись в потроха USB.

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

По функционалу STM32 на лопатки кладет AVR, да. Сейчас, правда, есть нюанс - популярные чипы типа STM32F103 за вменяемые деньги не достать. Дошло до того, что F401 дешевле, чем F103.

Не понял, зачем считать тики отдельно?

Ну, мое видение реализации задачи на таймерах такое: таймер каждые, скажем, 5мс, сканирует клавиатуру и получает ее текущее состояние (состояние, если мы не рассматриваем одновременное нажатие - это то, какая кнопка нажата; если рассматриваем - то битовая маска для нажатости каждой кнопки или какой-то массив). Когда в течение какого-то числа тиков таймера состояние не изменятеся, то таймер отдает в остальной код новое, отфильтрованное состояние клавиатуры. И вот для отслеживания, сколько тиков прошло, и для сравнения с предыдущим состоянием, нужна память. Либо это один счетчик на всю клавиатуру, либо для каждой кнопки, возможны варианты.

Возможно, кто-то предложит другой способ, получше.

НЛО прилетело и опубликовало эту надпись здесь

А вот интересно, насколько сложно сделать NKRO-клавиатуру?
Как это обычно реализовано?
Есть ли готовые контроллеры?

Есть. Например TCA8418 или куча аналогов. Стоит недорого. Особенно удобно в тех случаях, когда кнопок опросить нужно много, а свободных ног у чипа нет.

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

В нормальных проектах (не на Ардуино) , совмещают опрос матричной клавиатуры с выводом на семисегментный дисплей или дискретные светодиоды. В подавляющем большинстве проектов , где есть клавиатура присутствует и индикация.

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

Так как вывод на дисплей производится , как правило , по прерываниям таймера , то появляется возможность измерять время нажатия кнопок в тиках выполненых прерываний.

	attachInterrupt(5, irqkb5, FALLING); // 'setup()
	attachInterrupt(6, irqkb6, FALLING); //RISING
	attachInterrupt(7, irqkb7, FALLING); //RISING
	attachInterrupt(8, irqkb8, FALLING); //RISING

Серьёзно? Вот прямо так беру в любом количестве первые попавшиеся пины и использую их в attachInterrupt()? На любой ардуине? А документацию почитать?

https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

Прерывание на "любом" пине делается совсем не так.

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

Сейчас под "ардуино" часто понимают любой чип, программируемый через среду Arduino IDE. А физически там что угодно может быть.

через среду Arduino IDE

В platformio тоже можно arduino framework и всякие avr.
Причем можно этот самый arduino framework даже для, например, esp использовать.

Дак и в Arduino IDE можно подключить ядро и для esp32 и для esp8266, и для stm32, и для nrf51/52. Что характерно, при этом ничего из этого не является ардуиной (ну ладно, по-моему есть ардуина на nrf52).

Данный скетч написан для Arduino Due. Добавлю в статье, спасибо.

Вот для Due - пойдёт

Очередная статья про ардуину и включение светодиода без токоограничивающего резистора.

Про одновременное нажатие кнопок, диоды и резисторы. Решение крайне простое и удивительно очевидное: переводим неактивные пины выбора "столбцов" из выхода на вход с подтяжкой. В каждый момент времени на выход работает только один пин. Никакого КЗ не получится автоматически.

Про дребезг. Да, самый простой способ его отфильтровать - это задержка. Но использовать блокирующую задержку в программе - это отвратительное решение. Для неблокирующей фильтрации делаем опрос клавиатуры в две стадии. Первая - устанавливаем пины "столбцов". Вторая - читаем пины "строк". Это можно делать по прерыванию или вызывая функцию опроса из основного цикла. Главное, периодично. Это может выглядеть как-то так:

int key_state[ROW_COUNT][ROW_LEN];
int key_column = 0;
bool key_pins_set = false;
void AskKey() //Вызывать примерно раз в 10-20мс
{
	if(key_pins_set) //Если столбец выбран
  {
  	for(int i=0;i<ROW_LEN;i++) //Бежим по строке
  		if(ReadRow(i)) key_state[key_column][i]++; //Если кнопка нажата - инкремент по 
      else key_state[key_column][i] = 0; //Если не нажата - сброс в 0
      
    key_column++; //Для следующей итерации увеличиваем номер столбца
    if(key_column >= ROW_COUNT) key_column = 0; //Если прошли все столбцы - начинаем сначала
	}
	else
  {
  	SetColumn(key_column); //Если столбец не выбран - выбираем его
  }
  key_pins_set = ~key_pins_set; //Инвертируем флаг выбора столбца
}

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

И на последок, автору совет, никогда так не делайте так: "For (i=0; i<100500;i++) analogRead(A0);" Скажу по секрету, у АЦП тоже есть прерывания.

Про одновременное нажатие кнопок, диоды и резисторы. Решение крайне простое и удивительно очевидное: переводим неактивные пины выбора "столбцов" из выхода на вход с подтяжкой. В каждый момент времени на выход работает только один пин. Никакого КЗ не получится автоматически.

Рад, что вам понравился описанный в статье метод )

использовать блокирующую задержку в программе - это отвратительное решение.

Вот знал, что будут до delay докапываться, но поленился писать millis -то, millis-сё. А зря! )
Решение всегда зависит от конкретной задачи, если delay будет чему-то мешать, значит будут другие варианты с неблокирующей фильтрацией. В данном случае delay ничему не мешает.

И на последок, автору совет, никогда так не делайте так: "For (i=0; i<100500;i++) analogRead(A0);" Скажу по секрету, у АЦП тоже есть прерывания.

А что не так с этим кодом?

kbd=0; // по прерыванию kbd=1
For (i=0; i<100500;i++)
{
if (kbd>0) break;
AnalogVal0[i] = analogRead(A0);
}

Ну молотит АЦП с какой-то скоростью. И чем мешают прерывания АЦП? И чему?
Ну кстати, можно попробовать, Due у меня подключена.

Рад, что вам понравился описанный в статье метод )

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

Вот знал, что будут до delay докапываться

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

Решение всегда зависит от конкретной задачи, если delay будет чему-то мешать, значит будут другие варианты с неблокирующей фильтрацией.

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

А что не так с этим кодом?

Этот код не делает почти ничего. Основную часть времени он ждет установки флага о завершении преобразования АЦП. Это тот-же делей, только в другой форме. Если использовать прерывание и складывать там результат в массив, можно сильно высвободить время основного цикла программы. В это освободившееся время, например, обрабатывать результат измерений предыдущей пачки из 100500 измерений.

Такие простые мелочи сильно увеличивают отзывчивость программы на внешние раздражители. Думаю, мы все сталкивались с ситуацией, когда жмешь на кнопку, а она тебя игнорирует. Или результат приходит через две секунды. Это раздражает. А корень - вот он, в подходе к написанию программ.

Основную часть времени он ждет установки флага о завершении преобразования АЦП. Это тот-же делей, только в другой форме.

Это конечно так, но опять же все зависит от задачи. AnalogRead в Due длится 5 мкс, в младших платах видел, что около 120 мкс. Если вы предлагаете заменить AnalogRead на что-то связанное с управлением АЦП через регистры, то это точно не для новичков. Ну и принципиально это ничего не меняет - 5 мкс или 1 мкс. Вопрос в том с какой частотой требуется оцифровывать сигнал. Может так случиться, что и с 1 мкс на преобразование внутри цикла свободного времени не будет. А если 50Гц оцифровывать по 128 точек на период, то внутрь цикла придется вставлять delay циклы ожидания и времени будет полно для выполнения других задач.

Это конечно так, но опять же все зависит от задачи. AnalogRead в Due длится 5 мкс, в младших платах видел, что около 120 мкс. Если вы предлагаете заменить AnalogRead на что-то связанное с управлением АЦП через регистры, то это точно не для новичков.

Если считать не в микросекундах, а в тактах, то потери времени становятся значительно более весомыми. Так, например, в ДШ на ATmega328 говорится о том, что АЦП может выдать "Up to 76.9kSPS (Up to 15kSPS at Maximum Resolution) ". При тактовой частоте 20МГц вызов функции AnalogRead  будет стоить от 260 до 1333 тактов. 260 тактов это примерно 3-4 операции целочисленного деления - этого как раз хватит на простую обработку одного значения. Использование AnalogRead снижает производительность вашей системы более чем вдвое от потенциально возможной. Конечно, речь не идет про одиночные измерения с помощью АЦП. Но даже в приведенном вами примере про 50Гц - 128 точек на 50Гц = 6.4 КГц, треть времени ваш камень будет простаивать при использовании максимальной точности. А если нужно еще пару входов опрашивать?

В SAM3X8E имеется DMA. Там опрос АЦП вообще может не требовать ресурсов ЦП. Не знаю, есть ли вообще поддержка DMA в ардуинах.

Всеми этими словами я пытаюсь сказать, что инфраструктура Arduino очень сильно ограничивает разработчика, загоняет его в свои примитивные рамки, заставляя его использовать AnalogRead и вот это вот всё в камнях с DMA на борту. Начинающих пугают страшными регистрами. А это очень зря, особенно на таких простых вещах, как atmega. И тут надо отделать обучающие материалы от решений в стиле "и так сойдет". По крайней мере, подобные решения всегда нужно дополнять соответствующим текстом, про то, что это не оптимально, можно по другому и я так сделал, потому что у меня условия такие.

Поставил лайк всему вами сказанному.

Извините, но это плохая статья, низкокачественная, а для начинающих ардуинщиков и вредная.

Во-первых, автор не знает, что у разных Ардуин разное число доступных прерываний, например у "дефолтной" Uno - всего 2 прерывания, что по методу автора позволяет использовать целых 2 столбца. То, что метод проверялся только на Due, мы узнаем только под конец статьи, а то, что метод неюзабелен на атмегавских арудинах - только от комментаторов. Границы применимости предлагаемого метода, разумеется, стоило указать в статье.

Если уж автор хочет использовать прерывания, то лучше использовать pin change interrupt - они на атмеговских ардуинах поддерживаются всеми ногами и как раз подходят для того, чтобы по событиям с нескольких ног запускать одно и то же действие - сканирование клавиатуры.

И главное - непонятно, какие проблемы автор решает использованием прерываний. Озвучены 2 проблемы: КЗ при одновременном нажатии и невозможность обновления состояния кнопок в долгом цикле loop.

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

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

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

Ну и частая проблема статей о ардуино - низкое качество кода с переменными типа zzz, использование goto (!), мешанины из безнес-логики и низкоуровневого кода и многое другое.

Спасибо за содержательный комментарий.

Границы применимости предлагаемого метода, разумеется, стоило указать в статье.

Указал.

невозможность обновления состояния кнопок в долгом цикле loop.
...
проблему можно было бы решить прерываниями (а также таймерами, если прерываний не хватает, а также -- самый хороший метод -- без прерываний и таймеров, просто написав нормальный код без задержек в loop)

К примеру, АЦП должно снять N отсчетов с заданной частотой дискретизации, и влезать внутрь этого цикла с опросом клавиатуры или другими прерываниями "по таймеру" нельзя. Поэтому для аварийного выхода их цикла ( ну мало ли что пошло не так, а ждать конца измерений не хочется) остается использовать прерывания. Да, для младших плат прерываний на все пины клавиатуры может не хватить, но действительно, можно использовать PCINT и затем определять нажатую кнопку обычным способом.

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

Конкретно в приведенном скетче одновременное нажатие кнопок ни к чему не приведет - пока не отпущена первая кнопка - вторая не сработает.

К примеру, АЦП должно снять N отсчетов с заданной частотой дискретизации, и влезать внутрь этого цикла с опросом клавиатуры или другими прерываниями "по таймеру" нельзя.

Правильно ли я понимаю, что задача у вас следующая: есть цикл опроса АЦП, по нажатию любой кнопки происходит выход из него и дальше определяется, какая функция нажата? Тогда для сканирования вполне можно использовать стандартный код, который сканирует всю клавиатуру, а все прерывания просто устанавливают один флаг "что-то нажато".

Ну и как выше написали, для этого не очень хорошо подходит busy loop, в течение которого ядро в основном, ждет, пока АЦП выдаст еще одно измерение. В атмегах можно запустить АЦП циклично и обрабатывать прерывания, когда новое измерение будет доступно. В ARM можно запускать АЦП по таймеру (что еще и позволит задавать необходимую частоту) и/или использовать DMA, не загружая ядро. Также то, что в busy-loop для чтения АЦП нельзя засунуть правильно написанный опрос клавиатуры (без задержек в 50мс и опроса в цикле пока дребезг не затихнет), тоже требует проверки. Может, и можно?

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

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

Уменьшил delay до 10 мс, полет нормальный. Но можно конечно использовать millis, если блокировка нежелательна.

Из недавнего: лень было опрашивать пины переключателей по отдельности, вычитывл весь порт целиком с маскированием нужного. По таймеру конечно. Полученное значение, если входы подключены пачкой - готовое значение переменной например, номера режима. Arm после сложности освоения даёт лёгкость использования

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории