/* * This file is part of Cleanflight and Betaflight. * * Cleanflight and Betaflight are free software. You can redistribute * this software and/or modify this software under the terms of the * GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) * any later version. * * Cleanflight and Betaflight are distributed in the hope that they * will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software. * * If not, see . */ #include #include #include #include "platform.h" #include "build/debug.h" #include "common/axis.h" #include "common/filter.h" #include "common/maths.h" #include "config/config.h" #include "config/feature.h" #include "drivers/dshot.h" #include "drivers/io.h" #include "drivers/motor.h" #include "drivers/time.h" #include "fc/controlrate_profile.h" #include "fc/core.h" #include "fc/rc.h" #include "fc/rc_controls.h" #include "fc/rc_modes.h" #include "fc/runtime_config.h" #include "flight/failsafe.h" #include "flight/gps_rescue.h" #include "flight/imu.h" #include "flight/mixer_init.h" #include "flight/mixer_tricopter.h" #include "flight/pid.h" #include "flight/rpm_filter.h" #include "pg/rx.h" #include "rx/rx.h" #include "sensors/battery.h" #include "sensors/gyro.h" #include "mixer.h" #define DYN_LPF_THROTTLE_STEPS 100 #define DYN_LPF_THROTTLE_UPDATE_DELAY_US 5000 // minimum of 5ms between updates static FAST_DATA_ZERO_INIT float motorMixRange; float FAST_DATA_ZERO_INIT motor[MAX_SUPPORTED_MOTORS]; float motor_disarmed[MAX_SUPPORTED_MOTORS]; static FAST_DATA_ZERO_INIT int throttleAngleCorrection; float getMotorMixRange(void) { return motorMixRange; } void writeMotors(void) { motorWriteAll(motor); } static void writeAllMotors(int16_t mc) { // Sends commands to all motors for (int i = 0; i < mixerRuntime.motorCount; i++) { motor[i] = mc; } writeMotors(); } void stopMotors(void) { writeAllMotors(mixerRuntime.disarmMotorOutput); delay(50); // give the timers and ESCs a chance to react. } static FAST_DATA_ZERO_INIT float throttle = 0; static FAST_DATA_ZERO_INIT float mixerThrottle = 0; static FAST_DATA_ZERO_INIT float motorOutputMin; static FAST_DATA_ZERO_INIT float motorRangeMin; static FAST_DATA_ZERO_INIT float motorRangeMax; static FAST_DATA_ZERO_INIT float motorOutputRange; static FAST_DATA_ZERO_INIT int8_t motorOutputMixSign; static void calculateThrottleAndCurrentMotorEndpoints(timeUs_t currentTimeUs) { static uint16_t rcThrottlePrevious = 0; // Store the last throttle direction for deadband transitions static timeUs_t reversalTimeUs = 0; // time when motors last reversed in 3D mode static float motorRangeMinIncrease = 0; float currentThrottleInputRange = 0; if (mixerRuntime.feature3dEnabled) { uint16_t rcCommand3dDeadBandLow; uint16_t rcCommand3dDeadBandHigh; if (!ARMING_FLAG(ARMED)) { rcThrottlePrevious = rxConfig()->midrc; // When disarmed set to mid_rc. It always results in positive direction after arming. } if (IS_RC_MODE_ACTIVE(BOX3D) || flight3DConfig()->switched_mode3d) { // The min_check range is halved because the output throttle is scaled to 500us. // So by using half of min_check we maintain the same low-throttle deadband // stick travel as normal non-3D mode. const int mincheckOffset = (rxConfig()->mincheck - PWM_RANGE_MIN) / 2; rcCommand3dDeadBandLow = rxConfig()->midrc - mincheckOffset; rcCommand3dDeadBandHigh = rxConfig()->midrc + mincheckOffset; } else { rcCommand3dDeadBandLow = rxConfig()->midrc - flight3DConfig()->deadband3d_throttle; rcCommand3dDeadBandHigh = rxConfig()->midrc + flight3DConfig()->deadband3d_throttle; } const float rcCommandThrottleRange3dLow = rcCommand3dDeadBandLow - PWM_RANGE_MIN; const float rcCommandThrottleRange3dHigh = PWM_RANGE_MAX - rcCommand3dDeadBandHigh; if (rcCommand[THROTTLE] <= rcCommand3dDeadBandLow || isFlipOverAfterCrashActive()) { // INVERTED motorRangeMin = mixerRuntime.motorOutputLow; motorRangeMax = mixerRuntime.deadbandMotor3dLow; #ifdef USE_DSHOT if (isMotorProtocolDshot()) { motorOutputMin = mixerRuntime.motorOutputLow; motorOutputRange = mixerRuntime.deadbandMotor3dLow - mixerRuntime.motorOutputLow; } else #endif { motorOutputMin = mixerRuntime.deadbandMotor3dLow; motorOutputRange = mixerRuntime.motorOutputLow - mixerRuntime.deadbandMotor3dLow; } if (motorOutputMixSign != -1) { reversalTimeUs = currentTimeUs; } motorOutputMixSign = -1; rcThrottlePrevious = rcCommand[THROTTLE]; throttle = rcCommand3dDeadBandLow - rcCommand[THROTTLE]; currentThrottleInputRange = rcCommandThrottleRange3dLow; } else if (rcCommand[THROTTLE] >= rcCommand3dDeadBandHigh) { // NORMAL motorRangeMin = mixerRuntime.deadbandMotor3dHigh; motorRangeMax = mixerRuntime.motorOutputHigh; motorOutputMin = mixerRuntime.deadbandMotor3dHigh; motorOutputRange = mixerRuntime.motorOutputHigh - mixerRuntime.deadbandMotor3dHigh; if (motorOutputMixSign != 1) { reversalTimeUs = currentTimeUs; } motorOutputMixSign = 1; rcThrottlePrevious = rcCommand[THROTTLE]; throttle = rcCommand[THROTTLE] - rcCommand3dDeadBandHigh; currentThrottleInputRange = rcCommandThrottleRange3dHigh; } else if ((rcThrottlePrevious <= rcCommand3dDeadBandLow && !flight3DConfigMutable()->switched_mode3d) || isMotorsReversed()) { // INVERTED_TO_DEADBAND motorRangeMin = mixerRuntime.motorOutputLow; motorRangeMax = mixerRuntime.deadbandMotor3dLow; #ifdef USE_DSHOT if (isMotorProtocolDshot()) { motorOutputMin = mixerRuntime.motorOutputLow; motorOutputRange = mixerRuntime.deadbandMotor3dLow - mixerRuntime.motorOutputLow; } else #endif { motorOutputMin = mixerRuntime.deadbandMotor3dLow; motorOutputRange = mixerRuntime.motorOutputLow - mixerRuntime.deadbandMotor3dLow; } if (motorOutputMixSign != -1) { reversalTimeUs = currentTimeUs; } motorOutputMixSign = -1; throttle = 0; currentThrottleInputRange = rcCommandThrottleRange3dLow; } else { // NORMAL_TO_DEADBAND motorRangeMin = mixerRuntime.deadbandMotor3dHigh; motorRangeMax = mixerRuntime.motorOutputHigh; motorOutputMin = mixerRuntime.deadbandMotor3dHigh; motorOutputRange = mixerRuntime.motorOutputHigh - mixerRuntime.deadbandMotor3dHigh; if (motorOutputMixSign != 1) { reversalTimeUs = currentTimeUs; } motorOutputMixSign = 1; throttle = 0; currentThrottleInputRange = rcCommandThrottleRange3dHigh; } if (currentTimeUs - reversalTimeUs < 250000) { // keep iterm zero for 250ms after motor reversal pidResetIterm(); } } else { throttle = rcCommand[THROTTLE] - PWM_RANGE_MIN + throttleAngleCorrection; currentThrottleInputRange = PWM_RANGE; #ifdef USE_DYN_IDLE if (mixerRuntime.dynIdleMinRps > 0.0f) { const float maxIncrease = isAirmodeActivated() ? mixerRuntime.dynIdleMaxIncrease : 0.04f; float minRps = rpmMinMotorFrequency(); DEBUG_SET(DEBUG_DYN_IDLE, 3, (minRps * 10)); float rpsError = mixerRuntime.dynIdleMinRps - minRps; // PT1 type lowpass delay and smoothing for D minRps = mixerRuntime.prevMinRps + mixerRuntime.minRpsDelayK * (minRps - mixerRuntime.prevMinRps); float dynIdleD = (mixerRuntime.prevMinRps - minRps) * mixerRuntime.dynIdleDGain; mixerRuntime.prevMinRps = minRps; float dynIdleP = rpsError * mixerRuntime.dynIdlePGain; rpsError = MAX(-0.1f, rpsError); //I rises fast, falls slowly mixerRuntime.dynIdleI += rpsError * mixerRuntime.dynIdleIGain; mixerRuntime.dynIdleI = constrainf(mixerRuntime.dynIdleI, 0.0f, maxIncrease); motorRangeMinIncrease = constrainf((dynIdleP + mixerRuntime.dynIdleI + dynIdleD), 0.0f, maxIncrease); DEBUG_SET(DEBUG_DYN_IDLE, 0, (MAX(-1000.0f, dynIdleP * 10000))); DEBUG_SET(DEBUG_DYN_IDLE, 1, (mixerRuntime.dynIdleI * 10000)); DEBUG_SET(DEBUG_DYN_IDLE, 2, (dynIdleD * 10000)); } else { motorRangeMinIncrease = 0; } #endif #if defined(USE_BATTERY_VOLTAGE_SAG_COMPENSATION) float motorRangeAttenuationFactor = 0; // reduce motorRangeMax when battery is full if (mixerRuntime.vbatSagCompensationFactor > 0.0f) { const uint16_t currentCellVoltage = getBatterySagCellVoltage(); // batteryGoodness = 1 when voltage is above vbatFull, and 0 when voltage is below vbatLow float batteryGoodness = 1.0f - constrainf((mixerRuntime.vbatFull - currentCellVoltage) / mixerRuntime.vbatRangeToCompensate, 0.0f, 1.0f); motorRangeAttenuationFactor = (mixerRuntime.vbatRangeToCompensate / mixerRuntime.vbatFull) * batteryGoodness * mixerRuntime.vbatSagCompensationFactor; DEBUG_SET(DEBUG_BATTERY, 2, batteryGoodness * 100); DEBUG_SET(DEBUG_BATTERY, 3, motorRangeAttenuationFactor * 1000); } motorRangeMax = mixerRuntime.motorOutputHigh - motorRangeAttenuationFactor * (mixerRuntime.motorOutputHigh - mixerRuntime.motorOutputLow); #else motorRangeMax = mixerRuntime.motorOutputHigh; #endif motorRangeMin = mixerRuntime.motorOutputLow + motorRangeMinIncrease * (mixerRuntime.motorOutputHigh - mixerRuntime.motorOutputLow); motorOutputMin = motorRangeMin; motorOutputRange = motorRangeMax - motorRangeMin; motorOutputMixSign = 1; } throttle = constrainf(throttle / currentThrottleInputRange, 0.0f, 1.0f); } #define CRASH_FLIP_DEADBAND 20 #define CRASH_FLIP_STICK_MINF 0.15f static void applyFlipOverAfterCrashModeToMotors(void) { if (ARMING_FLAG(ARMED)) { const float flipPowerFactor = 1.0f - mixerConfig()->crashflip_expo / 100.0f; const float stickDeflectionPitchAbs = getRcDeflectionAbs(FD_PITCH); const float stickDeflectionRollAbs = getRcDeflectionAbs(FD_ROLL); const float stickDeflectionYawAbs = getRcDeflectionAbs(FD_YAW); const float stickDeflectionPitchExpo = flipPowerFactor * stickDeflectionPitchAbs + power3(stickDeflectionPitchAbs) * (1 - flipPowerFactor); const float stickDeflectionRollExpo = flipPowerFactor * stickDeflectionRollAbs + power3(stickDeflectionRollAbs) * (1 - flipPowerFactor); const float stickDeflectionYawExpo = flipPowerFactor * stickDeflectionYawAbs + power3(stickDeflectionYawAbs) * (1 - flipPowerFactor); float signPitch = getRcDeflection(FD_PITCH) < 0 ? 1 : -1; float signRoll = getRcDeflection(FD_ROLL) < 0 ? 1 : -1; float signYaw = (getRcDeflection(FD_YAW) < 0 ? 1 : -1) * (mixerConfig()->yaw_motors_reversed ? 1 : -1); float stickDeflectionLength = sqrtf(sq(stickDeflectionPitchAbs) + sq(stickDeflectionRollAbs)); float stickDeflectionExpoLength = sqrtf(sq(stickDeflectionPitchExpo) + sq(stickDeflectionRollExpo)); if (stickDeflectionYawAbs > MAX(stickDeflectionPitchAbs, stickDeflectionRollAbs)) { // If yaw is the dominant, disable pitch and roll stickDeflectionLength = stickDeflectionYawAbs; stickDeflectionExpoLength = stickDeflectionYawExpo; signRoll = 0; signPitch = 0; } else { // If pitch/roll dominant, disable yaw signYaw = 0; } const float cosPhi = (stickDeflectionLength > 0) ? (stickDeflectionPitchAbs + stickDeflectionRollAbs) / (sqrtf(2.0f) * stickDeflectionLength) : 0; const float cosThreshold = sqrtf(3.0f)/2.0f; // cos(PI/6.0f) if (cosPhi < cosThreshold) { // Enforce either roll or pitch exclusively, if not on diagonal if (stickDeflectionRollAbs > stickDeflectionPitchAbs) { signPitch = 0; } else { signRoll = 0; } } // Apply a reasonable amount of stick deadband const float crashFlipStickMinExpo = flipPowerFactor * CRASH_FLIP_STICK_MINF + power3(CRASH_FLIP_STICK_MINF) * (1 - flipPowerFactor); const float flipStickRange = 1.0f - crashFlipStickMinExpo; const float flipPower = MAX(0.0f, stickDeflectionExpoLength - crashFlipStickMinExpo) / flipStickRange; for (int i = 0; i < mixerRuntime.motorCount; ++i) { float motorOutputNormalised = signPitch * mixerRuntime.currentMixer[i].pitch + signRoll * mixerRuntime.currentMixer[i].roll + signYaw * mixerRuntime.currentMixer[i].yaw; if (motorOutputNormalised < 0) { if (mixerConfig()->crashflip_motor_percent > 0) { motorOutputNormalised = -motorOutputNormalised * (float)mixerConfig()->crashflip_motor_percent / 100.0f; } else { motorOutputNormalised = 0; } } motorOutputNormalised = MIN(1.0f, flipPower * motorOutputNormalised); float motorOutput = motorOutputMin + motorOutputNormalised * motorOutputRange; // Add a little bit to the motorOutputMin so props aren't spinning when sticks are centered motorOutput = (motorOutput < motorOutputMin + CRASH_FLIP_DEADBAND) ? mixerRuntime.disarmMotorOutput : (motorOutput - CRASH_FLIP_DEADBAND); motor[i] = motorOutput; } } else { // Disarmed mode for (int i = 0; i < mixerRuntime.motorCount; i++) { motor[i] = motor_disarmed[i]; } } } static void applyMixToMotors(float motorMix[MAX_SUPPORTED_MOTORS], motorMixer_t *activeMixer) { // Now add in the desired throttle, but keep in a range that doesn't clip adjusted // roll/pitch/yaw. This could move throttle down, but also up for those low throttle flips. for (int i = 0; i < mixerRuntime.motorCount; i++) { float motorOutput = motorOutputMixSign * motorMix[i] + throttle * activeMixer[i].throttle; #ifdef USE_THRUST_LINEARIZATION motorOutput = pidApplyThrustLinearization(motorOutput); #endif motorOutput = motorOutputMin + motorOutputRange * motorOutput; #ifdef USE_SERVOS if (mixerIsTricopter()) { motorOutput += mixerTricopterMotorCorrection(i); } #endif if (failsafeIsActive()) { #ifdef USE_DSHOT if (isMotorProtocolDshot()) { motorOutput = (motorOutput < motorRangeMin) ? mixerRuntime.disarmMotorOutput : motorOutput; // Prevent getting into special reserved range } #endif motorOutput = constrain(motorOutput, mixerRuntime.disarmMotorOutput, motorRangeMax); } else { motorOutput = constrain(motorOutput, motorRangeMin, motorRangeMax); } motor[i] = motorOutput; } // Disarmed mode if (!ARMING_FLAG(ARMED)) { for (int i = 0; i < mixerRuntime.motorCount; i++) { motor[i] = motor_disarmed[i]; } } } static float applyThrottleLimit(float throttle) { if (currentControlRateProfile->throttle_limit_percent < 100) { const float throttleLimitFactor = currentControlRateProfile->throttle_limit_percent / 100.0f; switch (currentControlRateProfile->throttle_limit_type) { case THROTTLE_LIMIT_TYPE_SCALE: return throttle * throttleLimitFactor; case THROTTLE_LIMIT_TYPE_CLIP: return MIN(throttle, throttleLimitFactor); } } return throttle; } static void applyMotorStop(void) { for (int i = 0; i < mixerRuntime.motorCount; i++) { motor[i] = mixerRuntime.disarmMotorOutput; } } #ifdef USE_DYN_LPF static void updateDynLpfCutoffs(timeUs_t currentTimeUs, float throttle) { static timeUs_t lastDynLpfUpdateUs = 0; static int dynLpfPreviousQuantizedThrottle = -1; // to allow an initial zero throttle to set the filter cutoff if (cmpTimeUs(currentTimeUs, lastDynLpfUpdateUs) >= DYN_LPF_THROTTLE_UPDATE_DELAY_US) { const int quantizedThrottle = lrintf(throttle * DYN_LPF_THROTTLE_STEPS); // quantize the throttle reduce the number of filter updates if (quantizedThrottle != dynLpfPreviousQuantizedThrottle) { // scale the quantized value back to the throttle range so the filter cutoff steps are repeatable const float dynLpfThrottle = (float)quantizedThrottle / DYN_LPF_THROTTLE_STEPS; dynLpfGyroUpdate(dynLpfThrottle); dynLpfDTermUpdate(dynLpfThrottle); dynLpfPreviousQuantizedThrottle = quantizedThrottle; lastDynLpfUpdateUs = currentTimeUs; } } } #endif static void applyLinearMixerAdjustment(float *motorMix, float motorMixRange, bool airmodeEnabled) { float motorMixNormalizationFactor = motorMixRange > 1.0f ? motorMixRange : 1.0f; float motorMixDelta = 0.5f * motorMixRange; for (int i = 0; i < mixerRuntime.motorCount; ++i) { if (airmodeEnabled || throttle > 0.5f) { motorMix[i] = scaleRangef(throttle, 0.0f, 1.0f, motorMix[i] + motorMixDelta, motorMix[i] - motorMixDelta); } motorMix[i] /= motorMixNormalizationFactor; } } static void applyMixerAdjustment(float *motorMix, float motorMixMin, float motorMixMax, float motorMixRange, bool airmodeEnabled) { #ifdef USE_AIRMODE_LPF const float unadjustedThrottle = throttle; throttle += pidGetAirmodeThrottleOffset(); float airmodeThrottleChange = 0; #endif if (motorMixRange > 1.0f) { for (int i = 0; i < mixerRuntime.motorCount; i++) { motorMix[i] /= motorMixRange; } // Get the maximum correction by setting offset to center when airmode enabled if (airmodeEnabled) { throttle = 0.5f; } } else { if (airmodeEnabled || throttle > 0.5f) { throttle = constrainf(throttle, -motorMixMin, 1.0f - motorMixMax); #ifdef USE_AIRMODE_LPF airmodeThrottleChange = constrainf(unadjustedThrottle, -motorMixMin, 1.0f - motorMixMax) - unadjustedThrottle; #endif } } #ifdef USE_AIRMODE_LPF pidUpdateAirmodeLpf(airmodeThrottleChange); #endif } FAST_CODE_NOINLINE void mixTable(timeUs_t currentTimeUs) { // Find min and max throttle based on conditions. Throttle has to be known before mixing calculateThrottleAndCurrentMotorEndpoints(currentTimeUs); if (isFlipOverAfterCrashActive()) { applyFlipOverAfterCrashModeToMotors(); return; } const bool launchControlActive = isLaunchControlActive(); motorMixer_t * activeMixer = &mixerRuntime.currentMixer[0]; #ifdef USE_LAUNCH_CONTROL if (launchControlActive && (currentPidProfile->launchControlMode == LAUNCH_CONTROL_MODE_PITCHONLY)) { activeMixer = &mixerRuntime.launchControlMixer[0]; } #endif // Calculate and Limit the PID sum const float scaledAxisPidRoll = constrainf(pidData[FD_ROLL].Sum, -currentPidProfile->pidSumLimit, currentPidProfile->pidSumLimit) / PID_MIXER_SCALING; const float scaledAxisPidPitch = constrainf(pidData[FD_PITCH].Sum, -currentPidProfile->pidSumLimit, currentPidProfile->pidSumLimit) / PID_MIXER_SCALING; uint16_t yawPidSumLimit = currentPidProfile->pidSumLimitYaw; #ifdef USE_YAW_SPIN_RECOVERY const bool yawSpinDetected = gyroYawSpinDetected(); if (yawSpinDetected) { yawPidSumLimit = PIDSUM_LIMIT_MAX; // Set to the maximum limit during yaw spin recovery to prevent limiting motor authority } #endif // USE_YAW_SPIN_RECOVERY float scaledAxisPidYaw = constrainf(pidData[FD_YAW].Sum, -yawPidSumLimit, yawPidSumLimit) / PID_MIXER_SCALING; if (!mixerConfig()->yaw_motors_reversed) { scaledAxisPidYaw = -scaledAxisPidYaw; } // Apply the throttle_limit_percent to scale or limit the throttle based on throttle_limit_type if (currentControlRateProfile->throttle_limit_type != THROTTLE_LIMIT_TYPE_OFF) { throttle = applyThrottleLimit(throttle); } // use scaled throttle, without dynamic idle throttle offset, as the input to antigravity pidUpdateAntiGravityThrottleFilter(throttle); #ifdef USE_DYN_LPF // keep the changes to dynamic lowpass clean, without unnecessary dynamic changes updateDynLpfCutoffs(currentTimeUs, throttle); #endif // apply throttle boost when throttle moves quickly #if defined(USE_THROTTLE_BOOST) if (throttleBoost > 0.0f) { const float throttleHpf = throttle - pt1FilterApply(&throttleLpf, throttle); throttle = constrainf(throttle + throttleBoost * throttleHpf, 0.0f, 1.0f); } #endif // send throttle value to blackbox, including scaling and throttle boost, but not TL compensation, dyn idle or airmode mixerThrottle = throttle; #ifdef USE_DYN_IDLE // Apply digital idle throttle offset when stick is at zero after all other adjustments are complete if (mixerRuntime.dynIdleMinRps > 0.0f) { throttle = MAX(throttle, mixerRuntime.idleThrottleOffset); } #endif #ifdef USE_THRUST_LINEARIZATION // reduce throttle to offset additional motor output throttle = pidCompensateThrustLinearization(throttle); #endif // Find roll/pitch/yaw desired output // ??? Where is the optimal location for this code? float motorMix[MAX_SUPPORTED_MOTORS]; float motorMixMax = 0, motorMixMin = 0; for (int i = 0; i < mixerRuntime.motorCount; i++) { float mix = scaledAxisPidRoll * activeMixer[i].roll + scaledAxisPidPitch * activeMixer[i].pitch + scaledAxisPidYaw * activeMixer[i].yaw; if (mix > motorMixMax) { motorMixMax = mix; } else if (mix < motorMixMin) { motorMixMin = mix; } motorMix[i] = mix; } // The following fixed throttle values will not be shown in the blackbox log // ?? Should they be influenced by airmode? If not, should go after the apply airmode code. const bool airmodeEnabled = airmodeIsEnabled() || launchControlActive; #ifdef USE_YAW_SPIN_RECOVERY // 50% throttle provides the maximum authority for yaw recovery when airmode is not active. // When airmode is active the throttle setting doesn't impact recovery authority. if (yawSpinDetected && !airmodeEnabled) { throttle = 0.5f; } #endif // USE_YAW_SPIN_RECOVERY #ifdef USE_LAUNCH_CONTROL // While launch control is active keep the throttle at minimum. // Once the pilot triggers the launch throttle control will be reactivated. if (launchControlActive) { throttle = 0.0f; } #endif #ifdef USE_GPS_RESCUE // If gps rescue is active then override the throttle. This prevents things // like throttle boost or throttle limit from negatively affecting the throttle. if (FLIGHT_MODE(GPS_RESCUE_MODE)) { throttle = gpsRescueGetThrottle(); } #endif motorMixRange = motorMixMax - motorMixMin; if (mixerConfig()->linear_mixer) { applyLinearMixerAdjustment(motorMix, motorMixRange, airmodeEnabled); } else { applyMixerAdjustment(motorMix, motorMixMin, motorMixMax, motorMixRange, airmodeEnabled); } if (featureIsEnabled(FEATURE_MOTOR_STOP) && ARMING_FLAG(ARMED) && !mixerRuntime.feature3dEnabled && !airmodeEnabled && !FLIGHT_MODE(GPS_RESCUE_MODE) // disable motor_stop while GPS Rescue is active && (rcData[THROTTLE] < rxConfig()->mincheck)) { // motor_stop handling applyMotorStop(); } else { // Apply the mix to motor endpoints applyMixToMotors(motorMix, activeMixer); } } void mixerSetThrottleAngleCorrection(int correctionValue) { throttleAngleCorrection = correctionValue; } float mixerGetThrottle(void) { return mixerThrottle; }