Jerky servo movement - MG996R with ESP32WROOM

Hello all, I'm programming an ESP32-WROOM to control a MG996R servo, and I'd like it to do a sine-wave sweep between 40 and 140 degrees. I've gotten it to do that, but the servo movement gets pretty jerky at the beginning and end of the sweep, when it's accelerating and decelerating. I have the servo powered with its own 6v 3a power supply (not batteries) with 2 470uf capacitors across the positive and negative, I've connected a common ground with the ESP32, and I'm using pin 21 on the ESP32 which is PWM compatible.

I've uninstalled and reinstalled the libraries, and that didn't change anything.

Any ideas on how to improve the code to smooth the motion?

#include <ESP32Servo.h> // https://github.com/madhephaestus/ESP32Servo
#include <ServoEasing.h> // https://github.com/ArminJo/ServoEasing

Servo servoMotor;
unsigned long previousMillisServo = 0;
unsigned long previousMillisPause = 0;

const int servoMinAngle = 40;
const int servoMaxAngle = 140;

// Pattern durations
const unsigned long patternUp = 4000;   // 4 second INHALE
const unsigned long patternDown = 6000; // 6 second EXHALE
const unsigned long totalCycleDuration = 10000; // 10 seconds per full cycle

void setup() {
 servoMotor.attach(21); // Attach servo to pin 21 (PWM-capable pin)
}

void loop() {
 unsigned long currentMillis = millis();
 if (currentMillis - previousMillisServo >= 1) {
   unsigned long elapsedTime = currentMillis % totalCycleDuration;

   if (elapsedTime <= patternUp) {
     float progress = float(elapsedTime) / patternUp;
     float angle = servoMinAngle + (servoMaxAngle - servoMinAngle) * (0.5 * (1.0 + sin(PI * progress - PI / 2)));
     servoMotor.write(angle);

   } else {
     float progress = float(elapsedTime - patternUp) / patternDown;
     float angle = servoMaxAngle + (servoMinAngle - servoMaxAngle) * (0.5 * (1.0 + sin(PI * progress - PI / 2)));
     servoMotor.write(angle);
   }
   previousMillisServo = currentMillis;
 }
}

Have you tried asking ChatGPT?

Yeah, it sent me down a rabbit hole of increasingly complex code with no actual performance improvement. I decided to go back to the basic beginning code and see if I could find the most concise solution.

Never ask chatGPT.

Your sine takes 3000 steps to make half a sweep.

This

 if (currentMillis - previousMillisServo >= 1) {

updates the servo position 1000 times a second.

The servo is responding to its signal which is put out every 20 milliseconds. It can't respond to the additional changes, it does not see them, so they aren't helping.

I don't think they shou,d be hurting, but just in case change the update to something that makes a bit more sense and see what

 if (currentMillis - previousMillisServo >= 20) {

a7

Ahh that makes sense, though I changed it to 20 and it hasn't changed the behavior. I gradually increased it up to 60, but still no changes, so I set it back to 20.

I tried creating a specific number of steps with the following line, then changed 20 to stepInterval, but this also didn't affect anything:

const unsigned long stepInterval = totalCycleDuration / 1000;

if (currentMillis - previousMillisServo >= stepInterval)

Only 100 degrees are from minimum to maximum (40 to 140). Maybe step only when the sine wave has changed greater than or equal to 1/100th of its half wavelength.

You could also operate on the PWM duty cycle without a library. There are 1000 to 2000 microseconds at 50Hz between "0" and "180"

Would this be the best way to limit steps to 100?

And do you think the libraries are harming more than helping? I'm not sure how to do without them, but I might be able to figure it out.

#include <ESP32Servo.h> // https://github.com/madhephaestus/ESP32Servo
#include <ServoEasing.h> // https://github.com/ArminJo/ServoEasing

Servo servoMotor;
unsigned long previousMillisServo = 0;

const int servoMinAngle = 40;
const int servoMaxAngle = 140;

// Pattern durations
const unsigned long patternUp = 4000;   // 4 seconds INHALE
const unsigned long patternDown = 6000; // 6 seconds EXHALE
const unsigned long totalCycleDuration = 10000; // 10 seconds per full cycle

const float minWaveFraction = 1.0 / 100.0;
const float angleThreshold = (servoMaxAngle - servoMinAngle) * minWaveFraction;

float lastServoAngle = servoMinAngle; // Variable to track the last position

void setup() {
  servoMotor.attach(21); // Attach servo to pin 21 (PWM-capable pin)
}

void loop() {
  unsigned long currentMillis = millis();
  unsigned long elapsedTime = currentMillis % totalCycleDuration;

  float angle = 0.0;
  if (elapsedTime <= patternUp) { // INHALE
    float progress = float(elapsedTime) / patternUp;
    angle = servoMinAngle + (servoMaxAngle - servoMinAngle) * (0.5 * (1.0 + sin(PI * progress - PI / 2)));

  } else { // EXHALE
    float progress = float(elapsedTime - patternUp) / patternDown;
    angle = servoMaxAngle + (servoMinAngle - servoMaxAngle) * (0.5 * (1.0 + sin(PI * progress - PI / 2)));
  }

  if (abs(angle - lastServoAngle) >= angleThreshold) {
    servoMotor.write(angle);
    lastServoAngle = angle;
  }
}

The result is a linear curve... no accel or decel. I like the other curve, but note @alto777 post #5 and the 1000 "angle" pulses per second when the servo only updates 50 times per second.

Maybe angle is too large a step, and working with microseconds in a pulse width might be smoother. Here is a simulation of adjusting a pulse width to move a servo...