- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для педалей я решил выделить сразу 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 для работы в качестве клавиатуры.

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

Сейчас нам нужно сделать интерпретатор, который будет читать данные из массива и отправлять их в виде нажатий клавиш на машину, а так же выделить несколько значений под различные внутренние команды. Открываем страницу [2] с кодами клавиш, и смотрим что и как мы можем нажать. Я не стал глубоко копать и изучать всякие стандарты клавиатур, так как информации здесь мне показалось вполне достаточно для такого проекта. Первая половина отведена под стандартные 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};

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!"}, {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. Здесь [3] можно немного почитать о том как она работает и скачать её. Загрузить библиотеку можно и из самой 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 [4]

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

Он вот тут

#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;
  }
}

Послесловие

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

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

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

Автор: HukuToc2288

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/arduino/325836

Ссылки в тексте:

[1] эту: https://habr.com/ru/post/232177/

[2] страницу: https://doc.arduino.ua/ru/prog/KeyboardModifiers

[3] Здесь: https://soltau.ru/index.php/arduino/item/373-kak-vypolnyat-parallelnye-zadachi-threads-v-programme-dlya-arduino

[4] https://www.youtube.com/embed/pkjtRLlNZnU: https://www.youtube.com/embed/pkjtRLlNZnU

[5] Источник: https://habr.com/ru/post/462263/?utm_source=habrahabr&utm_medium=rss&utm_campaign=462263