In this tutorial, I’ll show you how to convert a quaternion to a three-dimensional rotation matrix. At the end of this post, I have provided the Python code to perform the conversion.
What is a Quaternion?
A quaternion is one of several mathematical ways to represent the orientation and rotation of an object in three dimensions. Another way is to use Euler angle-based rotation matrices like I did on this post and this post (i.e. roll, pitch, and yaw), as well as the cover image of this tutorial.
Quaternions are often used instead of Euler angle rotation matrices because “compared to rotation matrices they are more compact, more numerically stable, and more efficient” (Source: Wikipedia).
Note that a quaternion describes just the rotation of a coordinate frame (i.e. some object in 3D space) about an arbitrary axis, but it doesn’t tell you anything about that object’s position.
The Use of Quaternions in Robotics
Quaternions are the default method of representing orientations and rotations in ROS, the most popular platform for robotics software development.
In robotics, we are always trying to rotate stuff. For example, we might observe an object in a camera. In order to get a robotic arm to grab the object, we need to rotate the camera reference frame to the robot reference frame so that the robot “knows” the location of the object in its own coordinate frame.
Once the rotation from camera pixel coordinates to robot base frame coordinates is complete, the robotic arm can then move its motors to the appropriate angles to pick up the object.
How to Represent Quaternions
Quaternions are an extension of complex numbers. However instead of two values (e.g.a + bi or x + yi…same thing) that represent a point (or vector), we have four values (a, b, c, d):
q = a + bi + cj + dk
The four values in a quaternion consist of one scalar and a 3-element unit vector.
Instead of a, b, c, and d, you will commonly see:
q = w + xi + yj + zkor q = q0 + q1i + q2j + q3k
q0 is a scalar value that represents an angle of rotation
q1, q2, and q3 correspond to an axis of rotation about which the angle of rotation is performed.
Other ways you can write a quaternion are as follows:
q = (q0, q1, q2, q3)
q = (q0, q) = q0 + q
The cool thing about quaternions is they work just like complex numbers. In two dimensions, you can rotate a vector using complex number multiplication. You can do the same with quaternions. The math is more complicated with four terms instead of two, but the principle is the same.
Let’s take a look at a two-dimensional example of complex number multiplication so that you can understand the concept of multiplying imaginary (complex) numbers to rotate a vector. Quaternions add a couple more variables to extend this concept to represent rotation in the 3D space.
2D Example
Suppose we have a vector on a 2D plane with the following specifications:
(x = 3, y = 1)
This vector can be represented in complex numbers as:
3 + i (e.g. using the x +yi form of complex numbers)
Let’s rotate this vector 45 degrees (which is π/4 in radians).
As I mentioned earlier, the math for multiplying real quaternions together is more complex than this, but the principle is the same. Multiply an orientation (represented as a quaternion) by a rotation (represented as a quaternion) to get the new orientation.
In this tutorial, we will build a self-balancing robot from scratch, step-by-step. The “brain” of the robot will be based on Arduino, a popular microcontroller (i.e. mini computer) used for robotics projects.
Real-World Applications
Perhaps the most famous real-world example of a self-balancing robot is the Segway, a two-wheeled motorized personal vehicle.
We will build an early prototype of the product you see above. Let’s get started!
You have the Arduino IDE (Integrated Development Environment) installed on your PC (Windows, MacOS, or Linux) and know how to upload a new program to an Arduino.
Requirements
Here are the requirements:
Build a two-wheeled robot that is able to self-balance.
Use control theory and sensor fusion techniques (Feedback control system, PID, Kalman Filters, etc.) to keep the robot upright.
You Will Need
This section is the complete list of components you will need for this project.
Self-balancing Robot Car Kit (I’m using the one made by keyestudio, but if that kit isn’t available, there are a number of self-balancing robot car kits on places like Amazon and eBay). A couple of other good brands are Osoyoo and Elegoo.
Electrical Components
1 UNO R3 Board and 50 cm Cable
2 GM37-520 DC Gear Motor 12V
1 Keyestudio Balance Shield V3
1 Bluetooth XBee HC-06
1 18650 3-Cell AA Battery Case
Mechanical Components
2 Motor Iron Holder
1 Acrylic Plate Pack of 2 Pieces
2 6pin 30cm Connector Wire
2 Wheels
1 Phillips Screwdriver 3*40mm
1 L Type M2 Nickel Plating Inner Hex Wrench
1 Black Winding Tube 60cm
4 Copper Pillar M3*10mm
2 Copper Hex Couplers
4 Copper Pillar M3*45mm
2 Flat Head Screw M3*12mm
12 Nickel Plating Nut M3
2 Black M4*6 Screws
4 Flat Head Screw M3*8mm
6 Round Head Screw M3*6mm
10 Round Head Screw M3*8mm
10 Round Head Screw M3*12mm
The self-balancing car kits come and go. So while the keyestudio kit I’m using is available right now (on both eBay and Amazon), it might not always be available. If that is the case, you can buy another similar-looking kit or buy all the parts separately.
Here are the key electrical components you want to make sure to have:
DC Motor With Encoder
I’m using GM37-520 DC gear motors (with built-in encoder) that have the following specifications:
Operating voltage: DC12V
Reduction ratio: 1:30
No-load current <= 100mA
No-load speed: 247rpm
Rated torque: 1.4 Kg.cm
Rated torque: 137.4mN.m
Rated speed: 160 rpm
Rated current <= 0.45A
Stalled torque: 5.5 Kg.cm
Stop current: 2.4A
Reducer length: 22mm
You want to make sure your motors have encoders to help you measure motor speed.
Another motor that works well is the JGB37-520B 12V DC Geared Motor 12rpm with Encoder Disk.
I’m using the Arduino Uno Rev3. At present, it is the most used microcontroller board in the whole Arduino family. Without it, you will be unable to program the robot.
You want a dual H-bridge motor driver chip like the TB6612FNG. This electronic component receives signals from the Arduino microcontroller board and then relays those signals to each motor.
In the kit that I bought, the TB6612FNG is already built-in to the Keyestudio Balance Shield V3.
You need to have an IMU to measure position and orientation of the car. Without the IMU, the robot will not be able to self-balance.
The IMU that I’m using is the MPU-6050 sensor. Just like the TB6612FNG motor driver chip, the MPU-6050 is already built-in to the Keyestudio Balance Shield V3. This balance shield has an operating voltage of 7-12V.
I’m not using Bluetooth in this tutorial. However, if you want to tinker around with Bluetooth so that you can control the robot with your smartphone, I recommend buying a Bluetooth module like the HC-05 or HC-06.
Lay out all the project components on a table, and make sure you have everything I listed in the parts list above.
Grab the Bottom Acrylic Plate, the 4 Copper Pillars M3*10mm, battery holder, and the 4 M3*8mm Screws.
Screw the copper pillars into the bottom acrylic plate.
Grab 2 M3*12mm Flat Head Screws and 2 Nickel Plating Nuts M3. Mount the battery holder.
Grab the 2 Motor Iron Holders, 8 M3*12mm Round Head Screws, and 8 Nickel Plating Nuts M3.
Secure the 2 Motor Iron Holders to the Bottom Acrylic Plate.
Grab both motors and mount them into the Motor Iron Holders.
You have a bag with 12 small screws. Take those screws and secure the motor into the Motor Iron Holder.
Grab both of the Copper Hex Couplers.
Grab the Hex Wrench.
Loosen the screws on both sides of each Hex Wrench. Don’t pull them all the way out though.
Snap the Copper Hex Couplers over the motors. Then tighten the screws using the wrench to secure the Hex Couplers on to the motors.
Grab the two wheels.
Grab 2 Black M4*6 Screws.
Put the wheels over the Hex Couplers, and use the 2 Black M4*6 Screws to secure the wheels into place.
Grab the UNO R3 Board and 3 Round Head Screw M3*6mm (yes 3, not 4, even though there are four holes).
Secure the UNO to the copper pillars using the screws. I like to use needle-nose pliers to hold the pillar in place while I tighten the screw.
Grab the Balance Shield.
Stack it on top of the UNO. The spikes underneath the Balance Shield need to sink into the holes of the UNO.
Grab both 6pin 30cm Connector Wires.
Use the Connector Wires to connect the motors to the closest slot on the Balance Shield. Guide the Connector Wires through the holes of the Acrylic Board then plug them in.
Grab 4 M3*45mm copper pillars and 4 M3*8mm round head screws. Screw these in.
Grab the Acrylic Top Plate and 4 M3*8MM flat-head screws. Screw in the plate.
Grab the 2 black winding tubes and use them to wrap the Wire Connectors.
That’s it for the assembly. You should have a lot of extra screws and a couple nuts. That’s fine.
Here is how the robot should look:
Now, at this stage, you should install the Arduino IDE on your Windows, Linux, or Mac computer.
Plug the Arduino into the USB port of your computer using the USB cord.
Make sure your robot is upside down with the Acrylic plate on the bottom and the wheels facing upwards.
Copy and paste the code below into a new Arduino sketch.
// This is the code we use to test the motors of the keyesstudio Self-balancing car kit.
// Source: https://wiki.keyestudio.com/Ks0193_keyestudio_Self-balancing_Car
// Code Modified by Addison Sears-Collins at https://automaticaddison.com
// Digital Pins 8 and 12 control the right motor's direction.
// Speed is controlled by Digital Pin 10.
const int right_R1=8;
const int right_R2=12;
const int PWM_R=10;
// Digital Pins 7 and 6 control the left motor's direction.
// Speed is controlled by Digital Pin 9.
const int left_L1=7;
const int left_L2=6;
const int PWM_L=9;
void setup()
{
//set the baud rate to 9600
Serial.begin(9600);
// Set all the pins to OUTPUT
// The motors' speed and direction is controlled by
// the output of these pins
pinMode(right_R1,OUTPUT);
pinMode(right_R2,OUTPUT);
pinMode(PWM_R,OUTPUT);
pinMode(left_L1,OUTPUT);
pinMode(left_L2,OUTPUT);
pinMode(PWM_L,OUTPUT);
}
void loop()
{
// Go forward for 3 seconds
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,LOW);
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,HIGH);
analogWrite(PWM_R,100); // write into PWM value 0~255(speed)
analogWrite(PWM_L,100);
delay(3000);
// Stop for 3 seconds
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,HIGH);
digitalWrite(left_L1,HIGH);
digitalWrite(left_L2,HIGH);
analogWrite(PWM_R,100); // write into PWM value 0~255(speed)
analogWrite(PWM_L,100);
delay(3000);
// Reverse for 3 seconds
digitalWrite(right_R1,LOW);
digitalWrite(right_R2,HIGH);
digitalWrite(left_L1,HIGH);
digitalWrite(left_L2,LOW);
analogWrite(PWM_R,100); // write into PWM value 0~255(speed)
analogWrite(PWM_L,100); // The higher the PWM value, the faster the speed
delay(3000);
// Stop for 3 seconds
// Notice that we can do all LOW or all HIGH to stop the robot.
digitalWrite(right_R1,LOW);
digitalWrite(right_R2,LOW);
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,LOW);
analogWrite(PWM_R,100); // write into PWM value 0~255(speed)
analogWrite(PWM_L,100);
delay(3000);
}
Upload the sketch below into your Arduino. This sketch makes the motors go forward for three seconds, stop for three seconds, reverse for three seconds, and stop for three seconds. This code repeats over and over again.
To stop the sketch at any time, you can upload a blank sketch to the Arduino.
In the previous section, we learned how to set the speed and direction of each motor of our robot. Now, let’s see how we can measure the actual speed of each motor. To do that, we will use the built-in hall encoder.
A motor uses magnets to convert incoming electricity into motion. A hall encoder is able to measure the motion of the motor by detecting variations in the magnetic field as the motor (and its magnets) spins around and around. The output from a hall encoder is a voltage, which is directly proportional to the strength of the magnetic field. The Arduino, in turn, measures the output voltage of the hall encoder.
We will define the speed of each motor as the number of pulses measured by the Hall encoder within 100ms. A pulse in this context is defined when the voltage read by the digital pin on the Arduino goes from HIGH (i.e. high voltage) to LOW (i.e. low voltage) or from LOW to HIGH.
Let’s open a new sketch in the Arduino IDE.
Go to File -> Save As, and save the new sketch as hall_encoder_test.
Upload the code to your board:
// This is the code we use to test the hall encoder on the Keyesstudio self-balancing car
// Speed is based on the number of pulses generated by the Hall encoder within 100ms
// Source: https://wiki.keyestudio.com/Ks0193_keyestudio_Self-balancing_Car
// Code Modified by Addison Sears-Collins at https://automaticaddison.com
// Digital Pins 8 and 12 control the right motor.
// Speed is controlled by Digital Pin 10.
const int right_R1=8;
const int right_R2=12;
const int PWM_R=10;
// Digital Pins 7 and 6 control the left motor's direction.
// Speed is controlled by Digital Pin 9.
const int left_L1=7;
const int left_L2=6;
const int PWM_L=9;
const int PinA_left = 1; // set the left motor’s pulse pin to Digital Pin 1
const int PinA_right = 4; // set the right motor’s pulse pin to Digital Pin 4
int times = 0; // Time
int newtime = 0; // New Time
int d_time = 100; // Time Interval set to 100ms
int valA = 0; // Number of pulses for left motor
int flagA = 0;
int valB = 0; // Number of pulses for right motor
int flagB = 0;
void setup() {
//set the baud rate to 9600
Serial.begin(9600);
// Set all the pins to OUTPUT
// The motors' speed and direction is controlled by
// the output of these pins
pinMode(right_R1,OUTPUT);
pinMode(right_R2,OUTPUT);
pinMode(PWM_R,OUTPUT);
pinMode(left_L1,OUTPUT);
pinMode(left_L2,OUTPUT);
pinMode(PWM_L,OUTPUT);
// The pulse pins are set to INPUT
pinMode(PinA_right,INPUT);
pinMode(PinA_left,INPUT);
}
void loop() {
// Go forward
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,LOW);
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,HIGH);
// write into PWM value 0~255(speed)
analogWrite(PWM_R,100); // Right wheel
analogWrite(PWM_L,200); // Set Left wheel to be faster than the right wheel
// Set newtime and times to Returns the number of
// milliseconds passed since the Arduino board began
// running the current program
newtime = times = millis();
while ((newtime-times) < d_time) { // If we are less than 100ms, keep looping
if(digitalRead(PinA_left) == HIGH && flagA == 0) { // If HIGH is detected
valA++; // valA plus 1
flagA = 1;
}
if(digitalRead(PinA_left) == LOW && flagA == 1) { // if LOW is detected
valA++; // valA plus 1
flagA = 0;
}
if(digitalRead(PinA_right) == HIGH && flagB == 0) { // if HIGH is detected
valB++; // valB plus 1
flagB = 1;
}
if(digitalRead(PinA_right) == LOW && flagB == 1) { // if LOW is detected
valB++;
flagB = 0;
}
newtime = millis(); // Update the new time
}
// Print out the number of pulses in 100ms for the left motor
Serial.println(String("Left Motor: ") + valA);
// Print out the number of pulses in 100ms for the right motor
// valA (left motor) should be greater than valB (right motor)
Serial.println(String("Right Motor: ") + valB);
valA = valB = 0; // Reset the values to 0
}
Click the magnifying glass in the upper right portion of the IDE.
Here is what you should see for your output. You see the number of pulses measured by the Hall encoder of each motor within 100ms.
When you’re ready, you can upload a blank sketch to your board to stop the wheels from turning.
In the previous code, to calculate the time elapsed between intervals of the loop, we based the calculation on the millis() Arduino function which measures the number of milliseconds elapsed since the program started.
The disadvantage of this method is that each loop is not precisely 100 milliseconds. We can get more precise timing by using our Arduino’s built-in timer, so let’s do that now.
Open up the Arduino IDE. If you have trouble opening it up, and you’re using Windows, go to C:\Users\<username>\AppData\Local\Arduino15 and delete everything other than the preferences.txt file.
Now install the MsTimer2 library by going to Sketch -> Include Library -> Manage Libraries.
Then type MsTimer2 in the search box and press Enter.
Click Install.
Then click OK once it has installed.
Now open up a new sketch in the Arduino IDE, and type the following code:
// This is the code to test the built-in internal timer in the Arduino
// Speed is based on the number of pulses generated by the Hall encoder within 100ms
// Source: https://wiki.keyestudio.com/Ks0193_keyestudio_Self-balancing_Car
// Code Modified by Addison Sears-Collins at https://automaticaddison.com
#include <MsTimer2.h>
// Digital Pins 8 and 12 control the right motor.
// Speed is controlled by Digital Pin 10.
const int right_R1=8;
const int right_R2=12;
const int PWM_R=10;
// Digital Pins 7 and 6 control the left motor's direction.
// Speed is controlled by Digital Pin 9.
const int left_L1=7;
const int left_L2=6;
const int PWM_L=9;
const int PinA_left = 1; // set the left motor’s pulse pin to Digital Pin 1
const int PinA_right = 4; // set the right motor’s pulse pin to Digital Pin 4
int times = 0; // Time
int newtime = 0; // New Time
int d_time = 100; // Time Interval set to 100ms
int valA = 0; // Number of pulses for left motor
int flagA = 0;
int valB = 0; // Number of pulses for right motor
int flagB = 0;
void setup() {
//set the baud rate to 9600
Serial.begin(9600);
// Set all the pins to OUTPUT
// The motors' speed and direction is controlled by
// the output of these pins
pinMode(right_R1,OUTPUT);
pinMode(right_R2,OUTPUT);
pinMode(PWM_R,OUTPUT);
pinMode(left_L1,OUTPUT);
pinMode(left_L2,OUTPUT);
pinMode(PWM_L,OUTPUT);
// The pulse pins are set to INPUT
pinMode(PinA_right,INPUT);
pinMode(PinA_left,INPUT);
MsTimer2::set(100, inter); // trigger an interrupt every 100ms
MsTimer2::start(); // start interrupt
}
void loop() {
// Go forward
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,LOW);
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,HIGH);
// write into PWM value 0~255(speed)
analogWrite(PWM_R,100); // Right wheel
analogWrite(PWM_L,200); // Set Left wheel to be faster than the right wheel
if(digitalRead(PinA_left) == HIGH && flagA == 0) { // If HIGH is detected
valA++; // valA plus 1
flagA = 1;
}
if(digitalRead(PinA_left) == LOW && flagA == 1) { // if LOW is detected
valA++; // valA plus 1
flagA = 0;
}
if(digitalRead(PinA_right) == HIGH && flagB == 0) { // if HIGH is detected
valB++; // valB plus 1
flagB = 1;
}
if(digitalRead(PinA_right) == LOW && flagB == 1) { // if LOW is detected
valB++;
flagB = 0;
}
}
//interrupt function
void inter() {
sei(); //allow whole interrupts
Serial.print("Left Motor = "); //print out the pulse value on serial monitor
Serial.println(valA);
Serial.print("Right Motor: = ");
Serial.println(valB);
valA = valB = 0;
}
Save the file as internal_timer_test.ino.
Upload the code to your board, and then click the magnifying glass in the upper right portion of the IDE.
This sensor has a gyroscope to measure the angular velocity of the robot (relative to three different axes, x, y, and z) and an accelerometer to measure the linear acceleration of the robot (along three different axes, x, y, and z).
Our MPU6050’s gyroscope reports angular velocity in degrees/second. The range is +-250°/second.
Acceleration is reported in terms of G-forces (g). A single G-force (i.e. 1g) here on Earth is 9.8 m/s2.. The acceleration range is +-2g.
Open up a new sketch (test_mpu6050.ino) in Arduino, and type the following code. I recommend you type it one line at a time so that you read the comments slowly and understand what is going on:
Upload the code to your Arduino board, and save it as test_mpu6050.
/**
* Addison Sears-Collins
* June 11, 2020
* This code is used to test the MPU6050 IMU sensor chip that is built-in
* to the Keyestudio Balance Shield V3.
* This 3-axis gyroscope and the 3-axis accelerometer enable
* us to measure the angular velocity (in degrees/second)
* and the linear acceleration (in G-forces, g) of the
* self-balancing robot car.
*
**/
// Arduino's standard library for I2C communication
#include <Wire.h>
// Used to store accelerometer data
long accelX, accelY, accelZ;
float gForceX, gForceY, gForceZ;
// Used to store the gyroscope data
long gyroX, gyroY, gyroZ;
float rotX, rotY, rotZ; // Angular velocity around those axes
void setup() {
// Baud rate
Serial.begin(9600);
// Initialize I2C communication
Wire.begin();
setupMPU();
}
void loop() {
recordAccelRegisters();
recordGyroRegisters();
printData();
delay(100); // Delay of 100ms
}
/**
* Establish communication between Arduino and the MPU6050.
* Set up all registers we'll be using to read MPU6050 data
*/
void setupMPU(){
// REGISTER 0x6B/REGISTER 107:Power Management 1
// This is the I2C address of the MPU
//(b1101000/b1101001 for AC0 low/high datasheet Sec. 9.2)
// You can do a Google search to find the MPU6050 datasheet.
Wire.beginTransmission(0b1101000);
//Accessing the register 6B/107 - Power Management (Sec. 4.28)
Wire.write(0x6B);
//Setting SLEEP register to 0, using the internal 8 Mhz oscillator
Wire.write(0b00000000);
Wire.endTransmission();
// REGISTER 0x1b/REGISTER 27:Gyroscope Configuration
//I2C address of the MPU
Wire.beginTransmission(0b1101000);
//Accessing the register 1B - Gyroscope Configuration (Sec. 4.4)
Wire.write(0x1B);
// Setting the gyro to full scale +/- 250deg./s
// (250/360 * 60 = 41.67rpm)
// The highest can be converted to 2000deg./s (333.3 rpm), but
// gyro sensitivity will be reduced.
Wire.write(0x00000000);
Wire.endTransmission();
// REGISTER 0x1C/REGISTER 28:ACCELEROMETER CONFIGURATION
//I2C address of the MPU
Wire.beginTransmission(0b1101000);
// Accessing the register 1C - Acccelerometer Configuration (Sec. 4.5)
Wire.write(0x1C);
// Setting the accel to +/- 2g(if choose +/- 16g,
// the value would be 0b00011000)
Wire.write(0b00000000);
Wire.endTransmission();
}
void recordAccelRegisters() {
// REGISTER 0x3B~0x40/REGISTER 59~64
Wire.beginTransmission(0b1101000); //I2C address of the MPU
Wire.write(0x3B); //Starting register for Accelerometer Readings (see datasheet)
Wire.endTransmission();
Wire.requestFrom(0b1101000,6); //Request Accel Registers (3B - 40)
// Use left shift << and bit operations | Wire.read() read once 1bytes,and automatically read the data of the next address on the next call.
while(Wire.available() < 6); // Waiting for all the 6 bytes data to be sent from the slave machine (Must wait for all data to be stored in the buffer before reading)
accelX = Wire.read()<<8|Wire.read(); //Store first two bytes into accelX (Automatically stored as a defined long value)
accelY = Wire.read()<<8|Wire.read(); //Store middle two bytes into accelY
accelZ = Wire.read()<<8|Wire.read(); //Store last two bytes into accelZ
processAccelData();
}
// Divide the raw register reading to get a value that is meaningful for us
void processAccelData(){
gForceX = accelX / 16384.0; //float = long / float
gForceY = accelY / 16384.0;
gForceZ = accelZ / 16384.0;
}
void recordGyroRegisters() {
// REGISTER 0x43~0x48/REGISTER 67~72
Wire.beginTransmission(0b1101000); //I2C address of the MPU
Wire.write(0x43); //Starting register for Gyro Readings
Wire.endTransmission();
Wire.requestFrom(0b1101000,6); //Request Gyro Registers (43 ~ 48)
while(Wire.available() < 6);
gyroX = Wire.read()<<8|Wire.read(); //Store first two bytes into accelX
gyroY = Wire.read()<<8|Wire.read(); //Store middle two bytes into accelY
gyroZ = Wire.read()<<8|Wire.read(); //Store last two bytes into accelZ
processGyroData();
}
// The 131.0 comes from the MPU6050 datasheet. It helps us convert
// the raw data into a meaningful value.
void processGyroData() {
rotX = gyroX / 131.0;
rotY = gyroY / 131.0;
rotZ = gyroZ / 131.0;
}
// Print out 3-axis gyroscope data and 3-axis accelerometer data
void printData() {
Serial.print("Gyro (deg)");
Serial.print(" X=");
Serial.print(rotX);
Serial.print(" Y=");
Serial.print(rotY);
Serial.print(" Z=");
Serial.print(rotZ);
Serial.print(" Accel (g)");
Serial.print(" X=");
Serial.print(gForceX);
Serial.print(" Y=");
Serial.print(gForceY);
Serial.print(" Z=");
Serial.println(gForceZ);
}
Now open the Serial Monitor by clicking on the magnifying glass in the upper-right part of the Integrated Development Environment (IDE).
Here is what you should see:
The gyroscope readings show the speed at which you rotate about a certain axis. For example, imagine your robot is on a table with the wheels touching the table. The z-axis goes straight into the table.
If you rotate the robot clockwise, the Gyro reading (i.e. angular velocity) for Z will decrease until you stop rotating it.
Likewise, if you rotate the robot counterclockwise, the Gyro reading for Z will increase until you stop rotating the board.
The accelerometer readings are a measure of the forces acting on it. So, for example, when the robot is stationary with wheels on the table, the only force acting on it is the force in the z-direction (through the table) due to gravity. The reading in this case is ~1g due to the force of gravity long the z direction.
If you were to move the robot along the x-direction (i.e. forward), the accelerometer would measure the acceleration along the x-direction in terms of Earth’s gravity (i.e. 1g).
We now know how to use the gyroscope and accelerometer of the MPU6050, and we’ve verified the MPU6050 is working properly. Now, let’s use the MPU6050 to calculate the angle that the vehicle is tilting. I’ll call it the tilt angle. We will also measure the angular velocity of the self-balancing robot.
The first thing we need to do is install the MPU6050 library by Electronic Cats.
Go to Sketch -> Include Library -> Manage Libraries.
Then type “mpu6050”in the search box and press Enter.
Find the MPU6050 library by Electronic Cats.
Click Install.
Then click OK once it has installed.
Now open up a new sketch in the Arduino IDE, and type the following code:
Save the file as calc_tilt_angle.
Upload the code to your board, and then click the magnifying glass in the upper right portion of the IDE.
/**
* Addison Sears-Collins
* June 13, 2020
* This code is uses the MPU6050 IMU sensor chip that
* is built-in to the Keyestudio Balance Shield V3 to
* calculate the angle that the vehicle is tilting.
**/
// MPU6050 library
#include <MPU6050.h>
// Arduino's standard library for I2C communication
#include <Wire.h>
// Create an MPU6050 object named mpu6050
MPU6050 mpu6050;
// Define three-axis acceleration and
// three-axis gyroscope variables
// 16-bit signed integer is the data type
int16_t ax, ay, az, gx, gy, gz;
// Variable that will hold the tilt angle
float Angle;
// Variable that will hold the angle velocity
int16_t Gyro_x;
void setup() {
// Initialize I2C communication
Wire.begin();
// Set the baud rate
Serial.begin(9600);
// Don't do anything for 1500 milliseconds (1.5 seconds)
delay(1500);
// Initialize the MPU6050
mpu6050.initialize();
// Do nothing for 2 milliseconds
delay(2);
}
void loop() {
// I2C to get MPU6050 six-axis data ax ay az gx gy gz
mpu6050.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
// Radial rotation angle calculation formula;
// The negative sign indicates the direction.
// Convert radians to degrees.
Angle = -atan2(ay , az) * (180/ PI);
// The x-axis angular velocity calculated by the gyroscope;
// The negative sign indicates the direction. The 131 value comes
// from the MPU6050 datasheet.
Gyro_x = -gx / 131;
// Print the tilt angle in degrees
Serial.print("Angle = ");
Serial.print(Angle);
// Print the angular velocity in degrees per second
Serial.print(" Gyro_x = ");
Serial.println(Gyro_x);
}
Here is what you should see for your output:
Notice that as you tilt the robot back and forth, the value for the tilt angle and angular velocity change accordingly.
Calculate the Tilt Angle and Angular Velocity Using a Kalman Filter
In the previous sketch, we calculated the tilt angle and angular velocity of the robot directly from the MPU6050. However, sensor measurements contain error and noise, so we can never be 100% certain if the tilt angle and angular velocity values are precisely what we see printed out to the Serial Monitor.
So how can we minimize this measurement error so that we can calculate readings of the tilt angle and angular velocity that are as accurate as possible? A time-tested technique is to use what is known as a Kalman Filter.
A Kalman Filter is an algorithm that uses a series of sensor measurements over time (rather than just a single estimate) to estimate the true state (i.e. the true tilt angle and angular velocity) of an object (i.e. the self-balancing robot). By taking into account both past and current measurements, a Kalman Filter can be used to help “filter” out the noise and jitter involved in noisy and error-prone sensor (i.e. MPU6050) data. The result is increased accuracy and more stable readings.
Another thing the Kalman filter enables us to do is to average the data from the accelerometer and the gyroscope to get a more robust estimate of the title angle than if we were to just use data from the accelerometer (i.e. sensor fusion).
Up until now, you have seen how to calculate the tilt angle of the robot. In this section, we’ll use that information along with some control theory to write a program that will enable our self-balancing robot to stand upright on its wheels.
Before we open up the Arduino IDE, let’s take a brief look at some control theory.
A PID controller (proportional–integral–derivative controller) is one of the most common control systems used in robotics. It is an algorithm that can help you keep a variable (e.g. temperature in an oven, speed for a car, angle for an airplane, etc.) at a certain setpoint.
It works by continuously calculating the error between a desired value (i.e. setpoint) and the measured value. It then applies calculus to determine the correction that is needed to reduce the difference between the measured value and the desired value.
A common PID controller is the one in your cruise control system on your car. Imagine you had your cruise control set to a certain speed, like 60 miles per hour. If you tried to go up a hill, your car would slow down.
The PID controller algorithm in your cruise control calculates the difference between your measured speed and your desired speed (i.e. setpoint) of 60 miles per hour. It then uses some mathematics (three terms: the Proportional Term, Integral Term, and Derivative Term) to restore your speed to the desired speed by applying more engine power (and thereby, causing the wheels to turn faster). The cool thing about the PID controller is it offers smoother adjustments than if you were to code up a brute force control system like the following:
If (current speed > desired speed):
Apply less engine power
Else
Apply more engine power
That algorithm above would cause your car to jerk up and down in speed and wouldn’t be a smooth way to control your speed around 60 mph. If you are going slower than your desired speed, the car would accelerate upwards and might even overshoot to like 70 mph. If you are going too fast, the car might slow down too quickly and overshoot on the low end to some value (e.g. 45 mph). The driver in the car behind you would certainly think you’ve gone mad!
By using calculus, a PID controller (i.e. algorithm) helps the engine make smooth adjustments to keep your car moving at your desired speed. That’s PID in a nutshell.
You don’t need to master all the math behind a PID controller, but if you have some spare time one day, the best explanation of PID control is in Rick Anderson’s book Pro Arduino. In that book, he implements a basic PID controller step-by-step.
Just remember that a PID controller is like a thermostat in your house or cruise control in your car. When you want some measured variable to remain constant, a PID controller is a valuable algorithmic tool.
We know how to measure the tilt angle and the angular velocity of our robot. In order to keep our robot self-balanced, we need to write a controller so the robot knows how to move the wheels to keep it from falling over due to gravity.
Here, we will write a controller using the theory we learned in the last section to keep our robot balanced on a flat surface.
Click the green button that says “Clone or Download.”
Click the button that says “Download Zip”.
Go back to your Arduino IDE, and go to:
Sketch -> Include Library -> Add .Zip Library
Find the PinChangeInt library, and click Open in order to add it to your IDE.
Open a new sketch and save it as pi_controlled_self_balancing_robot.ino.
Type the following code:
#include <MsTimer2.h> //internal timer 2
#include <PinChangeInt.h> //This library file can make all pins on the REV4 board as external interrupts.
#include <MPU6050.h> //MPU6050 library
#include <Wire.h> //IIC library
MPU6050 mpu6050; //Instantiate an MPU6050 object; name mpu6050
int16_t ax, ay, az, gx, gy, gz; // Define three-axis acceleration, three-axis gyroscope variables
//TB6612 pins
const int right_R1=8;
const int right_R2=12;
const int PWM_R=10;
const int left_L1=7;
const int left_L2=6;
const int PWM_L=9;
///////////////////////angle parameters//////////////////////////////
float angle_X; //Calculate the tilt angle variable about the X axis from the acceleration
float angle_Y; //Calculate the tilt angle variable about the Y axis from the acceleration
float angle0 = 1; //Actual measured angle (ideally 0 degrees)
float Gyro_x,Gyro_y,Gyro_z; //Angular angular velocity by gyroscope calculation
///////////////////////angle parameters//////////////////////////////
///////////////////////Kalman_Filter////////////////////////////
float Q_angle = 0.001; //Covariance of gyroscope noise
float Q_gyro = 0.003; //Covariance of gyroscope drift noise
float R_angle = 0.5; //Covariance of accelerometer
char C_0 = 1;
float dt = 0.005; // The value of dt is the filter sampling time.
float K1 = 0.05; //a function containing the Kalman gain is used to calculate the deviation of the optimal estimate
float K_0,K_1,t_0,t_1;
float angle_err;
float q_bias; //Gyro drift
float accelz = 0;
float angle;
float angleY_one;
float angle_speed;
float Pdot[4] = { 0, 0, 0, 0};
float P[2][2] = {{ 1, 0 }, { 0, 1 }};
float PCt_0, PCt_1, E;
//////////////////////Kalman_Filter/////////////////////////
//////////////////////PID parameters///////////////////////////////
double kp = 34, ki = 0, kd = 0.62; //angle loop parameters
double kp_speed = 3.6, ki_speed = 0.080, kd_speed = 0; // speed loop parameters
double setp0 = 0; //angle balance point
int PD_pwm; //angle output
float pwm1=0,pwm2=0;
//////////////////Interrupt speed measurement/////////////////////////////
#define PinA_left 5 //external interrupts
#define PinA_right 4 //external interrupts
volatile long count_right = 0;//Used to calculate the pulse value calculated by the Hall encoder (the volatile long type is to ensure the value is valid)
volatile long count_left = 0;
int speedcc = 0;
//////////////////////pulse calculation/////////////////////////
int lz = 0;
int rz = 0;
int rpluse = 0;
int lpluse = 0;
int pulseright,pulseleft;
////////////////////////////////PI variable parameters//////////////////////////
float speeds_filterold=0;
float positions=0;
int flag1;
double PI_pwm;
int cc;
int speedout;
float speeds_filter;
void setup()
{
// set the pins of motor to OUTPUT
pinMode(right_R1,OUTPUT);
pinMode(right_R2,OUTPUT);
pinMode(left_L1,OUTPUT);
pinMode(left_L2,OUTPUT);
pinMode(PWM_R,OUTPUT);
pinMode(PWM_L,OUTPUT);
//assign initial state value
digitalWrite(right_R1,1);
digitalWrite(right_R2,0);
digitalWrite(left_L1,0);
digitalWrite(left_L2,1);
analogWrite(PWM_R,0);
analogWrite(PWM_L,0);
pinMode(PinA_left, INPUT); //speed code wheel input
pinMode(PinA_right, INPUT);
// join I2C bus
Wire.begin(); //join I2C bus sequence
Serial.begin(9600); //open the serial monitor to set the baud rate to 9600
delay(1500);
mpu6050.initialize(); //initialize MPU6050
delay(2);
//5ms; use timer2 to set timer interruption (note:using timer2 will affect the PWM output of pin3 pin11)
MsTimer2::set(5, Interrupt_Service_Routine); //5ms ; execute the function Interrupt_Service_Routine once
MsTimer2::start(); //start interrupt
}
void loop()
{
Serial.println(angle);
delay(100);
//Serial.println(PD_pwm);
//Serial.println(pwm1);
//Serial.println(pwm2);
//Serial.print("pulseright = ");
//Serial.println(pulseright);
//Serial.print("pulseleft = ");
//Serial.println(pulseleft);
//Serial.println(PI_pwm);
//Serial.println(speeds_filter);
//Serial.println (positions);
//External interrupt for calculating wheel speed
attachPinChangeInterrupt(PinA_left, Code_left, CHANGE); //PinA_left Level change triggers external interrupt; execute subfunction Code_left
attachPinChangeInterrupt(PinA_right, Code_right, CHANGE); //PinA_right Level change triggers external interrupt; execute subfunction Code_right
}
/////////////////////Hall calculation/////////////////////////
//left speed code wheel count
void Code_left()
{
count_left ++;
}
//Right speed code wheel count
void Code_right()
{
count_right ++;
}
////////////////////pulse calculation///////////////////////
void countpluse()
{
lz = count_left; //Assign the value counted by the code wheel to lz
rz = count_right;
count_left = 0; //Clear the code counter count
count_right = 0;
lpluse = lz;
rpluse = rz;
if ((pwm1 < 0) && (pwm2 < 0)) //judge the moving direction; if backwards(PWM, namely motor voltage is negative), pulse number is a negative number
{
rpluse = -rpluse;
lpluse = -lpluse;
}
else if ((pwm1 > 0) && (pwm2 > 0)) // if backwards(PWM, namely motor voltage is positive), pulse number is a positive number
{
rpluse = rpluse;
lpluse = lpluse;
}
else if ((pwm1 < 0) && (pwm2 > 0)) //Judge turning direction of the car; turn left; Right pulse number is a positive number; Left pulse number is a negative number.
{
rpluse = rpluse;
lpluse = -lpluse;
}
else if ((pwm1 > 0) && (pwm2 < 0)) //Judge turning direction of the car; turn right; Right pulse number is a negative number; Left pulse number is a positive number.
{
rpluse = -rpluse;
lpluse = lpluse;
}
//enter interrupts per 5ms; pulse number superposes
pulseright += rpluse;
pulseleft += lpluse;
}
/////////////////////////////////interrupts////////////////////////////
void Interrupt_Service_Routine()
{
sei(); //Allow global interrupts
countpluse(); //Pulse superposition subfunction
mpu6050.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); //IIC to get MPU6050 six-axis data ax ay az gx gy gz
angle_calculate(ax, ay, az, gx, gy, gz, dt, Q_angle, Q_gyro, R_angle, C_0, K1); //get angle and Kalman filtering
PD(); //angle loop PD control
anglePWM();
cc++;
if(cc>=8) //5*8=40,40ms entering once speed PI algorithm
{
speedpiout();
cc=0; //Clear
}
}
///////////////////////////////////////////////////////////
/////////////////////////////angle calculation///////////////////////
void angle_calculate(int16_t ax,int16_t ay,int16_t az,int16_t gx,int16_t gy,int16_t gz,float dt,float Q_angle,float Q_gyro,float R_angle,float C_0,float K1)
{
float Angle = -atan2(ay , az) * (180/ PI); //Radial rotation angle calculation formula; negative sign is direction processing
Gyro_x = -gx / 131; //The X-axis angular velocity calculated by the gyroscope; the negative sign is the direction processing
Kalman_Filter(Angle, Gyro_x); //Kalman Filtering
//Rotation angle Z-axis parameter
Gyro_z = -gz / 131; //Z-axis angular velocity
//accelz = az / 16.4;
float angleAx = -atan2(ax, az) * (180 / PI); //Calculate the angle with the x-axis
Gyro_y = -gy / 131.00; //Y-axis angular velocity
Yiorderfilter(angleAx, Gyro_y); //first-order filtering
}
////////////////////////////////////////////////////////////////
///////////////////////////////KalmanFilter/////////////////////
void Kalman_Filter(double angle_m, double gyro_m)
{
angle += (gyro_m - q_bias) * dt; //Prior estimate
angle_err = angle_m - angle;
Pdot[0] = Q_angle - P[0][1] - P[1][0]; //Differential of azimuth error covariance
Pdot[1] = - P[1][1];
Pdot[2] = - P[1][1];
Pdot[3] = Q_gyro;
P[0][0] += Pdot[0] * dt; //A priori estimation error covariance differential integral
P[0][1] += Pdot[1] * dt;
P[1][0] += Pdot[2] * dt;
P[1][1] += Pdot[3] * dt;
//Intermediate variable of matrix multiplication
PCt_0 = C_0 * P[0][0];
PCt_1 = C_0 * P[1][0];
//Denominator
E = R_angle + C_0 * PCt_0;
//gain value
K_0 = PCt_0 / E;
K_1 = PCt_1 / E;
t_0 = PCt_0; //Intermediate variable of matrix multiplication
t_1 = C_0 * P[0][1];
P[0][0] -= K_0 * t_0; //Posterior estimation error covariance
P[0][1] -= K_0 * t_1;
P[1][0] -= K_1 * t_0;
P[1][1] -= K_1 * t_1;
q_bias += K_1 * angle_err; //Posterior estimate
angle_speed = gyro_m - q_bias; //The differential of the output value gives the optimal angular velocity
angle += K_0 * angle_err; ////Posterior estimation to get the optimal angle
}
/////////////////////first-order filtering/////////////////
void Yiorderfilter(float angle_m, float gyro_m)
{
angleY_one = K1 * angle_m + (1 - K1) * (angleY_one + gyro_m * dt);
}
//////////////////angle PD////////////////////
void PD()
{
PD_pwm = kp * (angle + angle0) + kd * angle_speed; //PD angle loop control
}
//////////////////speed PI////////////////////
void speedpiout()
{
float speeds = (pulseleft + pulseright) * 1.0; //Vehicle speed pulse value
pulseright = pulseleft = 0; //Clear
speeds_filterold *= 0.7; //first-order complementary filtering
speeds_filter = speeds_filterold + speeds * 0.3;
speeds_filterold = speeds_filter;
positions += speeds_filter;
positions = constrain(positions, -3550,3550); //Anti-integral saturation
PI_pwm = ki_speed * (setp0 - positions) + kp_speed * (setp0 - speeds_filter); //speed loop control PI
}
//////////////////speed PI////////////////////
////////////////////////////PWM end value/////////////////////////////
void anglePWM()
{
pwm2=-PD_pwm - PI_pwm ; //assign the final value of PWM to motor
pwm1=-PD_pwm - PI_pwm ;
if(pwm1>255) //limit PWM value not greater than 255
{
pwm1=255;
}
if(pwm1<-255)
{
pwm1=-255;
}
if(pwm2>255)
{
pwm2=255;
}
if(pwm2<-255)
{
pwm2=-255;
}
// When the self-balancing trolley’s tilt angle gets less than a certain angle,
// the motor will stop.
if(angle>25 || angle<-25)
{
pwm1=pwm2=0;
}
// Determine the motor’s steering and speed by the positive and negative of PWM
if(pwm2>=0)
{
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,HIGH);
analogWrite(PWM_L,pwm2);
}
else
{
digitalWrite(left_L1,HIGH);
digitalWrite(left_L2,LOW);
analogWrite(PWM_L,-pwm2);
}
if(pwm1>=0)
{
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,LOW);
analogWrite(PWM_R,pwm1);
}
else
{
digitalWrite(right_R1,LOW);
digitalWrite(right_R2,HIGH);
analogWrite(PWM_R,-pwm1);
}
}
Upload the code to your Arduino.
Unplug your Arduino from your desktop or laptop computer.
Add three 3.7V batteries to the battery holder.
Plug the battery holder into the Balance Shield (not the Arduino board underneath).
Now, hold your robot stationary and upright with your hand with the wheels touching a flat surface (e.g. floor, table, desk, etc.).
Turn the switch on the side of the Balance Shield to the ON position.
The robot should self-balance.
If the motors fire and cause the robot to shoot away, grab the robot, and try again. Hold the robot really still and upright as you switch the toggle to ON on the Balance Shield. For the first few seconds that the robot is on, hold it still, the robot should begin to self-balance.
Another thing you can do, if you are having trouble is to tweak the angle in this line of code:
// When the self-balancing trolley’s tilt angle gets less than
// a certain angle, the motor will stop.
if(angle>25 || angle<-25)
{
pwm1=pwm2=0;
}
I used 25, but you can try other values like 50 or even 10. Don’t give up. Keep trying different numbers until you get the robot to self-balance. In robotics, it is almost always the rule that things don’t work as you expect the first time around.
Here is another piece of code courtesy of Keyestudio. This particular code makes the move around in circles. By changing the value assigned to the “val” variable on this line, you can make the robot go forwards (‘F’), backwards (‘B’), left (‘L’), right (‘R’), and stop (‘S’). I saved this code as go_forward_pi_controlled_self_balancing_robot.
#include <MsTimer2.h> //internal timer 2
#include <PinChangeInt.h> //this library can make all pins of arduino REV4 as external interrupt
#include <MPU6050.h> //MPU6050 library
#include <Wire.h> //IIC communication library
MPU6050 mpu6050; //Instantiate an MPU6050 object; name mpu6050
int16_t ax, ay, az, gx, gy, gz; //Define three-axis acceleration, three-axis gyroscope variables
//TB6612 pins
const int right_R1=8;
const int right_R2=12;
const int PWM_R=10;
const int left_L1=7;
const int left_L2=6;
const int PWM_L=9;
///////////////////////angle parameters//////////////////////////////
float angle_X; //calculate the inclined angle variable of X-axis by accelerometer
float angle_Y; //calculate the inclined angle variable of Y-axis by accelerometer
float angle0 = 1; //Actual measured angle (ideally 0 degrees)
float Gyro_x,Gyro_y,Gyro_z; //Angular angular velocity for gyroscope calculation
///////////////////////angle parameters//////////////////////////////
///////////////////////Kalman_Filter////////////////////////////
float Q_angle = 0.001; //Covariance of gyroscope noise
float Q_gyro = 0.003; //Covariance of gyroscope drift noise
float R_angle = 0.5; //Covariance of accelerometer
char C_0 = 1;
float dt = 0.005; //The value of dt is the filter sampling time
float K1 = 0.05; // a function containing the Kalman gain; used to calculate the deviation of the optimal estimate
float K_0,K_1,t_0,t_1;
float angle_err;
float q_bias; //gyroscope drift
float accelz = 0;
float angle;
float angleY_one;
float angle_speed;
float Pdot[4] = { 0, 0, 0, 0};
float P[2][2] = {{ 1, 0 }, { 0, 1 }};
float PCt_0, PCt_1, E;
//////////////////////Kalman_Filter/////////////////////////
//////////////////////PID parameters///////////////////////////////
double kp = 34, ki = 0, kd = 0.62; //angle loop parameters
double kp_speed = 3.6, ki_speed = 0.080, kd_speed = 0; // speed loop parameters
double kp_turn = 24, ki_turn = 0, kd_turn = 0.08; // steering loop parameters
double setp0 = 0; //Angle balance point
int PD_pwm; //angle output
float pwm1=0,pwm2=0;
//////////////////Interrupt speed count/////////////////////////////
#define PinA_left 5 //external interrupt
#define PinA_right 4 //external interrupt
volatile long count_right = 0;//Used to calculate the pulse value calculated by the Hall encoder (the volatile long type is to ensure the value is valid)
volatile long count_left = 0;
int speedcc = 0;
//////////////////////pulse count/////////////////////////
int lz = 0;
int rz = 0;
int rpluse = 0;
int lpluse = 0;
int pulseright,pulseleft;
////////////////////////////////PI variable parameter//////////////////////////
float speeds_filterold=0;
float positions=0;
int flag1;
double PI_pwm;
int cc;
int speedout;
float speeds_filter;
//////////////////////////////steering PD///////////////////
int turnmax,turnmin,turnout;
float Turn_pwm = 0;
int zz=0;
int turncc=0;
//Remote control or Bluetooth//
int front = 0;//go front variable
int back = 0;//go back variable
int left = 0;//turn left
int right = 0;//turn right
char val;
void setup()
{
//set the motor control pins to OUTPUT
pinMode(right_R1,OUTPUT);
pinMode(right_R2,OUTPUT);
pinMode(left_L1,OUTPUT);
pinMode(left_L2,OUTPUT);
pinMode(PWM_R,OUTPUT);
pinMode(PWM_L,OUTPUT);
//assign initial state value
digitalWrite(right_R1,1);
digitalWrite(right_R2,0);
digitalWrite(left_L1,0);
digitalWrite(left_L2,1);
analogWrite(PWM_R,0);
analogWrite(PWM_L,0);
pinMode(PinA_left, INPUT); //Speed encoder input
pinMode(PinA_right, INPUT);
// join I2C bus
Wire.begin(); //join I2C bus sequence
Serial.begin(9600); //open the serial monitor and set the baud rate to 9600
delay(1500);
mpu6050.initialize(); //initialize MPU6050
delay(2);
//5ms; use timer2 to set timer interruption (note:using timer2 will affect the PWM output of pin3 pin11)
MsTimer2::set(5, Interrupt_Service_Routine); //5ms ; execute the function Interrupt_Service_Routine once
MsTimer2::start(); //start interrupt
}
void loop()
{
Serial.print(angle_speed);
Serial.print(" ");
Serial.println(Gyro_x);
//Serial.println(angle);
//delay(100);
//Serial.println(PD_pwm);
//Serial.println(pwm1);
//Serial.println(pwm2);
//Serial.print("pulseright = ");
//Serial.println(pulseright);
//Serial.print("pulseleft = ");
//Serial.println(pulseleft);
//Serial.println(PI_pwm);
//Serial.println(speeds_filter);
//Serial.println (positions);
//Serial.println(Turn_pwm);
//Serial.println(Gyro_z);
//Serial.println(Turn_pwm);
// Used for Bluetooth control
// if(Serial.available()) {
// val = Serial.read(); //assign the value read from serial port to val
// Serial.println(val);
val = 'R';
switch(val) //switch statement
{
case 'F': front=250; break; //if val equals to F,front=250,balance robot goes front.
case 'B': back=-250; break; //go back
case 'L': left=1; break; //turn left
case 'R': right=1; break; // turn right
case 'S': front=0,back=0,left=0,right=0;break; //stop
case 'D': Serial.print(angle);break; //when receiving ‘D’,send value of angle to APP
}
//}
//external interrupt; used to calculate the wheel speed
attachPinChangeInterrupt(PinA_left, Code_left, CHANGE); //PinA_left Level change triggers external interrupt; execute subfunction Code_left
attachPinChangeInterrupt(PinA_right, Code_right, CHANGE); //PinA_right Level change triggers external interrupt; execute subfunction Code_right
}
/////////////////////Hall count/////////////////////////
//left speed encoder count
void Code_left()
{
count_left ++;
}
//right speed encoder count
void Code_right()
{
count_right ++;
}
////////////////////pulse count///////////////////////
void countpluse()
{
lz = count_left; //assign the value counted by encoder to lz
rz = count_right;
count_left = 0; //clear the count quantity
count_right = 0;
lpluse = lz;
rpluse = rz;
if ((pwm1 < 0) && (pwm2 < 0)) //judge the moving direction of balance robot; if go back (PWM the motor voltage is negative), the number of pulses is negative.
{
rpluse = -rpluse;
lpluse = -lpluse;
}
else if ((pwm1 > 0) && (pwm2 > 0)) // if go back (PWM the motor voltage is positive) , the number of pulses is positive.
{
rpluse = rpluse;
lpluse = lpluse;
}
else if ((pwm1 < 0) && (pwm2 > 0)) //judge the moving direction of balance robot; if turn left, right pulse is positive but left pulse is negative.
{
rpluse = rpluse;
lpluse = -lpluse;
}
else if ((pwm1 > 0) && (pwm2 < 0)) //judge the moving direction of balance robot; if turn right , right pulse is negative but left pulse is positive.
{
rpluse = -rpluse;
lpluse = lpluse;
}
//entering interrupt per 5ms,pulse will plus
pulseright += rpluse;
pulseleft += lpluse;
}
/////////////////////////////////interrupt ////////////////////////////
void Interrupt_Service_Routine()
{
sei(); //allow overall interrupt
countpluse(); //pulse count subfunction
mpu6050.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); //IIC to get MPU6050 six-axis data ax ay az gx gy gz
angle_calculate(ax, ay, az, gx, gy, gz, dt, Q_angle, Q_gyro, R_angle, C_0, K1); //get the angle and Kalman filtering
PD(); //PD control of angle loop
anglePWM();
cc++;
if(cc>=8) //5*8=40,40ms entering PI count of speed once
{
speedpiout();
cc=0; // Clear
}
turncc++;
if(turncc>4) //20ms entering PI count of steering once
{
turnspin();
turncc=0; //Clear
}
}
///////////////////////////////////////////////////////////
/////////////////////////////tilt angle calculation///////////////////////
void angle_calculate(int16_t ax,int16_t ay,int16_t az,int16_t gx,int16_t gy,int16_t gz,float dt,float Q_angle,float Q_gyro,float R_angle,float C_0,float K1)
{
float Angle = -atan2(ay , az) * (180/ PI); //Radial rotation angle calculation formula; the negative sign is direction processing.
Gyro_x = -gx / 131; //The X-axis angular velocity calculated by the gyroscope ; the negative sign is the direction processing.
Kalman_Filter(Angle, Gyro_x); // Kalman Filter
// Z-axis angular velocity
Gyro_z = -gz / 131; //speed of Z-axis
//accelz = az / 16.4;
float angleAx = -atan2(ax, az) * (180 / PI); //calculate the included angle of X-axis
Gyro_y = -gy / 131.00; //angle speed of Y-axis
Yiorderfilter(angleAx, Gyro_y); //first-order filtering
}
////////////////////////////////////////////////////////////////
///////////////////////////////KalmanFilter/////////////////////
void Kalman_Filter(double angle_m, double gyro_m)
{
angle += (gyro_m - q_bias) * dt; //prior estimate
angle_err = angle_m - angle;
Pdot[0] = Q_angle - P[0][1] - P[1][0]; //The differential of the covariance of the prior estimate error
Pdot[1] = - P[1][1];
Pdot[2] = - P[1][1];
Pdot[3] = Q_gyro;
P[0][0] += Pdot[0] * dt; //The integral of the covariance differential of the prior estimate error
P[0][1] += Pdot[1] * dt;
P[1][0] += Pdot[2] * dt;
P[1][1] += Pdot[3] * dt;
//Intermediate variables in matrix multiplication
PCt_0 = C_0 * P[0][0];
PCt_1 = C_0 * P[1][0];
//denominator
E = R_angle + C_0 * PCt_0;
//gain value
K_0 = PCt_0 / E;
K_1 = PCt_1 / E;
t_0 = PCt_0; //Intermediate variables in matrix multiplication
t_1 = C_0 * P[0][1];
P[0][0] -= K_0 * t_0; //the covariance of the prior estimate error
P[0][1] -= K_0 * t_1;
P[1][0] -= K_1 * t_0;
P[1][1] -= K_1 * t_1;
q_bias += K_1 * angle_err; //posterior estimate
angle_speed = gyro_m - q_bias; //The differential of the output value; get the optimal angular velocity
angle += K_0 * angle_err; ////posterior estimate; get the optimal angular velocity
}
/////////////////////first-order filtering/////////////////
void Yiorderfilter(float angle_m, float gyro_m)
{
angleY_one = K1 * angle_m + (1 - K1) * (angleY_one + gyro_m * dt);
}
//////////////////angle PD////////////////////
void PD()
{
PD_pwm = kp * (angle + angle0) + kd * angle_speed; //PD angle loop control
}
//////////////////speed PI////////////////////
void speedpiout()
{
float speeds = (pulseleft + pulseright) * 1.0; //pulse value of speed
pulseright = pulseleft = 0; //Clear
speeds_filterold *= 0.7; //first-order complementary filtering
speeds_filter = speeds_filterold + speeds * 0.3;
speeds_filterold = speeds_filter;
positions += speeds_filter;
positions += front; //Forward control fusion
positions += back; //backward control fusion
positions = constrain(positions, -3550,3550); //Anti-integral saturation
PI_pwm = ki_speed * (setp0 - positions) + kp_speed * (setp0 - speeds_filter); //speed loop controlling PI
}
//////////////////speed PI////////////////////
///////////////////////////steering/////////////////////////////////
void turnspin()
{
int flag = 0; //
float turnspeed = 0;
float rotationratio = 0;
if (left == 1 || right == 1)
{
if (flag == 0) //judge the speed before turning; increase the car’s flexibility.
{
turnspeed = ( pulseright + pulseleft); //current speed of car; pulse expression
flag=1;
}
if (turnspeed < 0) //Absolute value of the car's current speed
{
turnspeed = -turnspeed;
}
if(left==1||right==1) //if press left key or right key
{
turnmax=3; //maximum value of turning
turnmin=-3; // minimum value of turning
}
rotationratio = 5 / turnspeed; //set the value by speed
if (rotationratio < 0.5)
{
rotationratio = 0.5;
}
if (rotationratio > 5)
{
rotationratio = 5;
}
}
else
{
rotationratio = 0.5;
flag = 0;
turnspeed = 0;
}
if (left ==1)//plus by direction parameter
{
turnout += rotationratio;
}
else if (right == 1 )//plus by direction parameter
{
turnout -= rotationratio;
}
else turnout = 0;
if (turnout > turnmax) turnout = turnmax;//the max value setting of amplitude
if (turnout < turnmin) turnout = turnmin;//the min value setting of amplitude
Turn_pwm = -turnout * kp_turn - Gyro_z * kd_turn;//The rotation PD algorithm controls the fusion speed and Z axis rotation positioning
}
///////////////////////////turning/////////////////////////////////
////////////////////////////PWM end value/////////////////////////////
void anglePWM()
{
pwm2=-PD_pwm - PI_pwm + Turn_pwm; //assign the end value of PWM to motor
pwm1=-PD_pwm - PI_pwm - Turn_pwm;
if(pwm1>255) //limit the PWM value not more than 255
{
pwm1=255;
}
if(pwm1<-255)
{
pwm1=-255;
}
if(pwm2>255)
{
pwm2=255;
}
if(pwm2<-255)
{
pwm2=-255;
}
if(angle>25 || angle<-25) //if tilt angle is greater than 25° , motor will stop.
{
pwm1=pwm2=0;
}
// Determine the motor’s steering and speed by the positive and negative of PWM
if(pwm2>=0)
{
digitalWrite(left_L1,LOW);
digitalWrite(left_L2,HIGH);
analogWrite(PWM_L,pwm2);
}
else
{
digitalWrite(left_L1,HIGH);
digitalWrite(left_L2,LOW);
analogWrite(PWM_L,-pwm2);
}
if(pwm1>=0)
{
digitalWrite(right_R1,HIGH);
digitalWrite(right_R2,LOW);
analogWrite(PWM_R,pwm1);
}
else
{
digitalWrite(right_R1,LOW);
digitalWrite(right_R2,HIGH);
analogWrite(PWM_R,-pwm1);
}
}
Challenge Exercise: Connect the Arduino (via the pins in the Balance Shield) to a Bluetooth Receiver
That’s it for this tutorial. At this stage, if you want to dive deeper, you can connect your Arduino (via the pins above on the Balance Shield) to a Bluetooth dongle, like the HC-05 Wireless Bluetooth RF Transceiver.
You can see in the comments of the code in the previous section that it is partially setup to receive commands from a Bluetooth dongle.
// Used for Bluetooth control
// if(Serial.available()) {
// val = Serial.read(); //assign the value read from serial port to val
// Serial.println(val);
val = 'R';
switch(val) //switch statement
{
case 'F': front=250; break; //if val=F,front=250, bot goes forward.
case 'B': back=-250; break; //go back
case 'L': left=1; break; //turn left
case 'R': right=1; break; // turn right
case 'S': front=0,back=0,left=0,right=0;break; //stop
case 'D': Serial.print(angle);break; //when receiving ‘D’,send value of angle to APP
}
With some code additions and modifications, you can hook up your Arduino to a Bluetooth dongle so that you can control the robot’s movement directly.
In addition to connecting your Arduino to something like the HC-05 Wireless Bluetooth RF Transceiver, you would need to download a Bluetooth smartphone app (e.g. the Serial Bluetooth Terminal app from the Google Play store). You would then send commands (e.g. type in F on your app) to the Arduino (via the HC-05) from your smartphone. The Arduino would read these commands from its serial port, and the robot would move in the direction of your command.
You have the Arduino IDE (Integrated Development Environment) installed on either your PC (Windows, MacOS, or Linux).
If you have experience building a basic wheeled robot using Arduino, you will find this tutorial easier to follow. If you don’t have that experience, don’t worry. I’ll explain everything as we go.
Grab the Arduino and attach it just behind the 9V battery on the front of the robot (Ignore all those wires in the photo for now. You’ll get to those in a bit).
You can use the Velcro squares to make sure the Arduino stays put. Make sure the black power input on the Arduino can easily be reached by the 9V battery connector (don’t plug anything in yet).
Attach the 400-point solderless breadboard just behind the Arduino using some Velcro squares.
Now that the robot has its brain (Arduino mounted on the back of the robot) and a body, it needs a “nervous system,” communication lines that enable the brain to transmit signals to and from different parts of its body. In the context of this project, those communication lines are the wires that we need to connect between the different parts of the robot we’re building.
Design the Circuit Diagram
Before we connect any wires or components, it is good practice to design the circuit diagram. For all my Arduino projects, I use a software called Fritzing, but you can even plan out your circuits by hand.
To test your motor connection, you will need to connect the 9V battery (the one at the back of the robot sandwiched between the upper and lower base pieces) to the 400-point solderless breadboard as shown in the circuit diagram.
I used alligator clips and some small pieces of male-to-male wire to connect the + and – terminals of the 9V battery to the red (positive) and blue (ground) rails of the solderless breadboard.
Here is the code to test the motors. Upload it to your Arduino from your computer.
/**
* Addison Sears-Collins
* March 21, 2020
* This code is used to test the DC motors
* with the Dual H-Bridge Motor Driver (L293D)
**/
/*---------------------------Definitions-------------------------------------*/
//Define pins for Motor A
#define ENABLE_A 5
#define MOTOR_A1 6
#define MOTOR_A2 4
// Define pins for Motor B
#define ENABLE_B 8
#define MOTOR_B1 7
#define MOTOR_B2 9
/*---------------------------Helper-Function Prototypes----------------------*/
void disableMotors(void);
void enableMotors(void);
void goForward(void);
void goLeft(void);
void goRight(void);
void setupPins(void);
/*---------------------------Module Code-------------------------------------*/
void setup() {
setupPins();
// Set the data rate in bits per second
Serial.begin(9600);
}
void loop() {
enableMotors();
goForward();
delay(2000);
goRight();
delay(500);
goForward();
delay(2000);
goLeft();
delay(500);
goForward();
delay(2000);
disableMotors();
delay(3000);
}
void disableMotors(){
digitalWrite(ENABLE_A, LOW);
digitalWrite(ENABLE_B, LOW);
}
void enableMotors(){
digitalWrite(ENABLE_A, HIGH);
digitalWrite(ENABLE_B, HIGH);
}
void goForward(){
digitalWrite(MOTOR_A1, LOW);
digitalWrite(MOTOR_A2, HIGH);
digitalWrite(MOTOR_B1, LOW);
digitalWrite (MOTOR_B2, HIGH);
}
void goLeft(){
digitalWrite(MOTOR_A1, LOW);
digitalWrite(MOTOR_A2, HIGH);
digitalWrite(MOTOR_B1, HIGH);
digitalWrite (MOTOR_B2, LOW);
}
void goRight(){
digitalWrite(MOTOR_A1, HIGH);
digitalWrite(MOTOR_A2, LOW);
digitalWrite(MOTOR_B1, LOW);
digitalWrite (MOTOR_B2, HIGH);
}
void setupPins(){
// Configure motor pins
pinMode(ENABLE_A, OUTPUT);
pinMode(MOTOR_A1, OUTPUT);
pinMode(MOTOR_A2, OUTPUT);
pinMode(ENABLE_B, OUTPUT);
pinMode(MOTOR_B1, OUTPUT);
pinMode(MOTOR_B2, OUTPUT);
}
Place the robot on the ground, somewhere with a lot of space, and power up the Arduino board using the 9V battery. If the robot starts moving, everything is working properly.
Connect the BNO055 9-DOF Absolute Orientation IMU Fusion Breakout
The BNO055 has some pins that come with it. These pins need to be soldered to the BNO055. To see how to set up the BNO055 and make sure it is working, check out this video.
Sink the pins of the BNO055 down into the 400-point solderless breadboard so that it is at the other end of the L293D motor controller.
Connect the HC-SR04 Ultrasonic Sensors (the “Eyes”)
Attach the four HC-SR04 ultrasonic sensors to the front of the robot. These sensors are the “eyes” of the robot and are used to detect obstacles.
The pins of the sensors should slip right through the groove of the upper base piece.
Notice how I used some of the Velcro squares to keep the sensors in place and upright.
Wire the sensors exactly as shown in the circuit diagram. Take your time wiring everything up. There are a lot of wires, and you want to make sure everything is wired up correctly.
I used some Velcro squares at the point where the wires slip over the pins of the ultrasonic sensors so that everything stays firm in place.
Test Your HC-SR04 Ultrasonic Sensors
Upload this code to your Arduino:
/**
* This program tests the ultrasonic
* distance sensor (HC-SR04)
*
* @author Addison Sears-Collins
* @version 1.0 2020-03-22
*/
/*---------------------------Definitions-------------------------------------*/
// Right sensor
#define TRIG_RIGHT A2
#define ECHO_RIGHT A1
// Right-center sensor
#define TRIG_RIGHT_CTR 12
#define ECHO_RIGHT_CTR 13
// Left-center sensor
#define TRIG_LEFT_CTR 10
#define ECHO_LEFT_CTR 11
// Left sensor
#define TRIG_LEFT 3
#define ECHO_LEFT 2
/*---------------------------Helper-Function Prototypes----------------------*/
void doPingRight(void);
void doPingRightCtr(void);
void doPingLeftCtr(void);
void doPingLeft(void);
void getDistances(void);
void setupPins(void);
/*---------------------------Module Variables----------------------*/
int distance_right;
int distance_rightctr;
int distance_leftctr;
int distance_left;
/*---------------------------Module Code-------------------------------------*/
void setup(){
setupPins();
// Setup serial communication
Serial.begin(9600);
}
void loop(){
getDistances();
// Print the distances in inches
Serial.println((String)"Distance Right: " + distance_right);
Serial.println((String)"Distance Right Center: " + distance_rightctr);
Serial.println((String)"Distance Left Center: " + distance_leftctr);
Serial.println((String)"Distance Left: " + distance_left);
Serial.println("");
// Pause for 1.0 second
delay(1000);
}
void doPingRight() {
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_RIGHT, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_RIGHT, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_RIGHT, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_right = pulseIn(ECHO_RIGHT, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_right = distance_right / 74 / 2;
distance_right = abs(distance_right);
}
void doPingRightCtr(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_RIGHT_CTR, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_RIGHT_CTR, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_RIGHT_CTR, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_rightctr = pulseIn(ECHO_RIGHT_CTR, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_rightctr = distance_rightctr / 74 / 2;
distance_rightctr = abs(distance_rightctr);
}
void doPingLeftCtr(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_LEFT_CTR, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_LEFT_CTR, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_LEFT_CTR, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_leftctr = pulseIn(ECHO_LEFT_CTR, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_leftctr = distance_leftctr / 74 / 2;
distance_leftctr = abs(distance_leftctr);
}
void doPingLeft(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_LEFT, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_LEFT, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_LEFT, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_left = pulseIn(ECHO_LEFT, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_left = distance_left / 74 / 2;
distance_left = abs(distance_left);
}
void getDistances() {
// Take distance readings on the HC-SR04
doPingRight();
doPingRightCtr();
doPingLeftCtr();
doPingLeft();
}
void setupPins(){
// Configure HC-SR04 pins
pinMode(TRIG_RIGHT, OUTPUT);
pinMode(ECHO_RIGHT, INPUT);
pinMode(TRIG_RIGHT_CTR, OUTPUT);
pinMode(ECHO_RIGHT_CTR, INPUT);
pinMode(TRIG_LEFT_CTR, OUTPUT);
pinMode(ECHO_LEFT_CTR, INPUT);
pinMode(TRIG_LEFT, OUTPUT);
pinMode(ECHO_LEFT, INPUT);
}
While the program is running, click the magnifying glass in the upper right corner of the Arduino IDE. You should see the distances from each sensor to the closest object (in inches). If you wave your hand in front of the sensors, you can see how the readings change.
Connect the Reflectance Sensor
The reflectance sensor comes with some pins. Make sure those pins are soldered to the reflectance sensor.
Cut out a tiny piece of velcro and attach the reflectance sensor between the right and right-center HC-SR04 ultrasonic sensors.
Wire the reflectance sensor exactly as shown in the circuit diagram.
Test Your Reflectance Sensor
Upload this code to your Arduino:
/**
* Addison Sears-Collins
* March 25, 2020
* This code is used to test the QTR-1A Reflectance Sensor
**/
/*---------------------------Definitions-------------------------------------*/
//Define pin for the QTR-1A Reflectance Sensor
#define IR_SENSOR A3
/*---------------------------Helper-Function Prototypes----------------------*/
void blinkLED(void);
int readIRSensor(void);
/*---------------------------Module Variables--------------------------------*/
// Store sensor readings here
int ir_reflect_previous;
int ir_reflect_current;
// Try values between 100 and 600.
// Helps determine if the robot crosses the reflective tape
int threshold = 200;
// Keep track of the number of times the robot crosses over
// the reflective tape
int stage = 0;
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
// Setup serial communication
Serial.begin(9600);
// Get initial readings on the reflectance sensor
ir_reflect_previous = readIRSensor();
ir_reflect_current = ir_reflect_previous;
}
void loop() {
// Read the reflectance sensor
ir_reflect_current = readIRSensor();
// Check to see if we have crossed over the reflective tape
if ((ir_reflect_previous - ir_reflect_current) >= threshold) {
blinkLED();
// Update the number of crosses
stage = stage + 1;
// Print the stage
Serial.println("----------------------------------------");
Serial.println((String)"Stage: " + stage);
Serial.println("----------------------------------------");
}
// Print the readings
Serial.println((String)"Previous: " + ir_reflect_previous);
Serial.println((String)"Current: " + ir_reflect_current);
Serial.println("");
// Update the previous reading
ir_reflect_previous = ir_reflect_current;
delay(500);
}
void blinkLED(){
for (int i = 0; i < 5; i++) {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on
delay(50); // wait for half a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off
delay(50); // wait for half a second
}
}
int readIRSensor(){
/*
* Returns the sensor reading
*/
int temp_reading = 0;
int average = 0;
// Grab four measurements and calculate the average
for (int i = 0; i < 4; i++) {
temp_reading = analogRead(IR_SENSOR);
// Compute running sum
average += temp_reading;
delayMicroseconds(2);
}
// Return the average
return (average / 4);
}
Click the magnifying glass in the upper right corner of the Arduino IDE. You should see the reflectance sensor readings printing out to the screen.
Place a long piece of 3M silver reflective tape down on the floor. This is the starting line. As soon as the robot detects this tape, it will begin detecting obstacles (i.e. pink lemonade cans in my case).
Finish Line
15-20 feet away from the Starting Line, place a long piece of 3M silver reflective tape down on the floor. This is the finish line. As soon as the robot detects this tape, it will begin maintaining the same heading it had when it crossed the Starting line. It will maintain this heading for two seconds before coming to a complete stop.
Multi-Obstacle Environment
Place a bunch of soda cans within your course…before the finish line, but after the starting line. Make sure the cans are at least 18 inches apart from each other.
Now go back to your computer and upload the following code to your Arduino. If you’re interested in understanding what is going on inside the code, just go through it one line at a time, starting from the top, reading all my comments along the way.
#include <Wire.h> // Library for the BNO055
#include <Adafruit_Sensor.h> // Library for the BNO055
#include <Adafruit_BNO055.h> // Library for the BNO055
#include <utility/imumaths.h> // Library for the BNO055
/**
* Addison Sears-Collins
* April 3, 2020
* This code enables a robot to navigate through
* a multi-obstacle environment (MOE).
* The robot starts detecting obstacles once it crosses
* 3M silver reflective tape. The robot finishes once it
* crosses 3M silver reflective tape for the second time.
*
* To calibrate the BNO055 Orientation Sensor,
* start out with the robot facing
* due North (0 degrees).
**/
/*----------Definitions----------*/
//Define pins for Motor A
#define ENABLE_A 5
#define MOTOR_A1 6
#define MOTOR_A2 4
// Define pins for Motor B
#define ENABLE_B 8
#define MOTOR_B1 7
#define MOTOR_B2 9
// Define pin for the QTR-1A Reflectance Sensor
#define IR_SENSOR A3
// Right sensor
#define TRIG_RIGHT A2
#define ECHO_RIGHT A1
// Right-center sensor
#define TRIG_RIGHT_CTR 12
#define ECHO_RIGHT_CTR 13
// Left-center sensor
#define TRIG_LEFT_CTR 10
#define ECHO_LEFT_CTR 11
// Left sensor
#define TRIG_LEFT 3
#define ECHO_LEFT 2
// Avoidance delay
// Number of 50ms increments we want
// to move forward after
// moving away from the object
/***** Try 1-5 depending on battery strength********/
#define AVOIDANCE_DELAY 1
/*---Helper-Function Prototypes---*/
// Motors
void disableMotors(void);
void enableMotors(void);
void goForward(void);
void goLeft(void);
void goRight(void);
void leftAvoid(void);
void rightAvoid(void);
void setupPins(void);
// Headings
void calculateHeadingError(void);
void correctHeading(void);
// Ultrasonic sensors
void doPingRight(void);
void doPingRightCtr(void);
void doPingLeftCtr(void);
void doPingLeft(void);
void getDistances(void);
// IR sensor
void readIRSensor(void);
// Obstacle avoidance
void avoidObstacles(void);
/*--------Module Variables--------*/
bool crossed_the_tape = false;
bool done = false;
// Keep track of the headings
int desired_heading;
int current_heading;
int heading_threshold = 60; // 120 degree cone until stage 2
int heading_error;
// Store sensor readings here
int ir_reflect_previous;
int ir_reflect_current;
// Try values between 100 and 600.
// Helps determine if the robot crosses the reflective tape
int threshold = 200;
// For Ultrasonic sensor distance readings
int distance_right;
int distance_rightctr;
int distance_leftctr;
int distance_left;
int limit = 9; // Inches, try 5-10 depending on battery strength
bool right_or_left = false; // Switch
// Keep track of the time in milliseconds
unsigned long start_time;
unsigned long time_elapsed_threshold = 2000;
Adafruit_BNO055 bno = Adafruit_BNO055(55);
/*----------Module Code-----------*/
void setup(void) {
// Start the car
setupPins();
enableMotors();
// Get initial readings on the IR sensor
ir_reflect_previous = analogRead(IR_SENSOR);
ir_reflect_current = ir_reflect_previous;
// Initialize the orientation sensor
if(!bno.begin()) {
// There was a problem detecting the
// BNO055 ... check your connections
//Serial.print("Ooops, no BNO055 detected ");
//Serial.print("... Check your wiring or I2C ADDR!");
while(1);
}
bno.setExtCrystalUse(true);
}
void loop(void) {
// Stage 0 - before the robot enters the
// multi-obstacle environment
while(!crossed_the_tape) {
delay(50);
// Read desired heading
sensors_event_t event;
bno.getEvent(&event);
desired_heading = event.orientation.x;
goForward();
readIRSensor();
}
crossed_the_tape = false;
// Stage 1
while(!crossed_the_tape) {
// Read all four HC-SR04 sensors
getDistances();
// Avoid any obstacle along the way
avoidObstacles();
}
heading_threshold = 10;
// Capture the time
start_time = millis();
// Stage 2
while(!done) {
calculateHeadingError();
// Correct the heading if needed
if (abs(heading_error) <= abs(heading_threshold)){
goForward();
}
else {
correctHeading();
goForward();
}
// Check to see if we are done
if (((millis()) - start_time) > time_elapsed_threshold) {
done = true;
}
}
while(done) {
disableMotors();
delay(1000);
}
}
void avoidObstacles(){
// Avoid any objects
if ((distance_leftctr < limit) && (distance_rightctr < limit)){
// Switch back and forth
if (right_or_left) {
rightAvoid();
}
else {
leftAvoid();
}
right_or_left = !(right_or_left);
}
else if((distance_left < limit) || (distance_leftctr < limit)) {
rightAvoid();
}
else if((distance_right < limit) || (distance_rightctr < limit)) {
leftAvoid();
}
else {
calculateHeadingError();
// Correct the heading if needed
if (abs(heading_error) <= abs(heading_threshold)){
goForward();
}
else {
correctHeading();
goForward();
}
// Check to see if we have crossed the tape
readIRSensor();
delay(50);
}
}
void calculateHeadingError() {
// Read the current heading
sensors_event_t event;
bno.getEvent(&event);
current_heading = event.orientation.x;
// Calculate the heading error
heading_error = current_heading - desired_heading;
if (heading_error > 180) {
heading_error -= 360;
}
if (heading_error < -180) {
heading_error += 360;
}
}
void correctHeading(){
// Turn the vehicle until it is facing the correct
// direction
if (heading_error < -heading_threshold) {
while (heading_error < -heading_threshold) {
goRight();
delay(4);
calculateHeadingError();
}
}
else {
while (heading_error > heading_threshold) {
goLeft();
delay(4);
calculateHeadingError();
}
}
}
void doPingRight() {
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_RIGHT, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_RIGHT, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_RIGHT, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_right = pulseIn(ECHO_RIGHT, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_right = distance_right / 74 / 2;
distance_right = abs(distance_right);
}
void doPingRightCtr(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_RIGHT_CTR, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_RIGHT_CTR, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_RIGHT_CTR, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_rightctr = pulseIn(ECHO_RIGHT_CTR, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_rightctr = distance_rightctr / 74 / 2;
distance_rightctr = abs(distance_rightctr);
}
void doPingLeftCtr(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_LEFT_CTR, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_LEFT_CTR, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_LEFT_CTR, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_leftctr = pulseIn(ECHO_LEFT_CTR, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_leftctr = distance_leftctr / 74 / 2;
distance_leftctr = abs(distance_leftctr);
}
void doPingLeft(){
/*
* Returns the distance to the obstacle as an integer in inches
*/
// Make the Trigger LOW (0 volts) for 2 microseconds
digitalWrite(TRIG_LEFT, LOW);
delayMicroseconds(2);
// Emit high frequency 40kHz sound pulse
// (i.e. pull the Trigger)
// by making Trigger HIGH (5 volts)
// for 10 microseconds
digitalWrite(TRIG_LEFT, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_LEFT, LOW);
// Detect a pulse on the Echo pin.
// pulseIn() measures the time in
// microseconds until the sound pulse
// returns back to the sensor.
distance_left = pulseIn(ECHO_LEFT, HIGH);
// Speed of sound is:
// 13511.811023622 inches per second
// 13511.811023622/10^6 inches per microsecond
// 0.013511811 inches per microsecond
// Taking the reciprocal, we have:
// 74.00932414 microseconds per inch
// Below, we convert microseconds to inches by
// dividing by 74 and then dividing by 2
// to account for the roundtrip time.
distance_left = distance_left / 74 / 2;
distance_left = abs(distance_left);
}
void getDistances() {
// Take distance readings on the HC-SR04
doPingRight();
doPingLeftCtr();
doPingRightCtr();
doPingLeft();
}
void disableMotors(){
digitalWrite(ENABLE_A, LOW);
digitalWrite(ENABLE_B, LOW);
}
void enableMotors(){
digitalWrite(ENABLE_A, HIGH);
digitalWrite(ENABLE_B, HIGH);
}
void goForward(){
digitalWrite(MOTOR_A1, LOW);
digitalWrite(MOTOR_A2, HIGH);
digitalWrite(MOTOR_B1, LOW);
digitalWrite (MOTOR_B2, HIGH);
}
void goLeft(){
digitalWrite(MOTOR_A1, LOW);
digitalWrite(MOTOR_A2, HIGH);
digitalWrite(MOTOR_B1, HIGH);
digitalWrite (MOTOR_B2, LOW);
}
void goRight(){
digitalWrite(MOTOR_A1, HIGH);
digitalWrite(MOTOR_A2, LOW);
digitalWrite(MOTOR_B1, LOW);
digitalWrite (MOTOR_B2, HIGH);
}
void leftAvoid(){
// Go to the left when an object is detected
// on the right-side of the vehicle
while((distance_right < limit) || (distance_rightctr < limit)) {
goLeft();
delay(4);
doPingRight();
doPingRightCtr();
}
goForward();
for (int i = 0; i < AVOIDANCE_DELAY; i++) {
// Read the reflectance sensor
ir_reflect_current = analogRead(IR_SENSOR);
// Check to see if we have crossed over the reflective tape
if ((ir_reflect_previous - ir_reflect_current) >= threshold) {
// Update if we crossed the tape
crossed_the_tape = true;
break;
}
// Update the previous reading
ir_reflect_previous = ir_reflect_current;
delay(50);
}
}
void readIRSensor() {
// Read the reflectance sensor
ir_reflect_current = analogRead(IR_SENSOR);
// Check to see if we have crossed over the reflective tape
if ((ir_reflect_previous - ir_reflect_current) >= threshold) {
// Update if we crossed the tape
crossed_the_tape = true;
}
// Update the previous reading
ir_reflect_previous = ir_reflect_current;
}
void rightAvoid(){
// Go to the right when an object is detected
// on the left-side of the vehicle
while((distance_left < limit) || (distance_leftctr < limit)) {
goRight();
delay(4);
doPingLeft();
doPingLeftCtr();
}
goForward();
for (int i = 0; i < AVOIDANCE_DELAY; i++) {
// Read the reflectance sensor
ir_reflect_current = analogRead(IR_SENSOR);
// Check to see if we have crossed over the reflective tape
if ((ir_reflect_previous - ir_reflect_current) >= threshold) {
// Update if we crossed the tape
crossed_the_tape = true;
break;
}
// Update the previous reading
ir_reflect_previous = ir_reflect_current;
delay(50);
}
}
void setupPins(){
// Configure motor pins
pinMode(ENABLE_A, OUTPUT);
pinMode(MOTOR_A1, OUTPUT);
pinMode(MOTOR_A2, OUTPUT);
pinMode(ENABLE_B, OUTPUT);
pinMode(MOTOR_B1, OUTPUT);
pinMode(MOTOR_B2, OUTPUT);
// Configure HC-SR04 pins
pinMode(TRIG_RIGHT, OUTPUT);
pinMode(ECHO_RIGHT, INPUT);
pinMode(TRIG_RIGHT_CTR, OUTPUT);
pinMode(ECHO_RIGHT_CTR, INPUT);
pinMode(TRIG_LEFT_CTR, OUTPUT);
pinMode(ECHO_LEFT_CTR, INPUT);
pinMode(TRIG_LEFT, OUTPUT);
pinMode(ECHO_LEFT, INPUT);
}
Now, we’ve come to the time you’ve been waiting for. It’s time to move the robot through the obstacle course from start to finish.
Set your robot on the floor 3-5 feet before the starting line.
Plug in the Arduino, and within a second or two, you should see your robot take off, moving through the obstacle all by itself! Two seconds after it crosses the finish line, it should come to a complete stop.
If your robot is not moving through the obstacle course, go back to the beginning of this post and make sure that everything is wired up and connected as I have shown.
Also, you can try tweaking the defined constants in the final code to see if your robot behaves better.
That’s it for now. Congratulations, you have built an autonomous mobile robot from scratch that can sense, think, and act.