gin66 / FastAccelStepper

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

move(1) maximum call rate #165

Closed jschoch closed 1 year ago

jschoch commented 1 year ago

It seems that there is a limit on how quickly you can call stepper->move(1). I'm on esp32 with RTM enabled. If if call RTM directly i can go pretty fast, but when I start to get over 1khz i get a big lag in position between where I expect to be and where stepper->getCurrentPosition().

I've also noticed that sometimes I get the correct distance, but in the next call it will not move the expected distance (according to a DRO). The 3rd call will go the correct distance, it is perplexing.

Some details on my use case. My code is here: https://github.com/jschoch/ESPels/tree/sse This is a electronic leadscrew for a lathe. It figures how how many stepper pulses to fire ( for a given pitch ) per encoder pulse (Bresenham's line algorithm). I have my own RMT stepping implementation but it does not do any acceleration calculations. I'd like to use Acceleration for movements which are not coupled to the lathe spindle position.

gin66 commented 1 year ago

Thanks for sharing the link. Nice project and a bigger code base, which cannot be grasped so quickly.

High frequency move(1) calls and the ramp generator may not co-exist very well.

  1. The ramp generator creates commands, which then are processed by the stepper queue. The ramp generator is invoked every 4ms.
  2. If move(1) is called for a running stepper, then only the target position is adjusted and the new value is handed over to the ramp generator.
  3. If move(1) is e.g. called every ms, then the ramp generator will see on the next cycle, that the target position has advanced by 4. Depending, how long the move(1) has been called the ramp generator can be anywhere on a speed ramp (accelerating, coasting or decelerating) and the target position may be 4 steps or 400 steps away. It is hard to predict, what the outcome will be.
  4. In any case, steps must not be lost. If the position does not match and the stepper has not jumped a full step position, then II would consider this a bug in the library.

One possible solution is to avoid the ramp generator completely, as you do not need it anyway. Instead of using move(1) try:

struct stepper_command_s cmd = {
        .ticks = 8000, .steps = steps, .count_up = direction};
    while (true) {
      int rc = stepper->addQueueEntry(&cmd);

FastAccelStepper can support on the timing, if your code properly maintains ticks/steps and eventually create pause commands (steps=0).

An alternative would be to not use your Bresenham algorithm and instead for each move adjust speed and acceleration, so that two pseudo-synchronized steppers are arrive at the target position at the same time. Just I am not sure, if during acceleration and deceleration the linearity may be violated.

Looking at your code, I am not so sure, with the rmt stepper code. Not so clear, how this works. Is the app controlling another stepper with rmt module?

gin66 commented 1 year ago

BTW: FastAccelStepper will use RMT module, if it runs out of mcpwm channels. The rmt implementation was actually quite easy and perfectly fitting to the stepper queue scheme. The rmt is kept running in continuous mode and the rmt buffer is split into two halves. One half is executed by the rmt module and the other one is prepared by the driver. If nothing is to be done, the half buffer is filled with pauses (no change on step pin). If the stepper queue is out of commands, then the rmt module is stopped. The uC must be finished preparing that half buffer, before the rmt reach it. Actually this implementation was by far less complex than the mcpwm/pcnt combo.

gin66 commented 1 year ago

Another comment for the stepper queue. Adding commands via addQueueEntry() to a stepper under controll of the ramp generator can be done. Outcome may be surprising, but even in this case: no steps must be lost. If the addQueueEnty() returns full queue, then the command will be ignored. So the return code of addQueueEntry should be monitored.

This means too, the stepper can be controlled by addQueueEntry() on low level and when the code is done with processing, use move() via ramp generator and let the stepper/mechanics enjoy acceleration/deceleration again.

jschoch commented 1 year ago

i'm going much faster than the 4ms window so I don't think move() will work.

"Looking at your code, I am not so sure, with the rmt stepper code. Not so clear, how this works. Is the app controlling another stepper with rmt module?"

it is a bit kludgy since i've made fastaccelstepper optional and I'm using some of the old code to deal with the complexity of direction changes.

I don't think I can approximate the position. This is designed for a lathe where you might be cutting a thread and you really really don't want to be in the wrong place, othewise you can break a tool, ruin your part, or worse case; crash the lathe.

the encoder i'm setup with generates 2400 pulses per revolution and so at 600RPM you are dealing with a 24khz signal. The pitch setting determines what ratio of encoder signals maps to stepper pulses.

tracking on/off would add quite a lot of complexity vs directly using the RMT peripheral like I currently do.

Can you also confirm that if I use the command queue directly I don't get any benefit of acceleration ramps. Right know i'm relying on the "slop" in the close loop stepper driver to make up for the lack of acceleration ramp in my RMT implementation.

gin66 commented 1 year ago

yes. command queue and ramps should not be used simultaneously. But they can be used one after the other. this means run your process with command queue and return to kind of home position with ramps. If you need ONLY acceleration, it should be possible to ramp up with FastAccelStepper, then wait for the state RAMP_STATE_COAST. If the state appears, disconnect the ramp generator with forceStop() and control the stepper queue using addQueueEntry(). Consequently deceleration cannot be done by ramp generator. This bears some risk of having not a 100% smooth transition from automatic to manual control.

On the other hand, if you need a fixed ratio of encoder signals to stepper pulses, then - I wonder - where is the freedom to accelerate or decelerate on a ramp ? Or should the stepper ramp up to the required speed to be in sync with the spindle and then start the process !?

How fast does the RPM of the spindle drops down ? Can the system afford a delay in reacting by 4ms and more ? Perhaps the moveByAcceleration() feature would be in that case more appropriate or call setSpeedInxxx() and applySpeedAcceleration() ?

Anyway, FastAccelStepper is ramping up or down relatively straightforward. It tracks the number of steps since start (or end). Then the needed time distance between two steps falls out of s = 1/2 a t^2. The only tricky part was to implement a fast sqrt routine (table lookup) and a matching floating point format, which supports table lookup within 256 Bytes. This is implemented in the PoorManFloat.h/cpp. During acceleration/deceleration FastAccelStepper only needs to count steps up to/down from the max step count. And the max step count equals the max speed: s_max = v^2/2a. So not too complicated to reinvent the wheel in a more suitable way for your application.