// Copyright (c) 2023 Oleg Kalachev // Repository: https://github.com/okalachev/flix // Flight control #include "vector.h" #include "quaternion.h" #include "pid.h" #include "lpf.h" #include "util.h" #define ARMED_THRUST 0.1 // thrust to indicate armed state #define PITCHRATE_P 0.05 #define PITCHRATE_I 0.2 #define PITCHRATE_D 0.001 #define PITCHRATE_I_LIM 0.3 #define ROLLRATE_P PITCHRATE_P #define ROLLRATE_I PITCHRATE_I #define ROLLRATE_D PITCHRATE_D #define ROLLRATE_I_LIM PITCHRATE_I_LIM #define YAWRATE_P 0.3 #define YAWRATE_I 0.0 #define YAWRATE_D 0.0 #define YAWRATE_I_LIM 0.3 #define ROLL_P 6 #define ROLL_I 0 #define ROLL_D 0 #define PITCH_P ROLL_P #define PITCH_I ROLL_I #define PITCH_D ROLL_D #define YAW_P 3 #define PITCHRATE_MAX radians(360) #define ROLLRATE_MAX radians(360) #define YAWRATE_MAX radians(300) #define TILT_MAX radians(30) #define RATES_D_LPF_ALPHA 0.2 // cutoff frequency ~ 40 Hz const int MANUAL = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes int mode = STAB; bool armed = false; PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA); PID pitchRatePID(PITCHRATE_P, PITCHRATE_I, PITCHRATE_D, PITCHRATE_I_LIM, RATES_D_LPF_ALPHA); PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D); PID rollPID(ROLL_P, ROLL_I, ROLL_D); PID pitchPID(PITCH_P, PITCH_I, PITCH_D); PID yawPID(YAW_P, 0, 0); Vector maxRate(ROLLRATE_MAX, PITCHRATE_MAX, YAWRATE_MAX); float tiltMax = TILT_MAX; Quaternion attitudeTarget; Vector ratesTarget; Vector ratesExtra; // feedforward rates Vector torqueTarget; float thrustTarget; extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT; extern float controlRoll, controlPitch, controlThrottle, controlYaw, controlMode; void control() { interpretControls(); failsafe(); controlAttitude(); controlRates(); controlTorque(); } void interpretControls() { // NOTE: put ACRO or MANUAL modes there if you want to use them if (controlMode < 0.25) mode = STAB; if (controlMode < 0.75) mode = STAB; if (controlMode > 0.75) mode = STAB; if (mode == AUTO) return; // pilot is not effective in AUTO mode if (controlThrottle < 0.05 && controlYaw > 0.95) armed = true; // arm gesture if (controlThrottle < 0.05 && controlYaw < -0.95) armed = false; // disarm gesture thrustTarget = controlThrottle; if (mode == STAB) { float yawTarget = attitudeTarget.getYaw(); if (invalid(yawTarget) || controlYaw != 0) yawTarget = attitude.getYaw(); // reset yaw target if NAN or pilot commands yaw rate attitudeTarget = Quaternion::fromEuler(Vector(controlRoll * tiltMax, controlPitch * tiltMax, yawTarget)); ratesExtra = Vector(0, 0, -controlYaw * maxRate.z); // positive yaw stick means clockwise rotation in FLU } if (mode == ACRO) { attitudeTarget.invalidate(); // skip attitude control ratesTarget.x = controlRoll * maxRate.x; ratesTarget.y = controlPitch * maxRate.y; ratesTarget.z = -controlYaw * maxRate.z; // positive yaw stick means clockwise rotation in FLU } if (mode == MANUAL) { // passthrough mode attitudeTarget.invalidate(); // skip attitude control ratesTarget.invalidate(); // skip rate control torqueTarget = Vector(controlRoll, controlPitch, -controlYaw) * 0.01; } } void controlAttitude() { if (!armed || attitudeTarget.invalid()) { // skip attitude control rollPID.reset(); pitchPID.reset(); yawPID.reset(); return; } const Vector up(0, 0, 1); Vector upActual = Quaternion::rotateVector(up, attitude); Vector upTarget = Quaternion::rotateVector(up, attitudeTarget); Vector error = Vector::rotationVectorBetween(upTarget, upActual); ratesTarget.x = rollPID.update(error.x, dt) + ratesExtra.x; ratesTarget.y = pitchPID.update(error.y, dt) + ratesExtra.y; float yawError = wrapAngle(attitudeTarget.getYaw() - attitude.getYaw()); ratesTarget.z = yawPID.update(yawError, dt) + ratesExtra.z; } void controlRates() { if (!armed || ratesTarget.invalid()) { // skip rates control rollRatePID.reset(); pitchRatePID.reset(); yawRatePID.reset(); return; } Vector error = ratesTarget - rates; // Calculate desired torque, where 0 - no torque, 1 - maximum possible torque torqueTarget.x = rollRatePID.update(error.x, dt); torqueTarget.y = pitchRatePID.update(error.y, dt); torqueTarget.z = yawRatePID.update(error.z, dt); } void controlTorque() { if (!torqueTarget.valid()) return; // skip torque control if (!armed) { memset(motors, 0, sizeof(motors)); // stop motors if disarmed return; } if (thrustTarget < 0.05) { // minimal thrust to indicate armed state motors[0] = ARMED_THRUST; motors[1] = ARMED_THRUST; motors[2] = ARMED_THRUST; motors[3] = ARMED_THRUST; return; } motors[MOTOR_FRONT_LEFT] = thrustTarget + torqueTarget.x - torqueTarget.y + torqueTarget.z; motors[MOTOR_FRONT_RIGHT] = thrustTarget - torqueTarget.x - torqueTarget.y - torqueTarget.z; motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z; motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z; motors[0] = constrain(motors[0], 0, 1); motors[1] = constrain(motors[1], 0, 1); motors[2] = constrain(motors[2], 0, 1); motors[3] = constrain(motors[3], 0, 1); } const char* getModeName() { switch (mode) { case MANUAL: return "MANUAL"; case ACRO: return "ACRO"; case STAB: return "STAB"; case AUTO: return "AUTO"; default: return "UNKNOWN"; } }