gin66 / FastAccelStepper

A high speed stepper library for Atmega 168/328p (nano), Atmega32u4, Atmega 2560, ESP32, ESP32S2, ESP32S3, ESP32C3 and Atmel SAM Due

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Driving steppers by animation

kthod861 opened this issue · comments

Hello,

[ Don't hesitate to remove this if it's too dumb of a question ]

I'm almost 100% successful using your lib to drive 5 steppers from an ESP32 board.
But i'm wondering if i'm using this lib the right way in my situation... any guidance is welcome.

So, every 1/30 sec i give to all five steppers a new set of frequency and MoveTo position ( that are precalculated to fit the needs ).
This work quit correctly but still i have two issues i don't really know how to deal.

  • I have to set the acceleration to 1000000 as it seems the only way to get a linear motion but i'm really not confortable with this as i've no clue exactly how it behave ( and probably cost some process time for no reason).

  • I found it difficult to ensure smooth motion between frames (when the input values come from a smooth animation) as the MoveTo will have tendency to stop (yes i know it's a normal behavior ;) ).

So i can totally live with the current results i have but i can't stop thinking i may have choosen the wrong path here from the begining...

Again, don't hesitate to close/erase this if not clear or too dumb. i can share more details if needed but i think the above description should be enough.

Regards

commented

Hi,

happy to hear, that you can successfully drive five steppers with esp32. Still one to go. From your description it is not clear, what your application really is. Perhaps a short video would help to understand it.

So i can only comment on the provided information as I interpret your application from it:

  • Apparently you need a coordinated movement of five steppers along a path given in equidistant points in time with time steps of 1/30s. For each path point you should be able to provide position and speed. (Or in other words: provide the position, but the stepper should arrive at that position with the appropriate speed for running to the next position.) If you only work with moveTo(), you lack the speed. If you focus on the speed and calculate the moveTo() value accordingly, then at the next path point you may have deviation from the target position.
  • You are using an acceleration value of 1000000. This means the steppers are more or less instantly jumping to the next speed. So no acceleration. The funny thing is, if you use acceleration, then you will have a really hard time to provide moveTo() commands, which ensure arrival at the right time and speed at the next path point.
  • The API does not allow it to define a series of path points, the stepper has to follow with position (+speed ?). This would require a motion planner not available yet. Would be for sure a nice feature to be added, but pretty complex to be implemented.
  • An alternative would be to use moveByAcceleration() and correct over/undershooting on the fly.

As I have written in the README, the recommendation is - especially because you use a performant esp32 - to switch to raw commands using addQueueEntry(). If acceleration/deceleration is not needed, then the implementation is more or less straightforward. For each time step of 1/30s aka 33ms you need to create 16 commands for each stepper (every command should cover 1-2ms). One command then issue either no step (aka a pause of x timer ticks) or 1 step or many steps at defined frequency.

If acceleration/deceleration is needed, then the question is, if all five steppers should run synchronously aka the fastest stepper determine the acceleration profile for the other steppers. Or every stepper can use its own acceleration profile, but just need to ensure to be at the next path point at the right time and with the right speed. In any case, the implementation is much more complex.

Not sure, if this explanation is clear enough.

Hi,
Thanks for the fast answer!

Here's an already old stage of the project showing a little bit more what i'm trying to achieve:
https://www.youtube.com/watch?v=QUfriYirfP0

I'm currently providing position and speed for each path point and make sure i use the last known position as a reference for the next frame/move calculation ( this way i think i minimize the deviation/drift ).

And yes i don't need any accel/decel as it's provided by the incoming animation, i agree if i had to use it ,it would be a hell to manage ;)

So as you mentioned :

  • i'll definitly see what i can do with moveByAcceleration + corrections and see where it's going
  • or see the raw commands but i'm afraid i'm not able to understand how this work or at least have more [dumb] questions about it like:
    1-2 ms is the max lenght the queue can handle?
    16 commands for 5 motors ? is it not supposed to be step/ticks/count_up * 5 motors ( i need to check )?
    i don't think i've seen an example for the queue management, is there one ?

Again, thanks a lot for your support.

commented

Hi,

nice video. Now it gets more clear. Thank you.

For the raw commands, there are two examples: RawAccess and RawAccessWithPause

For your questions and additional explanation:
1-2 ms is the typical length of a command sent to the queue.
Each stepper has its own queue, which can keep 32 commands. So with 5 Steppers, there are 5 queues.
Maximum command length (duration) is 255*65535/16 Mhz= 1s, which issues 255 steps at a frequency of 16MHz/65535=244Hz. Anything lower than 244 Hz requires additional pause commands.

Hi,
Ok, will try to decypher those examples again :)...

I'm far from being comfortable with that kind of low level coding as i'm learning by myself all those subtleties...
For example, 3 weeks ago if you've asked me how struct works i couldn't have answered ;)..now i can ( enough so i can use them at least ).

i'll stop asking questions because i've too much of them like why ticks are set to 32768 if > 65535...what's the relationship between ticks and step etc etc etc... and will do some more unit testing.

Regards

So here is the unit test i'm trying to put in place :

  • i simulate an array of 30 frames to mimic a 360 degree rotation in 1 sec for 3200 steps
  • each frame i create 15 commands

Honestly, it....work....but i've no real clue why, if i double the frame array it won't do 2 turns, etc.

Pretty sure i'm missing something obvious here but i don't really see outside of Ticks where i may have totally misunderstood them or those delay that seems to have a big impact here.

void setup() {
  Serial.begin(115200);

  engine.init();
  stepper = engine.stepperConnectToPin(stepPinStepper);
  if (stepper) {
    stepper->setDirectionPin(dirPinStepper);
    stepper->setEnablePin(enablePinStepper);
    stepper->setAutoEnable(true);
    stepper->enableOutputs();
  }

  int lframes[] = {107,107,107,107,107,107,107,107,107,107,
                    107,107,107,107,107,107,107,107,107,107,
                    107,107,107,107,107,107,107,107,107,97};
// a representation of a 360 degree rot at 30 fps 
  int lenAnim = 30;
  
 for (uint16_t i = 0; i < lenAnim ; i++) {//for each frame

    int steps = lframes[i];
    int divider = 15;
    int m = 1;
  
    while (steps > 0 && divider > 0) {// divide and prep 15 step values
        
        ////a way to get 15 int values averaged..kind of
        uint8_t subvalue = floor(steps / divider / m) * m; // 
        steps -= subvalue;
        divider--;
        ///

        ////2880 is the freq needed for a 360 deg rotation in 1 sec using 3200 steps
        const struct stepper_command_s cmd_step = {
           .ticks = 2880, .steps = subvalue, .count_up = true};
        int rc = stepper->addQueueEntry(&cmd_step);
        
        delayMicroseconds(1000); // i believe this is to avoid overfiling the queue ?

    } 
    delayMicroseconds(3000);// i believe this is to avoid overfiling the queue ?
  }

}
commented

This compiles, but not sure, if this works. I have used the overall frame of your code and changed it accordingly.Hopefully it is good enough commented to be understandable:

#include "FastAccelStepper.h"

// for avr: either use pin 9 or 10 aka OC1A or OC1B
#define stepPinStepper 17
#define enablePinStepper 26
#define dirPinStepper 18

FastAccelStepperEngine engine = FastAccelStepperEngine();
FastAccelStepper *stepper;

void setup() {
  Serial.begin(115200);

  engine.init();
  stepper = engine.stepperConnectToPin(stepPinStepper);
  if (stepper) {
    stepper->setDirectionPin(dirPinStepper);
    stepper->setEnablePin(enablePinStepper);
    stepper->setAutoEnable(true);
    stepper->enableOutputs();
  }

  int lframes[] = {107, 107, 107, 107, 107, 107, 107, 107, 107, 107,
                   107, 107, 107, 107, 107, 107, 107, 107, 107, 107,
                   107, 107, 107, 107, 107, 107, 107, 107, 107, 97};
  // a representation of a 360 degree rot at 30 fps
  int lenAnim = 30;

#define TICKS_PER_FRAME (TICKS_PER_S / 30)
  for (uint16_t i = 0; i < lenAnim; i++) {  // for each frame

    int steps = lframes[i];

    // THIS WORKS ONLY, IF STEPS > 0
    uint32_t ticks_per_step = TICKS_PER_FRAME / steps;
    uint32_t this_frame_steps = 0;

    while (this_frame_steps < steps) {
      // repeat as long as not all steps for this frame created...

      if (ticks_per_step <= 65535) {  // does it fit into an uint16_t ?
        // Yes, so every command will generate at least one step
        uint8_t this_cmd_steps = 1;
        if (this_cmd_steps * ticks_per_step < TICKS_PER_S / 500) {
          // in order to have ~2ms long command, this command should issue
          // multiple steps
          this_cmd_steps = TICKS_PER_S / 500 / ticks_per_step;
        }
        struct stepper_command_s cmd_step = {.ticks = (uint16_t)ticks_per_step,
                                             .steps = this_cmd_steps,
                                             .count_up = true};

        // add command to the queue
        int rc;
        do {
          rc = stepper->addQueueEntry(&cmd_step);
          if (rc > 0) {
            // so the queue is busy => put the task to sleep for
            // portTICK_PERIOD_MS
            vTaskDelay(1);
          }
        } while (rc > 0);  // repeat addQueueEntry, if queue is busy

        // sum up the generated steps
        this_frame_steps += this_cmd_steps;
      } else {
        // For one step, one command with a step + one or several pauses are
        // needed

        // let's remember how many ticks still to be generated
        uint32_t remaining_ticks = ticks_per_step;

        // Let's first do the step
        struct stepper_command_s cmd_step = {
            .ticks = 16384, .steps = 1, .count_up = true};

        // add command to the queue
        int rc;
        do {
          rc = stepper->addQueueEntry(&cmd_step);
          if (rc > 0) {
            // so the queue is busy => put the task to sleep for
            // portTICK_PERIOD_MS
            vTaskDelay(1);
          }
        } while (rc > 0);  // repeat addQueueEntry, if queue is busy

        // sum up this one step
        this_frame_steps += 1;

        // and reduce the remaining ticks
        remaining_ticks -= 16384;

        // Now create the pauses until remaining_ticks is 0
        while (remaining_ticks > 0) {
          uint16_t this_cmd_ticks;

          // do remaining ticks fit into an uint16_t ?
          if (remaining_ticks > 65535) {
            // nope, so make a pause of 32768 ticks
            this_cmd_ticks = 32768;
          } else {
            // yeah, fits. So use that value
            this_cmd_ticks = (uint16_t)remaining_ticks;
          }

          // make a pause command
          struct stepper_command_s cmd_pause = {
              .ticks = this_cmd_ticks, .steps = 0, .count_up = true};

          // and add it to the queue
          int rc;
          do {
            rc = stepper->addQueueEntry(&cmd_pause);
            if (rc > 0) {
              // so the queue is busy => put the task to sleep for
              // portTICK_PERIOD_MS
              vTaskDelay(1);
            }
          } while (rc > 0);  // repeat addQueueEntry, if queue is busy

          // reduce the remaining ticks
          remaining_ticks -= this_cmd_ticks;
        }
      }
    }
  }
}

void loop() {}
commented

This multiplication may be problematic:this_cmd_steps * ticks_per_step
Eventually need to be (uint32_t)this_cmd_steps * ticks_per_step

Thanks a lot,

Tried it (with the patch) and it work but seems to output more than 3200 steps, if i trigger it multiple time i can see it drifting.
Now i need to understand ,) !

commented

Could be, that this calculation causes the last command of a frame to add too many steps:

        if (this_cmd_steps * ticks_per_step < TICKS_PER_S / 500) {
          // in order to have ~2ms long command, this command should issue
          // multiple steps
          this_cmd_steps = TICKS_PER_S / 500 / ticks_per_step;
        }

Eventually need to add:

        if (this_cmd_steps + this_frame_steps > steps) {
             this_cmd_steps = this_frame_steps - steps;
        }

hum this just make 2 and a half turn almost :(..but don't worry i know what i'm doing tonight !, with some time i should be able to debug

commented

Yeah. The stupidest errors are in the easiest code parts. This is correct:

        if (this_cmd_steps + this_frame_steps > steps) {
             this_cmd_steps = steps - this_frame_steps;
        }

The previous one has caused an 8bit overflow and this way 10586 steps have been generated. The correct version creates exactly 3200 steps. Just have verified it with the avr-simulator.

Sorry for the inconvenience

No worries, you're already helping me way much than i was expecting...
And my overheating brain says thanks, sounds like it's ok now.

out of curiosity, is this something you plan to add as an official command at some point ?

commented

Eventually add it as an example

Thanks a lot !

Works perfectly, i just need now to implement frames with no move, shouldn't be complicated.

Here's the current project status:
https://youtu.be/fm2_VkUG10k

commented

Thanks for sharing this cool video and for the intro. Hope it is ok to list it under third party videos.