Two wheel balncing robot with adxl345

I am a begginer in arduino (I practice it for 4 months) and I decided to create a two wheel balancing robot as shown in the pictures. In the code I uploaded and some versions of it, it tryes to stay vertical but it always falls after 2 second or something.

I use :

  1. An ADXL345 (for tilt mesurement)
  2. An L298N (for motor control)
  3. Two tt motors
  4. 3D printed chassis
  5. 9V battery for L298N power
  6. DJI NEO battery hub for arduino power (as a power bank, I didn't have an actual power bank)

The center of gravity is high, to make the fall slower and have more time to react. I have used if (filteredX > 1 && filteredX < 5) so it doesn't "overcorrect" it's position and fall from the other side. The filteredX variable is the tilt and the tilt is mesured by taking the starting position (for one second) of the ADXL345 as zero degree tilt. The code for the tilt is completely fine I think.

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>

Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345);

#define ENA 2
#define IN1 3
#define IN2 4
#define IN3 5
#define IN4 6
#define ENB 7

float referenceAngleX = 0;
float filteredX = 0;
float alpha = 0.3;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  if (!accel.begin()) {   
    Serial.println("No sensor found");
    while(1);
  }

  accel.setRange(ADXL345_RANGE_2_G);
  delay(1000);

  pinMode(ENA, OUTPUT);
  pinMode(ENB, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);

  float sum = 0;
  for (int i = 0; i < 50; i++) {
    sensors_event_t event;
    accel.getEvent(&event);

    float x = event.acceleration.x;
    float y = event.acceleration.y;
    float z = event.acceleration.z;

    float angle = atan(x / sqrt(y*y + z*z)) * 180 / PI;
    sum += angle;
    delay(10);
  }

  referenceAngleX = sum / 50.0;
}

void loop() {

  sensors_event_t event;
  accel.getEvent(&event);

  float x = event.acceleration.x;
  float y = event.acceleration.y;
  float z = event.acceleration.z;

  float angle = atan(x / sqrt(y*y + z*z)) * 180 / PI;


  float relativeX = angle - referenceAngleX;

  filteredX = alpha * relativeX + (1 - alpha) * filteredX;

  delay(10);

  if (filteredX > 1 && filteredX < 7) {
    analogWrite(ENA, 150);
    analogWrite(ENB, 150);
    digitalWrite(IN1, HIGH);
    digitalWrite(IN3, HIGH);
    }
     else {
    digitalWrite(IN1, LOW);
    digitalWrite(IN3, LOW);
  }

    if (filteredX < -1 && filteredX > -7) {
    analogWrite(ENA, 150);
    analogWrite(ENB, 150);
    digitalWrite(IN2, HIGH);
    digitalWrite(IN4, HIGH);
    }
     else {
    digitalWrite(IN2, LOW);
    digitalWrite(IN4, LOW);
  }


  Serial.println(filteredX);

}

The serial prints are for when i check it connected to the computer.

Please help me and make me suggestions.

I don’t see any form of proportional control of motor speed in your code. If I read it correctly you just run the motors at one speed- on or off. Maybe this could work but I think you’d have to be lucky.

I built a self balancing robot where the motor speed was proportional to the tilt angle, so the motors only ran slower for a small tilt from the vertical and sped up the worse the tilt got. I used a PID controller library but only used P, not I or D.

You can also expect to have to spend a lot of time playing with different tuning of motor speeds. This is quite a difficult application and I think you’d have to be very lucky for it to work stably the very first time.

If it only has two wheels, it does not need roll, pitch and yaw... pick the one along the vertical plane through the center of the vehicle and the axis through the rotation (top to bottom). It won't fall "left or right."

yes, it just calculates it

I spent a lot of time tuning and I didn't have much luck. How to make it proportional to the tilt angle. Can you give me that part of your code?

No, it uses "x" in calculations. What if "x" is not in alignment and injects bad values? What if "x" is zero?

It does not look like there is any tuning. Your sketch sees an angle change and applies full power to the motors. It does nothing above 7, below -7, or from -1 to 1. I would fall over, too.

"Proportion" means, with a little angle change, apply a little motor force, and with a lot of angle, apply a lot of force. Tuning can be from the (graphed) data of your project showing "too much, too little" to find "just right." Read about PID or "proportional, integral, derivative."

that is what i was planning to do, i tried to tune it by changing the speed and the angles. Check this new code out :

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>

Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345);

#define ENA 2
#define IN1 3
#define IN2 4
#define IN3 5
#define IN4 6
#define ENB 7

float referenceAngleX = 0;
float filteredX = 0;
float alpha = 0.3;

int speed;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  if (!accel.begin()) {   
    Serial.println("Δεν βρέθηκε αισθητήρας ADXL345");
    while(1);
  }

  accel.setRange(ADXL345_RANGE_2_G);
  delay(1000);

  pinMode(ENA, OUTPUT);
  pinMode(ENB, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);

  float sum = 0;
  for (int i = 0; i < 50; i++) {
    sensors_event_t event;
    accel.getEvent(&event);

    float x = event.acceleration.x;
    float y = event.acceleration.y;
    float z = event.acceleration.z;

    float angle = atan(x / sqrt(y*y + z*z)) * 180 / PI;
    sum += angle;
    delay(10);
  }

  referenceAngleX = sum / 50.0;
}

void loop() {

  sensors_event_t event;
  accel.getEvent(&event);

  float x = event.acceleration.x;
  float y = event.acceleration.y;
  float z = event.acceleration.z;

  float angle = atan(x / sqrt(y*y + z*z)) * 180 / PI;


  float relativeX = angle - referenceAngleX;

  filteredX = alpha * relativeX + (1 - alpha) * filteredX;

  delay(10);

  if (filteredX < 1 || filteredX > -1) {
    speed = 0;
  }

  if (filteredX > 1 || filteredX < -1) {
    speed = 110;
  }

  if (filteredX > 2.3 || filteredX < -2.3) {
    speed = 140;
  } 

  if (filteredX > 3.5 || filteredX < -3.5) {
    speed = 160;
  }

  if (filteredX > 5.4 || filteredX < -5.4) {
    speed = 180;
  }

  if (filteredX > 8 || filteredX < -8) {
    speed = 210;
  }

  if (filteredX > 9.5 || filteredX < -9.5) {
    speed = 250;
  }

  if (filteredX > 1) {
    analogWrite(ENA, speed);
    analogWrite(ENB, speed);
    digitalWrite(IN1, HIGH);
    digitalWrite(IN3, HIGH);
    }
     else {
    digitalWrite(IN1, LOW);
    digitalWrite(IN3, LOW);
  }

    if (filteredX < -1) {
    analogWrite(ENA, speed);
    analogWrite(ENB, speed);
    digitalWrite(IN2, HIGH);
    digitalWrite(IN4, HIGH);
    }
     else {
    digitalWrite(IN2, LOW);
    digitalWrite(IN4, LOW);
  }


  Serial.println(speed);

}

I dont raelly undersand that part of the code, chatgbt wrote it, but the values i get from the filteredX variable are right, I believe.

You should take time to read about PID and how you use it to balance your robot.

You know the goal is to quickly, gradually, bring the robot into balance with the motors... but how? Here's a flowchart seen everywhere describing PID... All the stuff on the left feeds into the stuff on the right... and the magic happens when the result on the right "feedback" to the stuff on the left to further refine the output.

You can search for "arduino pid" and find loads of material to read (and should), like this link and this link.

Understand what you are doing (math and error feedback) and you will understand what you are doing (balancing a robot). chatGPT is, at best, a tool to consolidate your information and make it look nice. You need to know the input to know if the output is good. chatGPT can also generate GIGO like no other.

Here's an algebraic exercise... Place your two-motor vehicle on the ground, point it at a "target" and give both motors equal PWM. If your vehicle hits the target, your world is perfect (heh).... but, if the vehicle does not hit the tartget... the ratio (proportion) of the distance traveled (call it "x") to the distance the vehicle missed the target (call it "y") is the proportion of motor power you need to take from the "stronger" motor. That's a 2D example of your 3D problem. Read, experiment, repeat. You will understand how to arrive, and when chatGPT gives you GIGO.

i'll try

but other than that (and thank you really much for ypur interest) how controll the motors correctly?

knowing that the tilt value is correct?

You are controlling the motors "correctly" (enable the motors, telling each motor to the direction to move, and sending each motor the amount of movement with PWM). The answer (feedback) is in the results.

the results are dissapointing it falls

"that" is everything going into the calculation to keep it steady... that takes reading and experimenting.

Keep reading, keep experimenting.

I feel you want an answer like "7"... okay, "7"... if that did not work, start from the beginning.

i kept expirimentg, not so much reading that is why i uploaded it here, but i'll manage it

You can also reference topics on this forum using the "search" box in the upper-right of the page. It will take some looking, but this subject is well described and discussed on the forum.

i did it but every body with the same project uses a different sensor

The sensor library will have examples.

I think this is the wrong approach. The higher the CoG, the greater the toppling moment and the harder it is to correct for it. Start by making it easy for yourself with a design that is only just top-heavy and no more. Like this one:

Robot

Are you sure that your motors have enough torque to keep the robot vertical? It would be a good idea to estimate the CoG and calculate the toppling moment at, say, 10 degrees from vertical. Then your motors need to have at least that much torque at whatever speed you will run them at. Preferably a lot more. Check the datasheet.

I’m not familiar with the ADXL345 but you need to be aware that calculating the tilt angle from the three acceleration vectors like your code does will only give good results when the only acceleration is due to gravity. But a self-balancing robot is always making small accelerations, and these will mess up the readings. Sophisticated accelerometers do some clever math and give you the tilt angle directly.

Here’s my code. You’re not going to be able to use it directly but it will give you an idea. This is cascade control where the outer speed control PID loop cascades onto the inner roll control PID loop. This allows the two loops to be tuned independently of each other.

This robot uses stepper motors and the PWM pulse train goes to a stepper controller rather than a DC bridge but the principle is similar. I’m not very happy with this design - I think stepper motors are not right for this application, and one day I will rebuild it with DC motors under current control (not speed control).

    // The detailed strategy is that a speed control PID controller is cascaded onto a roll control PID controller.
    // THe setpoint of the speed controller is zero, and this has the effect of limiting any tendency for the robot
    // to run continually in any direction.

    // The roll controller (which is actually controlling pitch) carries out the balancing around zero roll.      
speedControlPID.SetSampleTime(5); 
      speedControlPID.SetOutputLimits(-speedControlRange, speedControlRange);
      speedControlPID.Start(0.0,0.0,speedControlPIDSetpoint); // input, current output and setpoint for PID controller

      rollControlPID.SetSampleTime(5);
      rollControlPID.SetOutputLimits(-rollControlRange, rollControlRange);
      rollControlPID.Start(0.0,0.0,0.0);                      // input, current output and setpoint for PID controller. Overwritten by tuning pots in next tab

      digitalWrite(DRVENBL, false);                           // drive output is low to enable
    } 

    if(oneShotOff) {digitalWrite(DRVENBL, true);}             // drive output is high to disable

    // The PID controller output is mapped to the PWM period used by TCC0 when generating the PWM pulse train to drive the motors.
    // The floating-point PID output and scaling variables are mutiplied by 10000 in the "map" function below,
    // because the absolute values can be quite low (typical max < 20),
    // and, when converted to integers, may not have sufficient granularity to drive the PWM output variable smooothly.  

    // Note that the rollDeadband value below is not applied to the measured roll in degrees, but rather to the 
    // PID controller output. The value therefore has no meaning in terms of degrees and should not be mistaken for that.
    // It is an empirical number that provides a means of disabling the motor drive output around the narrow band
    // where it is changing direction.

    speedControlPID.Setpoint(speedControlPIDSetpoint);                       // Speed control PID setpoint is zero
    speedControlPIDOutput = speedControlPID.Run(rollControlPIDOutput);       // speed control PID PV (speed) is approximated by output of roll control PID
    rollControlPID.Setpoint(-speedControlPIDOutput);                         // speed control PID output cascades to setpoint of roll control
    rollControlPIDOutput = rollControlPID.Run(measuredRollFiltered);         // PV input to PID controller is the measured roll. Setpoint is zero (i.e. vertical).

    rollControlPIDOutputFiltered = rollControlPIDOutput;                    // default unlimited case which may get overwritten if parameter has changed by more than MAXCHANGEPERSAMPLE


    // Control intent is to map PID controller output linearly to drive speed command,
    // with speed increasing with increasing controller output (tilt away from vertical).
    // First convert the PID controller output over the required control range, into 
    // the required speed in steps per second, over speed range from slowest to fastest

    int i = abs(rollControlPIDOutputFiltered * 10000.0);      // Remove sign, as the pulse train is the same in either direction.
    int j = rollControlRange * 10000.0;
    speedStepsPerSec = map(i, 0, j, speedSlowest, speedFastest);

    // Then calculate corresponding PWM period in microsecond ticks. 
    PWMperiod = (1E6 / speedStepsPerSec);                 // divide into 1E6 since PWM ticks are 1Mhz and that is what TCC0 is counting. 
    REG_TCC0_PERB = PWMperiod;                                // load desired PWM period into TCC0 PER register to generate PWM pulse train on PULSE (D5)
    while(TCC0->SYNCBUSY.bit.PERB);                           // Wait for synchronization 

    REG_TCC0_CCB1 = PWMperiod / 2;                            // Pulse train duty always half the period
    while(TCC0->SYNCBUSY.bit.CCB1);                           // Wait for synchronization 

    // This section adjusts the balance point of the robot in real time. The direction of drive as
    // required by the PID output loop is used to increase or decrease the balance point value.
    // Effectively this is a closed loop that averages out the direction of drive to close in on the perfect balance point.

    if (rollControlPIDOutputFiltered > 0.5 && sustainedOn) {
      if (COGcompensation > -45) COGcompensation -= COGadder;
    }
    if (rollControlPIDOutputFiltered < -0.5 && sustainedOn) {
      if(COGcompensation < - 25) COGcompensation += COGadder;
    }
  
    // The PID controller output is now mapped to the drive direction logic taking account of a deadband around zero.
    // Note that the variables below controlling the drive are not measured roll but the PID controller output.
    // This could theoretically lead to the situation where the drive is driven in what would be the wrong direction
    // based on the roll at that instant. However this would probably only occur if there was a lot of integral accumulated,
    // since that is the only reason why the PID output would be the opposite sign to the measured roll.
  
    if (-rollControlPIDOutputFiltered > rollDeadband){ // negative PID output will occur when roll is positive (forward).
      rollPositive = true;
      rollLessThanDeadband = false;
      rollNegative = false;
    }

    if (-rollControlPIDOutputFiltered < rollDeadband && rollControlPIDOutputFiltered > -rollDeadband){ 
      rollPositive = false;
      rollLessThanDeadband = true;
      rollNegative = false;
    }

    if (-rollControlPIDOutputFiltered < -rollDeadband){
      rollPositive = false;
      rollLessThanDeadband = false;
      rollNegative = true;
    }

    // driveForward() and DriveReverse() do not need to persist; in fact it is preferable not to issue continuous TCC0 enable instructions.
    // So generate a one-shot for each.
    driveFwdOneShot = rollPositive && !digitalRead (SWITCH) && !driveFwdOneShotSetup;
    driveFwdOneShotSetup = !(rollPositive && !digitalRead (SWITCH));
    if(driveFwdOneShot){driveForward();} 
    
    if (rollLessThanDeadband || digitalRead (SWITCH)){stopDrive();}  

    driveRevOneShot = rollNegative && !digitalRead (SWITCH) && !driveRevOneShotSetup;
    driveRevOneShotSetup = !(rollNegative && !digitalRead (SWITCH));
    if(driveRevOneShot){driveReverse();}