Обновлено 07 февраля 2018
Кравченко Виктор

Инкрементальный энкодер: принцип действия, схемы подключения, работа с Arduino

Цифровые устройства Arduino Arduino Lang Датчики, модули Микроконтроллеры и мини ПК
02 На заметку:
У данной статьи есть видеоверсия!
Подписывайтесь на канал, чтобы быть в курсе обновлений!

03

Введение

Инкрементальный (или инкрементный, от англ. increment «увеличение») энкодер (датчик угла поворота) — это устройство, которое преобразовывает вращательное движение вала в серию электрических импульсов, позволяющих определить направление и угол его вращения. Также, исходя из найденных величин, можно определить и скорость вращения. Основным отличием инкрементальных энкодеров от абсолютных является то, что они могут сообщать лишь о величине изменения их положения, а не об абсолютном своем состоянии. Самым популярным примером использования инкрементального энкодера в повседневной жизни, является ручка регулировки громкости автомобильной магнитолы.

04

Также энкодеры идеально подходят для реализации навигации по различным меню.

05

Инкрементальные энкодеры бывают оптическими, магнитными, механическими и т.д. Вне зависимости от принципа устройства все инкрементальные энкодеры на выходе генерируют 2 линии (A и B) с импульсами смещенными относительно друг друга. Именно по смещению импульсов можно судить о направлении вращения. А по количеству импульсов — об угле поворота.

06

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

07

Каждый инкрементальный энкодер имеет следующую основную характеристику — дискретность (количество шагов, положений между импульсами, на один оборот вала). Благодаря дискретности, можно вычислить угол единичного изменения положения. В нашем примере, энкодер ЕС11 за полный оборот генерирует 20 серий импульсов. А это значит, что каждый шаг эквивалентен повороту на 18°. Помимо этого, вал энкодера фиксируется в каждом положении между каждой серией импульсов.

08

Внешний вид устройства:

09
10

Сердцем энкодера являются 2 пары контактов и металлическая пластина с засечками. При вращении вала, каждая пара контактов замыкается и размыкается. Но эти пары контактов расположены таким образом, что при вращении вала в разные стороны порядок замыкания/размыкания контактов разный — и, благодаря этому, можно определить направление вращения.

11
Проверено — автор рекомендует:
Ручка черная d 6мм разных размеров для инкрементального энкодера EC11
12

Энкодер с кнопкой имеет 5 выходов — 2 выхода (D и E) отвечают за переключатель (кнопку), 1 (С) — общий (GND, земля), а оставшихся 2 (A и B) — импульсные линии, сигнализирующие о вращении.

13
Схематичное представление энкодера
Схематичное представление энкодера
14

Поскольку подключение кнопки вала энкодера (контакты D и E) не отличаются от подключения обычной кнопки, информация по ней будет опущена.

15

Схематично работу инкрементального энкодера можно представить следующим образом:

16
17

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

18
19

После чего в состоянии покоя на обоих сигнальных выходах будет присутствовать логическая единица (5 В). При вращении по часовой, или против часовой стрелки на сигнальных линиях, с противоположным смещением друг относительно друга, будут появляться отрицательные импульсы — по одному на 1 шаг на каждой линии:

20
В состоянии покоя подтягивающие резисторы (10КОм) подтягивают сигнальные линии к логической единице
В состоянии покоя подтягивающие резисторы (10КОм) подтягивают сигнальные линии к логической единице
Если анимация не загружается её можно скачать (1,98 MB) и посмотреть отдельно.
21

Реальная осциллограмма вращения энкодера немного отличается от идеальной.

22
Слева осциллограмма вращения по часовой стрелке, справа — против часовой стрелки
23

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

24
25

Устранение дребезга

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

26
27

Программное устранение дребезга

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

28 На заметку:
Автор считает, что определение состояния пинов энкодера в цикле loop() и последующее вычисление направления вращения является недопустимо затратным, по отношению к ресурсам МК, методом. Именно поэтому будет использован метод борьбы с дребезгом, при помощи прерываний.
29

После того как энкодер подключен к Arduino (на примере Arduino Uno):

30
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

Суть работы кода можно изобразить графически:

34
35

При корректном выполнении сценария, по завершению каждого шага переменная state будет иметь состояние 4 или -4. Если что-то пойдет не так, программа никак не это не отреагирует. Но если программа увидит, что в состоянии покоя (A=1 и B=1), переменная state не равна нулю, то вернет её в исходное состояние.

36

Аппаратное устранение дребезга

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

37
Инвертирующий триггер Шмитта 74HC14N необходим для преобразования аналогового сигнала в цифровой, именно из-за него изменена полярность подключения
Инвертирующий триггер Шмитта 74HC14N необходим для преобразования аналогового сигнала в цифровой, именно из-за него изменена полярность подключения
О том, почему изменена полярность подключения и для чего необходим инвертирующий триггер Шмитта 74HC14N, можно почитать в статье Arduino: Дребезг - программное и аппаратное устранение.


Проверено — автор рекомендует:
Купить 74HC14N Инвертирующий триггер Шмитта (DIP-14)
Как покупать на Aliexpress со скидкой от 5,5%

Видео-инструкция о покупке со скидками на Aliexpress
38 На заметку:
Даташит на инвертирующий триггер Шмитта — sn74hc14.pdf.
39

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

40
Вторая линия подключена к 7 пину, освобождая 3 пин для других источников прерываний
Вторая линия подключена к 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 реакции на действия: вращение вправо/влево, короткое нажатие и длинное нажатие (реакция на нажатия реализуется программно).

45
46

Схема подключения:

47
Освободившийся 3 пин (с прерываниями) теперь используется для перехвата нажатия кнопки
Освободившийся 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 можно настроить реакцию программы на длительное удержание кнопки энкодера:

51
52

Энкодер и навигация по меню

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

53
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
Микросхема SN74HC00N SN74HC74N SN74HC08N
Аналог К155ЛА3 К155ТМ2 К155ЛИ1
Логика 4 логических элемента 2И-НЕ 2 D-триггера 4 логических элемента И
Даташит sn74hc00.pdf sn74hc74.pdf sn74hc08.pdf
Где купить: Купить Купить Купить
62

Принципиальная схема:

63
64

Цифрами на схеме обозначены места, в которых будут сниматься осциллограммы логическим анализатором (число в круге соответствует номеру канала ЛА). Далее, поэтапно будет показано, как работает каждая из ТТЛ-микросхем и что происходит на каждом обозначенном шаге. За основу будет взята ситуация вращения энкодера на 2 шага по часовой стрелке и 4 шага против часовой стрелки (сигналы в точках 0 и 1):

65
66
Этап №1: SN74HC00N (155ЛА3) — отделяем полезный сигнал
67

Работа этой микросхемы 2И-НЕ эквивалентна логике И, за тем лишь исключением, что результат будет инвертирован — 0 превращается в 1, и наоборот. Схема распиновки микросхемы и таблица истинности выглядит следующим образом:

68
69

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

70
71
Этап №2: SN74HC74N (155ТМ2) — разделяем сигналы по направлению вращения
72

Особенностью работы D-триггера является возможность сохранять свое состояние после его установки. Данный триггер принимает на вход 2 сигнала — сигнал синхронизации и информационный.

73
На пинах микросхемы, сигнальный пин C обозначен как 1CK и 2CK — на обоих изображениях треугольник на входе
На пинах микросхемы, сигнальный пин C обозначен как 1CK и 2CK — на обоих изображениях треугольник на входе
74

По восходящему фронту сигнала синхронизации, триггер принимает состояние информационного сигнала и сохраняет его до тех пор, пока не придет новый сигнал синхронизации. Для того чтобы понять как работает D-триггер, достаточно посмотреть на сигнал в точке 3:

75
76

Далее необходимо снова пропустить сигналы 0 и 3 через 2И-НЕ:

77
На рисунке можно видеть полезный сигнал
На рисунке можно видеть полезный сигнал
78
Этап №3: SN74HC08N (155ЛИ1) — извлекаем только полезный сигнал
79

Теперь остается пропустить 3 и 5 сигнал через логику И (74HC08N, 155ЛИ1). Схема распиновки микросхемы и таблица истинности выглядит следующим образом:

80
81

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

82
83

А вот так выглядит реальная осциллограмма, захваченная логическим анализатором и результат работы схемы:

Файл с данными для программы Saleae LogicEncoder_with_TTL.logicdata (4,93 KB)
84
7 и 8 каналы — результаты работы схемы
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В
Напряжение логической единицы на выходе К155ЛА3 — 3,9В

88

Похожие запросы:

  • Как работает энкодер??
  • Как устроен энкодер
  • Как правильно подключать энкодер к микроконтроллеру
  • Подключение инкрементального энкодера к микроконтроллеру
  • Сопрягаем энкодер и микроконтроллер
  • Замена Энкодера На Кнопки Up И Down
  • Cхема энкодера для замены им двух кнопок
  • Ищу схему энкодера для замены им двух кнопок
  • Замена кнопок на энкодер
  • Энкодер заменяет кнопки
  • Энкодер вместо кнопок
comments powered by HyperComments