Управление с телеметрией по serial

Ножки обозначеные символом ~ можно использовать как выход PWM а также как входы для режима захвата или подключения энуодера, зависит от настройки режима работы таймера.
На обратной стороне есть место под установку FLASH, туда можно поставить FRAM с SPI интерфейсом и будет вам быстрая энергонезависимая память с огромным ресурсом по перезаписи.

Они могут быть и входами и выходами - как запрограммируешь. Таймеров в корпусе 8. 6 из них могут выдавать ШИМ 5 по 4 канала один -2 канала. Но из за ограниченного числа ног одновременно могут только 19 ШИМ.

Можно считать что в режиме удержания милиампер 60, а в режиме трогания больше 100.

1 лайк

честно не понял, потому что на распиновке платы помечено 25 выводов под PWM… хотя 19 для меня бы тоже хватило

Потому что некоторые каналы ШИМ можно выводить на разные ноги в зависимости от пересечения функций ног с другими аппаратными блоками, но не одновременно. Например ногу РА0 можно подключить ещё к АЦП, таймеру 2, UART2… Соответственно канал 1 таймера 2 можно вывести на ноги РА0 или РА5. Промаркированы они будут как ШИМовые, но использовать можно только одну. Всё это есть в мануле на МК.
P.S. Это если считать только чисто аппаратный вывод. На всё (!) остальные ноги тоже можно вывести ШИМ, даже если они не будут промаркированы как ШИМовые, через прерывания таймеров, которых ещё 3 канала. Это займёт совсем мало ресурсов МК. Но надо же ещё помнить что нужны ноги и на другие нужды - связь, индикация…

3 лайка

но надо отметить, что софтовый ШИМ дает существенно большую загрузку ядра, чем аппаратный. Я бы не рекомендовал его использовать для частот выше 50-100 КГц

а если через DMA ))

Через прерывания таймера ШИМ почти не затратный. Всей софтовости там только команда переворота ноги - меньше микросекунды, а серве нужно 20 мс период ШИМ.

там главное - это вход в прерывание и выход из него. Если запустить этот процесс, например, каждую микросекунду - то загрузка ядра на блекпиле одним только этим ШИМ будет 50-80%.

Хотя да, на периоде 20 мс это незаметно.
Я то эти прерывания пытался на 5-10 МГц запускать :slight_smile:

А вот это не понятно зачем. На блюпиле просто софтовая генерация меандра без задержек, просто цикл дёргания одной (!) ногой, больше 6 МГц не получить. И иметь загрузку близкую к 100%

одной или 16-тью… если на одном порту - разницы нет.
Но то что 6 МГц близко к пределу - это да.

В итоге для строкового обмена получился такой-вот недокласс

class cWirelessLink {

  public:
  
    // открытие порта, проверка работоспособности модуля
    String start(HardwareSerial *_serial, uint8_t _setPin) {
      
      serial = _serial;
      setPin = _setPin;

      String log = "Methood start (pin " + String(setPin) + " might been connected to HC-12 SET pad)";

      pinMode(setPin, OUTPUT); // конфигурация пина активации режима AT
      serial -> begin(115200); // поднятие порта на 115200
      delay(500);
      digitalWrite(setPin, LOW); // активация режима AT команд
     
      // далее идет отправка AT команд в модуль и получение результата
      // по datasheet так и не стало понятно, перед отправкой нужны задержки 40мс или после, или между
      // поэтому используются задержки по 40мс между каждыми взаимодействиями с модулем
     
      // отправка запроса на проверку подключения
      delay(40);
      serial -> print("AT\r");
      delay(40);
     
      if (serial -> readString().indexOf("OK") != -1) {
     
        log += "\r\nSucessfully connected on 115200"; // подключение удалось
     
        serial -> print("AT+RX\r"); // запрос конфигурации
        delay(40);
        log += "\r\n" + Serial1.readString();
     
      } else {
     
        log += "\r\n Failed to connect on 115200, trying configurate on 9600"; // походу подключен новый модуль, попытка настроить на 9600
        
        serial -> end(); // закрытие порта
        serial -> begin(9600); // поднятие порта на стандартнх 9600
     
        delay(40);
        serial -> print("AT+B115200\r"); // отправка конфигурации - скорость порта
        delay(40);
        serial -> print("AT+FU1\r"); // отправка конфигурации - режим
        delay(40);
        serial -> print("AT+P1\r"); // отправка конфигурации - мощность
        delay(40);
     
        digitalWrite(setPin, HIGH);  // выход из режима AT команд
        delay(80); // задержка для применения настроек модулем по datasheet 80мс
     
        serial -> flush(); // очистка буфера порта (там результат применения конфигов, возможно прочий мусор)
        serial -> end(); // закрытие порта
        serial -> begin(9600); // поднятие порта на вожделенных 115200
        
        digitalWrite(setPin, LOW); // запуск режима AT команд
        delay(40);
        serial -> print("AT\r"); // отправка запроса на проверку подключения
        delay(40);
     
        if (serial -> readString().indexOf("OK") != -1) {
     
          log += "\r\nSucessfully connected on 115200 after configurate"; // подключение после настройки удалось
     
          serial -> print("AT+RX\r"); // запрос конфигурации
          delay(40);
          log += "\r\n" + serial -> readString(); // вывод конфигурации
     
        } else log += "\r\nFailed to configure HC-12 wia AT commands, check physical connection"; // попытка настройки не удалась - проверяем провода, подаем питание закоротив SET на GND (отключив от Arduino!!!)
        
      }
     
      // serial -> print("AT+DEFAULT\r"); delay(40); // сброс модуля на заводские (еси нужно потестить функцию)

      digitalWrite(setPin, HIGH);  // выход из режима AT команд
      delay(80);

      serial -> flush(); // очистка буфера порта

      return log;
     
    }

    
    // отправка сообщения
    void send (String data) {

      // обрамляется в <>, сразу после стартового символа помещается хеш
      serial -> print("<" + hash(data) + ";" + data + ">");
      lastTransmittingTime = millis();

    }

    // вернет строку, когда получит пакет целиком, иначе вернет пустую строку
    String get() {

        // принимаемый символ
        char character = '_';
        // счетчик принятых символов
        uint16_t bytesReeded = 0;
        // время последнего успешного приема символа
        uint32_t lastAvaliable = millis();

        // ожидание символа начала сообщения
        while (character != '<') {
          if (serial -> available()) {
            character = (char)serial -> read();
            lastAvaliable = millis();
            bytesReeded++;
            if (bytesReeded > byteTreshold) return "";
          } else if (millis() - lastAvaliable > timeTreshold) return "";
        }

        String message = "";
        message += character;

        // ожидание символа конца сообщения
        while (character != '>') {
          if (serial -> available()) {
            character = (char)serial -> read();
            message += character;
            lastAvaliable = millis();
            bytesReeded++;
            if (bytesReeded > byteTreshold) return "";
          } else if (millis() - lastAvaliable > timeTreshold) return "";
        }

        // сообщение получено, необходимо проверить хеш
        String recievedHash = message.substring(1, message.indexOf(";"));
        message.remove(0, message.indexOf(";") + 1);
        message.remove(message.indexOf(">"));

        // если вычисленый хеш равен полученному, то возвращается полученное сообщение, в противном случае пустую строку
        return (recievedHash == hash(message)) ? message : "";

    }

    uint16_t silence () { return millis() - lastTransmittingTime; }

  private:
  
    // ссылка на serial порт
    HardwareSerial *serial;
    // номер вывода, к которому подключен вход SET платы HC-12
    uint8_t setPin;
    // максимальное количество бит данных, по достижении которого будет выполнен принудительный сброс при приеме данных
    const uint16_t byteTreshold = 256;
    // количество миллисекунд неполучения ответа от serial, по достижении которых происходит сброс при приеме данных
    const uint16_t timeTreshold = 10;
    // время последнего взаимодействия
    uint16_t lastTransmittingTime = 0;

    String hash (String& data) {
      // в качестве хеша используем XOR по четным и нечетным символам (байтам) строки
      uint8_t odd = 0, even = 0;
      for (uint8_t i = 0; i < data.length(); i++)
        if (i % 2) odd ^= data[i]; // нечетные
        else even ^= data[i]; // четные
      return String((uint16_t) even * 256 + odd);
    }

};

задача принять из очереди цельный пакет произвольной размерности (есть максимальное ограничение), проверить типа хеш, и отдать тело сообщения если оно не битое. Скоро дополню методами на формирование/парсинг параметров.

Сама концепция, раз связь двусторонняя, то и робот и пульт не высылают исходящие запросы, пока не получат входящий запрос. Но время последнего отправленного запроса запоминается в классе, и если давно не было - робот направит инициативный. по сути такое возможно только если произошла потеря пакетов (и при первом взаимодействии естессно), у меня как ни старался она происходила только когда отправлял запросы без ожидания ответов - переполнялись буферы serial.

Запросы робота будут состоять из доступных пунктов менюшки в текстовом виде, может быть в последующем какой-нибудь телеметрии.

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

В роботе будет логический контроль принятых состояний органов управления, он будет “решать”, что отображать на экране пульта и как реагировать на показания органов управления.

Задача пульта будет лишь в том, чтобы выводить на экран видимую часть строкового меню. Самостоятельные функции пульта: калибровка стиков, всякие банальности из серии определения короткого и долгого нажатия кнопок, сглаживание АЦП.

П.с. печать закончилась

Если хотите делать быстрого паука, то про while при приёме лучше забыть. И от стрингов избавляться.

Да, логично. Но пока что тест с выводом Serial только на пульте и отправкой времени выполнения loop от робота в пульт показал в среднем чуть больше 200 итераций за секунду. Посмотрю на практике, если будет слишком тормозить то придется переделывать…

А какая альтернатива у while?

Например - разбор на лету через конечный автомат. На каждом лупе анализируем один байт, во while не торчим.

боюсь что он тогда будет не очень успевать очередь из 115200 забирать, loop не будет быстрым, там же алгебра и тригонометрия…

но даже если по таймеру например в свой буфер стягивать, то по 1 байт разбирать будет не очень быстро

про конечный автомат я не очень понял, универ был 12 лет назад, и больше я этими терминами с тех пор не пользовался - не доводилось

Это скорость обмена по UART, а не поступления пакетов…

но я ее могу только искусственно ограничить, радиомодули в режиме FU1 по документации обмениваются еще быстрее, узкое место скорее всего только в Serial.print

Я просто не дожидался ответа от принимающей стороны и спамил - очередь принимающего захлебывалась моментально, и это я пытался всё принятое вынимать из нее при вызове метода приема, а не по 1 байту

Аккуратней. Закон о неприкосновенности частной жизни!:smile:

Вообще да, как-то всё муторно, что ли. Строки, парсинг… как бы лагов не было больших.
Для себя я определил оптимальную архитектуру ПО передатчика и приёмника(хотя в моём случае граница между ними размыта):

  1. В системе приёмник-передатчик существует один единственный main-буфер. Каждый элемент которого расписан в спецификации: мл.байт стика1 по X, ст.байт стика1 по X, кнопка5, кнопка10, и т.д и т.п.
  2. Элементы массива доступны по указателям из любого модуля( радиосвязь, моторы, клавиатура, аналоговые органы…) для чтения/записи.
  3. Данные с пульта уходят “как есть”, задача по интерпретации и калибровке сигналов лежит на приёмной стороне. Тем самым делая пульт унифицированным. Мало ли, сегодня гексапод, завтра лодка, послезавтра межгалактическая ракета. Ну или межконтинентальная😄
  4. Также данные могут идти обратно на приёмник(опционально, делов на пару строк)
  5. Естественно, каждый модуль работает независимо, с разным периодом, для экономии ресурсов.
    В общем, ну их, строки эти, только хардкор, только числа!
1 лайк

Вот это самое правильное направление! Посылать надо не стринги а однобайтные команды. Благо символов на клавиатуре больше 30. На всё хватить должно. А реакция будет куда как быстрее и обработка проще. А уж если нужны какие то строки - можно команду прими строку добавить. И парсить на стороне робота не надо.