02 | На заметку: |
|
|
03 |
Введение Инкрементальный (или инкрементный, от англ. increment — «увеличение») энкодер (датчик угла поворота) — это устройство, которое преобразовывает вращательное движение вала в серию электрических импульсов, позволяющих определить направление и угол его вращения. Также, исходя из найденных величин, можно определить и скорость вращения. Основным отличием инкрементальных энкодеров от абсолютных является то, что они могут сообщать лишь о величине изменения их положения, а не об абсолютном своем состоянии. Самым популярным примером использования инкрементального энкодера в повседневной жизни, является ручка регулировки громкости автомобильной магнитолы. |
|
04 |
Также энкодеры идеально подходят для реализации навигации по различным меню. |
|
05 |
Инкрементальные энкодеры бывают оптическими, магнитными, механическими и т.д. Вне зависимости от принципа устройства все инкрементальные энкодеры на выходе генерируют 2 линии (A и B) с импульсами смещенными относительно друг друга. Именно по смещению импульсов можно судить о направлении вращения. А по количеству импульсов — об угле поворота. |
|
06 |
В данной статье будет рассмотрен механический инкрементальный энкодер EC11 с переключателем (кнопкой) и пошаговой фиксацией положения вала (между каждой серией импульсов). |
07 |
Каждый инкрементальный энкодер имеет следующую основную характеристику — дискретность (количество шагов, положений между импульсами, на один оборот вала). Благодаря дискретности, можно вычислить угол единичного изменения положения. В нашем примере, энкодер ЕС11 за полный оборот генерирует 20 серий импульсов. А это значит, что каждый шаг эквивалентен повороту на 18°. Помимо этого, вал энкодера фиксируется в каждом положении между каждой серией импульсов. |
|
08 |
Внешний вид устройства: |
|
10 |
Сердцем энкодера являются 2 пары контактов и металлическая пластина с засечками. При вращении вала, каждая пара контактов замыкается и размыкается. Но эти пары контактов расположены таким образом, что при вращении вала в разные стороны порядок замыкания/размыкания контактов разный — и, благодаря этому, можно определить направление вращения. |
|
11 |
|
Проверено — автор рекомендует: Ручка черная d 6мм разных размеров для инкрементального энкодера EC11 |
12 |
Энкодер с кнопкой имеет 5 выходов — 2 выхода (D и E) отвечают за переключатель (кнопку), 1 (С) — общий (GND, земля), а оставшихся 2 (A и B) — импульсные линии, сигнализирующие о вращении. |
|
13 |
|
Схематичное представление энкодера
|
14 |
Поскольку подключение кнопки вала энкодера (контакты D и E) не отличаются от подключения обычной кнопки, информация по ней будет опущена. |
|
15 |
Схематично работу инкрементального энкодера можно представить следующим образом: |
|
17 |
Как видно из рисунка, в состоянии покоя обе пары контактов разомкнуты, а значит сигнальные линии A и B пребывают в высокоомном состоянии (состоянии Z). Поэтому их необходимо притягивать к логической единице подтягивающими резисторами. Стандартная схема подключения энкодера выглядит следующим образом: |
|
19 |
После чего в состоянии покоя на обоих сигнальных выходах будет присутствовать логическая единица (5 В). При вращении по часовой, или против часовой стрелки на сигнальных линиях, с противоположным смещением друг относительно друга, будут появляться отрицательные импульсы — по одному на 1 шаг на каждой линии: |
|
20 |
В состоянии покоя подтягивающие резисторы (10КОм) подтягивают сигнальные линии к логической единице
|
Если анимация не загружается её можно скачать (1,98 MB) и посмотреть отдельно.
|
21 |
Реальная осциллограмма вращения энкодера немного отличается от идеальной. |
|
22 |
Слева осциллограмма вращения по часовой стрелке, справа — против часовой стрелки |
|
23 |
При подключении энкодера к МК со встроенными подтягивающими резисторами, их можно исключить из схемы, не забыв при этом включить встроенные подтягивающие резисторы: |
|
25 |
Устранение дребезга Как и в любой кнопке, контакты энкодера также подвержены дребезгу при смыкании/размыкании. И, поскольку, подавляющее большинство программных реализаций взаимодействия энкодера с Arduino использует прерывания, дребезг будет мешать корректной работе самого отлаженного и работоспособного кода. |
|
27 |
Программное устранение дребезга В отличие от программного устранения дребезга обычной кнопки дребезг энкодера можно устранить программно, при этом серьезно не нагружая ресурсы микроконтроллера. И программное устранение будет работать на прерываниях и флагах. |
|
28 | На заметку: |
Автор считает, что определение состояния пинов энкодера в цикле loop() и последующее вычисление направления вращения является недопустимо затратным, по отношению к ресурсам МК, методом. Именно поэтому будет использован метод борьбы с дребезгом, при помощи прерываний.
|
|
29 |
После того как энкодер подключен к Arduino (на примере Arduino Uno): |
|
31 |
Сигнальные линии энкодера подключены к 2 и 3 пину Arduino Uno, так как на этих выходах реализованы прерывания. Замена пинов подключения приведет к неработоспособности примера. Скетч: |
|
32 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 // Объявляем переменные
int pinA = 2; // Пины прерываний
int pinB = 3; // Пины прерываний
volatile long pause = 50; // Пауза для борьбы с дребезгом
volatile long lastTurn = 0; // Переменная для хранения времени последнего изменения
volatile int count = 0; // Счетчик оборотов
int actualcount = 0; // Временная переменная определяющая изменение основного счетчика
volatile int state = 0; // Статус одного шага - от 0 до 4 в одну сторону, от 0 до -4 - в другую
volatile int pinAValue = 0; // Переменные хранящие состояние пина, для экономии времени
volatile int pinBValue = 0; // Переменные хранящие состояние пина, для экономии времени
void setup()
{
pinMode(pinA, INPUT); // Пины в режим приема INPUT
pinMode(pinB, INPUT); // Пины в режим приема INPUT
attachInterrupt(0, A, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала
attachInterrupt(1, B, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала
Serial.begin(9600); // Включаем Serial
}
void loop()
{
if (actualcount != count) { // Чтобы не загружать ненужным выводом в Serial, выводим состояние
actualcount = count; // счетчика только в момент изменения
Serial.println(actualcount);
}
}
void A()
{
if (micros() - lastTurn < pause) return; // Если с момента последнего изменения состояния не прошло
// достаточно времени - выходим из прерывания
pinAValue = digitalRead(pinA); // Получаем состояние пинов A и B
pinBValue = digitalRead(pinB);
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
if (state == 0 && !pinAValue && pinBValue || state == 2 && pinAValue && !pinBValue) {
state += 1; // Если выполняется условие, наращиваем переменную state
lastTurn = micros();
}
if (state == -1 && !pinAValue && !pinBValue || state == -3 && pinAValue && pinBValue) {
state -= 1; // Если выполняется условие, наращиваем в минус переменную state
lastTurn = micros();
}
setCount(state); // Проверяем не было ли полного шага из 4 изменений сигналов (2 импульсов)
sei(); // Разрешаем обработку прерываний
if (pinAValue && pinBValue && state != 0) state = 0; // Если что-то пошло не так, возвращаем статус в исходное состояние
}
void B()
{
if (micros() - lastTurn < pause) return;
pinAValue = digitalRead(pinA);
pinBValue = digitalRead(pinB);
cli();
if (state == 1 && !pinAValue && !pinBValue || state == 3 && pinAValue && pinBValue) {
state += 1; // Если выполняется условие, наращиваем переменную state
lastTurn = micros();
}
if (state == 0 && pinAValue && !pinBValue || state == -2 && !pinAValue && pinBValue) {
state -= 1; // Если выполняется условие, наращиваем в минус переменную state
lastTurn = micros();
}
setCount(state); // Проверяем не было ли полного шага из 4 изменений сигналов (2 импульсов)
sei();
if (pinAValue && pinBValue && state != 0) state = 0; // Если что-то пошло не так, возвращаем статус в исходное состояние
}
void setCount(int state) { // Устанавливаем значение счетчика
if (state == 4 || state == -4) { // Если переменная state приняла заданное значение приращения
count += (int)(state / 4); // Увеличиваем/уменьшаем счетчик
lastTurn = micros(); // Запоминаем последнее изменение
}
} |
|
33 |
Суть работы кода можно изобразить графически: |
|
35 |
При корректном выполнении сценария, по завершению каждого шага переменная state будет иметь состояние 4 или -4. Если что-то пойдет не так, программа никак не это не отреагирует. Но если программа увидит, что в состоянии покоя (A=1 и B=1), переменная state не равна нулю, то вернет её в исходное состояние. |
|
36 |
Аппаратное устранение дребезга Несмотря на незатратное, по отношению к ресурсам МК, программное решение устранения дребезга на прерываниях, более предпочтительным является его аппаратное устранение. Решение строится по принципу устранения дребезга обычной кнопки и выглядит так: |
|
37 |
Инвертирующий триггер Шмитта 74HC14N необходим для преобразования аналогового сигнала в цифровой, именно из-за него изменена полярность подключения
|
О том, почему изменена полярность подключения и для чего необходим инвертирующий триггер Шмитта 74HC14N, можно почитать в статье Arduino: Дребезг - программное и аппаратное устранение.
Проверено — автор рекомендует: Купить 74HC14N Инвертирующий триггер Шмитта (DIP-14)Видео-инструкция о покупке со скидками на Aliexpress |
38 | На заметку: |
Даташит на инвертирующий триггер Шмитта — sn74hc14.pdf.
|
|
39 |
После того, как дребезг подавлен аппаратно, программная реализация может быть значительно упрощена. |
|
40 |
Вторая линия подключена к 7 пину, освобождая 3 пин для других источников прерываний
|
|
41 |
Дополнительным бонусом может служить высвобождение одного из двух (для Arduino Uno) пинов с функционалом прерывания. Таким образом прерывание будет провоцироваться импульсом только одной линии, а далее, в обработчике, можно смотреть на состояние второй линии и делать выводы: |
|
42 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // Объявляем переменные
int pinA = 2; //Пин прерывания
int pinB = 7; //Любой другой пин
volatile int count = 0; // Счетчик оборотов
int actualcount = 0; // Временная переменная определяющая изменение основного счетчика
volatile int state = 0; // Переменная хранящая статус вращения
volatile int pinAValue = 0; // Переменные хранящие состояние пина, для экономии времени
volatile int pinBValue = 0; // Переменные хранящие состояние пина, для экономии времени
void setup()
{
pinMode(pinA, INPUT); // Пины в режим приема INPUT
pinMode(pinB, INPUT); // Пины в режим приема INPUT
attachInterrupt(0, A, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала - в этом примере отслеживаем только 1 пин
Serial.begin(9600); // Включаем Serial
}
void loop()
{
if (actualcount != count) { // Чтобы не загружать ненужным выводом в Serial, выводим состояние
actualcount = count; // счетчика только в момент изменения
Serial.println(actualcount);
}
}
void A()
{
pinAValue = digitalRead(pinA); // Получаем состояние пинов A и B
pinBValue = digitalRead(pinB);
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
if (!pinAValue && pinBValue) state = 1; // Если при спаде линии А на линии B лог. единица, то вращение в одну сторону
if (!pinAValue && !pinBValue) state = -1; // Если при спаде линии А на линии B лог. ноль, то вращение в другую сторону
if (pinAValue && state != 0) {
if (state == 1 && !pinBValue || state == -1 && pinBValue) { // Если на линии А снова единица, фиксируем шаг
count += state;
state = 0;
}
}
sei(); // Разрешаем обработку прерываний
} |
|
43 |
Этот пример работает корректно. |
|
44 |
Навигация при помощи энкодера Теперь можно организовать управление чем-либо при помощи энкодера. Его уникальность состоит в том, что одним элементом управления можно запрограммировать 4 реакции на действия: вращение вправо/влево, короткое нажатие и длинное нажатие (реакция на нажатия реализуется программно). |
|
46 |
Схема подключения: |
|
47 |
Освободившийся 3 пин (с прерываниями) теперь используется для перехвата нажатия кнопки
|
|
48 |
И скетч: |
|
49 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 int pinA = 2; //Пин прерывания сигнальной линии
int pinButton = 3; //Пин прерывания нажатия кнопки
int pinB = 7; //Любой другой пин
long timeButtonPressed = 1500; // Долгое удержание кнопки после 1,5 секунд
volatile int state = 0; // Переменная хранящая статус вращения
// Переменные хранящие состояние действия до его выполнения
volatile bool flagCW = false; // Было ли вращение по часовой стрелке
volatile bool flagCCW = false; // Было ли вращение против часовой стрелки
volatile bool flagButton = false; // Было ли нажатие кнопки
volatile bool flagButtonLong = false; // Было ли долгое удержание кнопки
volatile long timeButtonDown = 0; // Переменная хранящая время нажатия кнопки
volatile bool isButtonDown = false; // Переменная хранящая время нажатия кнопки
volatile bool longPressReleased = false; // Переменная для фиксации срабатывания долгого нажатия
void setup()
{
pinMode(pinA, INPUT); // Пины в режим приема INPUT
pinMode(pinB, INPUT); // Пины в режим приема INPUT
pinMode(pinButton, INPUT); // Пины в режим приема INPUT
attachInterrupt(0, A, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала на линии A
attachInterrupt(1, Button, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала нажатия кнопки
Serial.begin(9600); // Включаем Serial
}
void loop()
{
if (millis() - timeButtonDown > timeButtonPressed && isButtonDown) { // Время длительного удержания наступило
flagButtonLong = true;
}
if (flagCW) { // Шаг вращения по часовой стрелке
// ...
Serial.println("turn_right");
flagCW = false; // Действие обработано - сбрасываем флаг
}
if (flagCCW) { // Шаг вращения против часовой стрелки
// ...
Serial.println("turn_left");
flagCCW = false; // Действие обработано - сбрасываем флаг
}
if (flagButton) { // Кнопка нажата
// ...
Serial.println("short_press");
flagButton = false; // Действие обработано - сбрасываем флаг
}
if (flagButtonLong && isButtonDown) { // Кнопка удерживается
if (!digitalRead(pinButton) && millis() - timeButtonDown > timeButtonPressed) { // Защита от ложного срабатывания
// ...
Serial.println("long_press");
}
//=========================================== Настраиваем реакцию на долгое удержание кнопки ===============================================
// Чтобы событие long_press во время удержания срботало только один раз, необходимо раскомментировать блок и закомментировать следующий
//isButtonDown = false; // Программно "отжимаем" кнопку
// Эти две строки отвечают за то, чтобы при долгом удержании кнопки, событие long_press повторялось каждые 1,5 секунды
// Для того, чтобы изменить это поведение нужно закомментировать две эти строки и раскомментировать строку из предыдущего блока
timeButtonDown = millis(); // Сбрасываем таймер
longPressReleased = true; // Флаг срабатывания долгого удержания, чтобы отсечь генерацию обычного нажатия при отпускании кнопки
//==========================================================================================================================================
flagButtonLong = false; // Действие обработано - сбрасываем флаг
}
}
void A()
{
int pinAValue = digitalRead(pinA); // Получаем состояние пинов A и B
int pinBValue = digitalRead(pinB);
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
if (!pinAValue && pinBValue) state = 1; // Если при спаде линии А на линии B лог. единица, то вращение в одну сторону
if (!pinAValue && !pinBValue) state = -1; // Если при спаде линии А на линии B лог. ноль, то вращение в другую сторону
if (pinAValue && state != 0) {
if (state == 1 && !pinBValue || state == -1 && pinBValue) { // Если на линии А снова единица, фиксируем шаг
if (state == 1) flagCW = true; // Флаг вращения по часовой стрелке
if (state == -1) flagCCW = true; // Флаг вращения против часовой стрелки
state = 0;
}
}
sei(); // Разрешаем обработку прерываний
}
void Button()
{
if (millis() - timeButtonDown < 50) return;
int pinButValue = digitalRead(pinButton); // Получаем состояние пина кнопки
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
timeButtonDown = millis(); // Запоминаем время нажатия/отжатия
if (!pinButValue) { // При нажатии подается инвертированный сигнал
isButtonDown = true; // Устанавливаем флаг нажатия кнопки
}
else if (isButtonDown) { // Если кнопка отжата, смотрим не было ли выполнено действие
if (!longPressReleased) { // Если долгое нажатие не было ни разу отработано, то...
flagButton = true; // Если не было удержания, ставим флаг события обычного нажатия
}
isButtonDown = false; // Сбрасываем флаг нажатия
longPressReleased = false; // Сбрасываем флаг длительного удержания
}
sei(); // Разрешаем обработку прерываний
} |
|
50 |
В строках 59-67 можно настроить реакцию программы на длительное удержание кнопки энкодера: |
|
52 |
Энкодер и навигация по меню Для того чтобы продемонстрировать навигационные возможности энкодера необходимо создать меню. Для примера будем создавать такое меню: |
|
54 |
Для этого объявим структуру menu одного пункта и далее, создадим массив из элементов структуры menu: |
|
55 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 struct menu // Структура описывающая меню
{
int id; // Идентификационный уникальный индекс ID
int parentid; // ID родителя
bool isParam; // Является ли пункт изменяемым параметром
String _name; // Название
int value; // Актуальное значение
int _min; // Минимально возможное значение параметра
int _max; // Максимально возможное значение параметра
};
int menuArraySize = 22; // Задаем размер массива
menu menus[] = { // Задаем пункты меню
{0, -1, false, "Main", 0, 0, 0},
{1, 0, false, " File", 0, 0, 0},
{2, 1, false, " New", 0, 0, 0},
{3, 1, false, " Open", 0, 0, 0},
{4, 1, false, " Save", 0, 0, 0},
{5, 1, false, " Close", 0, 0, 0},
{6, 1, false, " Options", 0, 0, 0},
{7, 6, false, " General", 0, 0, 0},
{8, 6, false, " Presets", 0, 0, 0},
{9, 6, false, " Network", 0, 0, 0},
{10, 6, false, " Reset", 0, 0, 0},
{11, 0, false, " Edit", 0, 0, 0},
{12, 11, false, " Cut", 0, 0, 0},
{13, 11, false, " Copy", 0, 0, 0},
{14, 11, false, " Paste", 0, 0, 0},
{15, 0, false, " Effects", 0, 0, 0},
{16, 15, true, " Brightness ", 0, -50, 50},
{17, 15, true, " Contrast", 10, -50, 50},
{18, 15, true, " Exposure", 10, -100, 100},
{19, 15, true, " Highlights", 5, 0, 50},
{20, 15, true, " Shadows", 0, 0, 10},
{21, 15, true, " Clarity", 1, -10, 10}
}; |
|
56 |
Скетч и результат работы выглядит так: |
|
57 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 // Объявляем переменные
int pinA = 2; //Пин прерывания сигнальной линии
int pinButton = 3; //Пин прерывания нажатия кнопки
int pinB = 7; //Любой другой пин
long timeButtonPressed = 1500; // Долгое удержание кнопки после 1,5 секунд
volatile int state = 0; // Статус одного шага - от 0 до 4 в одну сторону, от 0 до -4 - в другую
// Переменные хранящие состояние действия до его выполнения
volatile bool flagCW = false; // Было ли вращение по часовой стрелке
volatile bool flagCCW = false; // Было ли вращение против часовой стрелки
volatile bool flagButton = false; // Было ли нажатие кнопки
volatile bool flagButtonLong = false; // Было ли долгое удержание кнопки
volatile long timeButtonDown = 0; // Переменная хранящая время нажатия кнопки
volatile bool isButtonDown = false; // Переменная хранящая время нажатия кнопки
volatile bool longPressReleased = false; // Переменная для фиксации срабатывания долгого нажатия
struct menu // Структура описывающая меню
{
int id; // Идентификационный уникальный индекс ID
int parentid; // ID родителя
bool isParam; // Является ли пункт изменяемым параметром
String _name; // Название
int value; // Актуальное значение
int _min; // Минимально возможное значение параметра
int _max; // Максимально возможное значение параметра
};
int menuArraySize = 22; // Задаем размер массива
menu menus[] = { // Задаем пункты меню
{0, -1, false, "Main", 0, 0, 0},
{1, 0, false, " File", 0, 0, 0},
{2, 1, false, " New", 0, 0, 0},
{3, 1, false, " Open", 0, 0, 0},
{4, 1, false, " Save", 0, 0, 0},
{5, 1, false, " Close", 0, 0, 0},
{6, 1, false, " Options", 0, 0, 0},
{7, 6, false, " General", 0, 0, 0},
{8, 6, false, " Presets", 0, 0, 0},
{9, 6, false, " Network", 0, 0, 0},
{10, 6, false, " Reset", 0, 0, 0},
{11, 0, false, " Edit", 0, 0, 0},
{12, 11, false, " Cut", 0, 0, 0},
{13, 11, false, " Copy", 0, 0, 0},
{14, 11, false, " Paste", 0, 0, 0},
{15, 0, false, " Effects", 0, 0, 0},
{16, 15, true, " Brightness ", 0, -50, 50},
{17, 15, true, " Contrast", 10, -50, 50},
{18, 15, true, " Exposure", 10, -100, 100},
{19, 15, true, " Highlights", 5, 0, 50},
{20, 15, true, " Shadows", 0, 0, 10},
{21, 15, true, " Clarity", 1, -10, 10}
};
int actualIndex = 0;
void setup()
{
actualIndex = getMenuIndexByID(0); // Main - актуальный элемент меню
pinMode(pinA, INPUT); // Пины в режим приема INPUT
pinMode(pinB, INPUT); // Пины в режим приема INPUT
pinMode(pinButton, INPUT); // Пины в режим приема INPUT
attachInterrupt(0, A, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала
attachInterrupt(1, Button, CHANGE); // Настраиваем обработчик прерываний по изменению сигнала нажатия кнопки
Serial.begin(9600); // Включаем Serial
setActualMenu(0, 0); // Выводим в Serial актуальный элемент меню
}
void loop()
{
int vmenu = 0; // Переменная хранящая действие по вертикали 1 - вход в меню, -1 - выход из меню
int hmenu = 0; // Переменная хранящая действие по горизонтали 1 - вправо, -1 - влево
if (millis() - timeButtonDown > timeButtonPressed && isButtonDown) { // Время длительного удержания наступило
flagButtonLong = true;
}
if (flagCW) { // Шаг вращения по часовой стрелке
hmenu = 1;
//Serial.println("right");
flagCW = false; // Действие обработано - сбрасываем флаг
}
if (flagCCW) { // Шаг вращения против часовой стрелки
hmenu = -1;
//Serial.println("left");
flagCCW = false; // Действие обработано - сбрасываем флаг
}
if (flagButton) { // Кнопка нажата
vmenu = 1; // По нажатию кнопки - переходим на уровень вниз
//Serial.println("button");
flagButton = false; // Действие обработано - сбрасываем флаг
}
if (flagButtonLong && isButtonDown) { // Кнопка удерживается
if (!digitalRead(pinButton) && millis() - timeButtonDown > timeButtonPressed) { // Защита от ложного срабатывания
// ...
vmenu = -1; // По удержанию кнопки - возвращаемся на уровень вверх
//Serial.println("long_press");
}
//=========================================== Настраиваем реакцию на долгое удержание кнопки ===============================================
// Чтобы событие long_press во время удержания срботало только один раз, необходимо раскомментировать блок и закомментировать следующий
//isButtonDown = false; // Программно "отжимаем" кнопку
// Эти две строки отвечают за то, чтобы при долгом удержании кнопки, событие long_press повторялось каждые 1,5 секунды
// Для того, чтобы изменить это поведение нужно закомментировать две эти строки и раскомментировать строку из предыдущего блока
timeButtonDown = millis(); // Сбрасываем таймер
longPressReleased = true; // Флаг срабатывания долгого удержания, чтобы отсечь генерацию обычного нажатия при отпускании кнопки
//==========================================================================================================================================
flagButtonLong = false; // Действие обработано - сбрасываем флаг
}
if (vmenu != 0 || hmenu != 0) setActualMenu(vmenu, hmenu); // Если было действие - реагируем на него
}
bool isParamEditMode = false; // Флаг режима редактирования параметра
int tmpValue = 0; // Временная переменная для хранения изменяемого параметра
void setActualMenu(int v, int h) {
if (v != 0) { // Двигаемся по вертикали
if (v == -1) { // Команда ВВЕРХ (отмена)
if (isParamEditMode) { // Если параметр в режиме редактирования, то отменяем изменения
isParamEditMode = false;
}
else { // Если пункт меню не в режиме редактирования, перемещаемся к родителю
if (menus[actualIndex].parentid > 0) { // Если есть куда перемещаться вверх (ParentID>0)
actualIndex = getMenuIndexByID(menus[actualIndex].parentid);
}
}
}
else { // Если команда ВНИЗ - входа/редактирования
if (menus[actualIndex].isParam && !isParamEditMode) { // Если не в режиме редактирования, то ...
isParamEditMode = true; // Переходим в режим редактирования параметра
tmpValue = menus[actualIndex].value; // Временной переменной присваиваем актуальное значение параметра
}
else if (menus[actualIndex].isParam && isParamEditMode) { // Если в режиме редактирования
menus[actualIndex].value = tmpValue; // Сохраняем заданное значение
isParamEditMode = false; // И выходим из режима редактирования
}
else {
bool nochild = true; // Флаг, есть ли дочерние элементы
for (int i = 0; i < menuArraySize; i++) {
if (menus[i].parentid == menus[actualIndex].id) {
actualIndex = i; // Если есть, делаем первый попавшийся актуальным элементом
nochild = false; // Потомки есть
break; // Выходим из for
}
}
if (nochild) { // Если же потомков нет, воспринимаем как команду
Serial.println("Executing command..."); // И здесь обрабатываем по своему усмотрению
}
}
}
}
if (h != 0) { // Если горизонтальная навигация
if (isParamEditMode) { // В режиме редактирования параметра
tmpValue += h; // Изменяем его значение и ...
// ... контроллируем, чтобы оно осталось в заданном диапазоне
if (tmpValue > menus[actualIndex]._max) tmpValue = menus[actualIndex]._max;
if (tmpValue < menus[actualIndex]._min) tmpValue = menus[actualIndex]._min;
}
else { // Если режим редактирования не активен, навигация среди потомков одного родителя
actualIndex = getNearMenuIndexByID(menus[actualIndex].parentid, menus[actualIndex].id, h);
}
}
// Отображаем информацию в Serial
if (isParamEditMode) {
Serial.println(" > " + (String)menus[actualIndex]._name + ": " +
(String)tmpValue +
" min:" + (String)menus[actualIndex]._min +
", max:" + (String)menus[actualIndex]._max);
}
else {
if (menus[actualIndex].isParam) {
Serial.println((String)menus[actualIndex]._name + ": " + (String)menus[actualIndex].value);
}
else {
Serial.println((String)menus[actualIndex]._name);
}
}
}
int getMenuIndexByID(int id) { // Функция получения индекса пункта меню по его ID
for (int i = 0; i < menuArraySize; i++) {
if (menus[i].id == id) return i;
}
return -1;
}
int getNearMenuIndexByID(int parentid, int id, int side) { // Функция получения индекса пункта меню следующего или предыдущего от актуального
int prevID = -1; // Переменная для хранения индекса предыдущего элемента
int nextID = -1; // Переменная для хранения индекса следующего элемента
int actualID = -1; // Переменная для хранения индекса актуального элемента
int firstID = -1; // Переменная для хранения индекса первого элемента
int lastID = -1; // Переменная для хранения индекса последнего элемента
for (int i = 0; i < menuArraySize; i++) {
if (menus[i].parentid == parentid) { // Перебираем все элементы с одним родителем
if (firstID == -1) firstID = i; // Запоминаем первый элемент списка
if (menus[i].id == id) {
actualID = i; // Запоминаем актальный элемент списка
}
else {
if (actualID == -1) { // Если встретился элемент до актуального, делаем его предыдущим
prevID = i;
}
else if (actualID != -1 && nextID == -1) { // Если встретился элемент после актуального, делаем его следующим
nextID = i;
}
}
lastID = i; // Каждый последующий элемент - последний
}
}
if (nextID == -1) nextID = firstID; // Если следующего элемента нет - по кругу выдаем первый
if (prevID == -1) prevID = lastID; // Если предыдущего элемента нет - по кругу выдаем последний
//Serial.println("previusindex:" + (String)prevID + " nextindex:" + (String)nextID);
if (side == -1) return prevID ; // В зависимости от направления вращения, выдаем нужный индекс
else return nextID;
return -1;
}
void A()
{
int pinAValue = digitalRead(pinA); // Получаем состояние пинов A и B
int pinBValue = digitalRead(pinB);
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
if (!pinAValue && pinBValue) state = 1; // Если при спаде линии А на линии B лог. единица, то вращение в одну сторону
if (!pinAValue && !pinBValue) state = -1; // Если при спаде линии А на линии B лог. ноль, то вращение в другую сторону
if (pinAValue && state != 0) {
if (state == 1 && !pinBValue || state == -1 && pinBValue) { // Если на линии А снова единица, фиксируем шаг
if (state == 1) flagCW = true; // Флаг вращения по часовой стрелке
if (state == -1) flagCCW = true; // Флаг вращения против часовой стрелки
state = 0;
}
}
sei(); // Разрешаем обработку прерываний
}
void Button()
{
if (millis() - timeButtonDown < 50) return;
int pinButValue = digitalRead(pinButton); // Получаем состояние пина кнопки
cli(); // Запрещаем обработку прерываний, чтобы не отвлекаться
timeButtonDown = millis(); // Запоминаем время нажатия/отжатия
if (!pinButValue) { // При нажатии подается инвертированный сигнал
isButtonDown = true; // Устанавливаем флаг нажатия кнопки
}
else if (isButtonDown) { // Если кнопка отжата, смотрим не было ли выполнено действие
if (!longPressReleased) { // Если долгое нажатие не было ни разу отработано, то...
flagButton = true; // Если не было удержания, ставим флаг события обычного нажатия
}
isButtonDown = false; // Сбрасываем флаг нажатия
longPressReleased = false; // Сбрасываем флаг длительного удержания
}
sei(); // Разрешаем обработку прерываний
} |
|
58 |
Бонус — как заменить энкодером кнопки Задача выглядит следующим образом: |
|
59 | Задача: |
Сделать без участия микроконтроллера (на микросхемах ТТЛ-логики) так, чтобы вращение энкодера конвертировалось в положительные импульсы на двух разных выходах — по часовой стрелке на одном выходе, против часовой — на другом, таким образом имитируя нажатия двух отдельных кнопок:
|
|
60 |
Для выполнения этой задачи понадобятся 2 микросхемы, реализующие стандартную логику и 1 микросхема — D-триггер (D от англ. delay — задержка): |
|
61 |
|
|
62 |
Принципиальная схема: |
|
64 |
Цифрами на схеме обозначены места, в которых будут сниматься осциллограммы логическим анализатором (число в круге соответствует номеру канала ЛА). Далее, поэтапно будет показано, как работает каждая из ТТЛ-микросхем и что происходит на каждом обозначенном шаге. За основу будет взята ситуация вращения энкодера на 2 шага по часовой стрелке и 4 шага против часовой стрелки (сигналы в точках 0 и 1): |
66 |
Этап №1: SN74HC00N (155ЛА3) — отделяем полезный сигнал
|
|
67 |
Работа этой микросхемы 2И-НЕ эквивалентна логике И, за тем лишь исключением, что результат будет инвертирован — 0 превращается в 1, и наоборот. Схема распиновки микросхемы и таблица истинности выглядит следующим образом: |
|
69 |
Результат работы этой микросхемы можно описать так — логический ноль на выходе будет, только когда на обоих входах будет логическая единица. Результат: |
|
71 |
Этап №2: SN74HC74N (155ТМ2) — разделяем сигналы по направлению вращения
|
|
72 |
Особенностью работы D-триггера является возможность сохранять свое состояние после его установки. Данный триггер принимает на вход 2 сигнала — сигнал синхронизации и информационный. |
|
73 |
На пинах микросхемы, сигнальный пин C обозначен как 1CK и 2CK — на обоих изображениях треугольник на входе
|
|
74 |
По восходящему фронту сигнала синхронизации, триггер принимает состояние информационного сигнала и сохраняет его до тех пор, пока не придет новый сигнал синхронизации. Для того чтобы понять как работает D-триггер, достаточно посмотреть на сигнал в точке 3: |
|
76 |
Далее необходимо снова пропустить сигналы 0 и 3 через 2И-НЕ: |
|
77 |
На рисунке можно видеть полезный сигнал
|
|
78 |
Этап №3: SN74HC08N (155ЛИ1) — извлекаем только полезный сигнал
|
|
79 |
Теперь остается пропустить 3 и 5 сигнал через логику И (74HC08N, 155ЛИ1). Схема распиновки микросхемы и таблица истинности выглядит следующим образом: |
|
81 |
На выходе этой микросхемы получаем необходимый нам результат: |
|
83 |
А вот так выглядит реальная осциллограмма, захваченная логическим анализатором и результат работы схемы: |
|
7 и 8 каналы — результаты работы схемы |
85 |
Таким образом, при помощи данного инкреметнального энкодера можно заменить 3 кнопки (2 вращение и 1 кнопка на валу). |
|
86 | На заметку: |
Кстати, несмотря на упоминание аналогичности микросхем, в этой схеме нельзя использовать отечественную микросхему К155ЛА3 в комбинации с импортной 74HC74N. Это связано с тем, что К155ЛА3 на выходе дает логическую единицу в 3.9 вольт, а триггер Шмитта выдает напряжение питания 5 В в качестве логической единицы. И получается, что на вход 74HC74N подаются логические единицы с разными значениями напряжений, и эта схема не может их корректно обрабатывать. Исходя из этого нужно использовать либо К155ЛА3 в связке с К155ТМ2, либо SN74HC00N в связке с SN74HC74N:
Напряжение логической единицы на выходе К155ЛА3 — 3,9В |
|
87 |
Что почитать:
|
|
88 |
Похожие запросы:
|
|