Год назад я писал статью про управление кнопками в Arduino. С тех пор родилась библиотека SButton с кучей наворотов, которая активно используется мной в различных проектах. В последнее время я использую в проектах контроллер ESP32 компании Espressif Systems и много времени уделяю программирование под FreeRTOS, хорошо раскрывающей возможности этого двухъядерного контроллера.
Итак, задача реализовать работу кнопки параллельно выполнению других задач. Данные примеры публикую как памятку себе )
Итак, приступим…
Пример работающий на всех Arduino
Этот пример взят из предыдущей статьи и будет работать на любом микроконтроллере, который поддерживает Arduino IDE. Нужно только установить требуемый GPIO, к которому подключена кнопка.
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON 27
// Минимальный таймаут между событиями нажатия кнопки
#define TM_BUTTON 100
uint32_t ms_btn=0;
boolstate_btn=true;
voidsetup(){
Serial.begin(115200);
Serial.println("Arduino button example");
// Определям режим работы GPIO с кнопкой
pinMode(PIN_BUTTON,INPUT_PULLUP);
}
voidloop(void){
uint32_t ms=millis();
// Условие, которое выполняется не чаще одного раза в TM_BUTTON ms
if(ms_btn==0||ms-ms_btn>TM_BUTTON){
ms_btn=ms;
boolst=digitalRead(PIN_BUTTON);
// Изменилось состояник кнопки
if(st!=state_btn){
state_btn=st;
if(st==LOW){
Serial.println("Button pressed");
}
else{
Serial.println("Button released");
}
}
}
}
Здесь отлавливаются события как на нажатие, так и на отпускание кнопки. Если померить время между этими событиями, то можно организовать обработку короткого нажатия кнопки и ее длинного удержания.
К недостаткам данного метода следует отнести то, что если в цикле loop() будут выполнятся еще какие то действия, занимающие, то мы вполне можем пропустить нажатие кнопки.
Пример 1. Цикл с кнопкой в параллельной задаче
В Arduino IDE Core ESP32 по умолчанию загружается FreeRTOS, где параллельными задачами запускается работа с WiFi на первом ядре и главный пользовательский цикл loop() и другие пользовательские задачи. При использовании WiFi лучше не трогать первое ядро, чтобы не получить «глючный ESP8266», когда всегда можно нарваться на «злобный WDT», если твоя задача будет что-то делать «не вовремя» или «слишком долго».
Чтобы кнопка работала постоянно, запускаю обработчик параллельной задачей с нужным приоритетом.
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON 27
// Минимальный таймаут между событиями нажатия кнопки
// Задержка, во время которой выполняются задачи с меньшим приоритетом
vTaskDelay(TM_BUTTON);
}
}
При работе с параллельными процессами FreeRTOS следует обратить на следующие особенности:
Приоритет задачи обработки кнопки должен быть выше задач, во время которых может быть востребовано нажатие кнопки. В моем примере главный цикл loop() выполняется в задаче loopTask с приоритетом 1, а задача обработки кнопки имеет приоритет 5.
Если в обработке кнопки будет какой то сложный алгоритм, то нужно следить за размером стека, выделяемым при старте задачи. Ошибка переполнения стека будет выглядеть следующим образом
При таком алгоритме цикл обработки кнопки постоянно крутится с задержкой 100 мсек, оставляя ровно столько времени другим задачам с более низким приоритетом. Непростительное расточение ресурсов микроконтроллера, поэтому следующий пример делаю на прерывании
Пример 2. Использование внешнего прерывания
Полностью запихать работу с кнопкой в обработчик прерывания мне не удалось. Дребезг, вызываемый при опускании кнопки, стабильно давал ложное срабатывания. Кроме того, внутри обработчика прерывания действует ряд ограничений, например, не работает delay() и не увеличивается значение millis(). Поэтому обработчик прерываний я использовал только для управления семафором, который «держит» задачу работы с кнопкой в ожидании.
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON 27
// Минимальный таймаут между событиями нажатия кнопки
// Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки
xSemaphoreTake(btn1Semaphore,100);
while(true){
// Запускаем обработчик прерывания (кнопка замыкает GPIO на землю)
attachInterrupt(PIN_BUTTON,ISR_btn1,CHANGE);
// Ждем "отпускание" семафора
xSemaphoreTake(btn1Semaphore,portMAX_DELAY);
// Отключаем прерывание для устранения повторного срабатывания прерывания во время обработки
detachInterrupt(PIN_BUTTON);
boolst=digitalRead(PIN_BUTTON);
uint32_t ms=millis();
// Проверка изменения состояния кнопки или превышение таймаута
if(st!=state_btn||ms-ms_btn>TM_BUTTON){
state_btn=st;
ms_btn=ms;
if(st==LOW){
Serial.println("Button pressed");
}
// Задержка для устранения дребезга контактов
vTaskDelay(TM_BUTTON);
}
}
}
В данном примере задача обработки кнопки ожидает освобождения семафора и не занимает ресурсов контроллера большую часть времени. При получении внешнего прерывания по кнопке задача выполняет какое-то действие. Для устранения ложного срабатывания статус кнопки должен дважды смениться на противоположный с соответствующей задержкой. Если отпускание кнопки не успевает сработать, то возврат в состояние готовности следующего нажатие производится по таймауту.
Хороший, надежный алгоритм с программным подавлением дребезга контактов не занимающий ресурсов микроконтроллера во время ожидания на нажатие кнопки. К его недостатком можно отнести ненадежное определение отпускания кнопки, что не позволяет рассчитать время нажатия (для обработки длинных и коротких нажатий)
Пример 3. Комбинирование внешнего прерывания и цикла обработки нажатия кнопки
При получении прерывания по кнопке цикл задачи снимается с ожидания светофора и работает по таймауту, как в первом примере, до тех пор пока не будет зафиксирована отпущенная кнопка с учетом дребезга контактов.
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON 27
// Минимальный таймаут между событиями нажатия кнопки
// Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки
xSemaphoreTake(btn1Semaphore,100);
// Запускаем обработчик прерывания (кнопка замыкает GPIO на землю)
attachInterrupt(PIN_BUTTON,ISR_btn1,FALLING);
while(true){
// Обработчик прерывания выключен, функция ждет окончания действия с кнопкой
if(isISR1){
// Ждем "отпускание" семафора
xSemaphoreTake(btn1Semaphore,portMAX_DELAY);
// Отключаем прерывание для устранения повторного срабатывания прерывания во время обработки
detachInterrupt(PIN_BUTTON);
// переводим задачу в цикл обработки кнопки
isISR1=false;
}
else{
boolst=digitalRead(PIN_BUTTON);
// Проверка изменения состояния кнопки
if(st!=state_btn){
state_btn=st;
if(st==LOW){
// Фиксируем время нажатия кнопки
ms_btn=millis();
Serial.println("Button pressed");
}
else{
// Рассчитываем продолжительность нажатия кнопки
uint32_t ms_duration=millis()-ms_btn;
Serial.print("Button released ");
Serial.println(ms_duration);
attachInterrupt(PIN_BUTTON,ISR_btn1,FALLING);
isISR1=true;
}
}
vTaskDelay(100);
}
}
}
В этом примере надежно фиксируется как нажатие, так и отпускание кнопки, что позволяет рассчитывать время ее нажатия. Данная задача «кушает ресурсы» только после нажатия кнопки до ее отпускания.
Пример 4. Обработка нескольких кнопок
При работе с несколькими кнопками вовсе не обязательно запускать несколько задач и обработчиков прерывания. Достаточно по прерыванию от любой кнопки переходить на цикл роботы по таймауту, а при фиксировании отпускания всех
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON1 25
#define PIN_BUTTON2 26
#define PIN_BUTTON3 27
// Минимальный таймаут между событиями нажатия кнопки
// Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки
xSemaphoreTake(btnSemaphore,100);
// Запускаем обработчик прерывания (кнопка замыкает GPIO на землю) на все кнопки
attachInterrupt(PIN_BUTTON1,ISR_btn,FALLING);
attachInterrupt(PIN_BUTTON2,ISR_btn,FALLING);
attachInterrupt(PIN_BUTTON3,ISR_btn,FALLING);
while(true){
// Обработчик прерывания выключен, функция ждет окончания действия с кнопкой
if(isISR){
// Ждем "отпускание" семафора
xSemaphoreTake(btnSemaphore,portMAX_DELAY);
// Отключаем прерывания по всем кнопкам
detachInterrupt(PIN_BUTTON1);
detachInterrupt(PIN_BUTTON2);
detachInterrupt(PIN_BUTTON3);
// переводим задачу в цикл обработки кнопки
isISR=false;
}
else{
boolst1=digitalRead(PIN_BUTTON1);
boolst2=digitalRead(PIN_BUTTON2);
boolst3=digitalRead(PIN_BUTTON3);
// Проверка изменения состояния кнопки1
if(st1!=state_btn1){
state_btn1=st1;
if(st1==LOW){Serial.println("Button1 pressed");}
else{Serial.println("Button1 released");}
}
// Проверка изменения состояния кнопки2
if(st2!=state_btn2){
state_btn2=st2;
if(st2==LOW){Serial.println("Button2 pressed");}
else{Serial.println("Button2 released");}
}
// Проверка изменения состояния кнопки3
if(st3!=state_btn3){
state_btn3=st3;
if(st3==LOW){Serial.println("Button3 pressed");}
else{Serial.println("Button3 released");}
}
// Проверка что все три кнопки отработали
if(st1==HIGH&&st2==HIGH&&st2==HIGH){
attachInterrupt(PIN_BUTTON1,ISR_btn,FALLING);
attachInterrupt(PIN_BUTTON2,ISR_btn,FALLING);
attachInterrupt(PIN_BUTTON3,ISR_btn,FALLING);
isISR=true;
}
vTaskDelay(100);
}
}
}
Пример 5. Обработка емкостного сенсорного входа по прерыванию
Touch интерфейс — чисто ESP-шная фича, позволяющая реализовать до 10 сенсорных кнопок. Далее привожу пример работы с сенсорной кнопкой на GPIO4 (TOUCH0) с прерыванием по порогу срабатывания. В остальном код ничем не отличается от Примера 3.
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
// Порт GPIO к которому подключена кнопка
#define PIN_BUTTON T0
// Минимальный таймаут между событиями нажатия кнопки
А Вы проверяли, действительно ли отключаются прерывания в примере 5 ?
У меня не получается отключить прерывания.
int threshold = 20;
bool touch1detected = false;
void gotTouch1() {
touch1detected = true;
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println(«ESP32 Touch Interrupt Test»);
touchAttachInterrupt(T0, gotTouch1, threshold);
delay(1000);
detachInterrupt(T0);
delay(1000);
}
void loop() {
if (touch1detected) {
detachInterrupt(T0);
touch1detected = false;
Serial.println(«Touch 1 detected»);
Serial.println(touchRead(T0));
}
}
С уважением.
Результат
06:47:12.228 -> Touch 1 detected
06:47:12.228 -> 15
06:47:12.275 -> Touch 1 detected
06:47:12.275 -> 0
06:47:12.275 -> Touch 1 detected
06:47:12.275 -> 9
06:47:12.322 -> Touch 1 detected
06:47:12.322 -> 8
06:47:12.322 -> Touch 1 detected
06:47:12.322 -> 6
06:47:12.368 -> Touch 1 detected
06:47:12.368 -> 5
06:47:12.368 -> Touch 1 detected
06:47:12.368 -> 6
06:47:12.415 -> Touch 1 detected
06:47:12.415 -> 5