600rpm based drivetrain weirdness

Hi all, so I’m prototyping the drivetrain pictured here:


from the drive catalogue thread:

The only difference is that I’m using 4 motors instead of 6, ie:

The benefit for us is that it should be possible to use 4 motors and be able to use either 200rpm or 600rpm cartridges depending on which drive shafts they are placed on.

The 200rpm version has been working well, so I’m trying now the 600rpm.

For some reason I’m seeing one motor on each side do most of the work and by way more than I would expect - e.g. without the wheels touching the ground I’m reading 50-100% of max torque. I can verify this using the motor.torque() function and on the brain device diagnostics screen.

For programming I’m just using the motor_group and drivetrain classes:

motor LeftFrontMotor = motor(PORT2, ratio6_1, true);
motor LeftRearMotor = motor(PORT4, ratio6_1, true);
motor_group LeftDrive = motor_group(LeftFrontMotor, LeftRearMotor);
motor RightFrontMotor = motor(PORT1, ratio6_1, false);
motor RightRearMotor = motor(PORT3, ratio6_1, false);
motor_group RightDrive = motor_group(RightFrontMotor, RightRearMotor);
drivetrain DriveTrain = drivetrain(LeftDrive, RightDrive, 4.15625 * M_PI, 15.0, 3.0, inches, 1.0);

and the testing with either drivetrain.arcade(),

LeftDrive.spin(fwd, 25.0, pct) or LeftFrontMotor.spin(...) and LeftRearMotor.spin(...),

ie

    LeftDrive.spin(fwd, 25.0, pct);
    RightDrive.spin(fwd, 25.0, pct);

or

    LeftFrontMotor.spin(fwd, 25.0, pct);
    LeftRearMotor.spin(fwd, 25.0, pct);
    RightFrontMotor.spin(fwd, 25.0, pct);
    RightRearMotor.spin(fwd, 25.0, pct);

What has me stumped is if I completely remove one motor from each side, I’m getting torque readings very close to the removed motor spinning with no load (ie around 5-7% of max torque). This says there’s not anything that’s dragging the drivetrain down friction-wize, which was my initial debug.

Baring anything else, all I can think of is that the motors are fighting against each other for some reason. I do hear a beat frequency with both motors connected for sure.

Appreciate any insights …

Thanks,
Nick

1 Like

Are you saying that 1 of the motors has a green cartridge and the other has a blue? What shafts are each connected to on the drivetrain? The 36-tooth gears in the diagram should/would be spinning at 600RPM (e.g. the blue motor cartridge) and the 72-tooth gears (as attached to the wheels) at 300 RPM due to the reduction. Did you replace the Xed out 36-tooth gear with a 12-tooth and plug the 200RPM (green cartridge) motor in there?

If 1 motor is green and the other blue, why do you declare both as ratio6_1 in your code?

1 Like

Ah, lol, sorry - no I meant that we can either run exclusively 200rpm by connecting to the larger 84T drive shafts, or exclusively 600rpm by connecting the motors to the 36T drive shafts. We are not mix and matching.

(The reasoning being that we don’t want to swap out cartridges wholesale for our teams, but have the flexibility to try the 600rpm, so the intent is to mostly use the 200rpm and this appears to be stable).

So - the configuration I am testing is with four blue cartridges connected to the 36T gears.

Note I now have a test rig with just 3 spindles and two motors and seeing the same results - will post more once I’ve gone some readings to share. Logging the torque/speed vs. time to share.

Thanks,
Nick.

Hmm, so if I use voltage based commands vs. speed based commands it works as expected (at least as far as having “evenly balanced motor torques in the more plausible range” go).

Here’s the test setup:

I’m driving a single idler with two 600rpm motors. I’m testing different configurations of gears:

36/84/36, 48/72/48, 60/60/60

When using the speed based commands I’m seeing the torques start even and then in a few seconds they diverge so that one motor approaches 100% toque (1.05/3 in this case) and quickly over heats, whereas the other motor falls to 0% torque. Simply unplugging one motor returns the torque on the remaining motor to a more reasonable level.

I’m guessing the motor PID is having challenges with the smaller gear reduction when using the blue cartridge, which is why using voltage seems to work better (or I’m doing some completely wrong :vulcan_salute:).

I need to have a simple isolated test code to post along with the motor reading graphs which will take me a few hours to generate.

OK, so to recap … the original idea was to test a 257rpm drivetrain using 4x 600rpm motors as pictured below connecting the motors to the 2 inline blue 36T gears:

Testing this I was curious why one motor on each side was rapidly overheating.

Long story short, I isolated this to a simple test rig containing only the middle three gears (36T/84T/36T), picture here:

For programming I’m using either a motor_group and then the spin() function using percent speed as an input, or setting the voltage on each motor directly (to approximate the same speed), ie:

motor m1 = motor(PORT11, ratio6_1, true);
motor m2 = motor(PORT12, ratio6_1, true);
motor_group mg1 = motor_group(m1, m2);

mg1.setStopping(coast);
mg1.spin(forward, 25, percent);

or

m1.spin(fwd, 3.0, volt);
m2.spin(fwd, 3.0, volt);

What I’m finding is that when using the speed based input the torque on the motors diverges over the space of a few seconds with one motor ramping to 100% torque and the other falling to zero:

If I use the voltage control the motors stay more or less at the same torque (although of course the speed is now more variable):

Full sample below - I seem to be unable to upload a v5cpp project:


#pragma region VEXcode Generated Robot Configuration
// Make sure all required headers are included.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <string.h>

#include “vex.h”

using namespace vex;

// Brain should be defined by default
brain Brain;

// START V5 MACROS
#define waitUntil(condition)
do {
wait(5, msec);
} while (!(condition))

#define repeat(iterations)
for (int iterator = 0; iterator < iterations; iterator++)
// END V5 MACROS

// Robot configuration code.

// generating and setting random seed
void initializeRandomSeed(){
int systemTime = Brain.Timer.systemHighResolution();
double batteryCurrent = Brain.Battery.current();
double batteryVoltage = Brain.Battery.voltage(voltageUnits::mV);

// Combine these values into a single integer
int seed = int(batteryVoltage + batteryCurrent * 100) + systemTime;

// Set the seed
srand(seed);
}

void vexcodeInit() {

//Initializing random seed.
initializeRandomSeed();
}

// Helper to make playing sounds from the V5 in VEXcode easier and
// keeps the code cleaner by making it clear what is happening.
void playVexcodeSound(const char *soundName) {
printf(“VEXPlaySound:%s\n”, soundName);
wait(5, msec);
}

#pragma endregion VEXcode Generated Robot Configuration

/----------------------------------------------------------------------------/
/* /
/
Module: main.cpp /
/
Author: {author} /
/
Created: {date} /
/
Description: V5 project /
/
/
/
----------------------------------------------------------------------------*/

// Include the V5 Library
#include “vex.h”

// Allows for easier use of the VEX Library
using namespace vex;

bool bFirstPrint = true;
int printID = 0;
double m1trq_avg = 0.0, m2trq_avg = 0.0;
double m1spd_avg = 0.0, m2spd_avg = 0.0;
double m1tmp_avg = 0.0, m2tmp_avg = 0.0;

void printThread()
{
if (bFirstPrint) {
printf(“time, m1 trq, m2 trq, m1 spd, m2 spd, m1 tmp, m2 tmp\n”);
bFirstPrint = false;
}

printf(“%4d, %6.2lf, %6.2lf, %6.2lf, %6.2lf, %6.2lf, %6.2lf\n”,
printID, m1trq_avg, m2trq_avg, m1spd_avg, m2spd_avg, m1tmp_avg, m2tmp_avg);

printID++;

}

int main() {
// Initializing Robot Configuration. DO NOT REMOVE!
vexcodeInit();
// Begin project code
wait(100, msec);

motor m1 = motor(PORT11, ratio6_1, true);
motor m2 = motor(PORT12, ratio6_1, true);
motor_group mg1 = motor_group(m1, m2);

mg1.setStopping(coast);
mg1.spin(forward, 25, percent);
// m1.spin(fwd, 3.0, volt);
// m2.spin(fwd, 3.0, volt);

int loopcount = 0;
int loopmax = 250;

double m1trq_accum = 0.0, m2trq_accum = 0.0;
double m1spd_accum = 0.0, m2spd_accum = 0.0;

while (true)
{
double m1trq = m1.torque(Nm);
double m2trq = m2.torque(Nm);
double m1spd = m1.velocity(percent);
double m2spd = m2.velocity(percent);
double m1tmp = m1.temperature(percent);
double m2tmp = m2.temperature(percent);

m1trq_accum += m1trq;
m2trq_accum += m2trq;
m1spd_accum += m1spd;
m2spd_accum += m2spd;

if (loopcount == loopmax - 1) {

  m1trq_avg = 100.0 * (m1trq_accum / (double) loopmax) / (1.05 / 3.0);
  m2trq_avg = 100.0 * (m2trq_accum / (double) loopmax) / (1.05 / 3.0);
  m1spd_avg = m1spd_accum / (double) loopmax;
  m2spd_avg = m2spd_accum / (double) loopmax;
  m1tmp_avg = m1tmp;
  m2tmp_avg = m2tmp;

  thread t1 = thread(printThread);

  loopcount = 0;

  m1trq_accum = 0.0, m2trq_accum = 0.0;
  m1spd_accum = 0.0, m2spd_accum = 0.0;

} else {
  loopcount++;
}

wait(1, msec);

}

}

4 Likes
#pragma region VEXcode Generated Robot Configuration
// Make sure all required headers are included.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <string.h>


#include "vex.h"

using namespace vex;

// Brain should be defined by default
brain Brain;


// START V5 MACROS
#define waitUntil(condition)                                                   \
  do {                                                                         \
    wait(5, msec);                                                             \
  } while (!(condition))

#define repeat(iterations)                                                     \
  for (int iterator = 0; iterator < iterations; iterator++)
// END V5 MACROS


// Robot configuration code.



// generating and setting random seed
void initializeRandomSeed(){
  int systemTime = Brain.Timer.systemHighResolution();
  double batteryCurrent = Brain.Battery.current();
  double batteryVoltage = Brain.Battery.voltage(voltageUnits::mV);

  // Combine these values into a single integer
  int seed = int(batteryVoltage + batteryCurrent * 100) + systemTime;

  // Set the seed
  srand(seed);
}



void vexcodeInit() {

  //Initializing random seed.
  initializeRandomSeed(); 
}


// Helper to make playing sounds from the V5 in VEXcode easier and
// keeps the code cleaner by making it clear what is happening.
void playVexcodeSound(const char *soundName) {
  printf("VEXPlaySound:%s\n", soundName);
  wait(5, msec);
}

#pragma endregion VEXcode Generated Robot Configuration

/*----------------------------------------------------------------------------*/
/*                                                                            */
/*    Module:       main.cpp                                                  */
/*    Author:       {author}                                                  */
/*    Created:      {date}                                                    */
/*    Description:  V5 project                                                */
/*                                                                            */
/*----------------------------------------------------------------------------*/

// Include the V5 Library
#include "vex.h"
  
// Allows for easier use of the VEX Library
using namespace vex;

bool bFirstPrint = true;
int printID = 0;
double m1trq_avg = 0.0, m2trq_avg = 0.0;
double m1spd_avg = 0.0, m2spd_avg = 0.0;
double m1tmp_avg = 0.0, m2tmp_avg = 0.0;

void printThread()
{
  if (bFirstPrint) {
    printf("time, m1 trq, m2 trq, m1 spd, m2 spd, m1 tmp, m2 tmp\n");
    bFirstPrint = false;
  }

  printf("%4d, %6.2lf, %6.2lf, %6.2lf, %6.2lf, %6.2lf, %6.2lf\n",
    printID, m1trq_avg, m2trq_avg, m1spd_avg, m2spd_avg, m1tmp_avg, m2tmp_avg);

  printID++;

}

int main() {
  // Initializing Robot Configuration. DO NOT REMOVE!
  vexcodeInit();
  // Begin project code
  wait(100, msec);

  motor m1 = motor(PORT11, ratio6_1, true);
  motor m2 = motor(PORT12, ratio6_1, true);
  motor_group mg1 = motor_group(m1, m2);

  mg1.setStopping(coast);
  // mg1.spin(forward, 25, percent);
  m1.spin(fwd, 3.0, volt);
  m2.spin(fwd, 3.0, volt);

  int loopcount = 0;
  int loopmax = 250;

  double m1trq_accum = 0.0, m2trq_accum = 0.0;
  double m1spd_accum = 0.0, m2spd_accum = 0.0;

  while (true)
  {
    double m1trq = m1.torque(Nm);
    double m2trq = m2.torque(Nm);
    double m1spd = m1.velocity(percent);
    double m2spd = m2.velocity(percent);
    double m1tmp = m1.temperature(percent);
    double m2tmp = m2.temperature(percent);

    m1trq_accum += m1trq;
    m2trq_accum += m2trq;
    m1spd_accum += m1spd;
    m2spd_accum += m2spd;

    if (loopcount == loopmax - 1) {

      m1trq_avg = 100.0 * (m1trq_accum / (double) loopmax) / (1.05 / 3.0);
      m2trq_avg = 100.0 * (m2trq_accum / (double) loopmax) / (1.05 / 3.0);
      m1spd_avg = m1spd_accum / (double) loopmax;
      m2spd_avg = m2spd_accum / (double) loopmax;
      m1tmp_avg = m1tmp;
      m2tmp_avg = m2tmp;

      thread t1 = thread(printThread);

      loopcount = 0;

      m1trq_accum = 0.0, m2trq_accum = 0.0;
      m1spd_accum = 0.0, m2spd_accum = 0.0;

    } else {
      loopcount++;
    }

    wait(1, msec);
  }
  
}

And just for grins here is the 3 motor test case running now at 50% speed.

Curiously it starts off almost immediately that motor 3 ramps up to full torque with motor 2 starting a slow ramp over about 30 seconds to join it. Motor 3 also is showing its speed hovering around 45% vs. the commanded 50%. All this time motor 1 is not doing any work according to the chart. However, soon after both motors 3 and 2 are at similar levels, motor 1 kicks in sporadically which leads to a fair amount of instability and noise from the system.

For sanity here was the code changes:

  motor m1 = motor(PORT11, ratio6_1, true);
  motor m2 = motor(PORT12, ratio6_1, true);
  motor m3 = motor(PORT13, ratio6_1, false);
  motor_group mg1 = motor_group(m1, m2, m3);

  mg1.setStopping(coast);
  mg1.spin(forward, 50, percent);

(also learning how to post code :smile_cat:)

And the big question is so what. In a realistic scenario, motors will be constantly changing speed and direction which this does not mimic. However, again, I flagged this because after putting this on a drivetrain I was seeing rapid motor heating. My guess is that for multi-motor systems like this that are tightly coupled using gears, individual PID has its limits and a more global PID is needed :man_shrugging:

4 Likes

This should be more like 10 or 20 msec. The motor can’t report data back any more frequently than 10 msec, I believe. Generally, most driver loops are on a 20msec delay.

3 Likes

Yeah, I went and cleaned up the logging code. I’m now running on a 10msec sample time which is what the V5 motor page says the internal loops run at, thanks.

I am puzzled why no-one else is seeing this. I’m reading what I can find about running multiple motors in a tightly coupled fashion like this and the evidence seems to point towards requiring a higher level control loop that has knowledge of the whole system vs. individual PID per motor that effectively have no knowledge of each other. I’ll try to write this up and add a few citations after I’ve gotten more time.

I added in a simple ramp up and down and the effect is just as bad as with a steady state input. Trying to make the plot clearer here:

As a side issue, I’m not sure why motor 3 can be that far off on speed - I’m guessing it has an issue with its encoder. However this effect was present with just 2 motors, so something to at least swap out but not fundamental to the problem statement.

Here’s my attempt at a system diagram describing what is going on:

Without any kind of feedback between each PID its seems this is setup to generate the instability I’m seeing.

So how should this work?

The goal overall seems to be to match the drivetrain to the commanded speed and to balance the load on each motor as evenly as possible, e.g. by monitoring the torque and then having separate PIDs to manage that.

To quickly test the theory I implemented the following. I’m still using the in-motor PID to control speed of one of the motors, but the other two motors I’m now combining the command speed along with the torque output from the 1st motor using a simple PID loop:


...
    // speed is command speed in percent
    float cmdSpeed = speed / 100.0; // normalize (0-1.0)
    float m1trq = m1.torque(Nm);
    float m2trq = m2.torque(Nm);

    float m2TrqError = (m1trq - m2trq) / MAX_TORQUE; // max_torque is 1.05/3.0
    // stop everything on zero control input
    if (cmdSpeed == 0.0) {
      m2TrqError = 0.0;
      m2PID_error_integral = 0.0;
    }
    // feedforward speed with P and I
    float m2CmdVlt = cmdSpeed + 0.25 * m2TrqError + 0.1 * m2PID_error_integral;
    // keep motor happy
    if (m2CmdVlt < 0.0) m2CmdVlt = 0.0;
    else if (m2CmdVlt > 1.0) m2CmdVlt = 1.0;

    m1.spin(fwd, speed, percent);
    m2.spin(fwd, 10.9 * m2CmdVlt, volt);

    m2PID_error_integral += m2TrqError;

    // m3 control is identical - omitted here

    wait(10, msec);
...

Output is already with this much better behaved with no motor going above 20% torque:

5 Likes

How did you get your graph images? They are nicely formatted.

That motor 3 has been bugging me all day so i finally swapped it out. Definitely faulty encoder (brand new - hellooo VEX - i have receipt). Put and old one in. Speed differences all fixed as expected. This certainly tames the excess on the individual PID case, but as predicted the overall load mismatch did not go away.

And for sanity with the proto torque compensating PIDs:

I figured someone would poke at that, so get ahead of it :slight_smile:

2 Likes

Hey @J.4 , if you look up at the sample code I’m planning ahead and printf() in CSV format (basically comma separated values with a newline between each new sample). This means I can save the log directly from the Print Console in VEXcode using the “SAVE” button. Helps to “CLEAR” anything before hand so you don’t have reems of other text there. After that, rename the file to .csv and open in Excel (or other spreadsheet program).

Print Console window in Vexcode shown here:

Once you have imported to Excel, you can convert the data to a table and then i just use the Insert Chart widget - specifically for this “Scatter with Smooth Lines”.

4 Likes

I should add - if you use printf) or python equivalent - use that sparingly. I limit to an output at a rate of a few times a second. If you want truly high speed logging, better to save to an in memory array and printf() to console at the end of your program or test sequence, or better still save to SD card. I can provide some very crude sample code for saving to SD card if needed. One thing i learnt from flying RC helis and planes for many year - logging is your friend. This is the way.

1 Like

Thanks. The Excel part is smart — I would have resorted to using Javascript.

1 Like

Sometimes less is more and more is less, but I figured the original diagram may lead to some confusion as to where certain control loops where running. To me this is a lot less clear, but hopefully is more explicit as to what is going on with the code sample povided.

1 Like