Strange behavior or sketch error? Timing or math problem?

Hi all!
I'm trying to create a project to control some (11) dc motors with BTS7960, and PCA9685 to generate PWM signal, both in speed and direction.

In the sketch I wrote a FSM with ramp function that calculate the speed.
If the speed is positive I'll write the 12bit equivalent to FW channel, if negative to BW channel of the BTS7960 (I'm not arrived to PCA9685 part of the sketch yet).

I faced the problem that if I use more than 1 motor, I'll get some spike in the calculated speed.

The sketch I wrote:

#include <Adafruit_PWMServoDriver.h>
#define NUM_MOTORS 3
#define MAX_NUM_STEPS 10
#define rampTime 1000

byte m = 0;
int out_SPEED;

enum States { STOP,
              FIRST_RAMP,
              RAMP,
              SPEED,
              REBOOT
            };

unsigned long startTime;

byte pwmValue;

//MOTORS
typedef struct {
  byte dim;
  byte index;
  const char *nome;
  States state;
  unsigned long timeStep;
  int vel[MAX_NUM_STEPS];
  unsigned long tempo[MAX_NUM_STEPS];
} Motors;

Motors motors[] = {
  { 2, 0, "M1", STOP, 0, { 25, -25 }, { 2000, 2000 } },
  { 2, 0, "M2", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M3", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M4", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M5", STOP, 0, { 10, 0 }, { 1000, 1000 } },
  { 2, 0, "M6", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M7", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M8", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M9", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M10", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M11", STOP, 0, { 100, -100 }, { 2000, 2000 } }
};

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:

  while (m < NUM_MOTORS) {
    pwmValue = updateMotor(motors[m], m);
    Serial.print(",");
    m += 1;
  };
  Serial.println();
  m = 0;
}

//function to interpolate speed based on machine state
byte updateMotor(Motors &motor, byte num) {

  //linear interpolation
  //y = mx + q

  byte index = motor.index;
  byte dim = motor.dim;

  if (index >= motor.dim) {
    motor.state = REBOOT;
  }

  //unsigned long now = millis();
  unsigned long elapsedTime = millis() - motor.timeStep;

  switch (motor.state) {
    case STOP:
      motor.timeStep = millis();
      out_SPEED = 0;
      motor.state = FIRST_RAMP;
      break;
      
    case FIRST_RAMP:
      if (elapsedTime < rampTime) {
        out_SPEED = (motor.vel[motor.index] / float(rampTime)) * float(elapsedTime);
      } else {
        motor.timeStep = millis();
        motor.state = SPEED;
      }
      break;
    case RAMP:
      if (elapsedTime < rampTime) {
        out_SPEED = ((motor.vel[motor.index] - motor.vel[motor.index - 1]) / float(rampTime)) * float(elapsedTime) + motor.vel[motor.index - 1];
      } else {
        motor.timeStep = millis();
        motor.state = SPEED;
      }
      break;
    case SPEED:
      if (elapsedTime < motor.tempo[motor.index]) {
        out_SPEED = motor.vel[motor.index];
      } else {
        motor.timeStep = millis();
        motor.index += 1;
        motor.state = RAMP;
      }
      break;
    case REBOOT:
      if (elapsedTime < rampTime) {
        out_SPEED = ((motor.vel[0] - motor.vel[motor.dim - 1]) / float(rampTime)) * float(elapsedTime) + motor.vel[motor.dim - 1];
      } else {
        motor.index = 0;
        motor.timeStep = millis();
        motor.state = SPEED;
      }
  }
  switch (num) {
    case 0:
      Serial.print("M1:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("index1:");
      Serial.print(motor.index);
      break;
    case 1:
      Serial.print("M2:");
      Serial.print(out_SPEED);
      break;
    case 2:
      Serial.print("M3:");
      Serial.print(out_SPEED);
      break;
    case 3:
      Serial.print("M4:");
      Serial.print(out_SPEED);
      break;
    case 4:
      Serial.print("M5:");
      Serial.print(out_SPEED);
      break;
    case 5:
      Serial.print("M6:");
      Serial.print(out_SPEED);
      break;
  }
  return out_SPEED;
}

Using serial plotter (with only 3 motors) I get this output

Maybe I'm blind but I can't figure out where the spike come from.

And the problem is that this spike will send to motor a voltage spike...

Any advice?

Thanks in advance

Stefano

well i suggest you get rid of the floating point calculations, they are slow and not needed and can cause unexpected results. If you do the multiplications first and the division after then you should able to do it in (32-bit) integer
I suspect you are using an AVR right ? Could it be that you have a division by (near) zero at times ?

Hello bigfefi

Read the current time using the millis() function once per run of the loop() function only.

HTH

Have a nice day and enjoy coding in C++.

@Deva_Rishi Yes, I'm using Arduino Uno. Maybe I could make calculation only if elapsedTime >= minimum value?

@paulpaulson ok,I'll try with, for example, now=millis() at the beginning of the function

I've tried as suggested, but the spikes persists...

Using Wokwi (due to several modifications to code) I have tried with

unsigned long rampTime = 500;

and this motors configuration, but using only first two

Motors motors[] = {
  { 2, 0, "M1", STOP, 0, { 25, -25 }, { 1000, 1000 } },
  { 2, 0, "M2", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M3", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M4", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M5", STOP, 0, { 10, 0 }, { 1000, 1000 } },
  { 2, 0, "M6", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M7", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M8", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M9", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M10", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M11", STOP, 0, { 100, -100 }, { 2000, 2000 } }
};

and looking trough the serial log I've noticed that it's like there's an overshoot (I'm using a timeRamp of 500 ms).

It seems that a "step" in the if statements is being missed... :thinking:
Or could it be a "variables misreading"? I mean....

I have always an array of 11 struct elements (motors) defined as wrote at the beginning of the reply and I change only the

#define NUM_MOTORS

at the beginning of the sketch.

I don't know...

Which parts of the spike are created by which state?

Make sure that the signal does not change between state changes.

Try

Serial.begin(115200);

good point !! in fact kick it upto 1Mbps to make sure the TX buffer does not overflow which will make any Serial.print() a blocking function.

I try with higher serial baudrate but nothing...

I was thinking about the fact that with only 1 motor everythings run smooth.
Could I assume that the code is ok and that the spike are due to the serial print?
In my real use I don't need serial print. I'm using it just for debug.

How can I debug this sketch without serial print/plotter? Any idea? Maybe converting speed to analog output and with an oscilloscope?

https://wokwi.com/projects/353524300654969857

Possibly and yes.

Perhaps for the test, fill an array with the results, then print the results after the test has completed.

The simulation provided by @dlloyd (THX) seems to clearly implicate the serial printing.

When run as is, there are beautiful waveforms in the serial plotter window.

By switching to a band rate of 9600, the plot shows the glitches that are the problem.

The wokwi simulation uses the same tool chain as the IDE and executes that code on an emulation of the microprocessor. It is very faithful.

If this is "flying to the Moon" critical, it would be nice to repeat the experiment and dheck the waveforms with an oscilloscope.

Otherwise I think high confidence can be had in the finding that this is a case of observation affecting that which is observed.

It does seem, however, that the glitches are not a necessary outcome of an arbitrary delay placed in the flow. Rather than let the solution be "it works as long as there is no serial printing", I suggest to @bigfefi that a real problem still Iives in the process that synthesizes the waveforms.

A timing function based on millis() but running much slower would allow @bigfefi to slow things down to a point where more serial printing could be used to trace the flow and ferret out the problem.

All timing is based on

 /* unsigned long */ now = millis();

At that point a simple shift right can reduce the speed as desired.

    now >>= 5;   // run at 1/32 speed, print without fear

a7

So this is a good starting point for finding the bug in the code.

Found a random glitch with printing at 115200 baud ...

the problem is that out_SPEED will be used to control speed and direction of DC motor (windscreen motor, rated 10A peak).
With map I'll convert speed to a pwm duty cycle (0-100 to 0-4095), I'll send command to R_PWM channel on the motor driver (forward direction), otherwise (negative,BACKWARD) to L_PWM.
If during the ramp I get a "real spike" with high absolute value but invers sign (the speed I use is referred as percentage) the BTS7960 will try to revert suddenly the direction, with (probably) back-emf generated... Am I right with this?

Where I have to write now>>= 5 ? After now = millis(); ?

@DrDiettrich You're right! :grinning:

Was advised, was good advice I assumed you followed.

Then yes, immediately you take the one reading upon which all timing depends, adjust that value.

  now = millis() >> 5;

I suggest shifting mainly because it will give the smoothest progression of time.

millis() skips a count from time to time. That came as a surprise to me and I discovered it late… anyhting relying on a millis() based timing must not only finish the work in the time slice, but also not rely on continuity of the returned time.

a7

@alto777

Was advised, was good advice I assumed you followed.

Yes I did, I didn't update the code.
now I'm using this one

// #include <Adafruit_PWMServoDriver.h>
#define NUM_MOTORS 3
#define MAX_NUM_STEPS 2
#define rampTime 1000

byte m;
int out_SPEED;
int pwmValue;

enum States { STOP,
              FIRST_RAMP,
              RAMP,
              SPEED,
              REBOOT
            };

//MOTORS
typedef struct {
  byte dim;
  byte index;
  const char *name;
  States state;
  unsigned long timeStep;
  int vel[MAX_NUM_STEPS];
  int tempo[MAX_NUM_STEPS];
} Motors;

Motors motors[] = {
  { 2, 0, "M1 ", STOP, 0, { 25, -25 }, { 1000, 1000 } },
  { 2, 0, "M2 ", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M3 ", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M4 ", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M5 ", STOP, 0, { 10, 0 }, { 1000, 1000 } },
  { 2, 0, "M6 ", STOP, 0, { 50, -50 }, { 2000, 2000 } },
  { 2, 0, "M7 ", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M8 ", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M9 ", STOP, 0, { 75, -75 }, { 2000, 2000 } },
  { 2, 0, "M10", STOP, 0, { 100, -100 }, { 2000, 2000 } },
  { 2, 0, "M11", STOP, 0, { 100, -100 }, { 2000, 2000 } }
};

void setup() {
  // put your setup code here, to run once:
  Serial.begin(250000);
}

void loop() {
  // put your main code here, to run repeatedly:

  while (m < NUM_MOTORS) {
    pwmValue = updateMotor(motors[m], m);
    Serial.print("\t");
    m += 1;
  };
  Serial.println();
  m = 0;
}

//function to interpolate speed based on machine state
byte updateMotor(Motors &motor, byte num) {

  //linear interpolation
  //y = mx + q

  // byte index = motor.index;
  // byte dim = motor.dim;

  if (motor.index >= motor.dim) {
    motor.state = REBOOT;
  }

  unsigned long now = millis();
  unsigned long elapsedTime = now - motor.timeStep;

  char ouputStr[100] = "";

  switch (motor.state) {
    case STOP:
      motor.timeStep = now;
      out_SPEED = 0.0;
      motor.state = FIRST_RAMP;
      break;

    case FIRST_RAMP:
      if (elapsedTime < rampTime) {
        out_SPEED = (motor.vel[motor.index] / float(rampTime)) * float(elapsedTime);
      } else {
        motor.timeStep = now;
        motor.state = SPEED;
      }
      break;
    case RAMP:
      if (elapsedTime < rampTime) {
        out_SPEED = ((motor.vel[motor.index] - motor.vel[motor.index - 1]) / float(rampTime)) * float(elapsedTime) + motor.vel[motor.index - 1];
      } else {
        motor.timeStep = now;
        motor.state = SPEED;
      }
      break;
    case SPEED:
      if (elapsedTime < motor.tempo[motor.index]) {
        out_SPEED = motor.vel[motor.index];
      } else {
        motor.timeStep = now;
        motor.index += 1;
        motor.state = RAMP;
      }
      break;
    case REBOOT:
      if (elapsedTime < rampTime) {
        out_SPEED = ((motor.vel[0] - motor.vel[motor.dim - 1]) / float(rampTime)) * float(elapsedTime) + motor.vel[motor.dim - 1];
      } else {
        motor.index = 0;
        motor.timeStep = now;
        motor.state = SPEED;
      }
  }

  switch (num) {
    case 0:
      // sprintf(buffer, "V1:%d,S1:%d,D1:%d", out_SPEED, motor.state, elapsedTime);
      // Serial.println(buffer);
      Serial.print("V1:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S1:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D1:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V1-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V1-1:");
        Serial.print(motor.vel[motor.index]);
      }
      break;
    case 1:
      Serial.print("V2:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S2:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D2:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V2-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V2-1:");
        Serial.print(motor.vel[motor.index]);
      }

      break;
    case 2:
      Serial.print("V3:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S3:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D3:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V3-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V3-1:");
        Serial.print(motor.vel[motor.index]);
      }

      break;
    case 3:
      Serial.print("V4:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S4:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D4:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V4-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V4-1:");
        Serial.print(motor.vel[motor.index]);
      }

      break;
    case 4:
      Serial.print("V5:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S5:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D5:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V5-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V5-1:");
        Serial.print(motor.vel[motor.index]);
      }

      break;
    case 5:
      Serial.print("V6:");
      Serial.print(out_SPEED);
      Serial.print(",");
      Serial.print("S6:");
      Serial.print(motor.state);
      Serial.print(",");
      Serial.print("D6:");
      Serial.print(elapsedTime);
      if (motor.state = 3) {
        Serial.print(",");
        Serial.print("V6-0:");
        Serial.print(motor.vel[motor.index - 1]);
        Serial.print(",");
        Serial.print("V6-1:");
        Serial.print(motor.vel[motor.index]);
      }

      break;
  }
  return out_SPEED;
}

I'll try with bit shift.

That is a reason why micros() is preferable for waveform generation.

The idea of slowing things down was just to allow for more diagnostic printing without the possibility that the printing itself is the problem.

The problem manifests at a known point in the sequencing. If it is indeed the logic, my own strong suspicion, it shouldn't be too hard to zero in on the why of it.

I may also place the code, as it is now, in the wokwi, my idea of fun is running things like this to ground.

Just now, however, I am to the beach. :wink:

a7

@alto777 have a nice sunbath! :smile:

I've made some tests on Wokwi.
If I understand your though, I have to slow down to understand if glitches come from code or "hardware".

The first screenshot is about using now=millis()>>1; , 4 motors. I got 2 random(?) spikes in 60 seconds of simulation (not visible here)

The second one is about using now=micros(); , 6 motors. I got 2/3 spikes just at the beginning.

From the screenshots I see that (at least in the last photo) every motor has completed its "full cycle". From this I would guess that the code is ok and glitches come from

...the printing itself is the problem.

Do you agree?

Meanwhile I'll prepare a partial hardware setup to try the code (1 phisical motor but 11 in software, without serial print command), possibly with an oscilloscope on the 12V that goes from the BTS7960 to the motor