Серводвигатель с линейным изменением занимает больше времени, чем рассчитано

Я учусь программировать "бот BOE Shield" с партнером в моем университете (первый курс). Поясняю для тех, кто не знает: бот BOE Shield — это небольшой робот, оснащенный 2 серводвигателями, аккумулятором и Arduino Uno. Если вы погуглите название, то найдете много картинок.

Нам было поручено написать программу, которая заставляла бы робота ускорять вращающиеся серводвигатели непрерывного действия в соответствии с заданным графиком зависимости скорости от времени в об/мин и секундах. Мы написали код, следовали довольно логическому методу и сдали задание. Однако одна вещь, и я потратил дни, пытаясь это исправить, все еще сбивает меня с толку. Код впереди, я постарался сделать его максимально читабельным. Преобразование RPM в PWM для двигателей:
PWM = 2 \x RPM + 1500

#include <Servo.h>

class motor {
public:
  double Speed = 0;  //Реальная скорость сервопривода в об/мин
public:
  Servo servo;

  //возвращает true, если нужная скорость "newSpeed" достигается, в противном случае увеличивается текущая скорость и возвращается false.
  //инкремент - расчетное увеличение скорости, основанное на разнице во времени (интервале) и постоянном ускорении.
  bool accel(double increment, double newSpeed) {
    if (abs(Speed - newSpeed) < 0.1) {
      return true;
    }

    Speed += increment;
    return false;
  }

  void writeMS(double value) {
    servo.writeMicroseconds(value);
  }
};

motor servoLeft = motor();
motor servoRight = motor();

void setup() {
  Serial.begin(9600);
  servoLeft.servo.attach(11);   //венстер -2 * скорость влево + 1500
  servoRight.servo.attach(10);  //höger 2 * скорость влево + 1500
}

//Этот вложенный массив содержит инструкции для обоих серводвигателей: желаемую скорость, сколько времени нужно, чтобы разогнаться до нее и сколько времени
//длительность инструкции. Первая инструкция начинается со второй строки. Первая строка пропускается и используется только для расчетов.
//Столбцы расположены в следующем порядке: {длина инструкции в мс, скорость левого двигателя в об/мин, скорость правого двигателя в об/мин, время разгона}.
double instructions[7][4] = {
  { 0, 0, 0, 0 },
  { 3000, 25, 25, 500 },
  { 2550, 50, 10, 500 },
  { 3000, 25, 25, 500 },
  { 2850, 10, 50, 500 },
  { 3000, 25, 25, 500 },
  { 3000, 15, -15, 500 },
};

//время
unsigned long currentMillis;
unsigned long previousMillis = 0;
unsigned long lastInstructionMillis = 0;
const double interval = 20;

//i - это индекс инструкций.
int i = 0;
bool updateL = false;
bool updateR = false;

//увеличение скорости для каждого сервопривода. Рассчитывается в цикле.
double incrementL;
double incrementR;

void loop() {
  // Это помогает отслеживать время, чтобы мы могли вычислять приращения. По сути, это означает, что скорости обновляются каждые 20 мс.
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    //если нужная скорость достигнута, прекращаем обновление. Это просто для оптимизации кода.
    if (updateL)
      updateL = !servoLeft.accel(incrementL, instructions[i][1]);

    if (updateR)
      updateR = !servoRight.accel(incrementR, instructions[i][2]);
  }

  // это выполняется только один раз после каждой инструкции.
  if (currentMillis - lastInstructionMillis >= instructions[i][0]) {
    lastInstructionMillis += instructions[i][0];
    i++;

    // выходим из программы после выполнения всех инструкций.
    if (i == 7)
      exit(0);

    // приращения рассчитываются так: приращение = ускорение * интервал
    //где постоянное ускорение = (newSpeed - oldSpeed) / AccelerationTime
    incrementL = (instructions[i][1] - instructions[i - 1][1]) * interval / instructions[i][3];
    incrementR = (instructions[i][2] - instructions[i - 1][2]) * interval / instructions[i][3];

    updateL = true;
    updateR = true;
  }

  Serial.println((String)servoLeft.Speed + " " + (String)servoRight.Speed + " " + (String)currentMillis + " " + (String)i);

  // фактические сигналы мс, отправленные на сервоприводы.
  servoLeft.writeMS(2 * servoLeft.Speed + 1500);
  servoRight.writeMS(-2 * servoRight.Speed + 1500); 
}

По какой-то причине разгон идет дольше, чем указано в инструкции. Например, первая инструкция указывает, что оба сервопривода будут разгоняться с 0 до 25 об/мин в течение 500 мс. Но распечатка скорости и времени на последовательном мониторе показывает, что на самом деле это занимает около 860 мс. То есть, если интервал установлен на 20. Если он установлен на 50, это занимает 560 мс.

Предполагаемые причины:

  1. Пустой цикл занимает больше времени, чем интервал

Это не так, так как измерение с помощью micros() показало, что это никогда не занимало больше 0,26 мс; это было сделано и с полной программой.

  1. Ошибки округления

Это не тот случай, поскольку переключение практически всех типов чисел на double ничего не исправило. И double более чем достаточно точен для этой программы.

  1. millis() недостаточно точен

Я не верю, что это так. Я исследовал, насколько он точен на самом деле, и хотя он далеко не идеален, он должен быть достаточно точным для программы. Вместо этого я попытался использовать micros(), и это привело к тому же результату, я не знаю, означает ли это что-то.

Кроме этого, я понятия не имею, в чем может быть причина. Сам код работает нормально, и кажется, что робот действительно следует инструкциям без ошибок. Просто скорость и ускорение почему-то неточны.

, 👍2

Обсуждение

Вы видели это? https://forum.arduino.cc/t/use-of-exit-0/210352, @VE7JRO

Вы уверены, что правильно управляете двигателями и что они не буксуют?, @Gil

Из ссылки Arduino на double: «На Uno и других платах на базе ATMEGA это занимает 4 байта. То есть реализация double точно такая же, как у float, без увеличения точности»., @chrisl

Когда вы вызываете метод accel() вашего класса, вы предоставляете сначала второй и третий элементы массива, описывающие инструкцию. Это целевые скорости согласно вашему комментарию. Затем в качестве второго параметра вы указываете incrementL или incrementR. Итак, сначала целевая скорость, а затем увеличение. Но в определении функции все наоборот: сначала приращение, затем новая скорость. Пожалуйста, попробуйте переключить параметры либо в определении функции, либо в вызове функции. Тогда он работает корректно?, @chrisl

@ VE7JRO Я не думаю, что проблема в этом. Хотя я знаю, насколько «запретным» является выход, он используется только в конце программы. Ни на что другое это не влияет., @ShootinLemons

@ Гил, хорошо видишь, значения на самом деле не зависят от двигателей. Другими словами, когда я запускаю программу с выключенными сервоприводами, она все еще работает. Переменная скорости в классе двигателя не считывает фактическую скорость сервоприводов. Он просто содержит рассчитанное значение, которое мы затем используем для записи в сервоприводы. Так что проблема вовсе не в сервоприводах., @ShootinLemons

@chrisl Разве этого недостаточно для двойного? Я напечатал приращения на последовательном мониторе, и они были правильными, поэтому я не думаю, что это проблема. Скорости увеличиваются правильно, единственная проблема в том, что это занимает больше времени, чем планировалось. Что касается вашего второго комментария, извините, это просто опечатка. Я отредактирую вопрос, чтобы он был правильным. Я переписал фрагменты кода и очистил его для вопроса, чтобы он был более читабельным. Я думаю, что я по ошибке поменял значения. Это все равно не было бы проблемой, потому что робот вообще бы не работал., @ShootinLemons

Да, точности с плавающей запятой должно быть достаточно. Вы уверены в продолжительности цикла ()? В настоящее время вы в значительной степени спамите последовательный интерфейс. При скорости всего 9600 бод это займет некоторое время (поскольку priting ждет, пока в буфере не освободится достаточно места), @chrisl


1 ответ


Лучший ответ:

2

Здесь происходят две вещи.

Вы слишком много печатаете через последовательный порт. После первой секунды (между 1000 и 9999 мс), каждая печатаемая строка занимает 20 байт, включая терминатор CRLF. При скорости 9600 бит/с каждый байт занимает 1,04 мс (1 стартовый бит, 8 битов данных и 1 стоповый бит). Это около 20,8 мс для всей линии, что немного больше, чем interval. Вы должны либо печатать меньше часто (скажем, каждую вторую итерацию цикла) или быстрее. Просто переход к следующей стандартной скорости (19200 бит/с) будет достаточно, чтобы вы не пропустили любой интервал. Однако вы можете ехать быстрее.

Вторая проблема заключается в том, как вы отслеживаете previousMillis:

if (currentMillis - previousMillis >= interval) {
  previousMillis = currentMillis;
  // ...
}

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

previousMillis += interval;

У вас по-прежнему будут ошибки синхронизации, но они проявятся в виде джиттера, а не систематического отклонения синхронизации.

Интересно, вы правильно обновляете lastInstructionMillis!


ОБНОВЛЕНИЕ: Вот несколько дополнительных комментариев по вашему вопросу и вашему скетч, не обязательно связанный с проблемой, которую вы видите.

Вы писали:

измерение [loop()] с помощью micros() показало, что это никогда не занимало больше времени, чем 0,26 мс.

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

Следующий код ненадежен:

bool accel(double increment, double newSpeed) {
  if (abs(Speed - newSpeed) < 0.1) {
    return true;
  }
  // ...
}

Значение 0,1 является произвольным, и «правильный выбор» зависит от программный тайминг. Я предлагаю что-то более надежное, например:

bool accel(double increment, double newSpeed) {
  if (increment > 0) {
    Speed = min(Speed + increment, newSpeed);
  } else {
    Speed = max(Speed + increment, newSpeed);
  }
  return Speed == newSpeed;
}

Обратите внимание, что, хотя обычно рекомендуется не тестировать точечных чисел для точного равенства, в данном случае это безопасно, т.к. min() и max() возвращают точно один из своих аргументов.

Оптимизация, заключающаяся в вызове motor::accel() только во время фаза ускорения бесполезна: этот метод занимает очень маленькую долю времени loop(). В том же духе не будет никакого вреда в вызов Servo::writeMicroseconds() на каждой итерации цикла, даже если некоторые звонки оказываются бесполезными.

Код был бы более читабельным, если бы instructions был массивом struct, а не массив массивов: таким образом поля каждого инструкции могут иметь выразительные имена, а не числовые индексы.

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

,

У меня сейчас нет доступа к роботу, он у моего напарника. Но я очень рад попробовать это решение. Я думаю, что вы, возможно, нашли проблему. @chrisl уже намекнул на рассылку спама через последовательный интерфейс с короткой скоростью. Все ваши другие предложения также оценены и проницательны. Я постараюсь заполучить робота как можно быстрее. А пока я приму ваш ответ, так как уверен, что вы его решили. Если нет, я вернусь к вам. Сейчас конец семестра, поэтому я не смогу получить доступ к роботу, и я не хочу, чтобы вопрос оставался без ответа., @ShootinLemons