Open 3ricj opened 3 years ago
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.
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.
@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
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 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
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 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
#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);
}
@pkerspe Same issues with ESP-Stepper-Motor-Server
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
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.
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.
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.
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:
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;
}
}
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.
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.
I wish i could help but mannnn this is over my head
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.
@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
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 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 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 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 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 as suggested I updated the README file with some content on the jitter topic
@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 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!
Compatibility is an issue, you are right. Just checked the datasheets: WROVER, WROOM and the rather new ESP32-S3 provide MCPWM.
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;
}
}
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
Hi @FlipEngineering In this use case It should not matter where you create a class instance in regard to the core. The only thing that matters is which core you specify when starting the service. You can always try just using the other core in the service invocation and see if things get better. Also if you do not need WiFi/Bluetooth then just disable it/shut it down.
How many stepper motors do you need to control and do you need the features to change speeds and directions all the time with smooth acceleration? For applications where running a stepper motor at a constant speed for a long time, it would be probably better to use a hardware PWM module to generate the steps.
Thanks for your reply @pkerspe. Unfortunately I will need the wifi module to be active. The goal is to send position targets over wifi and have two individual stepper + esp32 systems run synchronized without any wired connections between them. I've done now as well some fast testing with the Fastaccelstepper library which uses the hardware timers, and was able to have no issues with the jitter. But your library provides a very nice usability framework. Do you think it would be a big task to implement the hardware timing solutions from the fastaccelstepper lib into yours? Perhaps I would give it a go. But I'm by no means as experienced as you are, so I would trust your judgment if you'd say it's not that easy
As for the sync between two ESP32 i I would probably not use WiFi but the ESP-Now protocol. It will work without an existing WiFi Access point and also should be more efficient and maybe also result in less "traffic" on the WiFI stack.
As for the Fastaccelstepper: the library uses multiple hardware modules to achieve running up to 14 stepper motors (on ESP32, less on ESP32-S2/S3), that is quite some logic to interface all the different hardware modules on the different ESP32 models (on ESP32: mcpwm, rmt, on ESP32-S2: rmt, on ESP32-S3: mcpwm). I would go for the mcpwm implementation, but that would mean it is not supported on the ESP32-S2 (does not have mcpwm but a LED PWM module instead, whih maybe could be used as well).
The current versin of this very libary (flexystepper) uses a software only approach with the benefit of working on all ESP32 models (hpoefully), but the problem of limited step frequency and jitter.
If you would like to have a go, feel free to submit a Pull Request with suggested changes and I will review them. I would like to still keep the pure software implementation as a fallback and add the hardware steppe signal generation as an option that could be enable via a constructor parameter for example
The esp now looks like a great idea thanks for that! I'll do my best to work cleanly and if it's presentable will submit a pull request. Thank you a lot for the advice!
I just wanted to follow up on this bug as I filed it a few years ago. I changed directions with my projects -- I finally accepted that I would not get the stable performance I need on an ESP based software solution -- despite this library being completely awesome: the hardware + low level software support isn't there yet if you need low jitter and high performance.
I've been using the TIC controllers from Pololu - they are in charge of all of the pulses to the drivers and let you focus on driving at a speed or direction. Extremely nice drivers with great documentation. Of course, this comes at a cost, however, the i2c buses and support are just awesome -- so now my ESP chips can focus on other things. I keep hoping I can find an open source version of this product that works just as well -- but have not found it yet. In theory, we could hook the correct hardware features on an ESP chip and get stable performance, but it may limit what chips are support (eg: s2)
Interesting I wasn't aware of that, they could have been a good alternative. In a previous project we were as well successful with a teensy 3.6 to run 4 steppers over dmx/artnet. I'm trying to use the current project for building a base of wireless real time control of a single stepper, because this would be very handy in future projects. From my experience with the teensy I know that the hardware timer solution can provide a very accurate and reliable system. But I really would like to transit to the esp. Let's see where it brings us haha. Appreciate all of your input guys. Thanks a lot again.
Hi! I have been studying your library and it brings awesome function. I hard coded the control of two stepper motors for Arduino in the past and now I'm searching for a library like this with ESP. We are trying to control 5 steppers motor for a robotic arm and there is a lot of jitter with the simplest code. I can give you a video example and my code. Right now we are using drv8825 (same as pololu)
Code:
`
// IO pin assignments const int MOTOR1_STEP_PIN = 26; const int MOTOR1_DIRECTION_PIN = 25; const int MOTOR1_ENABLE_PIN = 32;
// Speed settings const int DISTANCE_TO_TRAVEL_IN_STEPS = 5000; const int SPEED_IN_STEPS_PER_SECOND = 2000; const int ACCELERATION_IN_STEPS_PER_SECOND = 200; const int DECELERATION_IN_STEPS_PER_SECOND = 200;
// create the stepper motor object ESP_FlexyStepper stepper1;
int previousDirection1 = -1;
void setup() { Serial.begin(115200);
// connect and configure the stepper motor to its IO pins stepper1.connectToPins(MOTOR1_STEP_PIN, MOTOR1_DIRECTION_PIN); // set the speed and acceleration rates for the stepper motor stepper1.setSpeedInStepsPerSecond(SPEED_IN_STEPS_PER_SECOND); stepper1.setAccelerationInStepsPerSecondPerSecond(ACCELERATION_IN_STEPS_PER_SECOND); stepper1.setDecelerationInStepsPerSecondPerSecond(DECELERATION_IN_STEPS_PER_SECOND);
// Not start the stepper instance as a service in the "background" as a separate task // and the OS of the ESP will take care of invoking the processMovement() task regularily so you can do whatever you want in the loop function stepper1.startAsService(); pinMode( MOTOR1_ENABLE_PIN, OUTPUT); digitalWrite(MOTOR1_ENABLE_PIN, LOW);
}
void loop() { // just move the stepper back and forth in an endless loop
if (stepper1.getDistanceToTargetSigned() == 0) { previousDirection1 = -1; long relativeTargetPosition = DISTANCE_TO_TRAVEL_IN_STEPS previousDirection1; Serial.printf("Moving stepper 1 by %ld steps\n", relativeTargetPosition); stepper1.setTargetPositionRelativeInSteps(relativeTargetPosition); }
// Notice that you can now do whatever you want in the loop function without the need to call processMovement(). // also you do not have to care if your loop processing times are too long. } `
Video: https://youtu.be/he3V48hPz28?si=DYzSet-NDicG8P1o
I would prefer this library because it bring nice function and documentations. Thank you very much.
@Lammpo which ESP model are you using? Is it a dual core version? Is the posted code the whole firmware that can be seen on the video or are there any additions not shown in you post?
It is an ESP Wroom-32 and the only code I'm using is what I posted. I tried microstepping and the behaviour was better but the controller and the nema17 were really hot.
I think that when I tried to control 2 steppers the behaviour was worst which doesn't make sense if the tasks are independent. Thank for the quick response. Tomorrow I will add more info and test.
You should check your wiring if the steppers are getting hot. You might still get movement sometimes if you wired the steppers incorrectly but with problems like temperature and jitter. Anyhow try assigning the task to another core and maybe even disable WiFi completely if you do not need it.
I forgot to check the ref values of drv and the current was too high so that's why the temperature was high. The jitter is reduced now but I got a strange acceleration. First it accelerates just fine and it jumps to the final spped. Same when it has to deccelerate. Update: It only happens if I'm using 2 steppers but if a use one core for each one it works like a swiss clock. If they are at the same core it doesn't work. I really appreciate your effort. Video: https://youtube.com/shorts/rqFZ17MKC-8
Code: `// // Example 5: // this example shows how run the flexy stepper in a non blocking way // without the need of any needed function calls in the loop function // A separate task is started to take care of sending the step signals // // Paul Kerspe 8.6.2020 // //
// IO pin assignments const int MOTOR_STEP_PIN = 26; const int MOTOR_DIRECTION_PIN = 25; const int MOTOR_ENABLE_PIN = 32;
// IO pin assignments const int MOTOR_STEP_PIN2 = 27; const int MOTOR_DIRECTION_PIN2 = 14; const int MOTOR_ENABLE_PIN2 = 33; // Speed settings const int DISTANCE_TO_TRAVEL_IN_STEPS = 35000; const int SPEED_IN_STEPS_PER_SECOND = 2000; const int ACCELERATION_IN_STEPS_PER_SECOND = 200; const int DECELERATION_IN_STEPS_PER_SECOND = 200;
// create the stepper motor object ESP_FlexyStepper stepper; ESP_FlexyStepper stepper2;
int previousDirection = 1; int previousDirection2 =1;
void setup() { Serial.begin(115200); // connect and configure the stepper motor to its IO pins stepper.connectToPins(MOTOR_STEP_PIN, MOTOR_DIRECTION_PIN); // set the speed and acceleration rates for the stepper motor stepper.setSpeedInStepsPerSecond(SPEED_IN_STEPS_PER_SECOND); stepper.setAccelerationInStepsPerSecondPerSecond(ACCELERATION_IN_STEPS_PER_SECOND); stepper.setDecelerationInStepsPerSecondPerSecond(DECELERATION_IN_STEPS_PER_SECOND);
// Not start the stepper instance as a service in the "background" as a separate task // and the OS of the ESP will take care of invoking the processMovement() task regularily so you can do whatever you want in the loop function stepper.startAsService(0);
pinMode(MOTOR_ENABLE_PIN, OUTPUT); digitalWrite(MOTOR_ENABLE_PIN, LOW);
// Second motor
stepper2.connectToPins(MOTOR_STEP_PIN2, MOTOR_DIRECTION_PIN2); // set the speed and acceleration rates for the stepper motor stepper2.setSpeedInStepsPerSecond(SPEED_IN_STEPS_PER_SECOND); stepper2.setAccelerationInStepsPerSecondPerSecond(ACCELERATION_IN_STEPS_PER_SECOND); stepper2.setDecelerationInStepsPerSecondPerSecond(DECELERATION_IN_STEPS_PER_SECOND);
// Not start the stepper instance as a service in the "background" as a separate task // and the OS of the ESP will take care of invoking the processMovement() task regularily so you can do whatever you want in the loop function stepper2.startAsService(0);
// pinMode(MOTOR_ENABLE_PIN2, OUTPUT); // digitalWrite(MOTOR_ENABLE_PIN2, LOW); delay(2000);
}
void loop() { // just move the stepper back and forth in an endless loop if (stepper.getDistanceToTargetSigned() == 0) { delay(1000); previousDirection = -1; long relativeTargetPosition = DISTANCE_TO_TRAVEL_IN_STEPS previousDirection; Serial.printf("Moving stepper by %ld steps\n", relativeTargetPosition); stepper.setTargetPositionRelativeInSteps(relativeTargetPosition); }
if (stepper2.getDistanceToTargetSigned() == 0)
{ delay(1000); previousDirection2 = -1; long relativeTargetPosition = DISTANCE_TO_TRAVEL_IN_STEPS previousDirection2; Serial.printf("Moving stepper by %ld steps\n", relativeTargetPosition); stepper2.setTargetPositionRelativeInSteps(relativeTargetPosition); } // delay(200); // Serial.printf("Current Position: %f \n", stepper.getCurrentPositionInSteps());
// Notice that you can now do whatever you want in the loop function without the need to call processMovement(). // also you do not have to care if your loop processing times are too long. } `
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:
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.
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