I played with this back in 2020 and flew it with a combat delta with elevons.
I used a promini with a MPU6050 on it's back and connected a PPM receiver to it.
I also used some libraries by RC-Navy to read the PPM stream and output the servo signals
you can find them here:
I think this code was the last version I flew with. I added a gain slider to adjust sensitivity in flight because with high gain settings I suffered from oscillations on the elevator.
I also added dynamic gain to have more stabilization around stick center and no stabilization when full up-down or left-right.
That worked better in aircombats.
Be sure to add an extra elco (470UF) between GND and VCC, or you will suffer from lockups in flight. I must have the hardware somewhere and could make a picture how I rigged it up.
It's definitely fun. good luck.
/*
https://www.youtube.com/watch?v=H7Ph6sIjl98&feature=youtu.be
*/
#define ELEVON_MIXING // comment out if elevon mixing routine is NOT required.
#define AUX_GAIN // comment out if no gain setting required via sliders
//#define DYNAMIC_GAIN // comment out for constant gyro gain
#include <Wire.h>
#define CALIBRATION_COUNT 200
#define MPU6050_I2C_ADDRESS 0x68
#define MPU6050_PWR_MGMT_1 0x6B // R/W
#define GYRO_ADDRESS 0x68
#define GYRO_VALUES_REGISTER 0x43
#define GYRO_HIGH_BYTE_FIRST
// Set the order in which the gyro values are read
#define GYRO_AXIS_1 gyro_pitch
#define GYRO_AXIS_2 gyro_roll
#define GYRO_AXIS_3 gyro_yaw
// Set a value of -1 for gyro axes that should be reversed
#define GYRO_INVERT_PITCH 1
#define GYRO_INVERT_ROLL 1
#define GYRO_INVERT_YAW 1
// Set a value of -1 for RC input axes that should be reversed
#define RC_INVERT_PITCH 1
#define RC_INVERT_ROLL 1
#define RC_INVERT_YAW 1
#define PITCH_RATE 100 // adjust to make servos move max range with tx control
#define ROLL_RATE 100 // adjust to make servos move max range with tx control
#define YAW_RATE 100 // adjust to make servos move max range with tx control
#define MIN_SERVO_PULSE 800
#define MAX_SERVO_PULSE 2200
#define RC_CHANS 6
#define ROLL_CHANNEL 1
#define PITCH_CHANNEL 2
#define THROTTLE_CHANNEL 3
#define YAW_CHANNEL 4
#define AUX1_CHANNEL 5
#define AUX2_CHANNEL 6
#include <SoftRcPulseOut.h>
#define SERVO1_PIN 11
#define SERVO2_PIN 12
#define SERVO3_PIN 13
#define SERVO4_PIN 14
#define NEUTRAL_US 1500 /* Default position in case of no pulse at startup */
#define NOW 1
enum {AILERON = 0, ELEVATOR, THROTTLE, RUDDER, SERVO_NB}; /* Trick: use an enumeration to declare the index of the servos AND the amount of servos */
SoftRcPulseOut ServoMotor[SERVO_NB]; /* Table Creation for SERVO_NB objects of SoftRcPulseOut type */
int servoVal3; // only throttle needs defined here (compilation error if defined in loop)
#include <TinyPpmReader.h>
#define PPM_INPUT_PIN 10
#define CHANNEL_NB 6 // does not work with 8 on stormRC receiver
float pid_p_gain_roll = 1.0; //Gain setting for the roll P-controller (1.3)
float pid_i_gain_roll = 0.0; //Gain setting for the roll I-controller (0.05)
float pid_d_gain_roll = 1.0; //Gain setting for the roll D-controller (15)
int pid_max_roll = 400; //Maximum output of the PID-controller (+/-)
float pid_p_gain_pitch = 1.0; //Gain setting for the pitch P-controller.
float pid_i_gain_pitch = 0.0; //Gain setting for the pitch I-controller.
float pid_d_gain_pitch = 1.0; //Gain setting for the pitch D-controller.
int pid_max_pitch = 400; //Maximum output of the PID-controller (+/-)
float pid_p_gain_yaw = 0.0; //Gain setting for the pitch P-controller. //4.0
float pid_i_gain_yaw = 0.0; //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 (+/-)
int pid_max_yaw_i = 100; //Maximum value of I term of the PID-controller (+/-)
float gyro_roll_cal, gyro_pitch_cal, gyro_yaw_cal;
float gyro_pitch, gyro_roll, gyro_yaw;
float gyro_roll_input, gyro_pitch_input, gyro_yaw_input;
int receiver_input_roll, receiver_input_pitch, receiver_input_throttle, receiver_input_yaw;
float pid_i_mem_roll, pid_roll_setpoint, pid_output_roll, pid_last_roll_d_error;
float pid_i_mem_pitch, pid_pitch_setpoint, pid_output_pitch, pid_last_pitch_d_error;
float pid_i_mem_yaw, pid_yaw_setpoint, pid_output_yaw, pid_last_yaw_d_error;
void setup() {
delay(5); //wait for the capacitor to charge and vcc stabilization
i2c_write_reg(GYRO_ADDRESS, 0x6B, 0x80); //PWR_MGMT_1 -- DEVICE_RESET 1
delay(5); // wait for the sensor to reset
i2c_write_reg(GYRO_ADDRESS, 0x6B, 0x03); //PWR_MGMT_1 -- SLEEP 0; CYCLE 0; TEMP_DIS 0; CLKSEL 3 (PLL with Z Gyro reference)
i2c_write_reg(GYRO_ADDRESS, 0x1A, 0); //CONFIG -- EXT_SYNC_SET 0 (disable input pin for data sync) ; default DLPF_CFG = 0 => ACC bandwidth = 260Hz GYRO bandwidth = 256Hz)
i2c_write_reg(GYRO_ADDRESS, 0x1B, 0x08); //GYRO_CONFIG -- FS_SEL = 3: Full scale set to 500 deg/sec
// Clear the 'sleep' bit to start the sensor.
MPU6050_write_reg (MPU6050_PWR_MGMT_1, 0);
delay(1); // wait for the sensor to startup
gyro_pitch_cal = 0;
gyro_roll_cal = 0;
gyro_yaw_cal = 0;
for (int i = 0; i < CALIBRATION_COUNT ; i++) {
gyro_read_raw(); //Read the gyro output.
gyro_pitch_cal += gyro_pitch; //Add pitch value to gyro_pitch_cal.
gyro_roll_cal += gyro_roll; //Add roll value to gyro_roll_cal.
gyro_yaw_cal += gyro_yaw; //Add yaw value to gyro_yaw_cal.
delay(1); //Wait one milliseconds before the next loop.
}
//Now that we have samples, we need to divide by the sample count to get the average gyro offset.
gyro_pitch_cal /= CALIBRATION_COUNT;
gyro_roll_cal /= CALIBRATION_COUNT;
gyro_yaw_cal /= CALIBRATION_COUNT;
//Configure servo pins as output.
ServoMotor[AILERON].attach(SERVO1_PIN); /* enumeration is used a index for the ServoMotor[] table */
ServoMotor[ELEVATOR].attach(SERVO2_PIN); /* enumeration is used a index for the ServoMotor[] table */
ServoMotor[THROTTLE].attach(SERVO3_PIN); /* enumeration is used a index for the ServoMotor[] table */
ServoMotor[RUDDER].attach(SERVO4_PIN); /* enumeration is used a index for the ServoMotor[] table */
TinyPpmReader.attach(PPM_INPUT_PIN); /* Attach TinyPpmReader to PPM_INPUT_PIN pin */
}
void loop() {
float rollStabMagnitude;
float pitchStabMagnitude;
static uint16_t Width_us = NEUTRAL_US; /* Static to keep the value at the next loop */
//Let's get the current gyro data and scale it to degrees per second for the pid calculations.
gyro_read_raw();
gyro_pitch -= gyro_pitch_cal; // apply calibration
gyro_roll -= gyro_roll_cal;
gyro_yaw -= gyro_yaw_cal;
gyro_pitch *= GYRO_INVERT_PITCH / 57.14286; //Gyro pid input is deg/sec. gyro_apply_inversion_and_scale
gyro_roll *= GYRO_INVERT_ROLL / 57.14286; //Gyro pid input is deg/sec.
gyro_yaw *= GYRO_INVERT_YAW / 57.14286; //Gyro pid input is deg/sec.
float uptake = 0.2; //0.2
float oneMinusUptake = 1 - uptake;
gyro_pitch_input = (gyro_pitch_input * oneMinusUptake) + (gyro_pitch * uptake);
gyro_roll_input = (gyro_roll_input * oneMinusUptake) + (gyro_roll * uptake);
gyro_yaw_input = (gyro_yaw_input * oneMinusUptake) + (gyro_yaw * uptake);
#ifdef AUX_GAIN
// gain setting via AUX channels
rollStabMagnitude = (constrain((float)TinyPpmReader.width_us(AUX1_CHANNEL) - 1000, 0, 1000)) / 1000.0f;
pitchStabMagnitude = (constrain((float)TinyPpmReader.width_us(AUX2_CHANNEL) - 1000, 0, 1000)) / 1000.0f;
gyro_roll_input *= rollStabMagnitude;
gyro_pitch_input *= pitchStabMagnitude;
#endif
#ifdef DYNAMIC_GAIN
// Dynamic gain setting
// stick center is 1 and at both end-points = 0 , like this curve /\
// so stabilization magnitude is max at centerstick and zero at full stick
rollStabMagnitude = (500 - fabs(constrain((float)TinyPpmReader.width_us(ROLL_CHANNEL) - 1500, -500, 500))) / 500.0f ;
gyro_roll_input *= rollStabMagnitude;
pitchStabMagnitude = (500 - fabs(constrain((float)TinyPpmReader.width_us(PITCH_CHANNEL) - 1500, -500, 500))) / 500.0f ;
gyro_pitch_input *= pitchStabMagnitude;
#endif // end dynamic gain
if ((TinyPpmReader.detectedChannelNb() >= CHANNEL_NB) && TinyPpmReader.isSynchro()) {
pid_roll_setpoint = RC_INVERT_ROLL * ((float)TinyPpmReader.width_us(ROLL_CHANNEL) - 1500.0f);
pid_pitch_setpoint = RC_INVERT_PITCH * ((float)TinyPpmReader.width_us(PITCH_CHANNEL) - 1500.0f);
servoVal3 = TinyPpmReader.width_us(THROTTLE_CHANNEL);
pid_yaw_setpoint = RC_INVERT_YAW * ((float)TinyPpmReader.width_us(YAW_CHANNEL) - 1500.0f);
}
//The PID set point in degrees per second is determined by the receiver input.
pid_pitch_setpoint *= PITCH_RATE / 100;
pid_roll_setpoint *= ROLL_RATE / 100;
pid_yaw_setpoint *= YAW_RATE / 100;
//PID inputs are known. So we can calculate the pid output.
float pid_error_temp;
//Pitch calculations
pid_error_temp = gyro_pitch_input - pid_pitch_setpoint;
pid_i_mem_pitch += pid_i_gain_pitch * pid_error_temp;
pid_i_mem_pitch = constrain(pid_i_mem_pitch, -pid_max_pitch, pid_max_pitch);
pid_output_pitch = pid_p_gain_pitch * pid_error_temp + pid_i_mem_pitch + pid_d_gain_pitch * (pid_error_temp - pid_last_pitch_d_error);
pid_output_pitch = constrain(pid_output_pitch, -pid_max_pitch, pid_max_pitch);
pid_last_pitch_d_error = pid_error_temp;
//Roll calculations
pid_error_temp = gyro_roll_input - pid_roll_setpoint;
pid_i_mem_roll += pid_i_gain_roll * pid_error_temp;
pid_i_mem_roll = constrain(pid_i_mem_roll, -pid_max_roll, pid_max_roll);
pid_output_roll = pid_p_gain_roll * pid_error_temp + pid_i_mem_roll + pid_d_gain_roll * (pid_error_temp - pid_last_roll_d_error);
pid_output_roll = constrain(pid_output_roll, -pid_max_roll, pid_max_roll);
pid_last_roll_d_error = pid_error_temp;
//Yaw calculations
pid_error_temp = gyro_yaw_input - pid_yaw_setpoint;
pid_i_mem_yaw += pid_i_gain_yaw * pid_error_temp;
pid_i_mem_yaw = constrain(pid_i_mem_yaw, -pid_max_yaw_i, pid_max_yaw_i);
pid_output_yaw = pid_p_gain_yaw * pid_error_temp + pid_i_mem_yaw + pid_d_gain_yaw * (pid_error_temp - pid_last_yaw_d_error);
pid_output_yaw = constrain(pid_output_yaw, -pid_max_yaw, pid_max_yaw);
pid_last_yaw_d_error = pid_error_temp;
int servoVal1 = pid_output_roll;
int servoVal2 = pid_output_pitch;
int servoVal4 = pid_output_yaw;
#ifdef ELEVON_MIXING // elevon mixing required
int leftServo = servoVal1 - servoVal2;
int rightServo = servoVal1 + servoVal2;
servoVal1 = leftServo;
servoVal2 = rightServo;
#endif // end elevon mixing
servoVal1 += 1500;
servoVal2 += 1500;
servoVal4 += 1500;
servoVal1 = constrain(servoVal1, MIN_SERVO_PULSE, MAX_SERVO_PULSE);
servoVal2 = constrain(servoVal2, MIN_SERVO_PULSE, MAX_SERVO_PULSE);
servoVal4 = constrain(servoVal4, MIN_SERVO_PULSE, MAX_SERVO_PULSE);
ServoMotor[AILERON].write_us(servoVal1);
ServoMotor[ELEVATOR].write_us(servoVal2);
ServoMotor[THROTTLE].write_us(servoVal3);
ServoMotor[RUDDER].write_us(servoVal4);
SoftRcPulseOut::refresh();
} //end loop
// --------------------------------------------------------
// MPU6050_write
//
// This is a common function to write multiple bytes to an I2C device.
//
// If only a single register is written,
// use the function MPU_6050_write_reg().
//
// Parameters:
// start : Start address, use a define for the register
// pData : A pointer to the data to write.
// size : The number of bytes to write.
//
// If only a single register is written, a pointer
// to the data has to be used, and the size is
// a single byte:
// int data = 0; // the data to write
// MPU6050_write (MPU6050_PWR_MGMT_1, &c, 1);
//
int MPU6050_write(int start, const uint8_t *pData, int size)
{
int n, error;
Wire.beginTransmission(MPU6050_I2C_ADDRESS);
n = Wire.write(start); // write the start address
if (n != 1)
return (-20);
n = Wire.write(pData, size); // write data bytes
if (n != size)
return (-21);
error = Wire.endTransmission(true); // release the I2C-bus
if (error != 0)
return (error);
return (0); // return : no error
}
// --------------------------------------------------------
// MPU6050_write_reg
//
// An extra function to write a single register.
// It is just a wrapper around the MPU_6050_write()
// function, and it is only a convenient function
// to make it easier to write a single register.
//
int MPU6050_write_reg(int reg, uint8_t data)
{
int error;
error = MPU6050_write(reg, &data, 1);
return (error);
}
void i2c_write_reg(int address, byte reg, byte val) {
Wire.beginTransmission(address);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission(true);
}
// Requires open I2C transmission with two bytes available
int gyro_read_int() {
byte highByte = Wire.read();
byte lowByte = Wire.read();
return (highByte << 8) | lowByte;
}
void gyro_read_raw() {
Wire.beginTransmission(GYRO_ADDRESS); //Start communication with the gyro
Wire.write(GYRO_VALUES_REGISTER); //Start reading and auto increment with every read
Wire.endTransmission(); //End the transmission
Wire.requestFrom(GYRO_ADDRESS, 6); //Request 6 bytes from the gyro
while (Wire.available() < 6); //Wait until the 6 bytes are received
GYRO_AXIS_1 = gyro_read_int();
GYRO_AXIS_2 = gyro_read_int();
GYRO_AXIS_3 = gyro_read_int();
}
The youtube is not mine. It's the source where I started from initially.