Неравномерное ускорение шагового двигателя/выполнение кода

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

У меня есть часть кода, отвечающая за ускорение. Она запускается в основном цикле так часто, как это возможно. Ускорение задается в шагах/с/с, а скорость в шагах/с. Вот метод, о котором идет речь:

void StepperMotor::accelerate() {
  speed = start_accelerating_speed + (micros() - startAccelTime) / 1000000.0 * acceleration;
  step_time = 1000000 / speed;
}

start_accelerating_speed устанавливается в micros() при запуске двигателя. Допустим, acceleration = 200.

step_time используется кодом в основном цикле для проверки того, пришло ли время сделать шаг.

Код, кажется, работает нормально, однако, при выполнении, двигатель начинает ускоряться, как и ожидалось, затем быстро увеличивает скорость, скажем, на 10% от целевой скорости и продолжает плавно ускоряться в течение некоторого времени. Эта модель повторяется. Скачки можно услышать только при изменении скорости двигателя или звуке зуммера (я использую зуммер вместо двигателя для простоты отладки). Скорость печати на последовательном порту полностью убивает производительность, заставляя код ждать последовательного порта.

Я использовал этот код для измерения времени цикла без добавления значительных накладных расходов Serial.println().

// Выполняется в основном цикле, циклы и step_time определены ранее.
loops++;
if (loops >= 10000) {
  Serial.println(micros()-loop_time);
  loop_time = micros();
  loops = 0;
}

Вывод: цикл выполняется примерно за 56 микросекунд, менее 70, когда двигатель работает и ускоряется. Запуск шагового двигателя в этой настройке со скоростью 1000 шагов в секунду требует мин. времени цикла 1000 микросекунд (или 1 миллисекунду). Код должен работать гладко.

Я запустил код на чистом C++ и построил графики результатов в Matlab. Результат — линейное ускорение.

Я попробовал замедлить основной цикл до 50 мс на цикл и соответственно замедлить двигатель и ускорение, чтобы также вывести скорости на последовательный порт; тогда все работает нормально.

Ни одна форма delay() никогда не используется в коде.

Протестировано с ATmega 328 и ATmega 2560 с одинаковыми результатами.

Прошел уже месяц, и я не могу найти в интернете похожей проблемы.

Кто-нибудь знает, что здесь происходит?

P.S. Я знаю, что линейное ускорение не является оптимальным, но сначала я хотел бы решить текущую проблему.

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

main.cpp

#include <Arduino.h>
#include <StepperMotor.h>

StepperMotor motor_1(3, 2, 4);

void setup(void) {
  Serial.begin(115200);
  motor_1.start();
}

void loop(void) {   
  motor_1.update();
}

ШаговыйДвигатель.cpp


// Constructor
StepperMotor::StepperMotor(uint8_t step_pin_, uint8_t enable_pin_, uint8_t direction_pin_):
step_pin(step_pin_), enable_pin(enable_pin_), direction_pin(direction_pin_) {
  // Initiating pinouts
  pinMode(step_pin, OUTPUT);
  pinMode(enable_pin, OUTPUT);
  pinMode(direction_pin, OUTPUT);

  // Setting pin states
  digitalWrite(step_pin, LOW);
  digitalWrite(enable_pin, HIGH);
  digitalWrite(direction_pin, LOW);
}

// Called while accelerating. Recalculate speed and check if target speed has been reached.
void StepperMotor::accelerate() {
  // Using start time - calculate current speed and step time.
  speed = start_accelerating_speed + (micros() - startAccelTime) / 1000000.0 * acceleration;
  step_time = 1000000 / speed;

  // Stop accelerating if target speed is reached
  if (speed >= target_speed) {
    accelerating = false;
  }
  // Serial.println(speed);
}

// Realising basic steps needed to start motor and prepare variables.
void StepperMotor::startMotion() {
  Serial.println("Start"); // Debug

  // Prepare variables for step control
  prev_step_time = time;
  speed = 0;
  step_time = 0;

  // Prepare acceleration
  startAccelTime = time;
  start_accelerating_speed = 0;
  accelerating = true; // Enable acceleration
}

// Overloaded methods for starting a motor.
// Start in the same direstion and speed as before.
void StepperMotor::start() {
  startMotion();
}

void StepperMotor::update() { // Main control method. Executed in loop by Stepper.cpp
  time = micros();

  if (time-prev_step_time >= step_time) { // Is it time to step?
    step_pin_state = !step_pin_state;    // Alter pin variable
    digitalWrite(step_pin, step_pin_state);    // Write pin state to pinout

    prev_step_time = time;
  }

  if (accelerating) { // If target speed not reached: accelerate
    accelerate();
  }
}

Шаговый двигатель.h

// StepperMotor.h

#ifndef STEPPERMOTOR_H
#define STEPPERMOTOR_H

#include <Arduino.h>

class StepperMotor {
public:
  // Constructor
  StepperMotor(uint8_t step_pin_, uint8_t enable_pin_, uint8_t direction_pin_);

  // Methods
  void start();

  // Main loop method called by StepperController
  void update();

private:
  // Pins
  uint8_t step_pin;      // Change of state = 1 step.
  uint8_t enable_pin;    // 0 = Disabled  1 = Enabled
  uint8_t direction_pin; // 0 = CW        1 = CCW
    
  double target_speed = 1000; // In steps per second

  // Timers
  unsigned long time;
  unsigned long startAccelTime;
  unsigned long startAccelTimeMillis;
  unsigned long step_time; // Indicates time between steps in microseconds
  unsigned long prev_step_time;
  unsigned long prev_accel_time;       // Usually the same as prev_stepTime //change comment

  double speed;        // Indicates the motor's speed in steps per second
  bool step_pin_state; // Indicates current state of step pin. FALSE = LOW; TRUE = HIGH

  bool direction;      // Indicates the motor's rotation direction
  bool moving;         // Indicates if stepper is moving or not. Do not confuse with enable - enable can be used to hold motor in place.

  void startMotion();
  void accelerate();

  bool accelerating = false;
  double acceleration = 200; // In steps per second^2
  double start_accelerating_speed;
};

#endif // STEPPERMOTOR_H

, 👍1

Обсуждение

Пожалуйста, включите в свой вопрос полный компилируемый код. Проблема может быть в остальной части кода, а не в маленьком фрагменте, который вы показали., @chrisl

Спасибо за ответ. Этот фрагмент является частью довольно большой библиотеки. Хотя я думаю, что большая часть, если не весь код класса, не влияет на проблему, я отредактировал исходный пост с полным исходным кодом класса. Надеюсь, его можно будет легко запустить без остальной части библиотеки. Также обратите внимание, что я разрабатываю код с помощью platformio на VSCode, поэтому включена библиотека <Arduino>., @Wojciech Kosela

В этой библиотеке происходит довольно много всего. Некоторые предложения: 1. Удалите все, кроме основного алгоритма, который вы хотите протестировать, чтобы получить минимальный проверяемый пример. Если проблема все еще видна, обновите вопрос и замените свою библиотеку этим примером. 2. Инструментируйте loop() для измерения _максимального_ (не _среднего_) времени выполнения цикла. 3. Вызывайте micros() только один раз в update() и не вызывайте его снова в accelerate(): для согласованности вы хотите, чтобы все вычисления основывались на одном и том же понятии «текущего времени обновления»., @Edgar Bonet

Спасибо за предложения @EdgarBonet. С этого момента я буду устанавливать микроконтроллеры только один раз в цикле, как хорошую практику. Я измерил максимальное время цикла. Оно составляет максимум ~340 мкс (когда двигатель разгоняется). Немного больше, чем мне бы хотелось, но, вероятно, это происходит только несколько раз за этот период времени, поскольку среднее значение было намного ниже. Тем не менее, это меньше, чем максимальное значение в 1000 мкс, необходимое для проверки скорости 1000 шагов/с. Я заменю код урезанной версией, если/когда я его создам. На данный момент проблема сохраняется, и мне еще нужно кое-что попробовать в первую очередь., @Wojciech Kosela


2 ответа


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

2

Я проверил этот скетч, глядя на ступенчатый вывод на осциллографе. Что Я вижу прямоугольный сигнал с периодом, который сначала плавно уменьшается, затем все более и более дерганым образом, с видимыми скачками.

Оказывается, это происходит из-за времени, затрачиваемого на итерации loop(). На моих тестах это занимает около 85 мкс. Кажется, что это не так уж много, но это так оказывает значительное влияние на время, поскольку период сигнала имеет быть кратным периоду выполнения loop(). Когда сигнал период большой, это ограничение не вызывает никакого видимого эффекта. Когда период сигнала короткий, однако тот факт, что он уменьшается на Целые кратные периода loop() делают видимые скачки. Последнее скачок - это изменение примерно на 8,5% (85 мкс / 1 мс).

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

prev_step_time = time;

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

prev_step_time += step_time;  // увеличение на период шага

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


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

if (пора сделать шаг):
    сделай шаг
    вычислить следующий шаг периода

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

step_period ← 1 / speed(current_step_time + step_period / 2)

Я попробовал эту стратегию, и, согласно моему прицелу, развертка частоты составляет действительно более плавно. Вот мой тестовый код. Обратите внимание, что я изменил некоторые обозначения:

// Возвращает скорость (в шагах/с) в момент времени t (в мкс).
// Реализация пропущена.
double speed(unsigned long t);

void update()
{
    // Ничего не делать, пока не придет время выполнить шаг.
    if (micros() - prev_step_time < step_period)
        return;

    // Выполняем шаг.
    step_pin_state = !step_pin_state;
    digitalWrite(step_pin, step_pin_state);

    // Обновляем временные переменные.
    prev_step_time += step_period;
    step_period = 1e6 / speed(prev_step_time + step_period/2);
}

Обратите внимание, что теперь большинство итераций loop() не будут делать ничего, кроме оценить

if (micros() - prev_step_time < step_period) return;

что делает цикл очень быстрым и улучшает плавность ускорение.

Однако в этом подходе есть одно предостережение: начало движения очень чувствителен к начальному значению step_period. Я не нашел лучшая идея, чем инициализировать его точным значением для первого шага:

step_period = 1e6 * sqrt(2/acceleration);

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

,

Абсолютно блестяще :), @VE7JRO

Ваш ответ, кажется, попал в точку. Осциллограф, должно быть, немного помог. Честно говоря, я думал, что кратность времени цикла является ограничивающим фактором, но я не думал, что это будет иметь такое значение. Ваш ответ помог мне настолько, насколько это было возможно. Однако я вижу, что проблема сохраняется до некоторой степени. Я думаю, что пока все в порядке, но предполагаю, что мне нужно будет получить что-то с более быстрыми тактовыми частотами, чтобы добиться более плавной работы. Учитывая все обстоятельства, я проверил библиотеку AccelStepper, и она не поддерживает скорости больше 1000 — так что, похоже, тактовые частоты являются пределом., @Wojciech Kosela

@WojciechKosela: см. измененный ответ., @Edgar Bonet

@EdgarBonet спасибо, это очень интересно. Вы представили мне новый подход к проблеме. Я продолжу свой путь по этой теме и опубликую любые новые идеи, если найду их в будущем. Надеюсь, это поможет другим., @Wojciech Kosela


0

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

Я заметил большие скачки скорости при ускорении. Теперь я знаю, почему.

Время между шагами меняется медленно с увеличением скорости. Поскольку измерения времени были ограничены временем цикла, то единственными возможными значениями времени были: n * время цикла. Имея это в виду, скачки, которые я испытывал, были вызваны выполнением шага каждые n-1 циклов (предполагая, что ранее он выполнялся каждые n циклов). Поскольку мой цикл занимал около 80 мкс, это было очень заметно и заставило меня думать, что ограниченная частота цикла не может быть единственной причиной этого. Может.

Для тех, у кого та же проблема, что и у меня. У меня есть НАСТОЯТЕЛЬНЫЙ СОВЕТ. Используйте таймер/счетчик Arduino. Я так и сделал. Он разработан для этой цели, и это правильный способ. Не знаете, как использовать таймеры? Изучите его. Даже если вы думаете, что это излишество для того, что вы пытаетесь сделать. Во-первых: вы не сможете разучиться этому, так что вы не уйдете с пустыми руками, несмотря ни на что. Во-вторых: если вы попытаетесь внедрить в этот код что-то еще, например, последовательную связь - это нарушит тайминги, и вы никогда не будете иметь с ними мира. Таймеры (прерывания) устраняют все эти проблемы.

И наконец, несколько важных замечаний о прерываниях таймера. Используйте осторожно. Некоторые таймеры используются Arduino для таких вещей, как millis() / micros(). Все таймеры используются для выходов ШИМ. Выбирайте мудро и знайте, что вы отключаете, используя данный таймер. Также прочтите некоторые «нельзя» делать внутри прерываний таймера. В основном избегайте всего, что само использует прерывания, например digitalWrite(), Serial.print(), delay(). Для выводов выводов вам придется использовать манипуляцию портами.

Это много поводов для беспокойства, но если вы читаете эту тему - вы, возможно, хотите сгенерировать какой-то точный сигнал с помощью Arduino. Если это так - вам лучше попробовать использовать таймеры/счетчики, это, вероятно, займет у вас меньше времени, чем устранение неполадок (и понимание того, что вам в любом случае придется использовать прерывания) обычных таймеров.

,

Это хороший совет: таймеры немного сложны, но могут определенно дать вам точные тайминги. Некоторые замечания: 1. Лучшие тайминги достигаются в режиме ШИМ, путем обновления регистра сравнения в ISR прерывания таймера. 2. digitalWrite() не полагается на прерывания. Причина, по которой вы можете предпочесть прямую манипуляцию портами, заключается в том, что это намного быстрее., @Edgar Bonet

@EdgarBonet Вместо прямого доступа к порту я предпочитаю [библиотеку "digitalWriteFast" от Watterot/Armin Joachimsmeyer](https://github.com/ArminJo/digitalWriteFast), которая компилируется в отдельные инструкции, конечно, только с постоянными параметрами времени компиляции. (Отказ от ответственности: я знаю, как использовать SFR. Но так гораздо более интуитивно понятно и удобно для новичков.), @the busybee

@EdgarBonet, возможно, digitalWrite() не полагается на прерывания. Кроме того, насколько мне известно, на запись уходит около 4 мкс, тогда как манипуляция портами занимает один или несколько тактов (~0,1 мкс). Тем не менее, я столкнулся с катастрофическими событиями при обновлении выводов с помощью digitalWrite() во время ISR. Что бы ни происходило, это приводило к повреждению памяти и последующему сбою контроллера несколькими строками кода позже. Отклонение от digitalWrite() к манипуляции портами решило проблему, хотя, возможно, причина была иной, чем я помню., @Wojciech Kosela