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

сдвиг + XOR со след. байтом на 90% закрывает потребности любителей контрольных сумм.

2 лайка

Смотрю сейчас на dallas crc8 - xor и сдвиг :wink:

Кажется дошло… размер при отправке нужен чтобы метод знал, когда перестать читать данные по указателю, а при приеме - чтобы не пихать в структуру больше данных чем она может принять. А работал код у меня лишь потому что не было рассинхрона по времени и биты принимались без сбоев. Верно?

Соответственно мне лучше повысить скорость до 115200, и хоть строками передавать данные.

В мейнлупе получателя делаю опрос сериал:

  • если там явное переполнение то сбросить его и дождаться новых данных в следующих итерациях loop
  • если нет переполнения, то читать данные в строку обмена (глобальная переменная)
  • после завершения чтения если в строке не более одного вхождения старт бита, то также будем ждать данные в следующих итерациях loop
  • если в строке 2 и более вхождения старт бита, удалить от начала строки обмена всё до второго (!) с конца вхождения стартового бита, данные между вторым и первым вхождением считать последним принятым пакетом, проверять crc, парсить

Вроде логично?

P.s. предвкушаю вопрос - каждую итерацию loop парсить, даже если ничего не поменялось - да, каждую. Так будет ровнее идти дельта времени выполнения loop и соотв. плавнее масштабирование по времени всей кинематики

1 лайк

А теперь представьте себе более сложную ситуацию. Из-за неодновременного включения (или ещё по какой причине), Вы начали читать пакет не с начала. Передающий передал своё и отвалился. А Вы, видя, что приняли не всё (Вы же не сначала читаете), сидите и ждёте оставшихся байтов до второго пришествия. Как Вам ситуация?

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

а конкретно по этой ситуации думал что пока в буфере не будет sizeOf чтение readBytes((byte*)&rxData, sizeof(rxData)) будет null/false возвращать… Ох как я ошибался))

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

А вот точно определить начало и конец … тут нужен продуманный протокол.

1 лайк

@Resin ну примерно как-то так.
Я вижу 2 варианта:

  1. Принимаем с любого момента(хоть с середины потока,например приняли 3й,4й,1й,2й) строго определённое кол-во байт, а потом уже имея в буфере данные ищем начало и т.п.
  2. Только при обнаружении начала пакета начинаем пихать его в буфер.

1й вариант, очевидно, требует ресурсы больше. Зато не надо ждать начала пакета.

1 лайк

А если взять готовый протокол типа модбас то вообще ничего обенного делать не надо. Всегда иметь полные валидные данные.

классно, но в моем случае избыточно. По вашей статье (неделя тестов) можно представить, что или задача была очень ответственная, или трансляция данных была энергоемкой для устройства, потому сочли необходимым с 1 раза успешно данные принимать. В любом случае познавательно :handshake:

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

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

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

Но если делать самогонный протокол, то как минимум я бы ввёл процедуры инициации связи (когда передающий сначала убеждается, что его вообще слушают) и завершения связи. В это случае, ситуация “начал принимать не с начала” невозможна в принципе, т.к. если не было инициации, то приёмник игнорирует всё, что прилетело. Также, по дороге можно вводить “сильное подтверждение” – т.е. приёмник может повторить то, что принял и передатчик убедиться, что принято правильно. Но это, возможно, избыточно.

Вот, смотрите:

  1. Морда, ответь Кирпичу. Приём. (ждём ответа, если не получен в течение некоего таймаута, повторяем);
  2. Морда на связи, слушаю тебя, Кирпич. Приём. (только после этого Кирпич начинает передачу (а Морда – приём) сутевого сообщения);
  3. Иду на сближение. Как понял? Приём. (это запрос повтора, можно обойтись, если не слишком критично)
  4. Понял тебя, идёшь на сближение. Приём. (возможно, Кирпич хочет ещё что-то передать, поэтому связь не разрываем)
  5. Конец связи. Как понял? Приём.
  6. Понял, конец связи.

Вот как-то так. Без инициации (п.п. 1 и 2) никакого приёма не ведётся. После разрыва (п.п. 5 и 6) всякий приём прекращается. А передатчик каждый раз убеждается, что его правильно поняли.

2 лайка

@ЕвгенийП , Вы в передаче “Просто о сложном” не участвовали?)

У первого варианта есть и еще один недостаток: в буфере у Вас будет “хвост” первого пакета и “голова” второго. При этом, где проходит граница, Вы не знаете. Весьма вероятно, что посередине числа. Так, что старшие байты будут от одного чиста, а младшие - совсем от другого.

1 лайк

@andriano точно, числа более 1 байта уже нельзя.

Делюсь промежуточкой, настроить HC-12 оказалось не совсем тривиальной задачей.
Идея в том, чтобы если подключен новый модуль - хотя бы попытаться его настроить в setup()

  const uint8_t setPin = 19; // пин SET модуля HC-12
  const uint8_t serialDelay = 30; // длительность задержки для взаимодействия по Serial
  pinMode(setPin, OUTPUT); // вывод конфигурации
  Serial2.begin(115200); // поднятие порта на 115200
  digitalWrite(setPin, LOW); // запуск режима AT команд
  delay(40); // задержка перед отправкой AT команд
  Serial2.print(F("AT")); // отправка запроса на проверку подключения
  delay(serialDelay); // задержка для обмена по Serial
  if (Serial2.readString().indexOf("OK") != -1) { // проверка наличия OK в ответе
    Serial.println("# RC OK"); // подключение удалось
    Serial2.print(F("AT+RX")); // запрос конфигурации
    delay(serialDelay); // задержка для обмена по Serial
    Serial.println(Serial2.readString()); // вывод конфигурации
  } else {
    Serial.println("# TRY CFG"); // походу подключен новый модуль, попытка настроить на 9600
    Serial2.end(); // закрытие порта
    Serial2.begin(9600); // поднятие порта на стандартнх 9600
    Serial2.print("AT+B115200\r"); // отправка конфигурации - скорость порта
    delay(serialDelay); // задержка для обмена по Serial
    Serial.println(Serial2.readString()); // вывод результата (оказывается, если не прочитать результат то следующие команды игнорируются)
    Serial2.print("AT+FU1\r"); // отправка конфигурации - режим
    delay(serialDelay); // задержка для обмена по Serial
    Serial.println(Serial2.readString()); // вывод результата (оказывается, если не прочитать результат то следующие команды игнорируются)
    Serial2.print("AT+P1\r"); // отправка конфигурации - мощность
    delay(serialDelay); // задержка для обмена по Serial
    Serial.println(Serial2.readString()); // вывод результата (оказывается, если не прочитать результат то следующие команды игнорируются, хотя тут неважно)
    digitalWrite(setPin, HIGH);  // выход из режима AT команд
    delay(80); // задержка для применения настроек модулем
    digitalWrite(setPin, LOW); // запуск режима AT команд
    delay(40); // задержка перед отправкой AT команд
    Serial2.flush(); // очистка буфера порта (там результат применения конфигов, возможно прочий мусор)
    Serial2.end(); // закрытие порта
    Serial2.begin(115200); // поднятие порта на вожделенных 115200
    Serial2.print(F("AT")); // отправка запроса на проверку подключения
    delay(serialDelay); // задержка для обмена по Serial
    if (Serial2.readString().indexOf("OK") != -1) { // проверка наличия OK в ответе
      Serial.println("# CFG OK"); // подключение после настройки удалось
      Serial2.print(F("AT+RX")); // запрос конфигурации
      delay(serialDelay); // задержка для обмена по Serial
      Serial.println(Serial2.readString()); // вывод конфигурации
    } else Serial.println("# CFG ERR"); // попытка настройки не удалась - проверяем провода, подаем питание закоротив SET на GND (отключив от Arduino!!!)
  }
  // Serial2.print(F("AT+DEFAULT")); delay(40); Serial.println(Serial2.readString()); // сброс модуля на заводские (тест на уже настроенном модуле)
  digitalWrite(setPin, HIGH);  // выход из режима AT команд
  delay(80); // задержка для применения настроек модулем (правда здесь они уже не менялись)

знаю что наверняка говнокод, и я не могу обосновать serialDelay = 30, просто ее хватает. Но если кто мучается с настройкой этих модулей, пара нюансов в комментариях имеется

Кстати, почему на +/- длинных простынях кода не работает подсветка синтаксиса?

Это фича форума. Так звёзды сошлись)

Постой-ка… модули hc-12 ? Они ж пакетную связь реализуют. Я почему-то думал что они просто радиоудлинители UART’а. Тогда вообще всё с ног на голову.

У меня цель сделать максимально дешевого гексапода с 3-dof лапами, чтобы все в нем было спроектировано почти снуля и достаточно просто для освоения новичком. Он должен по командам с пульта ходить (мультипликация поступательного движения по вектору и вращательного вокруг вертикальной оси) и по отдельным командам воспроизводить заранее прописанные анимационные движения. Также калибровка крайних положений серво тоже через пульт. Потом заделаю подробное описание проектика и любой желающий сможет повторить, внеся правки при желании, а заодно посмотреть изнутри некоторые вещи, пусть и упрощенные донельзя. Я по универу давно подобное делал, и оно даже ходило как новорожденный олененок. Но с пультом от RC вертолета, и технические возможности были совсем не те, что теперь (да и финансовые… клон sg90 стыдно вспомнить)

Если интересно, на данный момент готово:

Спойлер
  • выбраны железки робота (серво TowerPro mg90d оригинал дороговато, но все дешевле откровенно говно, mega 2560 pro мелкая в качестве мозгов робота, связь по HC-12, питание 2х18650, стаб напряжения скорее всего XL4016, а если повезет и его хватит то более компактный 4015)
  • выбраны железки пульта (pro micro, два аналоговых стика, iic oled экран сразу с энкодером и двумя кнопками для скролла по меню, HC-12, питание посмотрю, вдруг 1х18650 хватит)
  • готовы 3d модели робота, прям сейчас печатаются, относительно терпимы к некачественной печати (скомпоновал так, чтобы основная нагрузка шла на разрыв слоев, либо между слоев, но площадь спекания большая)
  • класс для масштабирования по времени (легкотня, вычисляет в начале loop долю секунды выполнения предыдущей итерации, на нее вся кинематика будет перемножаться, например параметры сплайнов)
  • класс для подключения серво, в нем же механические ограничения углов, проверка передаваемого угла, хранение калибровок в EEPROM, позволяет инверсию направления вращения (калибровочные значения просто местами меняются)
  • класс для одной ноги, т.е. 3 серво, адрес в EEPROM для их калибровок, длины плеч, метод вычисления углов серво по переданным координатам цели для кончика лапы в локальных координатах с проверкой что все серво останутся в допустимых механических ограничениях
  • вычисления парамемтрических спланов 4й степени (по 5ти точкам) для не прямолинейных движений и анимаций
    Осталось вот с пультом разбираться, чтобы откалиброваться нормально и можно тестить конечности (повторюсь, в OpenSCAD работает, тут тоже будет). А потом алгоритмы перестановки по 2 за раз и по 3 за раз, анимации…

Самое главное забыл: в контексте этой задачи, данные с пульта будут транслироваться с завидной регулярностью, и потеря 2 из 5 пакетов не сильно скажется на работе ползуна, т.к. он просто продолжит повторять текущую команду с предыдущими векторами, и очень скоро получит новые, а в само управление будет заложена небольшая интегральная составляющая чтобы не было слишком уж резкой смены курса. А нажатия на кнопки буду фиксировать на приеме и игнорировать какое-то время их повторы.
Как передавать меню еще подумаю - скорее всего кеширование в пульте при установлении связи по запросу пульта, чтобы энкодером быстрее пробегать по нему

А что это в итоге значит? С топика код таки подойдет? Я сколько ни читал в сети, они как раз описывались как uart свистки, даташит, каюсь, дальше 4й страницы не смотрел

Сейчас проверил, просто в принимающую сторону delay(300) добавил и таки да, он не успевает опрашивать очередь, растущий импутлаг и переполнение… Но т.к. это двусторонняя связь, можно не передавать по таймауту с пульта, а чтобы пульт ждал запроса на новые команды от робота и только потом передавал

Именно, связь пакетная. В корне меняется подход к организации. Никакая проверка данных не нужна- в модуле это всё есть на железном уровне. Всякие подтверждения приёма тоже должны быть.
Боюсь соврать, но в nrf24, как и в других модулях пакетной передачи +/- алгоритм схож:

  1. Записвваем пакетик данных в модуль
  2. Ждём подтверждения приёма(ack, если включили)
  3. Вместе с подтверждением читаем из модуля ответ от приёмника( ack payload, если включили)

В своё время выбрал nrf24 из-за шины spi, она быстрей и проще было под неё написать библиотечку.
Вот для примера первые попавшиеся труды:

1 лайк

Нормальные побайтовое чтение/анализ никогда не вредили. Сегодня за канал железо отвечает, завтра не отвечает… А алгоритм правильной приемопередачи уже реализован, только накидывай новому свистку в буфер.

А почему с управления начали?