Год назад я писал статью про управление кнопками в Arduino. С тех пор родилась библиотека SButton с кучей наворотов, которая активно используется мной в различных проектах. В последнее время я использую в проектах контроллер ESP32 компании Espressif Systems и много времени уделяю программирование под FreeRTOS, хорошо раскрывающей возможности этого двухъядерного контроллера.
Итак, задача реализовать работу кнопки параллельно выполнению других задач. Данные примеры публикую как памятку себе )
Итак, приступим…
Пример работающий на всех Arduino
Этот пример взят из предыдущей статьи и будет работать на любом микроконтроллере, который поддерживает Arduino IDE. Нужно только установить требуемый GPIO, к которому подключена кнопка.
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; bool state_btn = true; void setup() { Serial.begin(115200); Serial.println("Arduino button example"); // Определям режим работы GPIO с кнопкой pinMode(PIN_BUTTON,INPUT_PULLUP); } void loop(void) { uint32_t ms = millis(); // Условие, которое выполняется не чаще одного раза в TM_BUTTON ms if( ms_btn == 0 || ms - ms_btn > TM_BUTTON ){ ms_btn = ms; bool st = 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», если твоя задача будет что-то делать «не вовремя» или «слишком долго».
Чтобы кнопка работала постоянно, запускаю обработчик параллельной задачей с нужным приоритетом.
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 // Минимальный таймаут между событиями нажатия кнопки #define TM_BUTTON 100 void taskButton1( void *pvParameters ); bool state_btn = true; void setup() { Serial.begin(115200); Serial.println("ESP32 Button Example 1"); // Запускаем параллельную задачу обработки кнопок xTaskCreateUniversal(taskButton1, "button", 2048, NULL, 5, NULL,1); } void loop(void) { // Здесь выполняем что-то свое } void taskButton1( void *pvParameters ){ // Определяем режим работы GPIO с кнопкой pinMode(PIN_BUTTON,INPUT_PULLUP); while( true ){ bool st = digitalRead(PIN_BUTTON); // Проверка изменения состояния кнопки if( st != state_btn ){ state_btn = st; if( st == LOW ){ Serial.println("Button pressed"); } else { Serial.println("Button released"); } } // Задержка, во время которой выполняются задачи с меньшим приоритетом vTaskDelay(TM_BUTTON); } } |
При работе с параллельными процессами FreeRTOS следует обратить на следующие особенности:
- Приоритет задачи обработки кнопки должен быть выше задач, во время которых может быть востребовано нажатие кнопки. В моем примере главный цикл loop() выполняется в задаче loopTask с приоритетом 1, а задача обработки кнопки имеет приоритет 5.
- Если в обработке кнопки будет какой то сложный алгоритм, то нужно следить за размером стека, выделяемым при старте задачи. Ошибка переполнения стека будет выглядеть следующим образом
12345678910111213Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception)Debug exception reason: Stack canary watchpoint triggered (button)Core 1 register dump:PC : 0x4008a5ea PS : 0x00050036 A0 : 0x4008a4f3 A1 : 0x3ffb8320A2 : 0x3ffb8450 A3 : 0x00000000 A4 : 0x00000001 A5 : 0x4008a474A6 : 0x00000001 A7 : 0x00000085 A8 : 0x80081baa A9 : 0x3ffbe7a0A10 : 0x3ffbe7d8 A11 : 0x00000001 A12 : 0x00000001 A13 : 0x00000001A14 : 0x00060021 A15 : 0x00000000 SAR : 0x00000012 EXCCAUSE: 0x00000001EXCVADDR: 0x00000000 LBEG : 0x00000000 LEND : 0x00000000 LCOUNT : 0x00000000Backtrace: 0x4008a5ea:0x3ffb8320 0x4008a4f0:0x3ffb8330Rebooting... - При таком алгоритме цикл обработки кнопки постоянно крутится с задержкой 100 мсек, оставляя ровно столько времени другим задачам с более низким приоритетом. Непростительное расточение ресурсов микроконтроллера, поэтому следующий пример делаю на прерывании
Пример 2. Использование внешнего прерывания
Полностью запихать работу с кнопкой в обработчик прерывания мне не удалось. Дребезг, вызываемый при опускании кнопки, стабильно давал ложное срабатывания. Кроме того, внутри обработчика прерывания действует ряд ограничений, например, не работает delay() и не увеличивается значение millis(). Поэтому обработчик прерываний я использовал только для управления семафором, который «держит» задачу работы с кнопкой в ожидании.
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 // Минимальный таймаут между событиями нажатия кнопки #define TM_BUTTON 100 uint32_t ms_btn = 0; bool state_btn = true; void taskButton1( void *pvParameters ); SemaphoreHandle_t btn1Semaphore; void setup() { Serial.begin(115200); Serial.println("ESP32 button. Example 2"); // Запускается задача работы с кнопкой xTaskCreateUniversal(taskButton1, "btn1", 4096, NULL, 5, NULL,1); } void loop(void) { // Здесь выполняем что-то свое } void IRAM_ATTR ISR_btn1(){ // Прерывание по кнопке, отпускаем семафор xSemaphoreGiveFromISR( btn1Semaphore, NULL ); } void taskButton1( void *pvParameters ){ // Определяем режим работы GPIO с кнопкой pinMode(PIN_BUTTON,INPUT_PULLUP); // Создаем семафор btn1Semaphore = xSemaphoreCreateBinary(); // Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки xSemaphoreTake( btn1Semaphore, 100 ); while(true){ // Запускаем обработчик прерывания (кнопка замыкает GPIO на землю) attachInterrupt(PIN_BUTTON, ISR_btn1, CHANGE); // Ждем "отпускание" семафора xSemaphoreTake( btn1Semaphore, portMAX_DELAY ); // Отключаем прерывание для устранения повторного срабатывания прерывания во время обработки detachInterrupt(PIN_BUTTON); bool st = 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. Комбинирование внешнего прерывания и цикла обработки нажатия кнопки
При получении прерывания по кнопке цикл задачи снимается с ожидания светофора и работает по таймауту, как в первом примере, до тех пор пока не будет зафиксирована отпущенная кнопка с учетом дребезга контактов.
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 // Минимальный таймаут между событиями нажатия кнопки #define TM_BUTTON 100 void taskButton1( void *pvParameters ); SemaphoreHandle_t btn1Semaphore; void setup() { Serial.begin(115200); Serial.println("ESP32 button. Example 3"); // Запускается задача работы с кнопкой xTaskCreateUniversal(taskButton1, "btn1", 4096, NULL, 2, NULL,1); } void loop(void) {} void IRAM_ATTR ISR_btn1(){ // Прерывание по кнопке, отпускаем семафор xSemaphoreGiveFromISR( btn1Semaphore, NULL ); } void taskButton1( void *pvParameters ){ bool isISR1 = true; bool state_btn = true; uint32_t ms_btn = 0; // Определяем режим работы GPIO с кнопкой pinMode(PIN_BUTTON,INPUT_PULLUP); // Создаем семафор btn1Semaphore = xSemaphoreCreateBinary(); // Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки xSemaphoreTake( btn1Semaphore, 100 ); // Запускаем обработчик прерывания (кнопка замыкает GPIO на землю) attachInterrupt(PIN_BUTTON, ISR_btn1, FALLING); while(true){ // Обработчик прерывания выключен, функция ждет окончания действия с кнопкой if( isISR1 ){ // Ждем "отпускание" семафора xSemaphoreTake( btn1Semaphore, portMAX_DELAY ); // Отключаем прерывание для устранения повторного срабатывания прерывания во время обработки detachInterrupt(PIN_BUTTON); // переводим задачу в цикл обработки кнопки isISR1 = false; } else { bool st = 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. Обработка нескольких кнопок
При работе с несколькими кнопками вовсе не обязательно запускать несколько задач и обработчиков прерывания. Достаточно по прерыванию от любой кнопки переходить на цикл роботы по таймауту, а при фиксировании отпускания всех
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 // Минимальный таймаут между событиями нажатия кнопки #define TM_BUTTON 100 void taskButtons( void *pvParameters ); SemaphoreHandle_t btnSemaphore; void setup() { Serial.begin(115200); Serial.println("ESP32 button. Example 4"); // Запускается задача работы с кнопкой xTaskCreateUniversal(taskButtons, "buttons", 4096, NULL, 2, NULL,1); } void loop(void) {} void IRAM_ATTR ISR_btn(){ // Прерывание по кнопке, отпускаем семафор xSemaphoreGiveFromISR( btnSemaphore, NULL ); } void taskButtons( void *pvParameters ){ bool isISR = true; bool state_btn1 = true, state_btn2 = true, state_btn3 = true; // Определяем режим работы GPIO с кнопкой pinMode(PIN_BUTTON1,INPUT_PULLUP); pinMode(PIN_BUTTON2,INPUT_PULLUP); pinMode(PIN_BUTTON3,INPUT_PULLUP); // Создаем семафор btnSemaphore = xSemaphoreCreateBinary(); // Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки 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 { bool st1 = digitalRead(PIN_BUTTON1); bool st2 = digitalRead(PIN_BUTTON2); bool st3 = 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.
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 // Минимальный таймаут между событиями нажатия кнопки #define TM_BUTTON 100 #define THRESHOLD 20 void taskButton1( void *pvParameters ); SemaphoreHandle_t btn1Semaphore; void setup() { Serial.begin(115200); Serial.println("ESP32 button. Example 3"); // Запускается задача работы с кнопкой xTaskCreateUniversal(taskButton1, "btn1", 4096, NULL, 2, NULL,1); } void loop(void) { } void IRAM_ATTR ISR_btn1(){ // Прерывание по кнопке, отпускаем семафор xSemaphoreGiveFromISR( btn1Semaphore, NULL ); } void taskButton1( void *pvParameters ){ bool isISR1 = true; bool state_btn = true; uint32_t ms_btn = 0; // Создаем семафор btn1Semaphore = xSemaphoreCreateBinary(); // Сразу "берем" семафор чтобы не было первого ложного срабатывания кнопки xSemaphoreTake( btn1Semaphore, 100 ); // Запускаем обработчик прерывания по порогу срабатывания touchAttachInterrupt(PIN_BUTTON, ISR_btn1, THRESHOLD); while(true){ // Обработчик прерывания выключен, функция ждет окончания действия с кнопкой if( isISR1 ){ // Ждем "отпускание" семафора xSemaphoreTake( btn1Semaphore, portMAX_DELAY ); // Отключаем прерывание для устранения повторного срабатывания прерывания во время обработки detachInterrupt(PIN_BUTTON); // переводим задачу в цикл обработки кнопки isISR1 = false; } else { // Обрабатываем значение сенсорного входа uint16_t t = touchRead(PIN_BUTTON); bool st; if( t < THRESHOLD )st = LOW; else st = HIGH; // Проверка изменения состояния кнопки 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); touchAttachInterrupt(PIN_BUTTON, ISR_btn1, THRESHOLD); isISR1 = true; } } vTaskDelay(100); } } } |
Полезные ссылки
- Оригинальная документация FreeRTOS на английском языке
- Набор программисткой документации по ESP32 на английском языке
- Справочник по языку программирования Arduino на английском языке
- Полезная утилита для декодирования ошибок ESP32 с привязкой к исходным файлам
- Курниц А.»FreeRTOS — операционная система для микроконтроллеров. Части 1-10″
Хороший цикл статей про FreeRTOS на русском языке
- FreeRTOS: практическое применение, часть 1 (управление задачами)
- FreeRTOS: практическое применение, часть 2 (управление очередями)
- FreeRTOS: практическое применение, часть 3 (управление прерываниями)
- FreeRTOS: практическое применение, часть 4 (управление ресурсами)
- FreeRTOS: практическое применение, часть 5 (управление памятью)
- FreeRTOS: практическое применение, часть 6 (устранение проблем)
- FreeRTOS: практическое применение, дополнения, словарик
А Вы проверяли, действительно ли отключаются прерывания в примере 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