Reaction wheel inverted pendulum with IMU

Hi Guys,

I need your help to modify the project Arduino field oriented control (FOC) reaction wheel inverted pendulum from @Antun_Skuric with an IMU6050 in replacement of the axis encoder. The objective is to make a one axis self-balancing stick.

I have already rebuilt a pendulum with magnetic sensors AS5600, an Arduino nano, BLDC motor 2804 100KV and driver L6234. You can find a short video here after and the adapted code that I improved a little to make the swing-up more efficient .

Because it is working well, I thought that would be obvious to replace the pendulum encoder with an IMU6050. As said I would like to remove the encoder axis and get the pendulum standing vertical on the desk with two contact points aligned. In the Github it was indicated that we “could use the IMU and measure the inclination by combining the accelerometer and the gyro measurement”. So, I started by install the IMU on the pendulum but unfortunately I am struggling a lot with libraries to make working it with the simpleFOC library and also to set the system. Is someone has already done this or could advise me for modifying the code?

wheel inverted pendulum with AS5600 encoders

#include <SimpleFOC.h>

// Motor sensor
MagneticSensorAnalog sensorA = MagneticSensorAnalog(A7, 14, 1020); // motor axis
// Pendulum sensor 
MagneticSensorI2C sensorB = MagneticSensorI2C(AS5600_I2C); // pendulum axis
// BLDC motor init
BLDCMotor motor = BLDCMotor(7); 
// driver instance
BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); // 3 phase & enable

float LowPosOffset = 0; // self detect vertical

// function constraining the angle in between -pi and pi, in degrees -180 and 180
float constrainAngle(float x){
    x = fmod(x + M_PI, _2PI);
    if (x < 0)
        x += _2PI;
    return x - M_PI;
}

// LQR stabilization controller functions
// calculating the voltage that needs to be set to the motor in order to stabilize the pendulum
float controllerLQR(float p_angle, float p_vel, float m_vel){
  // if angle controllable
  // calculate the control law 
  // LQR controller u = k*x
  //  - k = [40, 7, 0.3]
  //  - x = [pendulum angle, pendulum velocity, motor velocity]' 
  float u =  60*p_angle + 7*p_vel + 0.3*m_vel; //40;7;0.3
  
  // limit the voltage set to the motor
  if(abs(u) > motor.voltage_limit*0.7) u = _sign(u)*motor.voltage_limit*0.7;
  
  return u;
}

void setup() {

  // monitoring port
  Serial.begin(115200);
  
// initialise magnetic sensor hardware
  sensorA.init();
  sensorB.init();
//  
  // link the motor to the sensor
  motor.linkSensor(&sensorA);
  
  // set control loop type to be used
  motor.controller = MotionControlType::torque;

  // link the motor to the encoder
  motor.linkSensor(&sensorA);
  
  // driver
  driver.voltage_power_supply = 12; 
  driver.init();
  // link the driver and the motor
  motor.linkDriver(&driver);

  // initialize motor
  motor.init();
  // align encoder and start FOC
  motor.initFOC();

  // sensor bottom position
  sensorB.update();
  LowPosOffset = PI - sensorB.getAngle();
  
}

// loop downsampling counter
long loop_count = 0;
float target_voltage;

void loop() {
  // ~1ms 
  motor.loopFOC();

  // control loop each ~20ms
  if(loop_count++ > 20){    
    // updating the pendulum angle sensor
    // NECESSARY for library versions > v2.2 
      sensorB.update();
    
    // calculate the pendulum angle 
    float pendulum_angle = constrainAngle(sensorB.getAngle() + LowPosOffset); 
    if( abs(pendulum_angle) < 0.4 ) // if angle small enough stabilize
      target_voltage = controllerLQR(pendulum_angle, sensorB.getVelocity(), motor.shaftVelocity());
    else // else do swing-up in both directions
//      target_voltage = -_sign(sensorB.getVelocity())*motor.voltage_limit*0.4;
      if (_sign(sensorB.getVelocity()) == 0){
      target_voltage = - motor.voltage_limit*0.3;
      }
      else{
        target_voltage = motor.voltage_limit*0.3;
      }
    // set the target voltage to the motor
    motor.move(target_voltage);

    // restart the counter
    loop_count=0;
  }
}

Hi @CREGIS21,

Nicely working pendulum, the video has already been is suggestions for me on youtube :smiley:

So in terms of using an imu, in my mind the biggest issue is the low memory of the Arduino Nano. As simplefoc is relatively large library and imu6050 ones are too, I imagine that you’re having hard time to include both?

I’ve tried several ways of using IMUs with for balancing:

  • Calculating the angle only from the accelerometers
  • Fusing accelerometer with a gyro using a simple kalman or complementary filter
  • Using the imu6050’s integrated dmp

In my setups the best choice was always to use the DMP. Like in the SimpleFOCbalancer. However it requires using a specialised library which is realtively heavy in terms of the memory usage. So it might require you to use another microcontroller.

Otherwise I’d suggest you to use a complementary filter and use the i2c directly to read the gyro and acceleration data. This should work good enough. But you’ll need to tune your filters a bit.
Here is a quick read that might be interesting.

Another issue that I’ve had a lot is due to the electromagnetic fields that are generated by the motor and their influence on the I2C communication. I’ve had a lot of them and I do not really have a good solution to it. Having your motor as far as possible from the imu and the mcu and reducing the i2c communication cable length usually helps though.

Hi Antun,

Many thanks for your prompt answer, advice, and links. About memory issues I agree with you that was challenging with the Arduino Nano. I suppose that I should definitely change the hardware and move to a more powerful board like ESP32 for instance.

It seems to me that inertia wheel pendulums are particularly sensitive to tiny measurement oscillations and requires a good filtering of the angles data. However, filters slow down too much the process on Arduino and jeopardize the motor control when you jump to the calculating angle loop, that is finally degrading all.

I will go trough the links and let you knows if I succeed to use this nice SimpleFOC library because my final goal would be to build a dual axis self-balancing Inverted stick.

I have a totally different application and precision is not so important in my case, but I noticed that calibrating the IMU first makes a huge difference, regarding the accuracy of derived angles.

Good point, I do agree with that.

Also we are really lucky to have the all time champion of inverted pendulums and unstable systems at the forum that we can ask for opinion @Rem666 :smiley:

@Rem666 we’d love to hear about your software stack that you use in your projects.
As far as I could understand, you are not using the IMU6050’s dmp, right?

Yes, I don’t use dmp. Because reading data from the MPU6050 is very simple. I tried it once - it works well, so I usually use it that way.
But I’ve used dmp before for other projects (without SimpleFOC). This is also a great option.

Here is the last code that I tried based on the MPU6050_tockn library that I needed modify with millis()/32 because the frequency of timer 0 was changed with simpleFOC. It doesn’t use dmp and looks to work but I never succeeded stabilize correctly whatever the settings. A more surprising behaves is when I uncomment the serial.print lines the system works much better. But up to now I have not found the reason in the code.

// Draft of inertia wheel pendulum with IMU6050 link to the pendulum and MPU6050_tockn library modified

#include <SimpleFOC.h>
#include <MPU6050_tockn.h> // modified: millis()/32 in MPU6050_tockn.cpp
#include <Wire.h> // already done by SimpleFOC with magnetic sensor if I2C used

MPU6050 mpu6050(Wire);

// Motor sensor
MagneticSensorAnalog sensorA = MagneticSensorAnalog(A7, 14, 1020);
// Pendulum sensor 
//MagneticSensorI2C sensorB = MagneticSensorI2C(AS5600_I2C); // simplified configuration constructor for AS5600 12-bit sensor 

// BLDC motor init
BLDCMotor motor = BLDCMotor(7);
// driver instance
BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8);

// function constraining the angle in between -pi and pi, in degrees -180 and 180
float constrainAngle(float x){
    x = fmod(x + M_PI, _2PI);
    if (x < 0)
        x += _2PI;
    return x - M_PI; //
}

// LQR stabilization controller functions
// calculating the voltage that needs to be set to the motor in order to stabilize the pendulum
float controllerLQR(float p_angle, float p_vel, float m_vel){
  // if angle controllable
  // calculate the control law 
  // LQR controller u = k*x
  //  - k = [40, 7, 0.3]
  //  - x = [pendulum angle, pendulum velocity, motor velocity]' 
  float u =  60*p_angle + 7*p_vel + 0.5*m_vel; //40;7;0.3 // 0.05
  
  // limit the voltage set to the motor
  if(abs(u) > motor.voltage_limit*0.9) u = _sign(u)*motor.voltage_limit*0.9;  //0.7
  return u;
}

float LowPosOffset = 0;

void setup() {

  // monitoring port
   Serial.begin(115200);
  
  // IMU 6050 initialise
    Wire.begin();
    mpu6050.begin();
    mpu6050.calcGyroOffsets(true); //-2.56, 1.84, 0.89 
    mpu6050.update();     
    LowPosOffset = mpu6050.getAngleX(); // get vertical when pendulum is in reverse pos
    if (LowPosOffset>0)
      LowPosOffset = LowPosOffset - 180;
      else
      LowPosOffset = LowPosOffset + 180;
    
  // initialise magnetic sensor hardware
  sensorA.init(); 
  // link the motor to the sensor
  motor.linkSensor(&sensorA);
  
  // set control loop type to be used
  motor.controller = MotionControlType::torque;

  // link the motor to the encoder
  motor.linkSensor(&sensorA);
  
  // driver
  driver.voltage_power_supply = 12; 
  driver.init();
  // link the driver and the motor
  motor.linkDriver(&driver);

  // initialize motor
  motor.init();
  // align encoder and start FOC
  motor.initFOC();
}

// loop downsampling counter
long loop_count = 0;
float target_voltage;

void loop() {
  // ~1ms 
  motor.loopFOC(); 
  // control loop each ~25ms
  if(loop_count++ > 15){    //20
    // updating the pendulum angle sensor   
    mpu6050.update();  
    // calculate the pendulum angle 
    float AngleRadX = (mpu6050.getAngleX()-LowPosOffset)*71/4068; // convert deg to rad 
    float pendulum_angle = constrainAngle(AngleRadX);  
    float pendulum_vel = mpu6050.getGyroX()*71/4068; // convert vel deg to rad
//     Serial.print("\tpendulum_angle : ");
//     Serial.print(pendulum_angle); //pendulum_angle
//     Serial.print("\tpendulum_vel : ");
//     Serial.println(pendulum_vel); // pendulum_velocity  
    if( abs(pendulum_angle) < 0.3 ) {// if angle small enough stabilize
      target_voltage = controllerLQR(pendulum_angle, pendulum_vel, motor.shaftVelocity()); 
//      Serial.print("\ttarget_voltage : ");
//      Serial.println(target_voltage); 
      } 
    else { // else do swing-up
//     target_voltage = 0;
      if (_sign(pendulum_vel) == 1){
      target_voltage = - motor.voltage_limit*0.35;
      }
      else{
        target_voltage = + motor.voltage_limit*0.35;
      }
    }
    // set the target voltage to the motor
    motor.move(target_voltage);
    // restart the counter
    loop_count=0;
  }
}

I don’t use this library, but it looks like it’s practically the same as reading the data directly.
Serial.print is slow, which is probably why commenting it out the balancing improves.
Anyway, there is no big difference in how the tilt angle is obtained, from the encoder or from the IMU - the angle will be the same.