Педаль в пол: создаём очередной ножной манипулятор для ПК

от автора

Буквально месяц назад я натолкнулся на эту статью, где повествуется о педалировании Vim. Чуть позже, после своего длительного трёхминутного исследования, я выяснил, что что тема эта уже не новая и довольно популярная. Сам я Vim использую только в случае крайней необходимости (если уж и приходится работать в консоли, то предпочитаю Nano), но ведь можно сделать подобное и под другие приложения.

Изначально я хотел сделать небольшую статейку, однако у меня получился целый туториал по созданию данного девайса с пошаговым написанием кода и пояснением что да как. Дабы не раздувать статью, под спойлерами будет различная информация, которая показалась мне интересной и достойной внимания новичков в Arduino, продвинутые и особо торопливые же пользователи могут не тратить на то время. Полный исходный код также представлен в конце статьи.

А зачем оно мне?

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

Во все времена программисты и дизайнеры старались сделать удобный и дружественный интерфейс, чтобы пользователь мог без лишних заморочек работать с приложением используя мышь и клавиатуру, так зачем же нам ещё один манипулятор? Что же, заглянем немного в историю, а точнее, в начало XVIII века, когда был изобретён такой музыкальный инструмент, как фортепиано. Как известно, это слово буквально переводится как «громко-тихо», но мало кто задумывается, что такой инструмент умный итальянский мастер получил, фактически «запедалировав» существовавший тогда клавесин, что и позволило в какой-то степени управлять громкостью звука, при этом не отнимая руки от клавиш.

Примеров можно приводить много. Педали есть у автомобиля, чтобы не бросать руль, если надо добавить газ. Барабанная установка тоже имеет педали, чтобы стучать в бас-бочку и тарелки. А что могут дать педали при использовании компьютера? Ну, например, можно задать какую-нибудь горячую комбинацию клавиш, или вообще добавить клавишу, которой нет, вроде включения и выключения звука. Педали могут помочь, если заняты руки: сам я играю на гитаре, при этом иногда под аккомпанемент, я было бы очень удобно проматывать подложку, не пытаясь постоянно дотянуться до клавиатуры. Ну и, наконец, контроллеры могут давать и совершенно нечеловеческие возможности в играх: было бы круто одним кликом построить себе всю базу в стратегии или крушить врагов со скоростью десятка ударов в секунду в шутерах, не так ли?

В общем, надеюсь, я вас убедил, а значит, пора приступать непосредственно к самой разработке.

Необходимые ресурсы

  • Собственно, педали. Тут сразу же возникли некоторые сложности из-за того, что я никак не мог придумать название для такой педали. Я знал лишь то, что подобные вещи используются в швейных машинках. В общем, по запросу electric pedal мне всё же удалось найти то, что нужно, на Aliexpress, и я, недолго думая, заказал 3 штуки.
  • Контроллер. Педалборд должен эмулировать работу клавиатуры и, возможно, мыши для возможности подключения к ПК без лишних драйверов. Для этого отлично подойдёт плата Arduino ProMicro, которая хоть и не содержит имеет некоторых выводов, но зато сделана максимально компактно. Идём на тот же Aliexpress, и покупаем китайскую версию этого чуда.
  • Провода. Чтобы поместить 3 педали под стол, нужен как минимум четырёхжильный провод длиной не меньше метра. Тут, думаю, проблем возникнуть не должно.
  • RGB-светодиод и кнопка. Первый нужен для индикации режимов, а вторая — для их переключения.
  • Ну и, понятное дело, нам нужны Arduino IDE, паяльник и прямые руки.

Схема устройства

Ещё до того, как мне пришли посылки, я притупил к созданию схемы устройства. Хотя это сильно сказано, так как мне надо было всего лишь подключить педали, диод и кнопку. Получилось как-то так:

Для педалей я решил выделить сразу 4 порта PB1-PB4, то есть две для левой, и две для правой ноги, хотя пока педали у меня только 3. К тому же, они все находятся в одной группе и расположены в одном месте. Под светодиод я отвёл выводы PD0, PD1 и PD4, под кнопку — PD7.
При этом нам не понадобятся никакие подтягивающие резисторы, если использовать те, что встроены в контроллер. Правда, тогда, при нажатии кнопки или педали, на входе будет низкий уровень, а при отпускании — высокий, то есть, нажатия будут инвертироваться, и об этом не стоит забывать.

Написание кода

Этот этап был самым трудным: из-за моего пары ошибок в указателях я несколько раз стёр загрузчик и в итоге чуть не завалил плату на программном уровне. Ниже подробно расписаны все этапы создания прошивки, для тех же, кто просто хочет получить работающий код, он будет в конце статьи.

Подготовка

Для начала нам нужно понять, что вообще такое педаль с точки зрения программы. Я решил сделать возможность задания педали одного из двух режимов — реального времени и триггера. Каждая педаль при этом имеет две программы: первая выполняется при удержании педали в режиме реального времени или при нечётных нажатиях в режиме триггера, вторая — при отпускании педали в режиме реального времени или при чётных нажатиях в режиме триггера. Так же у педали есть порт, состояние, и две переменные — текущие позиции в программах 1 и 2. У меня получилась вот такая структура:

struct pedal {   char port; // порт педали   char state; // состояние педали, для триггеров   char oldState; // старое состояние, для дебоунса   char pos1; // позиция 1   char pos2; // позиция 2   unsigned char type; //0 — режим реального времени, 1 — режим триггера;   unsigned char act1[16]; //программа 1   unsigned char act2[16]; //программа 2 }; 

Arduino имеет довольно мало памяти и к тому же 8-разрядная, так что лучше стараться использовать char нежели int там, где это возможно.

Так же нам понадобится стандартная библиотека Keyboard для работы в качестве клавиатуры.

Обработка нажатий

Сейчас нам нужно сделать интерпретатор, который будет читать данные из массива и отправлять их в виде нажатий клавиш на машину, а так же выделить несколько значений под различные внутренние команды. Открываем страницу с кодами клавиш, и смотрим что и как мы можем нажать. Я не стал глубоко копать и изучать всякие стандарты клавиатур, так как информации здесь мне показалось вполне достаточно для такого проекта. Первая половина отведена под стандартные ASCII-символы (хотя некоторые из них и непечатаемы или не используются), вторая же — под различные клавиши-модификаторы. Есть даже отдельные коды для левых и правых клавиш, что очень порадовало, а вот специальных кодов для цифр с нампада я не увидел, хотя, насколько я знаю, они немного по-особому воспринимаются в системе, нежели обычные цифры. Возможно, их коды находятся где-то в «дырах», между диапазонами, но сейчас не об этом. Итак, самый большой код имеет клавиша «вверх» — 218, а значит, диапазон 219-255 можно считать свободным, ну или по крайней мере там нет каких-то важных клавиш.

void pedalAction() {  //255 будет означать, что педаль не объявлена   if (pedal1->type == 255)     return; //указатель на массив с программой   unsigned char *prg; //указатель на позицию в программе   char *pos;   if (pedal1->type) { //код для определения педали в  режиме триггера     int current;     if ((current = digitalRead(ports[num])) != oldState[num]) {       if (!current)         state[num] = !state[num];       oldState[num] = current;     }     if (!state[num]) {       //act1       pos2[num] = 0;       pos = &(pos1[num]);       prg = pedal1->act1;     } else {       //act2       pos1[num] = 0;       pos = &(pos2[num]);       prg = pedal1->act2;     }   } else { //код для определения педали в  режиме реального времени     if (!digitalRead(ports[num])) {       //act1       pos2[num] = 0;       pos = &(pos1[num]);       prg = pedal1->act1;     } else {       //act2       pos1[num] = 0;       pos = &(pos2[num]);       prg = pedal1->act2;     }   }   while (1) {    if (prg[*pos] == 254) {       //Удерживать клавишу, следующую за *pos       Keyboard.press(prg[++*pos]);     } else if (prg[*pos] == 253) {       //Отпустить клавишу, следующую за *pos       Keyboard.release(prg[++*pos]);     } else if (prg[*pos] == 252) {       //"Пропуск хода", ничего не делать       ++*pos;       return;     } else if (prg[*pos] == 251) {       //Переместиться в программе на позицию в ячейке *pos+1       *pos = prg[*pos + 1];       return;     } else if (prg[*pos] == 255 || prg[*pos] == 0) {       //Конец программы, просто заглушка       return;     } else {       //Отправляем нажатие клавиши       Keyboard.write(prg[*pos]);     }     //Циклически переходим на ячейку вперёд после тех команд, после которых это необходимо     if (++*pos>=16)        pos = 0;   } } 

Думаю, даже у человека с не самым высоким уровнем знания Си не возникнет вопросов о том, что тут происходит. Сначала функция выбирает нужную педаль и определяет в зависимости от режима и состояния педали, какую программу стоит выполнять. При чтении каждого элемента массива, если он не является управляющим символом, вызывается функция Keyboard.write(), которая эмулирует нажатие и отпускание клавиши. Управляющие же символы обрабатывются отдельно и нужны для зажатия комбинаций клавиш и навигации по программе.

Некоторые особенности работы в режиме клавиатуры

У Keyboard.write() есть несколько простых, но не очевидных новичкам нюансов, исходящих из того, что мы отправляем данные не в сыром виде, а как нажатия клавиш. Во-первых, как ни странно, без дополнительных драйверов компьютер может принимать с клавиатуры только те символы, которые есть на клавиатуре, а значит отправить какой-нибудь 0x03 (сигнал прерывания) или 0x1B (начало ESCAPE-последовательности) у нас не выйдет. Во-вторых, мы можем оправлять заглавные буквы, как они есть в ASCII таблице, но машина при этом получит комбинацию клавиш Shift+<строчная буква>. Проблемой это может стать, если у нас включен CapsLock, и мы будем «неожиданно» получать маленькие буквы вместо больших и наоборот. В-третьих, мы не можем использовать русский язык, как и в общем-то и любой другой. Происходит это опять же происходит из-за такой надоедливой вещи, как коды клавиш. Хотя Keyboard.write() в качестве аргумента и принимает, но по USB всё равно отправляется код, соответствующий клавише, на которой он находится в стандартной английской раскладке, и если мы попытаемся отправить кириллицу, то получим неизвестно что. Поэтому, если мы хотим поздороваться с нашими русскоговорящими друзьями через Arduino, то в коде нам надо написать «Ghbdtn», а затем отправить это, предварительно выбрав русскую раскладку. Такое «приветствие» сработает и в украинской раскладке, а вот в болгарской, несмотря на то, что там так же есть кириллица, ничего не выйдет, так как буквы там стоят на совершенно других местах. (Как-то я слышал мнение, что для многих американских и английских разработчиков непостижим тот факт, что кому-то вообще может понадобиться не только использовать несколько раскладок, но ещё и переключать их.)

Итак, у нас есть интерпретатор и примерное понимание того, как наш педалборд взаимодействует с компьютером. Теперь надо всё это довести до состояния полноценной прошивки и проверить работоспособность на одной педали. Если создать экземпляр педали и циклично вызывать pedalAction(), то по идее у нас будет выполняться заданная в структуре программа.

struct pedal *pedal1 = {15, 0, 0, 0, 0, 0, "Hello, world!\0", 0};  void prepare () {    pinMode(15, 2); //2 - INPUT_PULLUP, то есть вход с подтяжкой к питанию    Keyboard.begin(); }  void loop() {    pedalAction(); } 

Кстати, никогда не забывайте про нуль-терминаторы в данных «программах», если их длина меньше размера массива и если они не цикличны, потому что Arduino будет не только пытаться интерпретировать не заданные данные, но и будет отправлять их в машину с огромной скоростью, а это всё равно, что дать клавиатуру обезьяне.

Одна педаль хорошо, а две — лучше

Теперь пришло время разобраться с обработкой сигналов с нескольких педалей, а также добавить переключение режимов. В начале статьи было выделено 4 порта под педали, каждой из которых надо позволить работать в семи режимах. Почему 7? Потому что без использования ШИМ наш светодиод может давать всего 7 цветов, и восьмой — выключенный. Такого количества вполне хватит обычному пользователю, ну а в крайнем случае его легко можно увеличить. Значит педали будем хранить двумерном в массиве 7 х 4. Чтобы не засорять память, общие для нескольких структур значения, такие, как номер порта можно вынести в отдельные массивы. В итоге мы получаем что-то такое:

struct pedal {   unsigned char type;   unsigned char act1[16];   unsigned char act2[16]; };  struct pedal pedals[7][4] = {   {     { 255, {"Hello, world!\0"}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   } };  char ports[4] = {15, 16, 14, 8}; char pos1[4]  = {0, 0, 0, 0}; char pos2[4]  = {0, 0, 0, 0}; char state[4]  = {0, 0, 0, 0}; char oldState[4]  = {0, 0, 0, 0};  char mode = 0; //текущий режим char curPedal = 0; //текущая обрабатываемая педаль 

Магия числа 255

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

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

void prepare(){   pinMode(2, 1);   pinMode(3, 1);   pinMode(4, 1);   pinMode(6, 2);   for (int i : ports)     pinMode(i, 2);   Keyboard.begin(); }  void loop() {  for (int i = 0; i < 6; i++) {    int current;   if ((current = digitalRead(modeButton)) != last) {     if (!current) {       if (++mode >= 7)         mode = 0;       while (pedals[mode][0].type == 255 && pedals[mode][1].type == 255 && pedals[mode][2].type == 255 && pedals[mode][3].type == 255)         if (++mode >= 7) {           mode = 0;           break;         }     }     last = current;     digitalWrite(2, (mode + 1) & 0b001);     digitalWrite(3, (mode + 1) & 0b010);     digitalWrite(4, (mode + 1) & 0b100);     for (int i = 0; i < 4; i++) {       pos1[i]  = 0;       pos2[i]  = 0;       state[i]  = 0;       oldState[i]  = 0;     }       delay(50);   }       curPedal = i;       pedalAction     }   } } 

Контроллер буде считать режим неиспользуемым, если в нём не объявлено ни одной педали (mode=255), а значит при попадании на него сразу перейдёт к следующему, но при этом первый режим всегда будет существовать. При переключении режима все значения в массивах зануляются, так как сохранять их для каждого режима нам не требуется (верно?), а затем цикл обходит все педали и вызывает pedalAction для них.

Также в начале метода pedalAction() нужно добавить следующую строчку, чтобы он понимал, с какой из структур надо иметь дело:

struct pedal *pedal1 = &pedals[mode][curPedal]; 

Уже существующую структуру pedal1 можно удалить за ненадобностью.

Всё это так же вполне работает, однако я столкнулся с одной проблемой: некоторые программы не успевают принимать нажатия с такой скоростью, с которой их отправляет Arduino. Самое очевидное решение — добавить возможность устанавливать задержки между действиями там, где это необходимо. Вот только когда мы садимся писать программы под микроконтроллеры, все фишки, вроде аппаратной многопоточности, остались где-то там, в высокоуровневых ЭВМ, у нас же при добавлении задержки останавливается вся программа, пока контроллер не отсчитает нужное количество циклов. Раз многопоточности у нас нет, то придётся её создать.

Тяжело сказать, да легко сделать

Я не стал изобретать велосипед, а взял готовую библиотеку ArduinoThread. Здесь можно немного почитать о том как она работает и скачать её. Загрузить библиотеку можно и из самой Arduino IDE. Кратко говоря, она позволяет периодически выполнять функцию с определённым интервалом, при этом не позволяя уйти в бесконечный цикл в случае, если выполнение займёт больше времени, чем интервал. То, что нужно. Создадим ещё один массив с потоками для каждой педали:

Thread pedalThreads[6] = {Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction,  10), Thread(pedalAction, 10)}; 

Теперь у нас есть 6 одинаковых виртуальных потоков, но при этом являющихся разными объектами.

Немного перепишем цикл обхода педалей для работы с новым функционалом:

...   for (int i = 0; i < 4; i++) {     if (pedalThreads[i].shouldRun()) {       curPedal = i;       pedalThreads[i].run();     }   } ... 

Теперь значение 252 в массиве программы, которое соответствует «ничегонеделанию», будет давать задержку в 10 миллисекунд (хотя на самом деле чуть больше, так как выполнение кода тоже занимает время). Добавив несколько строк в интерпретатор, получится сделать возможным установку задержки в несколько таких «квантов», потратив всего 2 байта массива:

... if (wait[num]) {       wait[num]--;       return;     }  else if (prg[*pos] == 250) {       wait[num] = prg[++*pos];     } ... 

В отличии от остальных команд, данную инструкцию необходимо добавить именно в начало интерпретатора, то есть сразу после «while (1) {», так как задержка должна обрабатываться до того, как интерпретатор перейдёт к чтению программы. Массив wait нужно так же объявить, как это было сделано с ports, state и т.д. и так же обнулять его ячейки при переключении режима, чтобы задержка не перешла в другую программу.

Теперь, при возможности установки задержки до 2.55 секунд проблем с определением клавиш программами возникать не должно.

Программирование «на ходу»

В принципе, тут можно было бы закончить с кодом и приступить к сборке устройства, но в этом случае, если кто-то вдруг захочет перепрограммировать педали, то ему придётся открывать Arduino IDE, править код, и заново загружать прошивку. Естественно, такой вариант не самый лучший, поэтому я решил добавить возможность менять программу с последовательного порта Arduino, а сами программы хранить в EEPROM. Для работы с энергонезависимой памятью необходимо подключить стандартную библиотеку EEPROM.h. Код режима программирования выглядит следующим образом:

...   if (!digitalRead(modeButton)) {     //Режим программирования     Serial.begin(9600);     while (!Serial) {       PORTD = 0b00000000 + (PORTD & 0b11101100);       delay(250);       PORTD = 0b00010000 + (PORTD & 0b11101100);       delay(250);     }      Serial.println(F("***Programming mode***"));     Serial.println(F("Write the command as <m> <p> <c>"));     Serial.println(F("m - number of mode, one digit"));     Serial.println(F("p - number of pedal, one digit"));     Serial.println(F("c - command, it can be:"));     Serial.println(F("\tr - read pedal info"));     Serial.println(F("\tw - enter to writing mode and change pedal programm"));     Serial.println(F("\te - erase pedal programm and delete it"));     Serial.println(F("There are up to 7 modes and 6 pedals per mode can be configured"));     Serial.println(F("Mode will be incative if there is no pedal configured in it"));     while (1) {       while (Serial.available()) {         Serial.read();         delay(1);       }       PORTD = 0b00000001 + (PORTD & 0b11101100);       Serial.println("");       Serial.println(F("Enter command"));       while (!Serial.available());       PORTD = 0b00000010 + (PORTD & 0b11101100);       delay(3);       if (Serial.available() == 3) {         int curMode = Serial.read() - 48;         int curPedal = Serial.read() - 48;         char cmd = Serial.read();         if (curMode > 6 || curMode < 0) {           Serial.print(F("Mode must be in 0-6. You entered "));           Serial.println(curMode);           continue;         }         if (curPedal > 3 || curPedal < 0) {           Serial.print(F("Pedal must be in 0-3. You entered "));           Serial.println(curPedal);           continue;         }         Serial.println();         if (cmd == 'r') {           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           Serial.print("type: ");           int curAddress = beginAddress;           Serial.println(EEPROM[curAddress++]);           Serial.print("act1: ");           for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {             Serial.print(EEPROM[i]);             Serial.print("\t");           }           Serial.println();           curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;           Serial.print("act2: ");           for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {             Serial.print(EEPROM[i]);             Serial.print("\t");           }           Serial.println();         } else if (cmd == 'w') {           Serial.println(F("Enter type:"));           PORTD = 0b00000001 + (PORTD & 0b11101100);           while (!Serial.available());           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           int curAddress = beginAddress;           PORTD = 0b00000010 + (PORTD & 0b11101100);           EEPROM[curAddress++] = (char)Serial.parseInt();           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Enter act1 in DEC divided by space:"));           while (Serial.available()) {             Serial.read();             delay(1);           }           while (!Serial.available());           PORTD = 0b00000010 + (PORTD & 0b11101100);           while (Serial.available()) {             EEPROM[curAddress++] = (char)Serial.parseInt();             delay(1);           }           PORTD = 0b00000001 + (PORTD & 0b11101100);           curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;           Serial.println(F("Enter act2 in DEC divided by space:"));            while (Serial.available()) {             Serial.read();             delay(1);           }           while (!Serial.available());           PORTD = 0b00000010 + (PORTD & 0b11101100);           while (Serial.available()) {             EEPROM[curAddress++] = (char)Serial.parseInt();             delay(1);           }           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Finished, don't forget to verify written data!"));         }  else if (cmd == 'e') {           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           Serial.println(F("Disabling pedal..."));           PORTD = 0b00000010 + (PORTD & 0b11101100);           EEPROM[beginAddress] = 255;           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Pedal disabled"));         }       } else {         Serial.println(F("Incorrect command, please read help above"));       }     };   } ... 

Что делает этот код поясняет содержащаяся в нём справка: через пробел вводится номер режима, номер педали, и команда, которых существует 3 — чтение, запись и выполнение удаление программы. Все данные о педалях хранятся друг за другом в виде последовательности из 33-х байт, то есть тип педали, и две программы, и того мы занимаем 7*4*33=924 из 1024 байт EEPROM. Вариант использования динамического размера педалей в памяти я отбросил, так как в этом случае при перепрограммировании одной педали придётся перезаписать почти все ячейки, а циклов перезаписи эта память имеет конечное количество, поэтому рекомендуют делать это как можно реже.

Особенности работы с EEPROM

Ещё хотелось бы обратить внимание на строки вида:

 PORTD = 0b00000010 + (PORTD & 0b11101100);          ...   PORTD = 0b00000001 + (PORTD & 0b11101100); 

Благодаря данной библиотеке, с точки зрения программиста, энергонезависимая память является обычным массивом char, но, как «ардуинщикам», нам нужно понимать, что запись в ПЗУ — очень тяжёлая операция, которая занимает у контроллера целых ~3 секунды, и желательно не прерывать этот процесс. Данная конструкция заставляет диод светить красным во время таких операций, а затем возвращает обратно «безопасный» зелёный цвет.

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

С сохранением структур разобрались, теперь надо наши данные как-то оттуда вытащить и преобразовать к «педальному» виду:

...  for (int i = 0; i < 7; i++) {     for (int j = 0; j < 4; j++) {       struct pedal *p = &pedals[i][j];       int beginAddress = sizeof(struct pedal) * (i * 6 + j);       int curAddress = beginAddress;       unsigned char type = EEPROM[curAddress++];       if (type == 0 || type == 1) {         p->type = type;         for (int k = 0 ; k < 16; k++) {           p->act1[k] = EEPROM[curAddress++];         }         for (int k = 0 ; k < 16; k++) {           p->act2[k] = EEPROM[curAddress++];         }       }     }   } ... 

Здесь так же не происходит ничего сверхъестественного: контроллер считывает данные из памяти и заполняет ими уже существующие структуры.

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

Демонстрация

https://www.youtube.com/embed/pkjtRLlNZnU

Полный исходный код

Он вот тут

#include <Keyboard.h> #include <Thread.h> #include <EEPROM.h> #define modeButton 6  struct pedal {   unsigned char type; //0 — режим реального времени, 1 — режим триггера, 255 — педаль не назначена   unsigned char act1[16];   unsigned char act2[16]; };  struct pedal pedals[7][4] = {   {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   },  {     { 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}   } };   char ports[4] = {8, 16, 15, 14}; char pos1[4]  = {0, 0, 0, 0}; char pos2[4]  = {0, 0, 0, 0}; char state[4]  = {0, 0, 0, 0}; char oldState[4]  = {0, 0, 0, 0}; char wait[4]  = {0, 0, 0, 0};  void pedalAction();  char mode = 0; char curPedal;   Thread pedalThreads[6] = {Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10)};  void setup() {   pinMode(2, 1);   pinMode(3, 1);   pinMode(4, 1);   pinMode(modeButton, 2);    if (!digitalRead(modeButton)) {     //Режим программирования     Serial.begin(9600);     while (!Serial) {       PORTD = 0b00000000 + (PORTD & 0b11101100);       delay(250);       PORTD = 0b00010000 + (PORTD & 0b11101100);       delay(250);     }      Serial.println(F("***Programming mode***"));     Serial.println(F("Write the command as <m> <p> <c>"));     Serial.println(F("m - number of mode, one digit"));     Serial.println(F("p - number of pedal, one digit"));     Serial.println(F("c - command, it can be:"));     Serial.println(F("\tr - read pedal info"));     Serial.println(F("\tw - enter to writing mode and change pedal programm"));     Serial.println(F("\te - erase pedal programm and delete it"));     Serial.println(F("There are up to 7 modes and 6 pedals per mode can be configured"));     Serial.println(F("Mode will be incative if there is no pedal configured in it"));     while (1) {       while (Serial.available()) {         Serial.read();         delay(1);       }       PORTD = 0b00000001 + (PORTD & 0b11101100);       Serial.println("");       Serial.println(F("Enter command"));       while (!Serial.available());       PORTD = 0b00000010 + (PORTD & 0b11101100);       delay(3);       if (Serial.available() == 3) {         int curMode = Serial.read() - 48;         int curPedal = Serial.read() - 48;         char cmd = Serial.read();         if (curMode > 6 || curMode < 0) {           Serial.print(F("Mode must be in 0-6. You entered "));           Serial.println(curMode);           continue;         }         if (curPedal > 3 || curPedal < 0) {           Serial.print(F("Pedal must be in 0-3. You entered "));           Serial.println(curPedal);           continue;         }         Serial.println();         if (cmd == 'r') {           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           Serial.print("type: ");           int curAddress = beginAddress;           Serial.println(EEPROM[curAddress++]);           Serial.print("act1: ");           for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {             Serial.print(EEPROM[i]);             Serial.print("\t");           }           Serial.println();           curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;           Serial.print("act2: ");           for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {             Serial.print(EEPROM[i]);             Serial.print("\t");           }           Serial.println();         } else if (cmd == 'w') {           Serial.println(F("Enter type:"));           PORTD = 0b00000001 + (PORTD & 0b11101100);           while (!Serial.available());           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           int curAddress = beginAddress;           PORTD = 0b00000010 + (PORTD & 0b11101100);           EEPROM[curAddress++] = (char)Serial.parseInt();           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Enter act1 in DEC divided by space:"));           while (Serial.available()) {             Serial.read();             delay(1);           }           while (!Serial.available());           PORTD = 0b00000010 + (PORTD & 0b11101100);           while (Serial.available()) {             EEPROM[curAddress++] = (char)Serial.parseInt();             delay(1);           }           PORTD = 0b00000001 + (PORTD & 0b11101100);           curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;           Serial.println(F("Enter act2 in DEC divided by space:"));            while (Serial.available()) {             Serial.read();             delay(1);           }           while (!Serial.available());           PORTD = 0b00000010 + (PORTD & 0b11101100);           while (Serial.available()) {             EEPROM[curAddress++] = (char)Serial.parseInt();             delay(1);           }           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Finished, don't forget to verify written data!"));         }  else if (cmd == 'e') {           int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);           Serial.println(F("Disabling pedal..."));           PORTD = 0b00000010 + (PORTD & 0b11101100);           EEPROM[beginAddress] = 255;           PORTD = 0b00000001 + (PORTD & 0b11101100);           Serial.println(F("Pedal disabled"));         }       } else {         Serial.println(F("Incorrect command, please read help above"));       }     };   }   for (int i : ports)     pinMode(i, 2);   pinMode(17, 1);   for (int i = 0; i < 7; i++) {     for (int j = 0; j < 4; j++) {       struct pedal *p = &pedals[i][j];       int beginAddress = sizeof(struct pedal) * (i * 6 + j);       int curAddress = beginAddress;       unsigned char type = EEPROM[curAddress++];       if (type == 0 || type == 1) {         p->type = type;         for (int k = 0 ; k < 16; k++) {           p->act1[k] = EEPROM[curAddress++];         }         for (int k = 0 ; k < 16; k++) {           p->act2[k] = EEPROM[curAddress++];         }       }     }   }   Keyboard.begin(); }  int last = 0;  void loop() {   int current;   if ((current = digitalRead(modeButton)) != last) {     if (!current) {       if (++mode >= 7)         mode = 0;       while (pedals[mode][0].type == 255 && pedals[mode][1].type == 255 && pedals[mode][2].type == 255 && pedals[mode][3].type == 255)         if (++mode >= 7) {           mode = 0;           break;         }     }     last = current;     digitalWrite(2, (mode + 1) & 0b001);     digitalWrite(3, (mode + 1) & 0b010);     digitalWrite(4, (mode + 1) & 0b100);     for (int i = 0; i < 4; i++) {       pos1[i]  = 0;       pos2[i]  = 0;       state[i]  = 0;       oldState[i]  = 0;       wait[i]  = 0;     }       delay(50);   }   for (int i = 0; i < 4; i++) {     if (pedalThreads[i].shouldRun()) {       curPedal = i;       pedalThreads[i].run();     }   } }  void pedalAction() {   struct pedal *pedal1 = &pedals[mode][curPedal];   if (pedal1->type == 255)     return;   unsigned char *prg;   char *pos;   if (pedal1->type) {     int current;     if ((current = digitalRead(ports[curPedal])) != oldState[curPedal]) {       if (!current)         state[curPedal] = !state[curPedal];       oldState[curPedal] = current;     }     if (!state[curPedal]) {       //act1       pos2[curPedal] = 0;       pos = &(pos1[curPedal]);       prg = pedal1->act1;     } else {       //act2       pos1[curPedal] = 0;       pos = &(pos2[curPedal]);       prg = pedal1->act2;     }   } else {     if (!digitalRead(ports[curPedal])) {       //act1       pos2[curPedal] = 0;       pos = &(pos1[curPedal]);       prg = pedal1->act1;     } else {       //act2       pos1[curPedal] = 0;       pos = &(pos2[curPedal]);       prg = pedal1->act2;     }   }   while (1) {     if (wait[curPedal]) {       wait[curPedal]--;       return;     }  else if (prg[*pos] == 250) {       wait[curPedal] = prg[++*pos];     } else if (prg[*pos] == 254) {       //Удерживать клавишу, следующую за *pos       Keyboard.press(prg[++*pos]);     } else if (prg[*pos] == 253) {       //Отпустить клавишу, следующую за *pos       Keyboard.release(prg[++*pos]);     } else if (prg[*pos] == 252) {       delay(10);       //"Пропуск хода", ничего не делать       ++*pos;       return;     } else if (prg[*pos] == 251) {       //Переместиться в программе на позицию в ячейке *pos+1       *pos = prg[*pos + 1];       return;     } else if (prg[*pos] == 255 || prg[*pos] == 0) {       //Конец программы, просто заглушка       return;     } else {       //Отправляем нажатие клавиши       Keyboard.write(prg[*pos]);     }     //Циклически переходим на ячейку вперёд после тех команд, после которых это необходимо     if (++*pos >= 16)       pos = 0;   } } 

Послесловие

Хотя изначально я и делал педальборд для возможности проматывания записи во время игры на гитаре, однако лично мне показалось удобным испольщование педалей и в обычных задачах, главное немного привыкнуть к такому необычному манипулятору. А вот тут кроется ещё одна проблема: уже без любимых педалей работать становится наоборот сложнее, так как приходится вспоминать, что, куда и для чего нажимать. Если в офис педали ещё можно носить и подключать, то в институте бегать с ними по кабинетам уже сложнее. Так что использовать этот девайс для чего-то, кроме его изначального предназначения стоит на свой страх и риск.

Собраный педальборд:


ссылка на оригинал статьи https://habr.com/ru/post/462263/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *