gin66 / FastAccelStepper

A high speed stepper library for Atmega 168/328p (nano), Atmega32u4, Atmega 2560, ESP32, ESP32S2, ESP32S3, ESP32C3 and Atmel SAM Due
MIT License
286 stars 67 forks source link

Driving steppers by animation #110

Closed kthod861 closed 2 years ago

kthod861 commented 2 years ago

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.

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

gin66 commented 2 years ago

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:

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.

kthod861 commented 2 years ago

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 :

Again, thanks a lot for your support.

gin66 commented 2 years ago

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.

kthod861 commented 2 years ago

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

kthod861 commented 2 years ago

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

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 ?
  }

}
gin66 commented 2 years ago

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() {}
gin66 commented 2 years ago

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

kthod861 commented 2 years ago

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 ,) !

gin66 commented 2 years ago

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;
        }
kthod861 commented 2 years ago

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

gin66 commented 2 years ago

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

kthod861 commented 2 years ago

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 ?

gin66 commented 2 years ago

Eventually add it as an example

kthod861 commented 2 years ago

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

gin66 commented 2 years ago

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