ESP-FlexyStepper icon indicating copy to clipboard operation
ESP-FlexyStepper copied to clipboard

lots of jitter on step movements

Open 3ricj opened this issue 3 years ago • 42 comments

Hi there,

I was having problems running this as a service at 6khz pulses. There was breaks in the pulses nearly 20ms (6-10ms typical) long! That made for very rough movement of the stage at times... it would jitter and chatter.. not to mention cause vibration.

I've started to try and optimize this a bit. It's much better now -- pretty stable pulses. I'm still seeing some minor bugaboos (slow fall times?) randomly seeing 1ms pulse lengths on the step line. But much much better now.

Before: https://www.dropbox.com/s/arb881iok61ibbv/stock.png?dl=0 https://www.dropbox.com/s/1k708hg0hwj6nsq/stock2.png?dl=0

After: https://www.dropbox.com/s/5knigkzt73tjq9a/changes.png?dl=0

Here's what I changed:

    disableCore0WDT(); // we have to disable the Watchdog timer to prevent it from rebooting the ESP all the time another option would be to add a vTaskDelay but it would slow down the stepper
    disableCore1WDT(); // we have to disable the Watchdog timer to prevent it from rebooting the ESP all the time another option would be to add a vTaskDelay but it would slow down the stepper
    xTaskCreatePinnedToCore(
        ESP_FlexyStepper::taskRunner, /* Task function. */
        "FlexyStepper",               /* String with name of task (by default max 16 characters long) */
        2000,                         /* Stack size in bytes. */
        this,                         /* Parameter passed as input of the task */
        1,                            /* Priority of the task, 1 seems to work just fine for us */
        &this->xHandle,              /* Task handle. */
        1  /*  what core? */
    );                          

This forces the step processing onto core1 -- it seems pretty happy there. This may have other bad side effects -- not sure.

Finally, there are some faster digital pin options on the ESP32 -- bitmask style. here's some info on that:

https://www.instructables.com/Faster-ESP32/

It may help drive things even faster.

Best I can tell the library is doing a bunch of stuff between the rising and falling edge of the pulse. Something in here may be causing the existing jitter, but I'm not sure how to best find it.

  // execute the step on the rising edge
  digitalWrite(stepPin, HIGH);

  // update the current position and speed
  currentPosition_InSteps += directionOfMotion;
  currentStepPeriod_InUS = nextStepPeriod_InUS;

  // remember the time that this step occured
  lastStepTime_InUS = currentTime_InUS;

  // figure out how long before the next step
  DeterminePeriodOfNextStep();

  // return the step line low
  digitalWrite(stepPin, LOW);

Here you can see the 1m pause between rising and falling: https://www.dropbox.com/s/n9lzfshqx4ar0r1/screenshot.png?dl=0

Thanks for the library, it's awesome.

Best, -3ric Johanson

3ricj avatar Dec 09 '20 01:12 3ricj

Is there any specific reason why it's doing all that work between the rising and falling edge of a short pulse? Maybe move the HIGH/LOW to be back to back, and process when waiting anyway? It looks like the DeterminePerdiodOfNextSlep is a bit heavy -- maybe include the clocks taken to process that in the delay for more stable frequency? But that's a guess.

3ricj avatar Dec 09 '20 01:12 3ricj

Thank you @3ricj for the effort you put into analyzing the signals and your inputs on this topic.

Your change moves the update task on core 1, but as far as I know this is also the core where the standard arduino function setup and loop are running. So when you move it there, it might interfere with some heavy work on core 1 from your loop(). Did you test what the jitter looks like if you perform some major operations in your main loop() function?

You should also try to disable the ESP Flexy Stepper service which will avoid the opening of a separate task completely and solely run in the main loop. Of Course then you cannot have other logic in the main loop that might cause jitter.

Can you please post your complete program you used while measuring the jitter? Do you use any Wifi functionality during the test? The RF radio stack Stack runs by default on Core 0. So in your case it could be, that you are using Wifi or Bluetooth at the same time, causing this jitter. If you move to core 1 you actually avoid this but you interfere with the Arduino functions and general other Libraries you might use. Try disabling wifi / BLE completely in your code to see if the jitter goes away.

As for your question of the pulse duration: The pulse duration (or even a variation of the length) should not matter to drivers that are triggered on the rising edge of the pulse. Additional drivers do have a minimum pulse length. Since this library is universal setting the pin/low in just two following cycles might cause the pulse to not be long enough. Using a fixed delay between high and low of the pin will waste CPU time with now benefit, thus the calculation is done in between to make best use of the CPU Time. Indeed quite some logic is happening in this DeterminePerdiodOfNextSlep function but it is necessary for the functionality.

As for the logic to set the Pins high and low: you are right this might be quicker, but the jitter is not caused by using the Arduino digitalWrite functions. These functions need the same time with every call, they do not need sometimes longer and sometimes shorter (unless interrupted by some wifi / rf stack logic on the same core, which of course is more likely the longer the function runs). It sure is worth a try to further optimize the code, but I am not sure it will fix the general jitter issue.

pkerspe avatar Dec 09 '20 08:12 pkerspe

@3ricj can you provide feedback on the above comment? I assume it is a CPU core related issue as indicated before. Currenlty there is a pull request that allows to define the core on which the service is running, maybe this will help if you use the 1 for the service once this pull request has been merged. It will be available in the next release 1.4.4 but your input on above questions would be helpfull

pkerspe avatar Jan 14 '21 11:01 pkerspe

I also encounter an exteme and constant amount of jitter... sometimes it stops for half a second in the middle of a move. Moves fine using other libraries. Any thoughts?

Humanoidx avatar Feb 05 '21 00:02 Humanoidx

@Humanoidx please provide some more details on your setup and code. 500ms breaks are very unusual and do not seem to have the same cause as the previously reported issues. 500ms is kind of an eternity for an ESP32 running at full speed.

pkerspe avatar Feb 05 '21 12:02 pkerspe

@pkerspe

ESP32-Wroom-32D https://www.amazon.com/gp/product/B0811LGWY2/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1

Nema 34 12nm Closed Loop Stepper https://www.amazon.com/Nema-Closed-Loop-Stepper-Motor-Driver/dp/B081N9FPRM/ref=sr_1_27?dchild=1&keywords=nema+34+stepper+motor+12nm&qid=1612650156&sr=8-27

Here is a video if it working smoothly with FastAccelStepper Library https://youtu.be/hPxJekex5zM

And here is the jittery ESP-FlexyStepper Library https://youtu.be/cA70uUXMIp0

Both Videos are 400 steps per revolution

16,000 steps per second max speed 2000 steps per second per second Move 160,000 steps

Depending on the settings sometimes it just stops all together even at low speed,s

Humanoidx avatar Feb 06 '21 23:02 Humanoidx

@Humanoidx thx for the hardware details, yet I was more referring to the software code and setup of esp FlexyStepper though :-) Can you please post your code?

pkerspe avatar Feb 06 '21 23:02 pkerspe

:) @pkerspe


#include <ESP_FlexyStepper.h>

// IO pin assignments
const int MOTOR_STEP_PIN = 12;
const int MOTOR_DIRECTION_PIN = 14;

// create the stepper motor object
ESP_FlexyStepper stepper;

void setup() 
{
  Serial.begin(115200);
  // connect and configure the stepper motor to its IO pins
  stepper.connectToPins(MOTOR_STEP_PIN, MOTOR_DIRECTION_PIN);
}

void loop() 
{
  //
  // Note 1: It is assumed that you are using a stepper motor with a 
  // 1.8 degree step angle (which is 200 steps/revolution). This is the
  // most common type of stepper.
  //
  // Note 2: It is also assumed that your stepper driver board is  
  // configured for 1x microstepping.
  //
  // It is OK if these assumptions are not correct, your motor will just
  // turn less than a full rotation when commanded to. 
  //
  // Note 3: This example uses "relative" motions.  This means that each
  // command will move the number of steps given, starting from it's 
  // current position.
  //

  // set the speed and acceleration rates for the stepper motor
  stepper.setSpeedInStepsPerSecond(16000);
  stepper.setAccelerationInStepsPerSecondPerSecond(2000);

  // Rotate the motor in the forward direction one revolution (200 steps). 
  // This function call will not return until the motion is complete.
  stepper.moveRelativeInSteps(160000);
  delay(1000);


}

Humanoidx avatar Feb 06 '21 23:02 Humanoidx

@pkerspe Same issues with ESP-Stepper-Motor-Server

Humanoidx avatar Feb 06 '21 23:02 Humanoidx

Did you try running the FlexyStepper stepper as a service as in example 5? Also please try with some less aggressive values in acceleration and speed first, something like 2000 steps/second and acceleration of 200 steps/s/s

I know it works with the other library, but we need to narrow down the cause of the problem.

pkerspe avatar Feb 06 '21 23:02 pkerspe

@pkerspe

Just tried it with the less aggressive values, and it is still jerky

Tried example 5 and it is still jerky (particularly when it accelerates or decelerates). But it seemed smooth when it reached the maximum speed.

Humanoidx avatar Feb 07 '21 00:02 Humanoidx

Thanks for the feedback. Can you try to figure out if your stepper driver reacts on a rising edge or a falling edge on the step input? So does it require a transition from 0 to 3.3V (rising edge) to send a step to the stepper motor or a transition from 3.3V to 0V (falling edge)? Since the ESP FlexyStepper stepper library performs quite some calculations between the signal changes this might make a difference. Currently the library expects a driver that fires on a rising edge step signal.

pkerspe avatar Feb 07 '21 09:02 pkerspe

Also one important question: did you connect to the ESP32 with a serial monitor to check if it crashes and performs a reboot/reset? This would explain the very long pauses you experienced before.

pkerspe avatar Feb 07 '21 09:02 pkerspe

I did some first tests with alternative methods of generating a square wave signal for step output using some of the build in modules from the ESP32. A thing I tested so far is the LED control (LEDC) peripheral (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html) in combination with the Pulse Counter module (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html).

This way I can create a pretty exact amount (not 100% sure about exact number though, still fine-tuning needed) of pulses with a static frequency up to several kHz (didn't really test the limit, only tested up to 20kHz so far, but according to the documentation a frequency of up to 40MHz should be possible with 50% duty cycle).

Downsides:

  • for each Stepper connected we will at least need three pins: -- direction pin -- step pin -- and, this is the new addition and input pin for the Pulse counter module to count the amount of steps generated by the PWM module
  • Unfortunately extending the logic to incorporate the logic for acceleration and deceleration is still tedious work to be done. So far I did not follow up in this, for now just a POC.
  • with using the hardware modules we are limited to 16 channels and the Pulse counter is limited to 8 units (both should not be a problem, since probably not many people need to control more than 8 steppers at the same time and we would be running out of pins before that limit could be reached anyway :-)

Find below a quick and dirty copy&paste programm used for tests with PWM and Pulse Counter (you can test with your setup and modify the variable totalPulseCountToReach to the amount of steps you want to perform and the variable pwmFreq for the step signal frequency in Hz (this example does not use a direction pin, you can hard wire it to GND or VCC for a test). To run this test you have to create a direct connection between the Step and the Counter IO Pin (in the example the IO Pin 4 (for Step signal) and 22 (for counting the step pulses). You can of course change those Pins to your liking, as long the the pins do not interfere with other functions). Another Option is to use the gpio_iomux_in() function to internally reroute the signal from the step output directly to the counter, and by that saving an GPIO PIN for other usage. Something like: gpio_iomux_in(PWM_PIN, PCNT_SIG_CH0_IN0_IDX);

So what does this program do? It initialized the PWM module on channel 0 and outputs a pwm signal (square wave with 50% duty cycle) with 20 kHz on Pin 4, this is where the stepper driver would be connected for the step signal. At the same time in initializes the Pulse Counter module (Unit 0 of the counter module) and registers a interrupt handler that fires whenever a specific threshold is reached (max value for the threshold is a 10 bit value, thus for large step counts multiple interrupts need to be chained with each occurrence incrementing are counter to check if another round is needed. Once the final step count has been reached, the PWM output will be stopped and the programme basically halts (main loop conditional output will stop printing status details to the console.

So once you connected the stepper, it will start directly to spin for (in this case 55555 steps with 20kHz) and then stop. The square wave signal is probably as good (stability and timing wise) as it gets with an ESP using this method. No acceleration/decelleration is implemented in this test!

#include <Arduino.h>
#include "driver/ledc.h"
#include "driver/pcnt.h"
//https://randomnerdtutorials.com/esp32-pwm-arduino-ide/
//https://esp32.com/viewtopic.php?f=19&t=14660
//@see also espressif example for non arduino specific setup
//https://github.com/espressif/esp-idf/blob/6e776946d01ec0d081d09000c36d23ec1d318c06/examples/peripherals/pcnt/pulse_count_event/main/pcnt_event_example_main.c

//PWM settings
#define PWM_PIN 4
const int pwmFreq = 20000;
const int pwmChannel = 0;
const int resolution = 8;

//variables to keep track of issued pulses
const int16_t maxPusleCountPerInterrupt = 32000;
unsigned int totalPulseCountToReach = 55555;
unsigned int pulsesFired = 0;
unsigned int remainingPulseCount = totalPulseCountToReach;
int16_t pulseCounterLimitForNextInterrupt = maxPusleCountPerInterrupt;

//pulse counter settings
#define COUNT_PIN 22 //use the same pin as PWM output
//https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html#_CPPv413pcnt_config_t
pcnt_config_t pcnt_config = {
    .pulse_gpio_num = COUNT_PIN,  // set gpio for pulse input gpio
    .ctrl_gpio_num = -1,          // no gpio for control
    .lctrl_mode = PCNT_MODE_KEEP, // when control signal is low, keep the primary counter mode
    .hctrl_mode = PCNT_MODE_KEEP, // when control signal is high, keep the primary counter mode
    .pos_mode = PCNT_COUNT_INC,   // increment the counter on positive edge
    .neg_mode = PCNT_COUNT_DIS,   // do nothing on falling edge
    .counter_h_lim = 32000,
    .counter_l_lim = 0,
    .unit = PCNT_UNIT_0, /*!< PCNT unit number */
    .channel = PCNT_CHANNEL_0
};

// https://esp32.com/viewtopic.php?t=6737
pcnt_isr_handle_t user_isr_handle = NULL; //user's ISR service handle

unsigned int isrCounter = 0;

static void IRAM_ATTR pcnt_intr_handler(void *arg)
{
  pulsesFired += pulseCounterLimitForNextInterrupt;
  if(pulsesFired >= totalPulseCountToReach){
    ledc_stop(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, 0);
  } else {
    remainingPulseCount = totalPulseCountToReach - pulsesFired;
    if(remainingPulseCount <= maxPusleCountPerInterrupt){
      pulseCounterLimitForNextInterrupt = remainingPulseCount;
    } else if(remainingPulseCount > maxPusleCountPerInterrupt){
      pulseCounterLimitForNextInterrupt = maxPusleCountPerInterrupt;
    }

    if(pulseCounterLimitForNextInterrupt > 0){
      pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_H_LIM, pulseCounterLimitForNextInterrupt);
    }
  }
  isrCounter++;
}

static void initPWM(void){
  ledcSetup(pwmChannel, pwmFreq, resolution);
  ledcAttachPin(PWM_PIN, pwmChannel);
  ledcWrite(pwmChannel, 125);
}

static void initPulseCounter(void){
  //pulse counter
  pinMode(COUNT_PIN,INPUT);
   //init counter unit 0
  if(pcnt_unit_config(&pcnt_config) != ESP_OK){
    Serial.println("Failed to config counter runit 0");
  }

  /* Configure and enable the input filter */
  pcnt_set_filter_value(PCNT_UNIT_0, 100);
  pcnt_filter_enable(PCNT_UNIT_0);

  pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM);
  /* Everything is set up, now go to counting */
  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);

  /* Install interrupt service and add isr callback handler */
  //use service so we do not have to deal with clearing interrupts manually
  pcnt_isr_service_install(0);
  pcnt_isr_handler_add(PCNT_UNIT_0, pcnt_intr_handler, (void *)PCNT_UNIT_0);

  pcnt_counter_resume(PCNT_UNIT_0);
}

void setup()
{
  Serial.begin(115200);
  initPWM();
  initPulseCounter();
}

int16_t pulseCounter = 0;
bool finalMessagePrinted = false;

void loop()
{
  if(pulsesFired < totalPulseCountToReach){
    if(pcnt_get_counter_value(PCNT_UNIT_0, &pulseCounter) != ESP_OK){
      Serial.println("Failed to get counter value for runit 0");
    }
    Serial.printf("%i out of %i pulses fired, %i remaining (current loop: pulses: %i, interrupt counter: %i)\n",pulsesFired, totalPulseCountToReach, remainingPulseCount, pulseCounter, isrCounter);
    delay(400);
  } else if(!finalMessagePrinted){
    Serial.printf("Done: %i out of %i pulses fired, %i remaining (current loop: pulses: %i, interrupt counter: %i)\n",pulsesFired, totalPulseCountToReach, remainingPulseCount, pulseCounter, isrCounter);
    finalMessagePrinted = true;
  }
}

pkerspe avatar Feb 07 '21 13:02 pkerspe

Another option to address the step signal generation issue would be to use the Remote Control (RMT) module (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html), using this approach the Step signal frequency could be set by setting the carrier signal frequency (I did not find details in the documentation about the maximum frequency, but since it is a unsigned 32 bit integer (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html#_CPPv4N15rmt_tx_config_t15carrier_freq_hzE) it should easily support some 100 kHz if not even several Mhz (possibly up to CPU frequency which by default would be 80 Mhz). I did not perform any tests.

Benefits of this solution would be, that you do not need any additional pins for a pulse counter, since basically the data transmitted via the RMT module would define the actual amount of pulses.

While I did not yet implement a POC this approach would face the same issue with acceleration and deceleration, which would need to be implemented by changing the carrierfrequency gradually. The amount of RMT Channels available is 8, so this would be sufficient for driving 8 stepper motors with one ESP32

Example script for generating square wave signal with the RMT module:

#include <Arduino.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/rmt.h"

static const char *TAG = "example";

#define CONFIG_EXAMPLE_RMT_TX_GPIO GPIO_NUM_4
/*
 * The table structure represents the RMT item structure:
 * {duration, level, duration, level}
 */
static const rmt_item32_t morse_esp[] = {
    // duration for low and high signal on carrier (here we send a pulse wave with 10kHz for 1000 ticks with FCPU/80 cyles)
    // so tick length is determined by FCPU (=80 Mhz) / 80 (clk_div configured below) = 1 Mhz tick frequency = 0.001 ms per tick = we send low signal for 1000 ticks = 1ms
    // at carrier frequency of 10kHz each period is 0.0001 seconds = 0.1 ms
    //so 1000 ticks = 10 square waves (the outputted signal will basically result in 11 high signals, since high is the idle status of the RMT module, so it will return to high after 10 squre waves have been sent)
    {{{ 1, 1, 1000, 0 }}}, 
    // RMT end marker to stop transmitting / end of buffer
    {{{ 0, 1, 0, 0 }}}
};

/*
 * Initialize the RMT Tx channel
 */
static void rmt_tx_init(void)
{
    rmt_config_t config = {};
    config.tx_config.carrier_en = true;
    config.tx_config.carrier_duty_percent = 50;
    config.tx_config.carrier_freq_hz = 10000;
    config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
    config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
    config.clk_div = 80;
    config.channel = RMT_CHANNEL_0;
    config.rmt_mode = RMT_MODE_TX;
    config.gpio_num = CONFIG_EXAMPLE_RMT_TX_GPIO;
    config.mem_block_num = 1;

    rmt_config(&config);
    rmt_driver_install(config.channel, 0, 0);
}

void setup(void)
{
    Serial.begin(115200);
    Serial.println("Starting");
    rmt_tx_init();
}

void loop(){
        rmt_write_items(RMT_CHANNEL_0, morse_esp, sizeof(morse_esp) / sizeof(morse_esp[0]), true);
        Serial.println("done");
        delay(10000);
}

So this program basically configured the RMT to use a carrier frequency of 10kHz and then sends "message" which consists of a low signal for 1000 ticks. Due to the configuration of the clock divider ticks are generated at 1 Mhz, so a tick is equal to 0.001 ms. when sending a signal with a duration of 1000 ticks, it means sending the carrier signal for 1ms, which with a given frequency of 10kHz results in 10 Step signals (but since then the signal level of the RMT returns to high, thus, it would issue an 11th step signal to the stepper driver. So for 10 step signals 900 ticks would nees to be used. You can change the number in LIne 32: {{{ 1, 1, 1000, 0 }}}, from 1000 ticks to a higher number, to generate more step pulses. According to the documentation (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html#transmit-data) the duration is a 15 bit value only: The minimum pattern recognized by the RMT controller later called an ‘item’, is provided in a structure rmt_item32_t. Each item consists of two pairs of two values. The first value in a pair describes the signal duration in ticks and is 15 bits long, the second provides the signal level (high or low) and is contained in a single bit. So we only have 15 bits for the duration value, so we seem to be limited to 32,768 ticks if I understood correctly, meaning with one item, resulting in about 32 steps per item with the given frequency and clock divider from the example code. If we change the clock divider to a higher value (max = 255) we can get more steps out of the duration value, but for the ease of calculation, the value of 80 was just more suitable. Another part to work around with this method would be that the memory blocks for the RMT module to provide item data to, is limited to 512 32bit blocks, this is shared with all 8 channels. So if all 8 channels are used, it would result in 64 items per channel that can be handed over to the RMT module. With the given amount of steps of 32 per item and the fact that we also need to provide a closing item, we can generate 2048 step pulses with one item collection. Then we need to register an ISR to basically start over as long as we have still steps to send pulses for. The RMT can be configured to also loop the last message continuously so we would not need to retrigger anything, but we would need to keep track of that amount of loop cycles to stop once the needed amount of step pulses has been generated.

The example program runs in an endless loop if issuing some step pulses and waiting 10 seconds.

pkerspe avatar Feb 07 '21 14:02 pkerspe

so all this POC code is basically to show that there are other ways to generate pulses than the current approach which is basically bit-banging. But it requires quite some reworking to implement the acceleration / deceleration, and I currently do not have the time to do so. So any help would be highly appreciated.

pkerspe avatar Feb 07 '21 15:02 pkerspe

I wish i could help but mannnn this is over my head

Humanoidx avatar Feb 10 '21 23:02 Humanoidx

I will investigate a bit further in the near future, but it is a time consuming change, but certainly a good one in regards to stability and much cleaner signal generation. But as often the devil is in the details :-) So I don't expect this to be an easy one.

pkerspe avatar Feb 11 '21 09:02 pkerspe

@Humanoidx did you try the changes suggested by 3ricj in his opening post? By changing the core for the service you will probably reduce jitter quite a bit and you could additionally increase the priority of the task a bit (say 2 or 3) to give it higher priority over other tasks. I did not experiment with these values and am not sure about the side effects but you could try.

So basically you can edit the lines 96 and following (the body of the void ESP_FlexyStepper::startAsService(void)function) to look like this:

void ESP_FlexyStepper::startAsService(void)
{
  disableCore1WDT(); // we have to disable the Watchdog timer to prevent it from rebooting the ESP all the time another option would be to add a vTaskDelay but it would slow down the stepper
  xTaskCreatePinnedToCore(
      ESP_FlexyStepper::taskRunner, /* Task function. */
      "FlexyStepper",               /* String with name of task (by default max 16 characters long) */
      2000,                         /* Stack size in bytes. */
      this,                         /* Parameter passed as input of the task */
      1,                            /* Priority of the task, 1 seems to work just fine for us */
      &this->xHandle,               /* Task handle. */
      1
  );
}

You can even try if it gets better with a priority value of 2 instead of 1

pkerspe avatar Feb 11 '21 10:02 pkerspe

I have struggled with this issue before, researched the ESP32 docs and lots and lots of forums. It turns out core 1 does not go much faster dan a 1000 cycles per second, no wonder I had so much jitter. Just try to schedule your program well on core 0 and the steppermotor runs very smooth!

@pkerspe I would just advice to stop using core 1 for the stepper motor, core 1 should only be used for tasks which do not need the speed core 0 offers...

xander-m2k avatar Sep 22 '21 08:09 xander-m2k

@xander-m2k thank you for your input. Just a minor "correction": as far as I am aware, core 1 is running at the very same speed as core 0. But due to the other tasks that the ESP32 is performing in FreeRTOS (specifically the WiFi Stack and probably some other housekeeping routines) the ESP Flexy Stepper task has to share the clock cycles with these other tasks depending on which core it is running.

But I honestly doubt that it will be only good for 1000 "cyles"/sec (or in other words a net clock speed of 1kHz). The CPU runs (depending on your settings) with up to 240 MHz (that is 240,000 kHz), I doubt that the WiFi stack consumes 99.99% of the CPU time.

Can you please provide a link to the source where you found this "1000 cycles per second" figure? I am by far not an expert on the internal architecture of the ESP or FreeRTOS, so I might be wrong of course as well.

But indeed the jitter seems to be caused by the fact that other tasks from the OS / WiFi Stack are running on the same core and obviously with a higher priority. As I pointed out in an earlier post, to my knowledge the time / cycles consuming WiFi Task is running on core 0, thus I used Core 1 in the code, but of course if you experienced a smooth running stepper when switching to core 0, it might be just the other way round :-)

By the way, according to some random tutorial (https://randomnerdtutorials.com/esp32-dual-core-arduino-ide/) the Arduino functions (so the setup and loop) function are running on core 1 of the ESP32, so according to your number, all code in a default Arduino project would be limited to the mentioned 1000 clock cycles/Second... I am not sure which priority the loop() task gets on core 1, so depending on what you do in your loop task and depending on its priority, it might have more or less impact on the ESP Flexy Stepper Task when running on the same core.

pkerspe avatar Sep 26 '21 20:09 pkerspe

@pkerspe I'm sorry, I was confused, it has been a while since I used the ESP32. So Core 1 is indeed the main core for your main Arduino/ESP program.

So to elaborate on how this works; in FreeRTOS you're only able to utilize core 0 by starting a task, with xTaskCreatePinnedToCore().

These tasks are ran by a with called 'tickrate' and currently the SDK only supports a tickrate of about a 1000Hz. This is because they are limited by vTaskDelay (unvisible to the user), where 1 ms is the minimum. This is to create space for Bluetooth, Wifi and other tasks like that which are handled by Core 0.

I could not find any official documentation about this, but people are talking about it on the esp32 forum: https://esp32.com/viewtopic.php?t=1341

Hope this informs you well :)

xander-m2k avatar Sep 26 '21 21:09 xander-m2k

@xander-m2k thanks for the update about the ticks, I checked the documentation of FreeRTOS (https://www.freertos.org/implementation/a00011.html) to better understand this task management routine.

I read it a bit differently than your explanation, since in my understanding a task does not get 1000 CPU cycles or "1000hz", but a maximum continuous processing time before a task might be suspended by the OS to allow another task to run. But at the end it is the explanation for the jitter that can be seen: In a way the behavior can be unexpected if other tasks are running on the same core and cause jitter. One can increase the priority of the task, but that could have unwanted side effects on other running tasks if they are essential.

I recommend not to put any code in the loop() function if using ESP Flexy Stepper as a service. Rather use interrupt-driven design patterns for the code. But obviously using the other core is also a viable option. I am just worried that in your scenario it might work just fine, but in other where the Wifi / BT / BLE stack is used heavily, you will have interference with these tasks on core 0 and again experience jitter, maybe even heavier than on core 1.

In general, a refactoring of the ESP flexy stepper code to an interrupt-driven or hardware module-based (e.g. hardware PWM, LEDC or the RMT module) solution as described earlier (https://github.com/pkerspe/ESP-FlexyStepper/issues/4#issuecomment-774684670 / https://github.com/pkerspe/ESP-FlexyStepper/issues/4#issuecomment-774678853) might be the best solution for a more stable / jitter-free signal generation, but this is a bigger task and affects quite some parts of the library.

pkerspe avatar Sep 27 '21 08:09 pkerspe

@pkerspe you're very complete in your answers sir, loving it.

I was using the Wifi capability while experiencing this jitter indeed, and was reaching about a 1000 cycles per second in my case, I don't remember how I measured it but it was about that amount.

Anyway, I think it is good to mention in the README that when using Wifi/Bluetooth/BLE it is not advised to use core 0.

xander-m2k avatar Sep 27 '21 15:09 xander-m2k

@xander-m2k as suggested I updated the README file with some content on the jitter topic

pkerspe avatar Sep 27 '21 20:09 pkerspe

@pkerspe did you concider using the mcpwm feature of the ESP32? the fastAccelStepper library is using them. They also state, that ledpwm wouldn't work for steppers. https://github.com/gin66/FastAccelStepper (scroll down for the "behind the curtains" section) This is a bit above my level, so I'm only able to point at this, not really able to help coding it.

hnnswldschtz avatar Oct 11 '21 09:10 hnnswldschtz

@hnnswldschtz thanks for the link, this is an interesting piece of information and might be useful in the future. Unfortunately, I do not see any explanation why the ledpwm function is not an option. it only says "ledpwm modules cannot be used for steppers, too." (and I am not sure if this refers to the previous sentence that only applies to ESP32-S2 and C3).

The comments regarding the mcpwm modules are very interesting though. Still it shows a lot needs to be considered when changing the implementation, also even though it is probably a cleaner approach to use a hardware module, it will also result in issues with newer ESP32 modules, like noted in the same documentation, since they seem to lack this mcpwm module, so the software solution is more "future proof" but less clean:

Compatibility with ESP32-S2 and ESP32-C3: Not supported due to lack of mcpwm modules. see reference in the related data sheets.

Still, I appreciate the input a lot!

pkerspe avatar Oct 11 '21 11:10 pkerspe

Compatibility is an issue, you are right. Just checked the datasheets: WROVER, WROOM and the rather new ESP32-S3 provide MCPWM.

hnnswldschtz avatar Oct 11 '21 12:10 hnnswldschtz

I started experimenting a bit again on this topic, the MCPWM peripheral seems not to be present in the ESP32-C3 and ESP32-S2 variants, thus I am not fancying this approach. I figured the LED Control (LEDC) module is present in all variants (S2, S3, C3) according to the documentation of Espressif (https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/search.html). I figured how to use the internal GPIO MUX / Pin remapping function to route the output of the Step Signal Pin directly to the Counter of the PCNT module without the need for sacrificing a physical GPIO Pin. Thus no extra wiring would be needed. Now investigating a bit on how to implement acceleration and deceleration in the best way.

#include "driver/ledc.h"
#include "driver/pcnt.h"
// PWM settings
#define PWM_PIN 4
const int pwmFreq = 20000;
const int pwmChannel = 0;
const int resolution = 8;

// variables to keep track of issued pulses
const int16_t maxPusleCountPerInterrupt = 32000;
unsigned int totalPulseCountToReach = 55555;
unsigned int pulsesFired = 0;
unsigned int remainingPulseCount = totalPulseCountToReach;
int16_t pulseCounterLimitForNextInterrupt = maxPusleCountPerInterrupt;

// pulse counter settings
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html#_CPPv413pcnt_config_t
pcnt_config_t pcnt_config = {
    .pulse_gpio_num = PWM_PIN,  // set gpio for pulse input gpio
    .ctrl_gpio_num = -1,          // no gpio for control
    .lctrl_mode = PCNT_MODE_KEEP, // when control signal is low, keep the primary counter mode
    .hctrl_mode = PCNT_MODE_KEEP, // when control signal is high, keep the primary counter mode
    .pos_mode = PCNT_COUNT_INC,   // increment the counter on positive edge
    .neg_mode = PCNT_COUNT_DIS,   // do nothing on falling edge
    .counter_h_lim = 32000,
    .counter_l_lim = 0,
    .unit = PCNT_UNIT_0, /*!< PCNT unit number */
    .channel = PCNT_CHANNEL_0};

// https://esp32.com/viewtopic.php?t=6737
pcnt_isr_handle_t user_isr_handle = NULL; // user's ISR service handle

unsigned int isrCounter = 0;

static void IRAM_ATTR pcnt_intr_handler(void *arg)
{
  pulsesFired += pulseCounterLimitForNextInterrupt;
  if (pulsesFired >= totalPulseCountToReach)
  {
    ledc_stop(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, 0);
  }
  else
  {
    remainingPulseCount = totalPulseCountToReach - pulsesFired;
    if (remainingPulseCount <= maxPusleCountPerInterrupt)
    {
      pulseCounterLimitForNextInterrupt = remainingPulseCount;
    }
    else if (remainingPulseCount > maxPusleCountPerInterrupt)
    {
      pulseCounterLimitForNextInterrupt = maxPusleCountPerInterrupt;
    }

    if (pulseCounterLimitForNextInterrupt > 0)
    {
      pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_H_LIM, pulseCounterLimitForNextInterrupt);
    }
  }
  isrCounter++;
}

static void initPWM(void)
{
  ledcSetup(pwmChannel, pwmFreq, resolution);
  ledcAttachPin(PWM_PIN, pwmChannel);
  ledcWrite(pwmChannel, 125);
}

static void initPulseCounter(void)
{
  gpio_iomux_in(PWM_PIN, PCNT_SIG_CH0_IN0_IDX); //this might auto set the pin to input, so make sure to set IO directon for Step PIN after this line
  // init counter unit 0
  if (pcnt_unit_config(&pcnt_config) != ESP_OK) //this might auto set the pin to input, so make sure to set IO directon for Step PIN after this line
  {
    Serial.println("Failed to config counter runit 0");
  }

  /* Configure and enable the input filter */
  pcnt_set_filter_value(PCNT_UNIT_0, 100);
  pcnt_filter_enable(PCNT_UNIT_0);

  pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM);
  /* Everything is set up, now go to counting */
  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);

  /* Install interrupt service and add isr callback handler */
  // use service so we do not have to deal with clearing interrupts manually
  pcnt_isr_service_install(0);
  pcnt_isr_handler_add(PCNT_UNIT_0, pcnt_intr_handler, (void *)PCNT_UNIT_0);

  pcnt_counter_resume(PCNT_UNIT_0);
}

void setup()
{
  Serial.begin(115200);
  initPulseCounter();
  initPWM(); //must to init after pulse counter setup, since config seems to auto set IO pin to input
}

int16_t pulseCounter = 0;
bool finalMessagePrinted = false;

void loop()
{
  if (pulsesFired < totalPulseCountToReach)
  {
    if (pcnt_get_counter_value(PCNT_UNIT_0, &pulseCounter) != ESP_OK)
    {
      Serial.println("Failed to get counter value for runit 0");
    }
    Serial.printf("%i out of %i pulses fired, %i remaining (current loop: pulses: %i, interrupt counter: %i)\n", pulsesFired, totalPulseCountToReach, remainingPulseCount, pulseCounter, isrCounter);
    delay(100);
  }
  else if (!finalMessagePrinted)
  {
    Serial.printf("Done: %i out of %i pulses fired, %i remaining (current loop: pulses: %i, interrupt counter: %i)\n", pulsesFired, totalPulseCountToReach, remainingPulseCount, pulseCounter, isrCounter);
    finalMessagePrinted = true;
  }
}

pkerspe avatar Mar 20 '22 19:03 pkerspe

Hey @pkerspe I'm working currently on an art project and indented to use your library because it looks very clean and structured and has all functions to work effectively out of the box.

It looks like I'm running into the same issues as your discussing here. I've tried to create a Task isolated to core 0 for my main loop and setup but still do experience some light jitter when running the stepper service on core 1.

Do you know if it matters if I initialize the stepper class on core 0 and run the service on core 1? Will there be some additional locking variables delay or something like that?

Since you were working towards the more robust hardware implementation of this solution, I was wondering if you've found some more time to work on implementing acceleration and deacceleration?

Kind Regards Flip

FlipEngineering avatar Mar 12 '23 14:03 FlipEngineering