///////////////////////////////////////////////////////////////////////////////////////
/*Terms of use
///////////////////////////////////////////////////////////////////////////////////////
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////////////
//Support
///////////////////////////////////////////////////////////////////////////////////////
Website: http://www.brokking.net/imu.html
Youtube: https://youtu.be/4BoIE8YQwM8
Version: 1.0 (May 5, 2016)
///////////////////////////////////////////////////////////////////////////////////////
//Connections
///////////////////////////////////////////////////////////////////////////////////////
Power (5V) is provided to the Arduino pro mini by the FTDI programmer
Gyro - Arduino pro mini
VCC - 5V
GND - GND
SDA - A4
SCL - A5
LCD - Arduino pro mini
VCC - 5V
GND - GND
SDA - A4
SCL - A5
*//////////////////////////////////////////////////////////////////////////////////////
//Include LCD and I2C library
#include <Wire.h>
//Declaring some global variables
//Gyro, Accelerometer related values
int gyro_x, gyro_y, gyro_z;
long gyro_x_cal, gyro_y_cal, gyro_z_cal;
long acc_x, acc_y, acc_z, acc_total_vector;
//Angles obtained from MPU6050
float angle_pitch, angle_roll;
float angle_roll_acc, angle_pitch_acc;
float angle_pitch_current_out, angle_roll_current_out, angle_yaw_current_out;
//Basic temperature reading
int temperature;
//For accurate precision to make the code run at 250Hz
long loop_timer;
//For Gyro Config at Startup (Takes place only once in the loop)
bool set_gyro_angles;
//Baterry Voltage for esc pulse compensation as voltage fluctuates
int battery_voltage;
int compensation_voltage = 0;
int voltage_pin = 0;
//ESC pulse_values
int esc_1, esc_2, esc_3, esc_4;
unsigned long timer_channel_1, timer_channel_2, timer_channel_3, timer_channel_4, esc_loop_timer;
// Value indicating the current state of the drone: Setup, Ready, Active
typedef enum {
SETUP, // Drone is in setup state
READY, // Drone is ready for operation
ACTIVE // Drone is actively performing tasks
} states;
states drone_state = SETUP;
//PID values for error calculation (not pid constants)
int pid_desired_pitch, pid_desired_roll, pid_desired_yaw, throttle = 1800;
int pid_output_pitch, pid_output_roll, pid_output_yaw;
int pid_error;
float pid_i_mem_roll, pid_last_roll_d_error;
float pid_i_mem_pitch, pid_last_pitch_d_error;
float pid_i_mem_yaw, pid_last_yaw_d_error;
//PID gain/constant and limit settings
float pid_p_gain_roll = 1.3; //Gain setting for the roll P-controller
float pid_i_gain_roll = 0.04; //Gain setting for the roll I-controller
float pid_d_gain_roll = 18.0; //Gain setting for the roll D-controller
int pid_max_roll = 400; //Maximum output of the PID-controller (+/-)
float pid_p_gain_pitch = pid_p_gain_roll; //Gain setting for the pitch P-controller.
float pid_i_gain_pitch = pid_i_gain_roll; //Gain setting for the pitch I-controller.
float pid_d_gain_pitch = pid_d_gain_roll; //Gain setting for the pitch D-controller.
int pid_max_pitch = 400; //Maximum output of the PID-controller (+/-)
float pid_p_gain_yaw = 4.0; //Gain setting for the pitch P-controller. //4.0
float pid_i_gain_yaw = 0.02; //Gain setting for the pitch I-controller. //0.02
float pid_d_gain_yaw = 0.0; //Gain setting for the pitch D-controller.
int pid_max_yaw = 400; //Maximum output of the PID-controller (+/-)
boolean auto_level = true; //Auto level on (true) or off (false)
void setup() {
Wire.begin(); //Start I2C as master
Serial.begin(9600); //Use only for debugging
pinMode(13, OUTPUT); //Set output 13 (LED) as output
setup_mpu_6050_registers();
TWBR = 12; //Set the I2C clock speed to 400kHz.
//Arduino (Atmega) pins default to inputs, so they don't need to be explicitly declared as inputs.
DDRD |= B11110000; //Configure digital poort 4, 5, 6 and 7 as output.
DDRB |= B00110000; //Configure digital poort 12 and 13 as output.
digitalWrite(13, HIGH); //Set digital output 13 high to indicate startup
for (int cal_int = 0; cal_int < 2000 ; cal_int ++){ //Run this code 2000 times
read_mpu_6050_data(); //Read the raw acc and gyro data from the MPU-6050
gyro_x_cal += gyro_x; //Add the gyro x-axis offset to the gyro_x_cal variable
gyro_y_cal += gyro_y; //Add the gyro y-axis offset to the gyro_y_cal variable
gyro_z_cal += gyro_z; //Add the gyro z-axis offset to the gyro_z_cal variable
//We don't want the esc's to be beeping annoyingly. So let's give them a 1000us puls while calibrating the gyro.
PORTD |= B11110000; //Set digital poort 4, 5, 6 and 7 high.
delayMicroseconds(1000); //Wait 1000us.
PORTD &= B00001111; //Set digital poort 4, 5, 6 and 7 low.
delay(3); //Wait 3 milliseconds before the next loop. //Delay 3us to simulate the 250Hz program loop
}
gyro_x_cal /= 2000; //Divide the gyro_x_cal variable by 2000 to get the avarage offset
gyro_y_cal /= 2000; //Divide the gyro_y_cal variable by 2000 to get the avarage offset
gyro_z_cal /= 2000; //Divide the gyro_z_cal variable by 2000 to get the avarage offset
digitalWrite(13, LOW); //All done, turn the LED off
//Load the battery voltage to the battery_voltage variable.
//Compensation voltage is compensating for the PDB-XT60
//12.6V equals ~5V @ Analog 0.
//12.6V equals 1023 analogRead(0).
//1260 / 1023 = 1.2317.
//The variable battery_voltage holds 1050 if the battery voltage is 10.5V.
battery_voltage = (voltage_pin + compensation_voltage) * 1.2317;
drone_state = READY;
loop_timer = micros(); //Reset the loop timer
}
void loop(){
TCCR1A = 0; //Setup the registers of the MPU-6050 (500dfs / +/-8g) and start the gyro
TCCR1B = bit(CS10);
TCNT1 = 0;
read_mpu_6050_data(); //Read the raw acc and gyro data from the MPU-6050
gyro_x -= gyro_x_cal; //Subtract the offset calibration value from the raw gyro_x value
gyro_y -= gyro_y_cal; //Subtract the offset calibration value from the raw gyro_y value
gyro_z -= gyro_z_cal; //Subtract the offset calibration value from the raw gyro_z value
//Gyro angle calculations
//0.0000611 = 1 / (250Hz / 65.5)
angle_pitch += gyro_x * 0.0000611; //Calculate the traveled pitch angle and add this to the angle_pitch variable
angle_roll += gyro_y * 0.0000611; //Calculate the traveled roll angle and add this to the angle_roll variable
//0.000001066 = 0.0000611 * (3.142(PI) / 180degr) The Arduino sin function is in radians
angle_pitch += angle_roll * sin(gyro_z * 0.000001066); //If the IMU has yawed transfer the roll angle to the pitch angel
angle_roll -= angle_pitch * sin(gyro_z * 0.000001066); //If the IMU has yawed transfer the pitch angle to the roll angel
//Accelerometer angle calculations
acc_total_vector = sqrt((acc_x*acc_x)+(acc_y*acc_y)+(acc_z*acc_z)); //Calculate the total accelerometer vector
//57.296 = 1 / (3.142 / 180) The Arduino asin function is in radians
if(abs(acc_y) < acc_total_vector){ //Prevent the asin function to produce a NaN
angle_pitch_acc = asin((float)acc_y/acc_total_vector)* 57.296; //Calculate the pitch angle.
}
if(abs(acc_x) < acc_total_vector){ //Prevent the asin function to produce a NaN
angle_roll_acc = asin((float)acc_x/acc_total_vector)* -57.296; //Calculate the roll angle.
}
//angle_pitch_acc = asin((float)acc_y/acc_total_vector)* 57.296; //Calculate the pitch angle
//angle_roll_acc = asin((float)acc_x/acc_total_vector)* -57.296; //Calculate the roll angle
//Place the MPU-6050 spirit level and note the values in the following two lines for calibration
angle_pitch_acc -= 0.0; //Accelerometer calibration value for pitch
angle_roll_acc -= 0.0; //Accelerometer calibration value for roll
if(set_gyro_angles){ //If the IMU is already started
angle_pitch = angle_pitch * 0.9996 + angle_pitch_acc * 0.0004; //Correct the drift of the gyro pitch angle with the accelerometer pitch angle
angle_roll = angle_roll * 0.9996 + angle_roll_acc * 0.0004; //Correct the drift of the gyro roll angle with the accelerometer roll angle
}
else{ //At first start
angle_pitch = angle_pitch_acc; //Set the gyro pitch angle equal to the accelerometer pitch angle
angle_roll = angle_roll_acc; //Set the gyro roll angle equal to the accelerometer roll angle
set_gyro_angles = true; //Set the IMU started flag
}
//To dampen the pitch and roll angles a complementary filter is used
angle_pitch_current_out = angle_pitch_current_out * 0.9 + angle_pitch * 0.1; //Take 90% of the output pitch value and add 10% of the raw pitch value
angle_roll_current_out = angle_roll_current_out * 0.9 + angle_roll * 0.1; //Take 90% of the output roll value and add 10% of the raw roll value
angle_yaw_current_out = gyro_z;
calculate_pid_orientation(); //Calculate PID
//The battery voltage is needed for compensation.
//A complementary filter is used to reduce noise.
//0.09853 = 0.08 * 1.2317.
battery_voltage = battery_voltage * 0.92 + (analogRead(voltage_pin) + compensation_voltage) * 0.09853;
//Turn on the led if battery voltage is to low.
if(battery_voltage < 1000 && battery_voltage > 600) digitalWrite(12, HIGH);
if (drone_state == READY || drone_state == ACTIVE){ //The motors are started.
if (throttle > 1800) throttle = 1800; //We need some room to keep full control at full throttle.
esc_1 = throttle - pid_output_pitch + pid_output_roll - pid_output_yaw; //Calculate the pulse for esc 1 (front-right - CCW)
esc_2 = throttle + pid_output_pitch + pid_output_roll + pid_output_yaw; //Calculate the pulse for esc 2 (rear-right - CW)
esc_3 = throttle + pid_output_pitch - pid_output_roll - pid_output_yaw; //Calculate the pulse for esc 3 (rear-left - CCW)
esc_4 = throttle - pid_output_pitch - pid_output_roll + pid_output_yaw; //Calculate the pulse for esc 4 (front-left - CW)
if (battery_voltage < 1240 && battery_voltage > 800){ //Is the battery connected?
esc_1 += esc_1 * ((1240 - battery_voltage)/(float)3500); //Compensate the esc-1 pulse for voltage drop.
esc_2 += esc_2 * ((1240 - battery_voltage)/(float)3500); //Compensate the esc-2 pulse for voltage drop.
esc_3 += esc_3 * ((1240 - battery_voltage)/(float)3500); //Compensate the esc-3 pulse for voltage drop.
esc_4 += esc_4 * ((1240 - battery_voltage)/(float)3500); //Compensate the esc-4 pulse for voltage drop.
}
esc_1 = constrain(esc_1, 1100, 2000); // Constrain values to desired range
esc_2 = constrain(esc_2, 1100, 2000);
esc_3 = constrain(esc_3, 1100, 2000);
esc_4 = constrain(esc_4, 1100, 2000);
}
else{
esc_1 = esc_2 = esc_3 = esc_4 = 1000;
}
//All the information for controlling the motor's is available.
//The refresh rate is 250Hz. That means the esc's need there pulse every 4ms.
PORTD |= B11110000; //Set digital outputs 4,5,6 and 7 high.
timer_channel_1 = esc_1 + loop_timer; //Calculate the time of the faling edge of the esc-1 pulse.
timer_channel_2 = esc_2 + loop_timer; //Calculate the time of the faling edge of the esc-2 pulse.
timer_channel_3 = esc_3 + loop_timer; //Calculate the time of the faling edge of the esc-3 pulse.
timer_channel_4 = esc_4 + loop_timer; //Calculate the time of the faling edge of the esc-4 pulse.
//There is always 1000us of spare time. So let's do something usefull that is very time consuming.
//Read the Serial Data for the next iteration of the loop
//read_serial_data();
while(PORTD >= 16){ //Stay in this loop until output 4,5,6 and 7 are high.
esc_loop_timer = micros(); //Read the current time.
if(timer_channel_1 <= esc_loop_timer)PORTD &= B11101111; //Set digital output 4 to low if the time is expired.
if(timer_channel_2 <= esc_loop_timer)PORTD &= B11011111; //Set digital output 5 to low if the time is expired.
if(timer_channel_3 <= esc_loop_timer)PORTD &= B10111111; //Set digital output 6 to low if the time is expired.
if(timer_channel_4 <= esc_loop_timer)PORTD &= B01111111; //Set digital output 7 to low if the time is expired.
}
//Set the timer for the next loop.
if(micros() - loop_timer > 4050) digitalWrite(12, HIGH);
else if(micros() - loop_timer < 4050) digitalWrite(12, LOW); //Turn on the LED if the loop time exceeds 4050us.
while(micros() - loop_timer < 4000); //We wait until 4000us are passed.
loop_timer = micros();
unsigned int cycles = TCNT1;
Serial.print("Cycles:");
Serial.println(cycles - 1);
Serial.print("Microseconds:");
Serial.println((float)(cycles - 1)/16);
}
inline void calculate_pid_orientation()
{
//Roll calculations
pid_error = angle_roll_current_out - pid_desired_roll;
pid_i_mem_roll += pid_i_gain_roll * pid_error;
if(pid_i_mem_roll > pid_max_roll) pid_i_mem_roll = pid_max_roll;
else if(pid_i_mem_roll < pid_max_roll * -1) pid_i_mem_roll = pid_max_roll * -1;
pid_output_roll = pid_p_gain_roll * pid_error + pid_i_mem_roll + pid_d_gain_roll * (pid_error - pid_last_roll_d_error);
if(pid_output_roll > pid_max_roll) pid_output_roll = pid_max_roll;
else if(pid_output_roll < pid_max_roll * -1) pid_output_roll = pid_max_roll * -1;
pid_last_roll_d_error = pid_error;
//Pitch calculations
pid_error = angle_roll_current_out - pid_desired_pitch;
pid_i_mem_pitch += pid_i_gain_pitch * pid_error;
if(pid_i_mem_pitch > pid_max_pitch) pid_i_mem_pitch = pid_max_pitch;
else if(pid_i_mem_pitch < pid_max_pitch * -1) pid_i_mem_pitch = pid_max_pitch * -1;
pid_output_pitch = pid_p_gain_pitch * pid_error + pid_i_mem_pitch + pid_d_gain_pitch * (pid_error - pid_last_pitch_d_error);
if(pid_output_pitch > pid_max_pitch) pid_output_pitch = pid_max_pitch;
else if(pid_output_pitch < pid_max_pitch * -1) pid_output_pitch = pid_max_pitch * -1;
pid_last_pitch_d_error = pid_error;
//Yaw calculations
pid_error = angle_yaw_current_out - pid_desired_yaw;
pid_i_mem_yaw += pid_i_gain_yaw * pid_error;
if(pid_i_mem_yaw > pid_max_yaw)pid_i_mem_yaw = pid_max_yaw;
else if(pid_i_mem_yaw < pid_max_yaw * -1)pid_i_mem_yaw = pid_max_yaw * -1;
pid_output_yaw = pid_p_gain_yaw * pid_error + pid_i_mem_yaw + pid_d_gain_yaw * (pid_error - pid_last_yaw_d_error);
if(pid_output_yaw > pid_max_yaw) pid_output_yaw = pid_max_yaw;
else if(pid_output_yaw < pid_max_yaw * -1) pid_output_yaw = pid_max_yaw * -1;
pid_last_yaw_d_error = pid_error;
}
inline void read_serial_data()
{
if(Serial.available())
{
}
else return 0;
}
inline void read_mpu_6050_data(){ //Subroutine for reading the raw gyro and accelerometer data
Wire.beginTransmission(0x68); //Start communicating with the MPU-6050
Wire.write(0x3B); //Send the requested starting register
Wire.endTransmission(); //End the transmission
Wire.requestFrom(0x68,14); //Request 14 bytes from the MPU-6050
while(Wire.available() < 14); //Wait until all the bytes are received
acc_x = Wire.read()<<8|Wire.read(); //Add the low and high byte to the acc_x variable
acc_y = Wire.read()<<8|Wire.read(); //Add the low and high byte to the acc_y variable
acc_z = Wire.read()<<8|Wire.read(); //Add the low and high byte to the acc_z variable
temperature = Wire.read()<<8|Wire.read(); //Add the low and high byte to the temperature variable
gyro_x = Wire.read()<<8|Wire.read(); //Add the low and high byte to the gyro_x variable
gyro_y = Wire.read()<<8|Wire.read(); //Add the low and high byte to the gyro_y variable
gyro_z = Wire.read()<<8|Wire.read(); //Add the low and high byte to the gyro_z variable
}
inline void setup_mpu_6050_registers(){
//Activate the MPU-6050
Wire.beginTransmission(0x68); //Start communicating with the MPU-6050
Wire.write(0x6B); //Send the requested starting register
Wire.write(0x00); //Set the requested starting register
Wire.endTransmission(); //End the transmission
//Configure the accelerometer (+/-8g)
Wire.beginTransmission(0x68); //Start communicating with the MPU-6050
Wire.write(0x1C); //Send the requested starting register
Wire.write(0x10); //Set the requested starting register
Wire.endTransmission(); //End the transmission
//Configure the gyro (500dps full scale)
Wire.beginTransmission(0x68); //Start communicating with the MPU-6050
Wire.write(0x1B); //Send the requested starting register
Wire.write(0x08); //Set the requested starting register
Wire.endTransmission(); //End the transmission
}