У каждого новичка рано или поздно возникает необходимость увеличить количество портов ввода-вывода для своего проекта и МК. В моем случае — ESP32 devboard. По крайней мере, на ней все тестировалось, а расширение портов планировалось на кастомной плате с тем же модулем esp32-WROOM на борту. В детали схемотехники и распиновки для моего случая вдаваться не будем, тема статьи — реализация клавиатуры на SN74HC165N в Arduino-фреймворке для esp32 с использованием функционала freeRTOS в проекте (т.е. будем писать код с планировщиком и задачами, а не в одном цикле, так же известном как «Round Robin»).
Если вы уже добрались до freeRTOS, думаю, как подключать кнопку и проводки на breadboard мне объяснять вам не требуется, поэтому кратко и по делу: подключаем кнопки с подтяжкой к «+» и расскажу, как получилось у меня. Решение, наверное, не оптимальное — буду рад услышать ваше мнение, если получилось сделать лучше. Работаю над своим первым проектом в электронике. В свое время не нашел подходящей информации на эту тему, что и сподвигло меня на написание статьи.
Полный код примера:
main.cpp
/* //каждая клавиша передается в очередь как отдельная структура со своими параметрами */ #include "main.h" void setup() { Serial.begin(9600); keyboard_queue = xQueueCreate(8, sizeof(gg_button)); xTaskCreate(keyboard_task, "KEYBOARD", 1500, NULL, 1, NULL); xTaskCreate(master_task, "MASTER", 1500, NULL, 1, NULL); } void loop() { delay(1000); } void keyboard_task(void* paramBaram) { //init pinMode(DATA_PIN, INPUT); // инициализация пинов pinMode(CLOCK_PIN, OUTPUT); pinMode(LATCH_PIN, OUTPUT); digitalWrite(LATCH_PIN, HIGH); gg_button input_buff; uint8_t x_clicks = 5; uint16_t timeout_button = 60000; // после взаимодействия с кнопкой прошло указанное время, мс [событие] uint16_t press_for_timeout = 2000; // время, которое кнопка удерживается (с начала нажатия), мс uint16_t hold_for_timeout = 2000; // время, которое кнопка удерживается (с начала удержания), мс uint16_t step_for_timeout = 2000; while(1) { digitalWrite(LATCH_PIN, LOW); // щелкнули защелкой digitalWrite(LATCH_PIN, HIGH); byte b = shiftIn(DATA_PIN, CLOCK_PIN, MSBFIRST); // считываем //опрос для каждой кнопки в массиве for (button = BUTT0; button <= BUTT7; button++) { buttons[button].tick(!bitRead(b, button)); //необходимо инвертировать вывод bitRead(), т.к. либа настроена на работу с подтяжкой к земле, а у меня к + } //считывание и передача значений с каждой кнопки масссива в структуру-буфер и его(буфера) передача в очередь 'keyboard_queue' for (button = BUTT0; button <= BUTT7; button++) { input_buff.id = button; input_buff.press = buttons[button].press(); input_buff.release = buttons[button].release(); input_buff.click = buttons[button].click(); input_buff.pressing = buttons[button].pressing(); input_buff.hold = buttons[button].hold(); input_buff.holding = buttons[button].holding(); input_buff.step = buttons[button].step(); input_buff.has_clicks = buttons[button].hasClicks(); input_buff.has_x_clicks = buttons[button].hasClicks(x_clicks); input_buff.get_clicks = buttons[button].getClicks(); input_buff.get_steps = buttons[button].getSteps(); input_buff.release_hold = buttons[button].releaseHold(); input_buff.release_step = buttons[button].releaseStep(); input_buff.timeout = buttons[button].timeout(timeout_button); input_buff.press_for = buttons[button].pressFor(); input_buff.press_for_t = buttons[button].pressFor(press_for_timeout); input_buff.hold_for = buttons[button].holdFor(); input_buff.hold_for_t = buttons[button].holdFor(hold_for_timeout); input_buff.step_for = buttons[button].stepFor(); input_buff.step_for_t = buttons[button].stepFor(step_for_timeout); xQueueSend(keyboard_queue, &input_buff, KEYBOARD_QUEUE_TIMEOUT_TICKS); } portYIELD(); //отдать управление планировщику } } void master_task(void* paramBaram) { gg_button output_buff; while(1) { xQueueReceive(keyboard_queue, &output_buff, portMAX_DELAY); switch(output_buff.id) { case BUTT0: //print_button(&output_buff); if(output_buff.release == 1) { std::cout << "release BUTT0\n"; } break; case BUTT1: if(output_buff.release == 1) { std::cout << "release BUTT1\n"; } break; case BUTT2: if(output_buff.release == 1) { std::cout << "release BUTT2\n"; } break; case BUTT3: if(output_buff.release == 1) { std::cout << "release BUTT3\n"; } break; case BUTT4: if(output_buff.release == 1) { std::cout << "release BUTT4\n"; } break; case BUTT5: if(output_buff.release == 1) { std::cout << "release BUTT5\n"; } break; case BUTT6: if(output_buff.release == 1) { std::cout << "release BUTT6\n"; } break; case BUTT7: if(output_buff.release == 1) { std::cout << "release BUTT7\n"; } break; } portYIELD(); } }
main.h
#ifndef MAIN_H #define MAIN_H #include <Arduino.h> #include <EncButton.h> #define DATA_PIN 12 // пин данных #define LATCH_PIN 27 // пин защелки #define CLOCK_PIN 14 // пин тактов синхронизации #define KEYBOARD_QUEUE_TIMEOUT_TICKS 5000 #define SIZE_KEYBOARD 8 //массив кнопок (общее количество кнопок для обработки) enum buttons_list { BUTT0, BUTT1, BUTT2, BUTT3, BUTT4, BUTT5, BUTT6, BUTT7 }; enum buttons_list button; //массив из 8 кнопок VirtButton buttons[SIZE_KEYBOARD]; //структура для передачи состояния кнопки в очередь: тут почти все фичи из класса 'VirtButton' typedef struct gg_button { buttons_list id; bool press; bool release; bool click; bool pressing; bool hold; bool holding; bool step; bool has_clicks; bool has_x_clicks; uint8_t get_clicks; uint16_t get_steps; bool release_hold; bool release_step; bool timeout; uint16_t press_for; bool press_for_t; uint16_t hold_for; bool hold_for_t; uint16_t step_for; bool step_for_t; } gg_button; QueueHandle_t keyboard_queue; //tasks void keyboard_task(void* paramBaram); void master_task(void* paramBaram); //enum не может неявно преобразовываться в другие типы в C++ //поэтому нужна перегрузка '++' (он нужен для использования enum в операторе for) //префиксный buttons_list operator++(buttons_list& b) { b = static_cast<buttons_list>(static_cast<int>(b) + 1); return b; } // Постфиксный инкремент buttons_list operator++(buttons_list& b, int) { buttons_list old = b; b = static_cast<buttons_list>(static_cast<int>(b) + 1); return old; } #endif
Итак, задачи у меня были следующие:
-
реализовать передачу данных с 8 кнопок через сдвиговый регистр (вместо 8 выводов занимаем 3)
-
для каждой кнопки должен фиксироваться целый набор действий: нажатие, x2/x3/xY нажатие, удержание, только отпускание, счетчик количества нажатий за один раз, мультитач и проч., чтобы в будущем в проекте было с чем поиграться (не терять функционал из-за другой аппаратной реализации)
-
ну и разумеется, все это должно работать для FreeRTOS, а значит — реализовано через очередь
Использованные библиотеки:
-
EncButton — здесь нашел весь необходимый функционал для отдельно взятой кнопки: разные виды нажатий, удержание и т.д.
Реализовано на c++ как класс, мне пойдет. Ну и не забывайте, что для platformio нужно саму Arduino.h подключить:
#include <Arduino.h> #include <EncButton.h>
Вот и все, все остальное у нас есть в базовой Arduino.h и будем делать вещи.
Разберем некоторые важные части кода по отдельности
1.Первым делом определяем порты для сдвигового регистра: шина данных, «защелка» а.к.а. затвор, а.к.а. «LATCH», тактовый контакт. Если значение контактов и устройство регистра не совсем ясно, читаем даташит для SN74HC165.
#define DATA_PIN 12 // пин данных #define LATCH_PIN 27 // пин защелки #define CLOCK_PIN 14 // пин тактов синхронизации
Это константа для последнего параметра функции xQueueSend(), который в нашем случае будет определять частоту обновления значений клавиатуры в случае, если очередь по какой-то причине занята и не принимает значения.
#define KEYBOARD_QUEUE_TIMEOUT_TICKS 5000
2. Переменная button типа enum и созданные для нее перегрузки нужны для повышения понятности и удобочитаемости текста. Теперь в цикле for у нас понятно что происходит, и в целом все выглядит более аккуратно, но не более того.
enum buttons_list
enum buttons_list { BUTT0, BUTT1, BUTT2, BUTT3, BUTT4, BUTT5, BUTT6, BUTT7 }; enum buttons_list button; //enum не может неявно преобразовываться в другие типы в C++ //поэтому нужна перегрузка '++' (он нужен для испольования enum в операторе for) //префиксный buttons_list operator++(buttons_list& b) { b = static_cast<buttons_list>(static_cast<int>(b) + 1); return b; } // Постфиксный инкремент buttons_list operator++(buttons_list& b, int) { buttons_list old = b; b = static_cast<buttons_list>(static_cast<int>(b) + 1); return old; }
gg_button по сути структура-контейнер, которую можно модифицировать в зависимости от того, какой функционал вам необходим в проекте. Для полного понимания нужно ознакомиться с документацией opensource библиотеки «EncButton», ссылка была выше.
struct gg_button
//структура для передачи состояния кнопки в очередь: тут почти все фичи из класса 'VirtButton' typedef struct gg_button { buttons_list id; bool press; bool release; bool click; bool pressing; bool hold; bool holding; bool step; bool has_clicks; bool has_x_clicks; uint8_t get_clicks; uint16_t get_steps; bool release_hold; bool release_step; bool timeout; uint16_t press_for; bool press_for_t; uint16_t hold_for; bool hold_for_t; uint16_t step_for; bool step_for_t; } gg_button;
3. Создаем handle (я его называю «ручник») для нашей главной очереди freeRTOS, по сути — указатель на очередь для ее индентификации.
QueueHandle_t keyboard_queue;
Далее объявляем о создании очереди в setup(), устанавливаем размер и количество элементов:
keyboard_queue = xQueueCreate(8, sizeof(gg_button));
4. Создаем задачи для клавиатуры и задачи-приемника (у меня это master_task) и не забываем прописать в loop() delay, т.к. это тоже freeRTOS задача, и если это забывать сделать, watchdog будет выдавать ошибку.
void setup() { Serial.begin(9600); keyboard_queue = xQueueCreate(8, sizeof(gg_button)); xTaskCreate(keyboard_task, "KEYBOARD", 1500, NULL, 1, NULL); xTaskCreate(master_task, "MASTER", 1500, NULL, 1, NULL); } void loop() { delay(1000); }
5. Разберем keyboard_task
После инициализации пинов создаем буффер input_buff для записи данных в очередь. Все остальное — внутренние настройки считывания данных с кнопок из «EncButton». Регулируйте их по своему усмотрению.
gg_button input_buff; uint8_t x_clicks = 5; uint16_t timeout_button = 60000; // после взаимодействия с кнопкой прошло указанное время, мс [событие] uint16_t press_for_timeout = 2000; // время, которое кнопка удерживается (с начала нажатия), мс uint16_t hold_for_timeout = 2000; // время, которое кнопка удерживается (с начала удержания), мс uint16_t step_for_timeout = 2000;
Далее логика следующая:
считываем данные со сдвигового регистра и записываем в переменную byte:
digitalWrite(LATCH_PIN, LOW); // щелкнули защелкой digitalWrite(LATCH_PIN, HIGH); byte b = shiftIn(DATA_PIN, CLOCK_PIN, MSBFIRST); // считываем
Функции shiftIn() и bitRead() — из встроенной в ядро ардуино библиотеки для работы со сдвиговыми регистрами, вы можете написать их сами.
Поочередно считываем данные с помощью функции tick() :
//опрос для каждой кнопки в массиве for (button = BUTT0; button <= BUTT7; button++) { buttons[button].tick(!bitRead(b, button)); //необходимо инвертировать вывод bitRead(), т.к. либа настроена на работу с подтяжкой к земле, а у меня к + }
А затем записываем все нужные нам данные в буффер и передаем в очередь:
//считывание и передача значений с каждой кнопки масссива в структуру-буфер и его(буфера) передача в очередь 'keyboard_queue' for (button = BUTT0; button <= BUTT7; button++) { input_buff.id = button; input_buff.press = buttons[button].press(); input_buff.release = buttons[button].release(); input_buff.click = buttons[button].click(); input_buff.pressing = buttons[button].pressing(); input_buff.hold = buttons[button].hold(); input_buff.holding = buttons[button].holding(); input_buff.step = buttons[button].step(); input_buff.has_clicks = buttons[button].hasClicks(); input_buff.has_x_clicks = buttons[button].hasClicks(x_clicks); input_buff.get_clicks = buttons[button].getClicks(); input_buff.get_steps = buttons[button].getSteps(); input_buff.release_hold = buttons[button].releaseHold(); input_buff.release_step = buttons[button].releaseStep(); input_buff.timeout = buttons[button].timeout(timeout_button); input_buff.press_for = buttons[button].pressFor(); input_buff.press_for_t = buttons[button].pressFor(press_for_timeout); input_buff.hold_for = buttons[button].holdFor(); input_buff.hold_for_t = buttons[button].holdFor(hold_for_timeout); input_buff.step_for = buttons[button].stepFor(); input_buff.step_for_t = buttons[button].stepFor(step_for_timeout); xQueueSend(keyboard_queue, &input_buff, KEYBOARD_QUEUE_TIMEOUT_TICKS); }
Как вы можете видеть, здесь каждая кнопка передается как отдельный элемент в очередь. Вы можете модифицировать этот пример, чтобы передавать сразу массив кнопок — я лично потом именно так и сделал для своего проекта.
6. Пишем задачу-приемник.
пример задачи-приемника
gg_button output_buff; while(1) { xQueueReceive(keyboard_queue, &output_buff, portMAX_DELAY); switch(output_buff.id) { case BUTT0: //print_button(&output_buff); if(output_buff.release == 1) { std::cout << "release BUTT0\n"; } break; case BUTT1: if(output_buff.release == 1) { std::cout << "release BUTT1\n"; } break; case BUTT2: if(output_buff.release == 1) { std::cout << "release BUTT2\n"; } break; case BUTT3: if(output_buff.release == 1) { std::cout << "release BUTT3\n"; } break; case BUTT4: if(output_buff.release == 1) { std::cout << "release BUTT4\n"; } break; case BUTT5: if(output_buff.release == 1) { std::cout << "release BUTT5\n"; } break; case BUTT6: if(output_buff.release == 1) { std::cout << "release BUTT6\n"; } break; case BUTT7: if(output_buff.release == 1) { std::cout << "release BUTT7\n"; } break; } portYIELD(); }
Я здесь сделал приемник чисто для проверки корректности вывода, т.е. без полезной нагрузки. Если все ок, при отпускании соответствующей кнопки вы увидите на экране:
«release BUTTX»
Таким же образом вы можете использовать в задаче-получателе все остальные параметры кнопки.
Таким образом:
-
Мы успешно сократили количество занимаемых выводов на esp32 с 8 до 3
-
Получили клавиатуру с широким набором функций, которую можно настроить под свои нужды
-
Обеспечили нашу задачу данными с клавиатуры
В принципе, цель достигнута. Однако я позволю себе развить тему немного дальше. Предположим, вы взяли этот пример за образец для своего проекта.
Есть вероятность, что:
-
В проектируемом устройстве клавиатура, как не крути, будет являться не единственным устройством ввода
-
Сразу несколько задач должны получать данные из очереди
Рассмотрим первый случай. На многим извесной пикче ниже демонстрируется образец модели, которую можно описать так: одна общая очередь для всех устройств ввода.
Элемент очереди вместе с данными из задачи-передатчика получает ID, а затем с его помощью приемник понимает какие данные откуда пришли.
Добавим к нашей клавиатуре энкодер. У него есть своя задача-передатчик, и для его параметров мы создаем новую структуру:
//в структуре создержатся переменные, покрывающие весь функционал энкодера typedef struct { bool left; bool right; bool leftH; bool rightH; int dir; bool press; bool pressing; int clicks; int click; bool fast; int counter; bool release; bool hold; uint16_t holdFor; uint16_t step; uint16_t action; } EncData;
Теперь, как и договаривались, будем передавать в общую очередь сразу массив кнопок. Также добавим переменную типа enum, которая будет являться идентификационной:
//---------------------------------ALL DATA ENUM--------------------------------------------- //data types for correct data queue read/write enum queue_data_type{ ENCODER_DATA, KEYBOARD_DATA}; //-------------------------------------------------------------------------------------------- //enum var for data type identificaion (joystick, encoder, etc.) in one queue
Главная очередь данных таким образом может выглядеть так:
typedef struct { EncData enc_data; gg_button_t keyboard_data[SIZE_KEYBOARD]; queue_data_type id; } ggData;
Все остальное остается так же. Вы настраиваете чтение на стороне задачи-приемника и проблем нет.
Второй случай. Ситуация становится сложнее, если вы хотите, чтобы было сразу несколько задач-получателей. Курниц называет такую задачу нетривиальной. Мне пришлось решать эту проблему самостоятельно, поэтому поднимаю ее здесь. Решение, которое предлагает интернет в таком случае — совместное использование функций синхронизации групп событий и просмотр элемента очереди без удаления.
«xQueuePeek() + xEventGroupSync() + xQueueRecieve()«
Суть: все задачи-получатели «подсматривают» в очередь с помощью функции xQueuePeek() после чего сразу устанавливают свой бит синхронизации (в специально созданной для этого группе событий EventGroupHandle_t syncEvent), давая понять системе, что она уже получила данные из очереди и ждет только остальных. Когда все задачи получили данные, условие синхронизации выполняется и последний читатель удаляет элемент из очереди с помощью xQueueRecieve().
Чтобы не понижать приоритет одной из задач-получателей, я сделал отдельную задачу синхронизации, единственной функцией которой является удаление элемента из очереди. Она обладает приоритетом ниже, чем задачи-читатели:
void sync_task(void* pvParameters) { ggData delete_buff; EventBits_t sync_return_check; while(1) { sync_return_check = xEventGroupSync(syncEvent, SYNC_START_BIT, ALL_SYNC_BITS, portMAX_DELAY); if( (sync_return_check & ALL_SYNC_BITS) == ALL_SYNC_BITS) xQueueReceive(ggDataQueue, &delete_buff, portMAX_DELAY); } }
И это сработало. Теперь все ваши задачи с полезной нагрузкой имеют информацию со всех устройств ввода. Буду рад обратной связи, надеюсь, что статья была полезна.
Источники:
1. API очередей freeRTOS (queueManagement)
2. Андрей Курниц цикл статей по freeRTOS. Компоненты и технологии, 2011
3. Сторонняя библиотека обработки кнопок (EncButton)
4. Перегрузка операторов перечисления (enum++)
5. Встроенные функции Ardunio.h для работы со сдвиговым регистром (shiftIn() и bitRead() )
6.Datasheet SN74HC165N (можно найти в сети)
ссылка на оригинал статьи https://habr.com/ru/articles/794278/
Добавить комментарий