Constant tension / Velocity issue

Hi -

I’m having issues keeping constant velocity and tension on my machine using two 5065 motors with 8192 Encoders.

My application moves a roll of film from one reel to another. I’m trying to keep the speed / velocity and tension the same for the entire transfer, regardless of the roll size (could be small roll, medium or very large taking an entire reel space. Here’s a video of the issue:

You will see that at the beginning of the roll, the speed is normal and as the roll transfers to the next it accelerates. The camera captures 5fps (frames per second) at beginning and by the end, it’s at 13fps - quite an increase.

This is the command set I use and it is sent via Arduino / hardware buttons.

Axis 0 is the feed reel and Axis 1 is the take-up reel.

drv0.axis0.controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL

odrv0.axis0.controller.input_torque = 0.1

odrv0.axis0.controller.input_vel = 0

odrv0.axis1.controller.config.input_mode = INPUT_MODE_VEL_RAMP

odrv0.axis1.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL

odrv0.axis1.controller.config.vel_ramp_rate = 0.1

odrv0.axis1.controller.input_torque = 0

odrv0.axis1.controller.input_vel = -.5

My motor tuning is prob not optimal (never was able to start the GUI) but I suspect that tuning might not be the only answer? Any help or guidance is appreciated.

1 Like

strange. What do you have vel_gain and vel_integrator_gain set to on axis1?

You need to make sure that the max torque (torqe_lim & current_lim) of axis1 is significantly higher than the set torque of axis0, since axis1 needs to govern the speed so has to be able to overpower the other axis.

What happens if you run axis1 on its own? (without film, obviously)

Nice machine btw :slight_smile:

Oh wait, have you got the ramp backwards? (obviously your control software needs to compensate for the diameter of the film, but maybe it is compensating the wrong way)

These are the gains for both reels

odrv0.axis0.controller.config.vel_integrator_gain = 10
odrv0.axis1.controller.config.vel_integrator_gain = 10
odrv0.axis0.controller.config.pos_gain = 20
odrv0.axis1.controller.config.pos_gain = 20
odrv0.axis0.controller.config.vel_gain = 2
odrv0.axis1.controller.config.vel_gain = 1

I will try this! I just want to make sure I don’t snap the film, tension can be strong’ish but not herculean.

The reel turns smoothly and evenly, I can stop the reel by pinching it with my fingers( have to pinch pretty hard but I can stop it)

Thanks very much!

That’s the part that I have no idea where to start :frowning: Is it possible to do this with Arduino?

I don’t see any issue with your setup. The right-hand reel is in velocity mode, the left-hand reel is in torque mode. As the right reel diameter increases, the linear velocity of the film increases. Seems very reasonable.

You’ll need to start at faster velocity and go to lower angular velocity according to the diameter of the wheel to keep the linear feet per second the same. OR, add an encoder to one of those idler pulleys and use that as the speed reference instead (dual encoder / Mirror mode).

1 Like

Can this be done with Odrivetools commands? Or do I have to code this in Arduino - sorry just not sure where to start with this solution. I see trajectory control in the docs - maybe that’s what you are explaining? I don’t see examples of trajectory control.

So first off, you need to calculate the wheel speed at the start of the run (based on the diameter of the roll at the start) and the wheel speed at the end of the run (based on the diameter of the roll at the end).
To do that you’d use the equation for the circumference of a circle.

Then, you have two options to apply this:

  1. Update the input_vel command inside a loop from your Arduino, or
  2. Use the vel_ramp mode in ODrive, using a ramp rate of (start_speed - end_speed) / running_time - this could be tricky as I don’t think it’s meant to be used this way. But you could put it into passthrough mode, then set the starting speed, then put it into vel_ramp and set the end speed. It should ramp smoothly from one speed to the other.

Or, as Wetmelon suggested, you could add a third encoder e.g. SPI. (That’s if ODrive supports more than one encoder per axis now)

It’d be nice to have the option of a single-wire pulse velocity encoder that could be just a GPIO input pin driven from a simple optical gate on the film holes, but we don’t have that. There’s nothing stopping you from doing that with the Arduino though.

2 Likes

Thanks for this detailed response! I will research all this. Really appreciate it and will report if I can make it work.

I like your suggestion of using the fiber optic laser pulses - that sensor is already in the system and working.

2 Likes

In that case, it’s easy: If you are already counting “pulses per second” then you just need a loop that increases the velocity command slightly if the PPS is too low, and decreases it if it is too high. Perhaps put an upper/lower bound with a warning if it hits them.
I don’t think you’d need a very sophisticated control scheme to make that adjustment.

1 Like

That sounds like a plan - I will let you know how it goes! Thanks again. Programming is not my forte but I’ll make it work.

On a side note if you are curious you can watch the first scan I did on the machine yesterday, and it came out very nice considering the piece of film was old, beat up and faded red. It’s a very rare TRON teaser I found at swap meet in L.A.

Thanks to Odrive for moving the film smoothly, just need to make it constant speed :wink:


Password: tron

2 Likes

Wow, that’s amazing! Very nice indeed! :clap:

To be fair to ODrive, it is constant speed. Constant angular speed, not constant linear speed. :stuck_out_tongue_closed_eyes:

2 Likes

I was able to add a sensor pulse frequency counter in the Arduino code - I get an accurate number that matches the fps frequency displayed in the camera capture software.

Now I have no idea how to get realtime feedback of the velocity. If anyone has code example I would really appreciate the help.

Here’s the working code so far:

#include <ezButton.h>
#include <HardwareSerial.h>
#include <ODriveArduino.h>
// Printing with stream operator helper functions
template<class T> inline Print& operator <<(Print &obj,     T arg) { obj.print(arg);    return obj; }
template<>        inline Print& operator <<(Print &obj, float arg) { obj.print(arg, 4); return obj; }

const int Sensor  = 23;

volatile unsigned long lastTimestamp = 0;

volatile double frequency; 

HardwareSerial& odrive_serial = Serial8;
ODriveArduino odrive(odrive_serial);



ezButton butt_idle(30);  // create ezButton object that attach to pin 7;
  ezButton butt_play(29);  // create ezButton object that attach to pin 7;
    ezButton butt_stop(28);  // create ezButton object that attach to pin 7;
      ezButton butt_rew(27);  // create ezButton object that attach to pin 7;
        ezButton butt_ff(26);  // create ezButton object that attach to pin 7;



void setup() {

  pinMode(Sensor , INPUT_PULLUP);
  
   attachInterrupt(digitalPinToInterrupt(Sensor), freqCounter, RISING);

   odrive_serial.begin(115200);
  Serial.begin(115200);

  butt_idle.setDebounceTime(20); // set debounce time to 50 milliseconds
    butt_play.setDebounceTime(20); // set debounce time to 50 milliseconds
     butt_stop.setDebounceTime(20); // set debounce time to 50 milliseconds
      butt_rew.setDebounceTime(20); // set debounce time to 50 milliseconds
        butt_ff.setDebounceTime(20); // set debounce time to 50 milliseconds

}




void freqCounter() {
  unsigned long timestamp = micros();
  // Ignore frequencies below 1Hz and above 1kHz (also allows handling counter wraps)
  if (lastTimestamp && lastTimestamp + 1000 < timestamp && timestamp < lastTimestamp + 1000000) {
    frequency = 1e6 / (timestamp - lastTimestamp);
  }
  
  Serial.println(frequency);
 
  lastTimestamp = timestamp;
}




void loop() {

  
  butt_idle.loop(); // MUST call the loop() function first
  butt_play.loop(); // MUST call the loop() function first
  butt_stop.loop(); // MUST call the loop() function first
  butt_rew.loop(); // MUST call the loop() function first
  butt_ff.loop(); // MUST call the loop() function first



// IDLE
 
  if(butt_idle.isPressed()) {

    odrive_serial << "w axis" << 0 << ".controller.config.input_mode " << 1 << '\n';
    odrive_serial << "w axis" << 1 << ".controller.config.input_mode " << 1 << '\n';
    odrive_serial << "w axis" << 0 << ".controller.config.control_mode " << CONTROL_MODE_TORQUE_CONTROL << '\n';
    odrive_serial << "w axis" << 1 << ".controller.config.control_mode " << CONTROL_MODE_TORQUE_CONTROL << '\n';
    odrive_serial << "w axis" << 0 << ".controller.config.vel_integrator_gain " << 15 << '\n';
    odrive_serial << "w axis" << 1 << ".controller.config.vel_integrator_gain " << 15 << '\n';
    odrive_serial << "w axis" << 0 << ".controller.config.pos_gain " << 30 << '\n';
    odrive_serial << "w axis" << 1 << ".controller.config.pos_gain " << 30 << '\n';
    odrive_serial << "w axis" << 0 << ".controller.config.vel_gain " << 0.6 << '\n';
    odrive_serial << "w axis" << 1 << ".controller.config.vel_gain " << 0.6 << '\n';
    
    odrive_serial << "w axis" << 0 << ".controller.input_vel " << 0 << '\n';
    odrive_serial << "w axis" << 1 << ".controller.input_vel " << 0 << '\n';
    odrive_serial << "w axis" << 0 << ".controller.input_torque " << 0.12f << '\n';
    odrive_serial << "w axis" << 1 << ".controller.input_torque " << -0.12f << '\n';

  }
  

// PLAY

 if(butt_play.isPressed()) {

   odrive_serial << "w axis" << 0 << ".controller.config.vel_integrator_gain " << 10 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.config.vel_integrator_gain " << 10 << '\n';
   odrive_serial << "w axis" << 0 << ".controller.config.pos_gain " << 20 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.config.pos_gain " << 20 << '\n';
   odrive_serial << "w axis" << 0 << ".controller.config.vel_gain " << 2 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.config.vel_gain " << 1 << '\n';
   
   odrive_serial << "w axis" << 0 << ".controller.config.control_mode " << CONTROL_MODE_TORQUE_CONTROL << '\n';
   odrive_serial << "w axis" << 0 << ".controller.input_torque " << 0.12 << '\n';
   odrive_serial << "w axis" << 0 << ".controller.input_vel " << 0 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.config.input_mode " << INPUT_MODE_VEL_RAMP << '\n';    
   odrive_serial << "w axis" << 1 << ".controller.config.control_mode " << CONTROL_MODE_VELOCITY_CONTROL << '\n';
   odrive_serial << "w axis" << 1 << ".controller.config.vel_ramp_rate " << 0.7 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.input_torque " << 0 << '\n';
   odrive_serial << "w axis" << 1 << ".controller.input_vel " << -.5 << '\n';
 }
 
     
// STOP

  if(butt_stop.isPressed()) {

odrive_serial << "w axis" << 0 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.vel_gain " << 0.6 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_gain " << 0.6 << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_torque " << 0 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_torque " << 0 << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_vel " << 0 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_vel " << 0 << '\n';

  }

// REW

  if(butt_rew.isPressed()) {

odrive_serial << "w axis" << 1 << ".controller.config.input_mode " << 1 << '\n';  
odrive_serial << "w axis" << 0 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.vel_gain " << .6 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_gain " << .6 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.control_mode " << CONTROL_MODE_TORQUE_CONTROL << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_torque " << -0.1 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_vel " << 0 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.input_mode " << INPUT_MODE_VEL_RAMP << '\n';    
odrive_serial << "w axis" << 0 << ".controller.config.control_mode " << CONTROL_MODE_VELOCITY_CONTROL << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.vel_ramp_rate " << 1.5 << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_torque " << 0 << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_vel " << 2.5 << '\n';

  }

// FF

  if(butt_ff.isPressed()) {

odrive_serial << "w axis" << 0 << ".controller.input_vel " << 0 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_vel " << 0 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_integrator_gain " << 10 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.pos_gain " << 20 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.vel_gain " << 2 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_gain " << 1 << '\n';
odrive_serial << "w axis" << 0 << ".controller.config.control_mode " << CONTROL_MODE_TORQUE_CONTROL << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_torque " << 0.1 << '\n';
odrive_serial << "w axis" << 0 << ".controller.input_vel " << 0 << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.input_mode " << INPUT_MODE_VEL_RAMP << '\n';    
odrive_serial << "w axis" << 1 << ".controller.config.control_mode " << CONTROL_MODE_VELOCITY_CONTROL << '\n';
odrive_serial << "w axis" << 1 << ".controller.config.vel_ramp_rate " << 1.5 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_torque " << 0 << '\n';
odrive_serial << "w axis" << 1 << ".controller.input_vel " << -2.5 << '\n';

  }


}

adding this to my code I can get the voltages in realtime:

 odrive_serial.print("r vbus_voltage\n");
  volt=odrive.readFloat();
   Serial.println(volt);

What is the command to replace r vbus_voltage\n with so I can get velocity? Once I have the velocity in realtime I will be able to figure out how to regulate it.

Have you tried odrive_serial.print("r axis0.encoder.vel_estimate\n"); ?
(I have never used the Arduino serial interface so this may be wrong)

But this should be exactly what you set the input_vel to be, right?
The point is, you need to adjust it to keep the fps constant.
If you know what the fps should be, then you could simply increase input_vel by a tiny amount if the fps is too low, and decrease it if it’s too high.
I’d recommend keeping more of a running average though so that you can reduce the rate that the interrupt needs to make the adjustment. So for example, count the time for 10 pulses and divide by 10, instead of measuring the time between each pulse.

Thanks @towen that worked -

This is the code I used:

odrive_serial.print("r axis0.encoder.vel_estimate\n");
Speed=odrive.readFloat();
Serial.println(Speed);

This is the line I use to start playback:

odrive_serial << "w axis" << 1 << ".controller.input_vel " << -.5 << '\n';

I tried setting the value (-.5) using a variable but it doesn’t work… this is prob not the right command to update the value in realtime? Any hints appreciated. Edit this was fixed by using a FLOAT variable…

Now you just need a PID controller to adjust the speed :slight_smile:

I was able to change the velocity on my command

odrive_serial << "w axis" << 1 << ".controller.input_vel " << -.5 << '\n';

…by using a FLOAT variable. Thanks to @Wetmelon via Discord for the tip.

So now I have:

A) Film strip sensor “pulses per second” in a variable. (Capturing every perforation on film strip as film moves)

B) Odrive “take-up reel” real time velocity in a variable

C) Ability to change the Velocity for “Take-up Reel” via a variable.

So I now need to implement the solution outlined by @towen from this post https://discourse.odriverobotics.com/t/constant-tension-velocity-issue/8394/8?u=robinojones

…or the PID controller reference that @Wetmelon just posted. I will check this out

I think I might be able to hire an Arduino programmer at this point to help with this since It looks like I have everything to make it work, and the programmer would not have to really know anything about oDrive. I think…

I think a full blown PID controller would be overkill for this tbh. A really simple controller that just increments the speed very slightly if it’s too slow, and decrements it very slightly if it’s too fast, might be easier to do in Arduino land… It would effectively implement just the ‘I’ term of a PID controller, and the gain wouldn’t need to be very high. And we can set a limit e.g. we should never need to go more than 200% or less than 50% of nominal speed.

@robinojones, can you subtract the actual speed from the intended speed to get a “correction” value, and then add a small portion of the “correction” value to the commanded speed each cycle. You can easily calculate how fast it would converge to within 10% of the intended speed. If that’s faster than the rate that the spools change size, then you should be good.

2 Likes

I have someone helping me with adjusting the speed on the fly, it’s advancing well so far but now I realize that maybe my tuning is not optimal because I see a lot of speed oscillation near the end of a roll.

Look at the screenshot below: (this is without the real time velocity adjustments - only running the machine forward)

You can see at the end it oscillates a lot. Any recommendations / commands on what to tweak?

This is more apparent at lower speeds. I should fix this first, it’s impacting the real time adjustment code…

Raising the “vel_integrator_gain” from 10 to 150 really helped at the end of the roll. It’s now almost perfect. Not sure if that’s the correct way to mitigate this…

1 Like