Энкодер - это устройство преобразования механического перемещения или угловых изменений положения в цифровой сигнал. В статье рассматривается самый популярный в DIY сообществе инкрементальный энкодер EC11 с кнопкой. При его вращении на выходах A и B формируются TTL сигналы в виде импульсов сдвинутые между собой по фазе на 90 градусов. Таким образом с его помощью, можно определить направление и скорость вращения, а так же рассчитать угол поворота. В отличие от потенциометров, энкодер KY-040 гораздо надежней и долговечный.
Немного подробностей
Собирая один из проектов с использованием encoder. Я не смог найти код для Ардуино выполняющий все мои условия. Так как для проекта нужно обрабатывать следующие команды: "Вращение без нажатия", "Вращение с нажатием", "Нажатие" и "Длинное нажатие", а так же требуется стабильная работа энкодера. Скетчи использующие один пин с прерыванием INT0 или INT1, работают отвратительно и при вращении вала энкодера вылетает очень много ошибок. Код без использования прерываний работает стабильно, но он не работает в фоновом режиме, его нужно встраивать в тело основной программы, что в свою очередь приводит к не своевременному срабатыванию обработчика и пропускам при вращении энкодера. Еще хуже обстоят дела с обработкой нажатия с вращением вала энкодера и обычным с нажатием. Пришлось написать свой код обработки, который исключает описанные выше проблемы. С дребезгом контактов я не стал бороться программно, так как это приводит к задержкам обработки. Проще и надежней использовать керамические конденсаторы.
Схема подключения энкодера к Ардуино
Для считывания сигналов с выходов EC-11, нужно использовать три цифровых входа Arduino. В схеме подключения я использовал редко используемые мной в своих проектах выводы Arduino(A1, A2 и A3). Внешние подтягивающие резисторы отсутствуют, так как я использовал внутреннюю подтяжку микроконтроллера. Конденсаторы нужны для гашения импульсов дребезга контактов. Если у вас новый и хороший энкодер, то можно обойтись и без них. Но на кнопку в любом случае потребуется конденсатор, так как ее дребезг неизбежен.
Используемые в схеме компоненты:
Arduino nano - 1 шт.
Энкодер EC11 -1 шт.
Соединительные повода - 4 шт.
Керамические конденсаторы 0,1 мкФ - 3 шт.
Скетч для Ардуино
Для того что бы отслеживать изменение положения энкодера в фоновом режиме, я использую прерывание PCINT1. Обработка всех функций происходит в прерывании, обработчик в зависимости от произошедшего действия изменяет переменную enc_state. Если значение переменной enc_state=0 - ничего не произошло, enc_state=1 - экодер вращался без нажатия, enc_state=2 - экодер вращался с нажатием, enc_state=3 - было нажатие на кнопку, enc_state=4 - было длинное нажатие на кнопку, Прерывание будет срабатывать каждый раз по изменению состояния входов, как с высокого уровня на низкий, так и наоборот. То есть при одном щелчке энкодера прерывание сработает 4 раза. Или по 2 раза для каждого из входов. Но обработчик выдаст сигнал поворота только 1 раз на все 4 прерывания.
Код обработчика при каждом срабатывании записывает в переменную lastcomb состояние входов, к которым подключен энкодер. И ждет состояние когда выходы A и B будут замкнуты на GND, это гарантированный сигнал того, что энкодер вращается. После того как этот сигнал получен, обработчик проверяет в какую сторону было вращение. Для этого он сравнивает его предыдущее значение из переменной lastcomb и в зависимости от фазы сдвига определит в какую сторону был поворот ротора. Как я писал ранее, сложнее всего отслеживать нажатие кнопки.
Так как использовать определенные тайминги я не планировал, потому, что они неизбежно приводят длительным задержкам работы обработчика и основной программы, или требуют использование таймера, которых в микроконтроллере всего 3 шт. их, как правило никогда не хватает. Собственно проблема состояла в том, чтобы разделить "нажатие с последующим вращением" от простого нажатия. В итоге как вы уже можете убедиться, я решил эту задачу. Оптимизацией кода я не стал заниматься, потому как все работает и меня все устраивает. Для наглядности в коде все действия с энкодером, отображаются в Serial мониторе программы Adruino IDE.
/*
При публичном размещении кода ссылка на первоисточник обязательна.
*/
#define btn_long_push 1000 // Длительность долинного нажатия кнопки
volatile uint8_t lastcomb=7, enc_state, btn_push=0;
volatile int enc_rotation=0, btn_enc_rotate=0;
volatile boolean btn_press=0;
volatile uint32_t timer;
//********************************
void setup()
{
pinMode(A1,INPUT_PULLUP); // ENC-A
pinMode(A2,INPUT_PULLUP); // ENC-B
pinMode(A3,INPUT_PULLUP); // BUTTON
PCICR = 0b00000010; // PCICR |= (1<<PCIE1); Включить прерывание PCINT1
PCMSK1 = 0b00001110; // Разрешить прерывание для A1, A2, A3
Serial.begin(115200);
}
//****************************************
void loop()
{
switch (enc_state)
{
case 1: {
Serial.print("Вращение без нажатия ");
Serial.println(enc_rotation);
}
break;
case 2: {
Serial.print("Вращение с нажатием ");
Serial.println(btn_enc_rotate);
}
break;
case 3: Serial.println("Нажатие кнопки ");
break;
case 4: Serial.println("Длинное нажатие кнопки ");
break;
}
enc_state=0; //обнуляем статус энкодера
}
//****************************************
ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3
{
uint8_t comb = bitRead(PINC, 3) << 2 | bitRead( PINC, 2)<<1 | bitRead(PINC, 1); //считываем состояние пинов энкодера и кнопки
if (comb == 3 && lastcomb == 7) btn_press=1; //Если было нажатие кнопки, то меняем статус
if (comb == 4) //Если было промежуточное положение энкодера, то проверяем его предыдущее состояние
{
if (lastcomb == 5) --enc_rotation; //вращение по часовой стрелке
if (lastcomb == 6) ++enc_rotation; //вращение против часовой
enc_state=1; // был поворот энкодера
btn_enc_rotate=0; //обнулить показания вращения с нажатием
}
if (comb == 0) //Если было промежуточное положение энкодера и нажатие, то проверяем его предыдущее состояние
{
if (lastcomb == 1) --btn_enc_rotate; //вращение по часовой стрелке
if (lastcomb == 2) ++btn_enc_rotate; //вращение против частовой
enc_state=2; // был поворот энкодера с нажатием
enc_rotation=0; //обнулить показания вращения без нажатия
btn_press=0; //обнулить показания кнопки
}
if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпускание кнопки, то проверяем ее предыдущее состояние
{
if (millis() - timer > btn_long_push) // проверяем сколько прошло миллисекунд
{
enc_state=4; // было длинное нажатие
} else {
enc_state=3; // было нажатие
}
btn_press=0; //обнулить статус кнопки
}
timer = millis(); //сброс таймера
lastcomb = comb; //сохраняем текущее состояние энкодера
}
Заключение
Результат работы кода меня порадовал и теперь я могу продолжить работу над своим новым проектом, который скоро здесь выложу. Надеюсь эта короткая статья вам понравилась и вы сможете воспользоваться моей наработкой в своих самоделках.
Если у Вас остались вопросы и замечания, пишите их в комментариях. Я с удовольствием на них отвечу.
Комментарии (101)
anonymous
00.00.0000 00:00CyberBot Автор
31.10.2021 19:31Если в мышь установить потенциометр вместо энкодера, то он умрет через 1 неделю интенсивного использования.
Для исключения дребезга контактов программно нужна задержка и немного дополнительного кода. И это все равно не избавит от ошибок.
Слишком Вы упрощаете. Если посмотреть на библиотеку analogRead повнимательней, да еще добавить обработку всех считанных показаний потенциометра, то быстрее не получится. Тем более при каждом чтении на АЦП программно выдерживается пауза, для того что бы входной конденсатор разрядилсяSerge78rus
31.10.2021 20:07На ардуиновской функции analogRead() свет клином не сошелся. Вполне можно работать с АЦП в фоновом режиме по прерываниям, а в основном цикле использовать готовые результаты измерения. Получится примерно так же, как Вы поступаете в своей программе с энкодером.
CyberBot Автор
31.10.2021 19:56На видео настольная лампа электронщика с энкодером. В течении 1 недели смонтирую видео про него и выложу
CyberBot Автор
31.10.2021 20:33Я всего одним энкодером выбираю нужный мне режим светильника, регулирую яркость, регулирую температуру цвета и еще длинным нажатием могу выключить светильник. На потенциометре сможете это все сделать?
sterr
01.11.2021 19:33+1Вы не сделали еще одну важную деталь - изменение скорости прокрутки. Допустим вам надо регулировать от 0 до 100. И каждый щелчок увеличивает переменную на 1. И если крутить просто по программе то надо сделать 100 щелчков. А в нормальной аппаратуре количество прибавления зависит от скорости вращения энкодера. Медленно вращаем - приращивается на 1, быстрее - на 2, еще быстрее на 3 и т.д.
anonymous
00.00.0000 00:00Mike-M
01.11.2021 22:11Вроде бы и крутишь, но часть щелчков фейковые.
Это известная проблема. Из описания Transistor Tester: "The figure 2.8 shows a rotary pulse encoder, which has not only bouncing contacts, but also has a unstable state of one of the switches at the indexed position (detent)".
serafims
31.10.2021 23:21+1Допустим, вам надо регулировать условную яркость из двух мест одновременно.
Конечно, есть моторизованные потенциометры, но программно синхронизировать уставку параметра, пришедшую "извне" со с переменной, привязанной к энкодеру - проще.
Впрочем, энкодер прекрасно заменяется на 3 кнопки, при этом их можно ловить тем же аналоговым входов или тремя обычными, но добавляем кучу состояний типа "нажали и держим две кнопки".
igarkam
31.10.2021 20:36У Алекса Гайвера есть неплохая библиотека собственной разработки для работы с энкодером. https://alexgyver.ru/encoder/
CyberBot Автор
31.10.2021 20:42А Вы сначала сравните мой код с гайверовским, а потом напишите результат. Я проверял его код, работает крайне нестабильно
AlexGyver
02.11.2021 18:27-1А вы сначала попробуйте завести код в равнозначных условиях: либо свой код без прерываний в забитом лупе, либо "гайверский" с прерываниями, как и рекомендуется в библиотеке. А потом уже делать выводы о крайней нестабильности. Да и про классы почитайте...
CyberBot Автор
06.11.2021 16:04+1Начнем с твоего плагиаторства, ты украл мой проект (2011г.) и выложил в своем видео блоге(2017г.) от своего имени и еще добавил, что ты сам разработал ))))). Хотя даже код не изменил для неузнаваемости. Ровно как и большинство твоих видео, это проекты других людей, выложенные тобой как от супер-гениального разработчика.
По поводу твоей библиотеки, я считаю ниже своего достоинства использовать в своих проектах, твои г..но библиотеки и все остальное, что как то связано с твоим ником или именем.AlexGyver
06.11.2021 16:20+1ахахах, это тебя пришлось забанить в группе за идиотию и клевету? Отвечу здесь ещё раз, мне не сложно:
Я ничего никогда ни у кого не крал. Код был взят с форума arduino.ru, он был там в свободном доступе. И я не выдавал его за свой.
По этой же причине я не менял код и взял твою библиотеку, не изменяя исходник. Как я делаю во всех проектах, чтобы зритель мог скачать архив и не собирать файлы по всему интернету. Кому интересно - всегда может выйти на твой репозиторий. Опять же, я не выдавал библиотеку за свою, я не совсем конченый в отличие от некоторых.
А вот плату разработал я, на чём сделан акцент в видео, схема там - классическая для симистора и детектора нуля, находится в гугле по запросу arduino dimmer.
Библиотеки не используй, никто не заставляет, но абсолютно беспричинно поливать говном человека и его работу это как то низко, даже если это зависть или хз что там в голове. Всего хорошего, береги достоинство =)
CyberBot Автор
06.11.2021 17:24Не крал говоришь? Смотрим здесь https://youtu.be/jPbptVGZisc?t=438
Твоя фраза в видео - "Мой алгоритм" )))
Если ты взял в интернете, то при публичном использовании ты обязан сослаться на первоисточник. Но ты вместо этого говоришь "Мой алгоритм" при этом показываешь мой код.
Схема нарисована мной в нулевых годах, я реализовал ее тогда на pic контроллере и выложил на форум Микрочипа. В те времена ардуино еще не родился. Потом в 2010г. реализовал на ардуино. Так, что схема тоже впервые нарисована мной.
Если ты взял проект в интернете, то в этом нет ничего плохого, но вот если ты используешь его в публичном доступе и говоришь, что это твое, то ты "Плагиатор".Про достоинство: Как вообще у тебя язык повернулся говорить про достоинство в твоей ситуации? Если оно у тебя еще осталось, то удали видео и пожмем руки.
AlexGyver
06.11.2021 18:00Раз уж заговорили про достоинство, то я считаю выше своего читать весь этот полоумный бред, особенно после того как уже попрощался. Глянул конец - я уже предлагал мирно решить этот вопрос, добавив любые ссылки и нужную тебе информацию в описание под видео.
Давай так - извиняешься за фонтан говна и ничем не обоснованной клеветы в мою сторону в отношении остальных проектов (мы это обсуждали года два назад, я подробно расписывал какие проекты были лично мои, а какие - ДОРАБОТАНЫ и об этом по-русски сказано в видео, а ты ни ничего не смог на это ответить) - и я прямо сейчас убираю видео про диммер, вообще без проблем, и также приношу свои извинения.
CyberBot Автор
06.11.2021 18:20Глянул конец - я уже предлагал мирно решить этот вопрос, добавив любые ссылки и нужную тебе информацию в описание под видео.
Я не помню такого, если предлагал то извини за невнимательность, это решает наш вопрос и ссылки будет достаточно. Хотя форум уже полумертвый, но тем не менее. Если разместишь ссылку на оригинал http://cyber-place.ru/showthread.php?t=525 , то все претензии снимаются. После чего дашь ссылки на то, что нужно мне удалить я удалю без проблем и извинюсь
AlexGyver
06.11.2021 18:28+1Ссылки добавил, бонусом ссылку на канал. В свою очередь извиняюсь за то, что в видео назвал алгоритм своим и переходил на личности. По удалению не очень понял, значит наверное ничего удалять не нужно и можно считать вопрос закрытым.
CyberBot Автор
06.11.2021 18:47Спасибо! Извини за нападки я все таки был неправ о достоинстве. Ссылка на мой канал в ютуб, это был жест твоей доброй воли и ставит меня в неловкое положение, за мои высказывания. Удалять видео не нужно, я снимаю все претензии и приношу извинения за слишком резкую и чрезмерную реакцию.
xshura
31.10.2021 20:57Вдруг кому пригодится информация (кто не в "теме").
Есть еще абсолютные энкодеры, которые "запоминают" свое положение.
Например: EAW0J-B24-AE0128L
Они дороже конечно, но не намного. Энкодер выше, стоит в чип-дип 1240р.
gorbln
31.10.2021 21:06+71240 это примерно в 10 раз дороже инкрементальника из статьи. Очень намного.
А ещё есть микросхема AS5600, которая представляет собой абсолютный энкодер на 4096 отсчётов. И вот она стоит 200 рублей. Радиально намагниченный магнит к ней - ещё 68 рублей. Работает изумительно. На температуру почти не реагирует, +- 2 отсчёта на 60 градусов.
xshura
31.10.2021 21:26Спасибо за инфу, буду иметь ввиду.
Так то мне нужен энкодер с разрешением 1градус (поворотное устройство для антенны придумываю). Вот на всякий и прикупил такой для "разминки".
gorbln
31.10.2021 21:37+2Нудык 12 бит - это разрешение меньше 0,1 градуса, и ошибка будет не более 0,3. Такие энкодеры продаются на алиэкспрессе и применяются в ЧПУ станочках для контроля шагового двигателя - что он шаги не пропускает. Стоят ещё дешевле, чем я написал - первый же линк из поиска али - 130 рублей.
xshura
31.10.2021 21:45Упс.. еще раз спасибо. А я чет копал вот в эту сторону: https://aliexpress.ru/item/4001309275150.html?algo_expid=cbf4a8b7-5c2a-4f09-8b53-0f41dc65b578-21&algo_pvid=cbf4a8b7-5c2a-4f09-8b53-0f41dc65b578&btsid=0b8b037216321737052994355e223b&item_id=4001309275150&sku_id=10000015687603252&spm=a2g0o.productlist.0.0.5a4f1011Jk9wvJ&ws_ab_test=searchweb0_0%2Csearchweb201602_%2Csearchweb201603_
gorbln
31.10.2021 21:49Такая штука имеет смысл, когда обороты под 20000 и есть специальная микросхема-обработчик энкодера, ну либо магнитные помехи просто ОМГ. Во всех остальных случаях сейчас нет большого резона применять оптический энкодер.
gorbln
31.10.2021 21:55Кстати говоря, для больших оборотов и минимальных задержек есть синуснокосинусные преобразователи. Это такой сенсор, внутри которого два магниторезистивных моста, причём они сдвинуты по фазе на 90°. Один выдаёт синус угла поворота, а другой - косинус. Таким образом, обрабатывая сигнал с обоих мостов, мы можем рассчитать точное значение угла поворота. Такие штуки бывают как со встроенным обработчиком и цифровым интерфейсом, так и чисто мосты "без никто". Например, TLE5501
kot_martovskiy
01.11.2021 02:21А этой AS5600 надолго хватит, если я ее следить за направлением ветра в метеостанцию поставлю? Сейчас стоит последовательность из 16 герконов и резисторов разных номиналов.
gorbln
01.11.2021 13:23Она бесконтактная. Будет работать, пока питание подано. Лет на 10 точно хватит, а может и больше. Магниту тоже ничего от времени не будет, плюс у AS-ки довольно большой запас по напряжённости магнитного поля и АРУ.
Кстати, для метеостанции там есть режимы энергосбережения с пониженной частотой опроса. 1.5мА при частоте опроса 100 мс. Ну или выключать-включать.
nochkin
01.11.2021 17:18AS5600 на I2C и нельзя менять адрес. То есть, если надо парочку, то это не очень удобно.
Есть вариант AS5048 на SPI, но они уже намного дороже. Есть ещё недорогие варианты типа такого, но что бы можно было без проблем больше одного подключить?
gorbln
01.11.2021 20:44Да, вы правы. Я как-то даже не смотрел на адрес. Не было необходимости подключать более одного датчика. Но это, конечно, тупо - не делать возможности подключить больше одной микросхемы.
Варианты, конечно, есть - поставить мультиплексор порта, например, но это какие-то адовые костыли, которых бы не было, если бы, например, у микрухи была нога "ENABLE".
nochkin
01.11.2021 21:21Ага, именно так. Видимо, расчёт на то, что обычно нужна одна такая.
С мультиплексором, конечно, перебор. Проще поставить AS5048, это явно проще и дешевле будет на фоне продукта.
А мне как раз надо на оба колеса для робота такие поставить.
gorbln
01.11.2021 21:27Если на оба колеса - то, как по мне, проще найти камень с двумя аппаратными I2C или сделать софтовый. Ну или, например, выводить ШИМ и его измерять - тупо, но работает. А ШИМ AS5600 делать точно умеет.
nochkin
02.11.2021 02:37+1Камень тут не я выбираю, к сожалению. Это просто начальное требование.
С ШИМом вроде говорят, что не так надёжно получается на этих датчиках. К сожалению, не помню подробностей -- может помехоустойчивость не очень или ещё что.
P.S. хммм... только сейчас понял, что у меня как раз есть два железных I2C на камне. Не знаю куда я смотрел до этого, но зато я точно знаю, что на улице за окном ровно 12 ворон летает.
Правда, проводов будет чуть больше. Но этот вариант уже есть смысл рассмотреть.
reticular
31.10.2021 21:00очень приятно пользоваться устройствами с отзывчивым управлением
но энкодеры, подвержены износу, и даже конденсаторы не спасают :(
пользуюсь решением от DI HALT опрос по таймеру 1-5 мс
если же необходимо по прерыванию: великолепное железное решение — MC14490 -аппаратный подавитель дребезгаCyberBot Автор
31.10.2021 21:11Все подвержено износу. Но вот колесики на мышках годами живут, а там инкрементальный энкодер
serafims
31.10.2021 23:24+4Часто в мышках энкодер делают на основе оптопары.
CyberBot Автор
01.11.2021 00:00Это с шариками мышки так делали, а колесо на оптических мышках обычно на инкрементном энкодере
aamonster
01.11.2021 01:40Так на оптопарах он тоже инкрементный... Но я не помню в мышках чаще оптический или механический на колесе (вот x/y на мышках с шариком точно оптические были).
Gengenid
01.11.2021 09:34+1Ни разу не видел мышь с механическим энкодером на колесе. Всегда диск и оптопары.
CyberBot Автор
01.11.2021 10:03Ну да, я забыл ты же писатель, а не читатель. Специально для тебя https://yandex.ru/images/search?from=tabbar&text=%D1%80%D0%B0%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F%20%D0%BC%D1%8B%D1%88%D1%8C
Только вот на многочисленных фото оптических энкодеров я не нашел, даже на старых мышках установлены механические, инкрементальные энкодеры
borisxm
01.11.2021 11:46Это так. Но полагаю, что обработка состояния энкодера в них сделана по таймеру, или аппаратно, с защелкиванием по тактовым импульсам на частоте опроса. Вешать 3 дребезжащих контакта на прерывание, это верный путь к проблемам.
Mike-M
01.11.2021 22:27энкодеры, подвержены износу
От конкретного энкодера зависит. Не знаю, какой энкодер установлен в MP3-ресивере Pioneer, но работает он без единого нарекания вот уже 18-й год. И это с учетом колебаний температуры и влажности в салоне автомобиля!
smart_pic
31.10.2021 21:12Конденсаторы можно уменьшить до 0.01мкФ. Номинал подтягивающих резисторов к +питания?
Непонятно зачем такой сложный код обработки энкодера? Достаточно одного прерывания по спаду (или фронту) сигнала на выходе А энкодера для того чтобы определять направление и вести подсчет импульсов.
Определение нажатия на кнопку энкодера лучше сделать в
void loop()
gorbln
31.10.2021 21:16+1Много раз порывался поставить энкодер в свои поделия, и каждый раз приходил к тому, что он прекрасно заменяется тремя кнопками: ▲,▼ и ●. Количество пинов - то же, количество кода - в 100 раз меньше, кнопка подтверждения - есть. Единственное отличие - требуется три дырки, и нет красивой и приятной тактильно крутилки. Тут да, крутилка - это другое ощущение.
smart_pic
31.10.2021 22:13-2Код обработки длинного и короткого нажатия кнопки без библиотек на чистом С
//**************************************************************************** void ScanKeyboardTast(void) { static DWORD Tdl_key=0; // штамп времени static BOOL Key_Up; static BOOL Key_Down; static BOOL LongPres=0; static BOOL Key_Sost=1; static BOOL Key_OldSost=1; static BYTE Status_Input; // Состояние входа Status_Input=Status_Input<<1; if (BXOD1 == 1) Status_Input=Status_Input | 0x01; else Status_Input=Status_Input & 0xFE; if (Status_Input== 0xFF) Key_Sost=0; else if (Status_Input== 0x00) Key_Sost=1; // Если не нужна защита от дребезга то нужна всего одна строка Key_Sost=BXOD1; // Кнопка замыкает подтянутый к +питания вход на землю if ((Key_Sost ==0) && (Key_OldSost ==1)) // Кнопка нажата ? { Key_Down=1; // Установили флаг нажатия Tdl_key = TickGet(); // сделали штамп времени } if ((Key_Sost ==1) && (Key_OldSost ==0)) // Если кнопка отжата ? { if (LongPres==1) // Проверяем было ли длинное нажатие { LongPres=0; // {.....} // Действие по длинному нажатию } Key_Down=0; // {.....} // Действие по короткому нажатию нажатию } if (Key_Down==1) // если было нажатие { if (TickGet() - Tdl_key > Time_long_press) // проверяем вдруг будет длинное нажатие { LongPres=1; // зафиксировали длинное нажатие } } Key_OldSost=Key_Sost; // запомнили предыдущее состояние кнопки } //**************************************************************************** // Примечание функция TickGet() очень похожа на функцию millis() в Ардуино // BXOD1 - вход подключения кнопки
CyberBot Автор
31.10.2021 22:17-3У меня в прерывании обрабатывается вот так:
if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпускание кнопки, то проверяем ее предыдущее состояние { if (millis() - timer > btn_long_push) // проверяем сколько прошло миллисекунд { enc_state=4; // было длинное нажатие } else { enc_state=3; // было нажатие } btn_press=0; //обнулить статус кнопки }
Можно так же добавить обработку очень длинных нажатий
Gromushka
31.10.2021 22:26+4В ардуино я не спец, но что-то мне подсказывает :) из опыта, что прерывание происходит не тогда когда хочется, а тогда когда приходит. В любой момент кода.
Поэтому в нормальной программе если используется глобальная переменная как enc_state и в обработчике, и в основном теле программы, то код, который изменяет значение переменной в основной части лупа (обнуляет) должен быть закрыт DISABLE_INT() / ENABLE_INT() в противном случае даже в такой простой программе, все может легко сбиваться. То, что работает - подозреваю дело времени. Хотя, возможно запрет на прерывания в процессе основного цикла в ардуино может быть автоматом.
По поводу 10 строк обнуления и кучи if () вместо обычного switch case прикрыт скромной фразой про отсутствие оптимизации, только оптимизация тут не причем.
Код грязный, я бы сказал неграмотный. Так как с такими железяками не работал никогда, не приходилось, точно сказать как сделать не возьмусь, но подозреваю, что весь кода сокращается в два раза легко.
Я уже молчу про то, что если сработало первое условие смысла продолжать код нет, и нет проверки на то, есть ли что-то в enc_state отличное от нуля.
Ну в общем - не код это как по мне.
ПыСы: добавлю, что в нашем случае когда состояний всего 4 основной цикл должен выглядеть так:
#define MAX_STATE 4 void * pFunc(void)[MAX_STATE] = { func1(), func2(), func3(), func4() }; void loop() { if ((enc_state > 0) && (enc_state < MAX_STATE)) { pFunc[enc_state](); enc_state=0; } }
CyberBot Автор
31.10.2021 22:34-5Поэтому в нормальной программе если используется глобальная переменная как enc_state и в обработчике, и в основном теле программы, то код, который изменяет значение переменной в основной части лупа (обнуляет) должен быть закрыт DISABLE_INT() / ENABLE_INT() в противном случае даже в такой простой программе, все может легко сбиваться. То, что работает - подозреваю дело времени.
Так для этого есть специальный тип переменных volatile. Про swich вообще жесть, Вы похоже не в курсе как он работает и стоит ли его применять в этом случие. Прежде чем судить о правильности кода, лучше почитайте основы програмирования
Gromushka
31.10.2021 22:47+7Причем тут volatile?
Эта инструкция не защищает от изменения переменной глобального типа в разных местах кода (обработчик и основной код) это лишь инструкция к компайлеру, что все ваши действия с переменной нельзя убирать, даже если он считает, что так будет оптимальнее.
Про switch я промолчу, но основы программирования - это то, на чем вы остановились. Не буду вам мешать - наслаждайтесь.
Serge78rus
31.10.2021 22:58+4volatile всего лишь предписывает компилятору не делать никаких предположений о значении переменной при оптимизации, а вовсе не обеспечивает синхронизации. В Вашем случае используется переменная байтовой длины, поэтому проблем не возникает, так как установка и считывание значения переменной происходит атомарно. Если бы переменная была длиннее, то она бы уже не была атомарной и были бы возможны проблемы, о которых и написал Gromushka.
Gromushka
31.10.2021 23:05+4Проблема глубже и дело даже не в атомарности операции обнуления / присвоения.
У глобального цикла конструкция if () / if () не прерывается при нахождении, а обнуляет переменную и идет проверять дальше, предполагая, что далее захода в условие не будет, но это не так.
Прерывание может произойти например внутри конструкции if () сразу после печати но перед обнулением. Прерывание (атомарно) поменяет значение переменной, а затем вернет управление в основной цикл, где переменную обнулят и значение потеряется. Или наоборот, прерывание произойдет в момент сразу после начала цикла (если значения enc_status больше 1) пока цикл пытается обработать первое значение опять происходит прерывание и значение становится 1 поэтому оно не обнулится не обработается так как в проходе будет уже за пределами первого if () и будет переписано следующим прерыванием (потеряются два шага).
В данном примере этого не будет заметно. Но в реальной программе, где шаги и обработка прерываний будет намного быстрее чем основной цикл, вся логика исчезнет. Решений много, и для данного случая подошло бы просто копирование текущего состояния в локальную переменную с последующей обработкой.
Serge78rus
31.10.2021 23:27+2Согласен. По хорошему, если время обработки события в основном цикле много больше, чем возможный темп возникновения событий, то необходимо организовывать очередь, иначе возможен пропуск событий. А при организации очереди необходимо позаботиться о синхронизации операций записи и извлечения.
CyberBot Автор
31.10.2021 23:29-3Еще решением может быть отключением прерывания и последующим включением после полной обработки. Например так:
ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3
{
uint8_t comb = bitRead(PINC, 3) << 2 | bitRead( PINC, 2)<<1 | bitRead(PINC, 1);
PCICR = 0b00000000; // PCICR |= (1<<PCIE1); Выключить прерывание PCINT1
INTstate=1;
}
RTFM13
02.11.2021 02:23В Вашем случае используется переменная байтовой длины, поэтому проблем не возникает, так как установка и считывание значения переменной происходит атомарно.
а enc_rotation не байтовая.
по этому если мы начинаем читать на 8-битном контроллере 0x01FF считали старший байт, в середине присвоения вызвалось прерывание которое инкрементировало значение до 0x0200, затем мы считали младший байт и получили на выходе 0x100.
По этому или запрет прерываний на время присваивания или двойная буферизация в прерывании и флаг запрета обновления в прерывании. Или любой другой механизм (который как правило сводится к этим двум).
Serge78rus
02.11.2021 11:58Согласен, либо я прозевал использование переменной enc_rotation в основном потоке, либо его там не было. Сейчас автор уже полностью переписал код в статье, так что дискутировать уже смысла нет.
Exclipt
31.10.2021 23:08+2А расскажите, чем вас не устроил коммент про swith? А то я вот смотрю на 4 одинаковых куска вида:
if(enc_state==1) // Если энкодер вращался без нажатия
{
Serial.print("Вращение без нажатия ");
Serial.println(enc_rotation);
enc_state=0; //обнуляем статус энкодера
}
Кусок, в котором последняя строка вообще всегда одинакова, и у меня такие же мысли возникают, и это очень мягко говоря.
CyberBot Автор
31.10.2021 23:21-1Нужно же было писать в какой функции Вы хотели вставить switch. Я то подумал, что Вы предлагаете в прерывании его вставить. В главном цикле loop этот кусок кода всего лишь демонстрация, он потом выбрасывается от туда
Exclipt
31.10.2021 23:27+3Это не я его предлагал вставить, другой комментатор, но и в прерывании правильное структурирование кода тоже никак бы не помешало, там вся логика завязана на 4 значения comb. Конечно - это все идеализм, особенно, если говорить про прототипы, но раз уж отреагировали так резко - будьте готовы объясниться.
CyberBot Автор
31.10.2021 23:40-2Во первых это многословность и громоздкость самой конструкции, только взгляните — switch, case, break и default
Вы бы написали свой пример обработки на switch, потом мы бы порассуждали, что лучше и компактнее
Exclipt
01.11.2021 00:12+2Я писал про структурированность кода, а не про компактность, странно, что это надо объяснять. Компактно вы можете хоть "a = 3 - a;" написать по мотивам легенд из прошлого века "как заменить 1 на 2 и наоборот". Но даже если это не вспоминать, у нас еще в 98-м на лабах универа снижали оценки за лишние бессмысленные if, жрущие производительность.
Вам написали, что код некрасив, уже неэффективен, и потенциально ошибочен при развитии (что не относится к самой статье, которую я прочитал с интересом, т.к. только два дня назад такой же энкодер заюзал), вы же зачем-то лезете в бутылку, вместо того, чтобы сделать выводы.
CyberBot Автор
01.11.2021 00:34Я Вам предложил написать такой же код на switch, что бы потом сравнить его размер после компиляции и производительность. Напишите , а потом поговорим.
Gromushka
01.11.2021 08:34В компиляторе С switch на 4 состояния и if () 4 раза обычно компилируется одинаково
switch (enc_status) { case 1: func(1); break; case 2: func(2); break; case 3: func(3) break; } enc_state = 0;
Сравните с вашей
if (enc_state == 1) { func(1); enc_state = 0; } if (enc_state == 2) { func(2); enc_state = 0; } if (enc_state == 3) { func(3); enc_state = 0; }
Даже в этом случае ваш код на три строчки длиннее, проходит все if () не зависимо обработалось или нет значение enc_state и во всех случаях содержит одну и ту же строку enc_state = 0
И вы еще заявляете о производительности? Учитывая volatile на все типы данных - у вас нет оптимизации и break в switch конструкции нужен для выхода из перебора значений - чего у вас вообще нет.
К сожалению - вы просто не умеете писать программы.
unsignedchar
01.11.2021 08:49+1if (enc_state !=0) { func(enc_state); enc_state=0; }
Когда вижу простыню if-ов, рука сама тянется к пистолету ;)
CyberBot Автор
01.11.2021 09:34-1Чукча не читатель, чукча писатель! Я же писал выше, что этот кусок кода не обязательный, вставлен только для демонстрации работы и да я согласился, что его лучше поменять. Речь же шла о замене if() в прерывании
CyberBot Автор
01.11.2021 10:39if (enc_state == 1) func(1); if (enc_state == 2) func(2); if (enc_state == 3) func(3); enc_state = 0;
сравнивайте
switch (enc_status) { case 1: func(1); break; case 2: func(2); break; case 3: func(3) break; } enc_state = 0;
unsignedchar
01.11.2021 11:03Дело вкуса. Если в вариант с if-ами добавить else (чтобы не гонять лишние проверки) и скобки (если вдруг вздумается делать что-то еще кроме func()) — никакой разницы.
ЗЫ: волшебные константы нехорошо в любом случае.
Serge78rus
01.11.2021 12:16+4К сожалению, привычка всегда окружать блок кода внутри if фигурными скобками, даже если это одиночное выражение, приходит только после набития некоторого числа шишек. А до этого превалируют рассуждения о краткости, лаконичности и т.д.
BigBeaver
01.11.2021 13:20Справедливости ради, можно и в однострочной записи окружить скобками. Поправьте, если ошибаюсь, но можно же всю программу на си написать в одну строку.
unsignedchar
01.11.2021 13:41Можно, конечно и в одну строчку (чтобы строки экономить), и все переменные длиной в 1 букву (чтобы символы экономить) ;)
Serge78rus
01.11.2021 13:48Поправляю: с директивами препроцессора будут проблемы — со всякими #define и т.д.
Gromushka
01.11.2021 14:45-1Сравнивайте, enc_state == 1 - сколько в вашем варианте лишних операций if ()?
Exclipt
01.11.2021 21:52Сравниваю: код с if выполняет всегда 3 проверки, вне зависимости от того, которая сработала. И сколько бы вы не лепили в одну строчку, такое отношение к производительности непозволительно даже джунам.
BigBeaver
01.11.2021 15:24+1но и в прерывании правильное структурирование кода тоже никак бы не помешало
Если говорить о серьезном коде, то надо в принципе переделать архитектуру программы. Обработчик прерывания должен быть предельно коротким — он должен просто преобразовывать его в некоторое событие (переменная comb), которое пишется в fifo очередь событий и уже обрабатывается в основном цикле или где-то еще. В общем, железные прерывания и их интерпретация должны быть в идеале разделены.
Нужно это чтобы прерывания были настолько атомарными, насколько возможно (если так можно сказать). А это нужно чтобы максимально исключить возможность прихода других прерываний во время обработки текущего. В крутилке для лампы это, конечно, не важно, но в сложных задачах с кучей переферии может быть критичным. Также это может быть важным в системах требовательных к таймингам разного рода (например, если есть софтварный bitbang генератор [например, для той же ws2812 ленты] и тд) — очевидно, что длинные обработчики прерываний будут сбивать тайминги, а очередь событий позволит нивелировать эффект, обрабатывая логику «между кадрами».
aamonster
01.11.2021 01:53Состояний не 4, а 16 (важны не только текущие значения пинов, но и предыдущие), из них 4 – стояние на месте (биты не поменялись), 4 – вращение в одну сторону, 4 – в другую и ещё 4 – некорректные (слишком быстрое вращение или ещё что, имеет смысл игнорировать).
Ну и по целой функции на состояние – жирно, достаточно значения +1, 0 или -1. Табличка на 16 signed char. Можно, конечно, поиграться с логическими операциями и избавиться от неё (кто тут умеет в карты Карно?), но смысла нет.
Да, и при такой обработке почти пофиг, работать по прерыванию с пина или по таймеру (с пина чуть хуже, есть мизерный шанс на дребезге "отстать" на 1 отсчёт энкодера... плюс таймер "съест" дребезг)
RTFM13
01.11.2021 00:28+4Проще и надежней использовать керамические конденсаторы.
RC фитр и вход в режиме триггера Шмитта, тогда уж.
И совсем не стоит коротить энкодером конденсаторы достаточно приличной емкости.
NanoVHF
01.11.2021 09:25Алекс(ей/андр?), вас не затруднит на ваш скетч нарисовать подробный алгоритм? Это позволит начинающим портировать код на другие языки. Если вы планируете несколько статей, то приведение в начале рисунка алгоритма было бы здорово.
srg27y
01.11.2021 10:18+2я кроме конденсаторов еще и 100Ом по общему проводу поставил
идея с вращением при нажатии понравилась, дажэ сходу понял где мне это надо.
uburame
06.12.2021 21:43В настольной лампе от Xiaomi такое используется: просто поворот - изменение яркости, поворот при нажатии - изменение цветовой температуры, нажатие - вкл/выкл.
dvserg
01.11.2021 10:29+1Вопрос автору. А не стоит ли всю логику из обработчика убрать, и в нем лишь фиксировать значения в некоторой очереди фиксированной длины? А саму логику вызывать в теле основной функции непосредственно перед использованием? Из плюсов, думаю, ускорится обработка прерывания и можно будет пропускать "дребезг" программно усреднением соседних значений в очереди?
FGV
01.11.2021 21:24+1Хм. Давно на авр не кодил, но в свое время программно ловил фронт одного канала по таймеру (период то ли 1мс то ли 100мкс) и в зависимости от состояния соседнего канала инкрементировал/декрементировал положение вала (так называемая одинарная точность, есть еще двойная - ловится фронт/спад одного канала и четверная - фронт/спад обоих каналов).
На современных мк (вроде стм32) квадратурный энкодер встроен в переферию и таких заморочек не нужно.
Maxon-top
А где Вы такую лампу купили?
CyberBot Автор
Сам сделал. Кольцо ws2812 на 45 светодиодов