Adaptive Resistance Machine Using ODrive S1

Hi everyone, I’m very new to motors, motor controllers, robotics, etc., so I would really appreciate any help.

I am creating an adaptive bicep curl machine that senses what the user is pulling (using a load cell + load cell amplifier) and slightly underperforms that weight so that the user is pulling at their max weight for each rep. I am using an ODrive S1 with a 1.4Nm 440W BLDC Motor + 20:1 Gearbox. I am powering these with the Meanwell LRS-600-48. For the code, I am using an ESP32, powered using a buck converter. I also have set up the ODrive with the brake resistor and other necessary options in the GUI.

Right now, I am running into a few problems, all of which I think are coding issues, but I might be wrong. Firstly, when pulling the cable, if I pull too fast, the ODrive flashes red and blue, the cable goes limp, and then goes back to flashing green and working as intended. Secondly, when I stop applying force on the cable, the retract force is (I think) matching the applied force instead of measuring around 0 lbs. What I wanted to do was let the cable reel back in slowly when the user is not applying any force.

This is my code. I would appreciate any help. Thank you!

#include <ODriveUART.h>
#include <HardwareSerial.h>
#include <HX711_ADC.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>

// Pin Definitions
#define ESP_RX      26
#define ESP_TX      27

#define HX711_DAT   18
#define HX711_CLK   19

#define LCD_SDA     21
#define LCD_SCL     22

// Constants
#define BAUD_ODRIVE         115200
#define CAL_FACTOR          976.56f
#define MAX_TORQUE_NM       6.75f
#define SPOOL_RADIUS        0.0635f
#define RESISTANCE_PCT      0.07f
#define MOTOR_CURRENT_LIMIT 20.0f
#define MOTOR_VEL_LIMIT     200.0f
#define RETRACT_SPEED       2.0f
#define TORQUE_RAMP_RATE    0.0055f
#define RETRACT_THRESHOLD   5.0f

// Objects
HardwareSerial odrive_serial(2);
ODriveUART odrive(odrive_serial);
HX711_ADC loadCell(HX711_DAT, HX711_CLK);
LiquidCrystal_I2C lcd(0x27,16,2);

// Variables
unsigned long startup_time = 0;
bool motorEnabled = false;
bool lifting = false;
float lbs = 0;
float filteredTorque = 0;

// ODrive Fault Check
void checkODriveFault()
{
  if(odrive.getState() != AXIS_STATE_CLOSED_LOOP_CONTROL)
  {
    Serial.println("ODrive left closed loop!");
    Serial.print("Axis Error: ");
    Serial.println(odrive.getParameterAsFloat("axis0.error"));
    Serial.print("Motor Error: ");
    Serial.println(odrive.getParameterAsFloat("axis0.motor.error"));
    Serial.print("Controller Error: ");
    Serial.println(odrive.getParameterAsFloat("axis0.controller.error"));
    odrive.clearErrors();
    delay(100);
    odrive.setState(AXIS_STATE_CLOSED_LOOP_CONTROL);
    delay(200);
  }
}

// Setup
void setup()
{
  Serial.begin(115200);
  delay(10);

  lcd.init();
  lcd.backlight();

  lcd.setCursor(0,0);
  lcd.print("Initializing");

  loadCell.begin();
  loadCell.start(2000,true);

  if(loadCell.getTareTimeoutFlag())
  {
    Serial.println("Load cell error");

    while(1)
    {
      delay(10);
    }
  }

  loadCell.setCalFactor(CAL_FACTOR);

  Serial.println("Load cell initialized");

  odrive_serial.begin(BAUD_ODRIVE, SERIAL_8N1, ESP_RX, ESP_TX);

  delay(100);

  unsigned long timeout = millis();
  bool connected = false;

  while(millis()-timeout < 3000)
  {
    if(odrive.getState() != AXIS_STATE_UNDEFINED)
    {
      connected = true;
      break;
    }

    Serial.println("Waiting for ODrive");
    delay(250);
  }

  if(!connected)
  {
    Serial.println("ODrive communication failed");

    while(1)
    {
      delay(10);
    }
  }

  Serial.println("ODrive connected");

  odrive.clearErrors();

  odrive.setParameter("axis0.motor.config.current_lim", MOTOR_CURRENT_LIMIT);
  odrive.setParameter("axis0.controller.config.vel_limit", MOTOR_VEL_LIMIT);

  startup_time = millis();

  lcd.clear();
  lcd.print("Wait 3 sec");
}

// Loop
void loop()
{
  if(loadCell.update())
  {
    float rawLbs = fabs(loadCell.getData());
    lbs = lbs * 0.85f + rawLbs * 0.15f;

    if(lbs < RETRACT_THRESHOLD)
    {
      lbs = 0;
    }
  }

  if(millis()-startup_time < 3000)
  {
    return;
  }

  if(!motorEnabled)
  {
    Serial.println("Enabling motor");

    odrive.setState(AXIS_STATE_CLOSED_LOOP_CONTROL);

    delay(500);

    motorEnabled = true;

    Serial.println("Motor enabled");
  }

  checkODriveFault();

  if(lbs > RETRACT_THRESHOLD)
  {
    if(!lifting)
    {
      odrive.setParameter("axis0.controller.config.control_mode", 1.0f);
      odrive.setParameter("axis0.controller.config.input_mode", 1.0f);
      lifting = true;
      filteredTorque = 0;
    }

    float newtons = lbs * 4.448f;
    float torque_command = (newtons * SPOOL_RADIUS) * RESISTANCE_PCT;

    torque_command = constrain(torque_command, 0.0f, MAX_TORQUE_NM);

    // Adaptive ramping
    float torque_diff = torque_command - filteredTorque;
    float adaptive_ramp = TORQUE_RAMP_RATE;

    // If user pulls hard and fast, ramp faster
    if(fabs(torque_diff) > 1.0f)
    {
      adaptive_ramp = TORQUE_RAMP_RATE * 3.0f;
    }

    if(filteredTorque < torque_command)
    {
      filteredTorque += adaptive_ramp;
    }
    
    else
    {
      filteredTorque -= adaptive_ramp;
    }

    filteredTorque = constrain(filteredTorque, 0.0f, torque_command);

    odrive.setTorque(
      filteredTorque
    );

  }

  else
  {
    if(lifting)
    {
      filteredTorque = 0;
      lifting = false;
      delay(100);
    }

    odrive.setParameter("axis0.controller.config.control_mode", 2.0f);

    odrive.setParameter("axis0.controller.config.input_mode", 1.0f);

    odrive.setParameter("axis0.controller.input_vel", RETRACT_SPEED);
  }

  lcd.setCursor(0,0);
  lcd.print("Force:");
  lcd.setCursor(8,0);
  lcd.print("      ");
  lcd.setCursor(8,0);
  lcd.print(lbs,1);
  lcd.print(" lb");

  lcd.setCursor(0,1);
  lcd.print("Torque:");
  lcd.setCursor(8,1);
  lcd.print("      ");
  lcd.setCursor(8,1);
  lcd.print(filteredTorque,2);
  lcd.print(" Nm");

  Serial.print("Force: ");
  Serial.print(lbs);
  Serial.print(" lb  Torque: ");
  Serial.print(filteredTorque);
  Serial.print(" Nm  Mode: ");
  Serial.println(lifting ? "Lift" : "Retract");
}

Also, if you need any additional information, please let me know, I will try my best to clarify. Thanks again

Nevermind, I was able to figure out the issue. The main issue (the flashing red and blue) was not caused by the code, it was with the configuration of the ODrive, especially with the bus overvoltage trip level. Previously, I had it at 52V, but after digging around, I realized that it should’ve been lower. Now it’s at 50.5V.

Ah, that all makes sense!

Taking a glance at your code, generally I’d recommend staying in torque mode – resist and retract are the same reel direction, so the user simply overpowers it during a rep. When the force drops below the threshold, command a small constant torque instead of switching modes, so the cable retracts. That way you don’t need to do all this control mode switching.

1 Like

Thank you for the advice! It did run a lot smoother once I changed it to torque mode only

Great to hear!

1 Like