1
0
Fork 0
mirror of https://github.com/iNavFlight/inav.git synced 2025-07-24 16:55:29 +03:00
inav/src/main/flight/pid.c
2019-05-10 20:15:53 +02:00

980 lines
38 KiB
C

/*
* This file is part of Cleanflight.
*
* Cleanflight is free software: you can redistribute it and/or modify
* it 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 is distributed in the hope that it 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 Cleanflight. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdint.h>
#include <math.h>
#include <platform.h>
#include "build/build_config.h"
#include "build/debug.h"
#include "common/axis.h"
#include "common/filter.h"
#include "common/maths.h"
#include "common/utils.h"
#include "config/parameter_group.h"
#include "config/parameter_group_ids.h"
#include "fc/config.h"
#include "fc/controlrate_profile.h"
#include "fc/rc_controls.h"
#include "fc/rc_modes.h"
#include "fc/runtime_config.h"
#include "flight/pid.h"
#include "flight/imu.h"
#include "flight/mixer.h"
#include "io/gps.h"
#include "navigation/navigation.h"
#include "rx/rx.h"
#include "sensors/sensors.h"
#include "sensors/gyro.h"
#include "sensors/acceleration.h"
#include "sensors/compass.h"
#include "sensors/pitotmeter.h"
typedef struct {
float kP; // Proportional gain
float kI; // Integral gain
float kD; // Derivative gain
float kFF; // Feed-forward gain
float kT; // Back-calculation tracking gain
float gyroRate;
float rateTarget;
// Buffer for derivative calculation
#define PID_GYRO_RATE_BUF_LENGTH 5
float gyroRateBuf[PID_GYRO_RATE_BUF_LENGTH];
firFilter_t gyroRateFilter;
// Rate integrator
float errorGyroIf;
float errorGyroIfLimit;
// Used for ANGLE filtering (PT1, we don't need super-sharpness here)
pt1Filter_t angleFilterState;
// Rate filtering
rateLimitFilter_t axisAccelFilter;
pt1Filter_t ptermLpfState;
biquadFilter_t deltaLpfState;
// Dterm notch filtering
#ifdef USE_DTERM_NOTCH
biquadFilter_t deltaNotchFilter;
#endif
float stickPosition;
} pidState_t;
#ifdef USE_DTERM_NOTCH
STATIC_FASTRAM filterApplyFnPtr notchFilterApplyFn;
#endif
extern float dT;
STATIC_FASTRAM bool pidFiltersConfigured = false;
FASTRAM float headingHoldCosZLimit;
FASTRAM int16_t headingHoldTarget;
STATIC_FASTRAM pt1Filter_t headingHoldRateFilter;
STATIC_FASTRAM pt1Filter_t fixedWingTpaFilter;
// Thrust PID Attenuation factor. 0.0f means fully attenuated, 1.0f no attenuation is applied
STATIC_FASTRAM bool pidGainsUpdateRequired;
FASTRAM int16_t axisPID[FLIGHT_DYNAMICS_INDEX_COUNT];
#ifdef USE_BLACKBOX
int32_t axisPID_P[FLIGHT_DYNAMICS_INDEX_COUNT], axisPID_I[FLIGHT_DYNAMICS_INDEX_COUNT], axisPID_D[FLIGHT_DYNAMICS_INDEX_COUNT], axisPID_Setpoint[FLIGHT_DYNAMICS_INDEX_COUNT];
#endif
STATIC_FASTRAM pidState_t pidState[FLIGHT_DYNAMICS_INDEX_COUNT];
static EXTENDED_FASTRAM pt1Filter_t windupLpf[XYZ_AXIS_COUNT];
static EXTENDED_FASTRAM uint8_t itermRelax;
static EXTENDED_FASTRAM uint8_t itermRelaxType;
static EXTENDED_FASTRAM float itermRelaxSetpointThreshold;
#if defined(USE_D_BOOST)
#define D_BOOST_GYRO_LPF_HZ 80 // Biquad lowpass input cutoff to peak D around propwash frequencies
#define D_BOOST_LPF_HZ 10 // PT1 lowpass cutoff to smooth the boost effect
static EXTENDED_FASTRAM float dBoostFactor;
static EXTENDED_FASTRAM float dBoostMaxAtAlleceleration;
static EXTENDED_FASTRAM pt1Filter_t dBoostLpf[XYZ_AXIS_COUNT];
static EXTENDED_FASTRAM biquadFilter_t dBoostGyroLpf[XYZ_AXIS_COUNT];
static EXTENDED_FASTRAM float previousRateTarget[XYZ_AXIS_COUNT] = {0, 0, 0};
static EXTENDED_FASTRAM float previousRateGyro[XYZ_AXIS_COUNT] = {0, 0, 0};
#endif
PG_REGISTER_PROFILE_WITH_RESET_TEMPLATE(pidProfile_t, pidProfile, PG_PID_PROFILE, 9);
PG_RESET_TEMPLATE(pidProfile_t, pidProfile,
.bank_mc = {
.pid = {
[PID_ROLL] = { 40, 30, 23, 0 },
[PID_PITCH] = { 40, 30, 23, 0 },
[PID_YAW] = { 85, 45, 0, 0 },
[PID_LEVEL] = {
.P = 20, // Self-level strength
.I = 15, // Self-leveing low-pass frequency (0 - disabled)
.D = 75, // 75% horizon strength
.FF = 0,
},
[PID_HEADING] = { 60, 0, 0, 0 },
[PID_POS_XY] = {
.P = 65, // NAV_POS_XY_P * 100
.I = 0,
.D = 0,
.FF = 0,
},
[PID_VEL_XY] = {
.P = 40, // NAV_VEL_XY_P * 20
.I = 15, // NAV_VEL_XY_I * 100
.D = 100, // NAV_VEL_XY_D * 100
.FF = 40, // NAV_VEL_XY_D * 100
},
[PID_POS_Z] = {
.P = 50, // NAV_POS_Z_P * 100
.I = 0, // not used
.D = 0, // not used
.FF = 0,
},
[PID_VEL_Z] = {
.P = 100, // NAV_VEL_Z_P * 66.7
.I = 50, // NAV_VEL_Z_I * 20
.D = 10, // NAV_VEL_Z_D * 100
.FF = 0,
}
}
},
.bank_fw = {
.pid = {
[PID_ROLL] = { 5, 7, 0, 50 },
[PID_PITCH] = { 5, 7, 0, 50 },
[PID_YAW] = { 6, 10, 0, 60 },
[PID_LEVEL] = {
.P = 20, // Self-level strength
.I = 5, // Self-leveing low-pass frequency (0 - disabled)
.D = 75, // 75% horizon strength
.FF = 0,
},
[PID_HEADING] = { 60, 0, 0, 0 },
[PID_POS_Z] = {
.P = 40, // FW_POS_Z_P * 10
.I = 5, // FW_POS_Z_I * 10
.D = 10, // FW_POS_Z_D * 10
.FF = 0,
},
[PID_POS_XY] = {
.P = 75, // FW_POS_XY_P * 100
.I = 5, // FW_POS_XY_I * 100
.D = 8, // FW_POS_XY_D * 100
.FF = 0,
}
}
},
.dterm_soft_notch_hz = 0,
.dterm_soft_notch_cutoff = 1,
.dterm_lpf_hz = 40,
.yaw_lpf_hz = 30,
.dterm_setpoint_weight = 1.0f,
.use_dterm_fir_filter = 1,
.itermWindupPointPercent = 50, // Percent
.axisAccelerationLimitYaw = 10000, // dps/s
.axisAccelerationLimitRollPitch = 0, // dps/s
.yaw_p_limit = YAW_P_LIMIT_DEFAULT,
.heading_hold_rate_limit = HEADING_HOLD_RATE_LIMIT_DEFAULT,
.max_angle_inclination[FD_ROLL] = 300, // 30 degrees
.max_angle_inclination[FD_PITCH] = 300, // 30 degrees
.pidSumLimit = PID_SUM_LIMIT_DEFAULT,
.fixedWingItermThrowLimit = FW_ITERM_THROW_LIMIT_DEFAULT,
.fixedWingReferenceAirspeed = 1000,
.fixedWingCoordinatedYawGain = 1.0f,
.fixedWingItermLimitOnStickPosition = 0.5f,
.loiter_direction = NAV_LOITER_RIGHT,
.navVelXyDTermLpfHz = NAV_ACCEL_CUTOFF_FREQUENCY_HZ,
.iterm_relax_type = ITERM_RELAX_SETPOINT,
.iterm_relax_cutoff = MC_ITERM_RELAX_CUTOFF_DEFAULT,
.iterm_relax = ITERM_RELAX_OFF,
.dBoostFactor = 1.0f,
.dBoostMaxAtAlleceleration = 7500.0f,
.dBoostGyroDeltaLpfHz = D_BOOST_GYRO_LPF_HZ,
);
void pidInit(void)
{
pidResetTPAFilter();
// Calculate max overall tilt (max pitch + max roll combined) as a limit to heading hold
headingHoldCosZLimit = cos_approx(DECIDEGREES_TO_RADIANS(pidProfile()->max_angle_inclination[FD_ROLL])) *
cos_approx(DECIDEGREES_TO_RADIANS(pidProfile()->max_angle_inclination[FD_PITCH]));
pidGainsUpdateRequired = false;
itermRelax = pidProfile()->iterm_relax;
itermRelaxType = pidProfile()->iterm_relax_type;
itermRelaxSetpointThreshold = MC_ITERM_RELAX_SETPOINT_THRESHOLD * MC_ITERM_RELAX_CUTOFF_DEFAULT / pidProfile()->iterm_relax_cutoff;
#ifdef USE_D_BOOST
dBoostFactor = pidProfile()->dBoostFactor;
dBoostMaxAtAlleceleration = pidProfile()->dBoostMaxAtAlleceleration;
for (int axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
biquadFilterInitLPF(&dBoostGyroLpf[axis], pidProfile()->dBoostGyroDeltaLpfHz, getLooptime());
}
#endif
}
bool pidInitFilters(void)
{
const uint32_t refreshRate = getLooptime();
if (refreshRate == 0) {
return false;
}
static float dtermCoeffs[PID_GYRO_RATE_BUF_LENGTH];
if (pidProfile()->use_dterm_fir_filter) {
// Calculate derivative using 5-point noise-robust differentiators without time delay (one-sided or forward filters)
// by Pavel Holoborodko, see http://www.holoborodko.com/pavel/numerical-methods/numerical-derivative/smooth-low-noise-differentiators/
// h[0] = 5/8, h[-1] = 1/4, h[-2] = -1, h[-3] = -1/4, h[-4] = 3/8
dtermCoeffs[0] = 5.0f/8;
dtermCoeffs[1] = 2.0f/8;
dtermCoeffs[2] = -8.0f/8;
dtermCoeffs[3] = -2.0f/8;
dtermCoeffs[4] = 3.0f/8;
} else {
//simple d(t) - d(t-1) differentiator
dtermCoeffs[0] = 1.0f;
dtermCoeffs[1] = -1.0f;
dtermCoeffs[2] = 0.0f;
dtermCoeffs[3] = 0.0f;
dtermCoeffs[4] = 0.0f;
}
for (int axis = 0; axis < 3; ++ axis) {
firFilterInit(&pidState[axis].gyroRateFilter, pidState[axis].gyroRateBuf, PID_GYRO_RATE_BUF_LENGTH, dtermCoeffs);
}
#ifdef USE_DTERM_NOTCH
notchFilterApplyFn = nullFilterApply;
if (pidProfile()->dterm_soft_notch_hz != 0) {
notchFilterApplyFn = (filterApplyFnPtr)biquadFilterApply;
for (int axis = 0; axis < 3; ++ axis) {
biquadFilterInitNotch(&pidState[axis].deltaNotchFilter, refreshRate, pidProfile()->dterm_soft_notch_hz, pidProfile()->dterm_soft_notch_cutoff);
}
}
#endif
// Init other filters
for (int axis = 0; axis < 3; ++ axis) {
biquadFilterInitLPF(&pidState[axis].deltaLpfState, pidProfile()->dterm_lpf_hz, refreshRate);
}
for (int i = 0; i < XYZ_AXIS_COUNT; i++) {
pt1FilterInit(&windupLpf[i], pidProfile()->iterm_relax_cutoff, refreshRate * 1e-6f);
}
pidFiltersConfigured = true;
return true;
}
void pidResetTPAFilter(void)
{
if (STATE(FIXED_WING) && currentControlRateProfile->throttle.fixedWingTauMs > 0) {
pt1FilterInitRC(&fixedWingTpaFilter, currentControlRateProfile->throttle.fixedWingTauMs * 1e-3f, getLooptime() * 1e-6f);
pt1FilterReset(&fixedWingTpaFilter, motorConfig()->minthrottle);
}
}
void pidResetErrorAccumulators(void)
{
// Reset R/P/Y integrator
for (int axis = 0; axis < 3; axis++) {
pidState[axis].errorGyroIf = 0.0f;
pidState[axis].errorGyroIfLimit = 0.0f;
}
}
static float pidRcCommandToAngle(int16_t stick, int16_t maxInclination)
{
stick = constrain(stick, -500, 500);
return scaleRangef((float) stick, -500.0f, 500.0f, (float) -maxInclination, (float) maxInclination);
}
int16_t pidAngleToRcCommand(float angleDeciDegrees, int16_t maxInclination)
{
angleDeciDegrees = constrainf(angleDeciDegrees, (float) -maxInclination, (float) maxInclination);
return scaleRangef((float) angleDeciDegrees, (float) -maxInclination, (float) maxInclination, -500.0f, 500.0f);
}
/*
Map stick positions to desired rotatrion rate in given axis.
Rotation rate in dps at full stick deflection is defined by axis rate measured in dps/10
Rate 20 means 200dps at full stick deflection
*/
float pidRateToRcCommand(float rateDPS, uint8_t rate)
{
const float maxRateDPS = rate * 10.0f;
return scaleRangef(rateDPS, -maxRateDPS, maxRateDPS, -500.0f, 500.0f);
}
float pidRcCommandToRate(int16_t stick, uint8_t rate)
{
const float maxRateDPS = rate * 10.0f;
return scaleRangef((float) stick, -500.0f, 500.0f, -maxRateDPS, maxRateDPS);
}
static float calculateFixedWingTPAFactor(uint16_t throttle)
{
float tpaFactor;
// tpa_rate is amount of curve TPA applied to PIDs
// tpa_breakpoint for fixed wing is cruise throttle value (value at which PIDs were tuned)
if (currentControlRateProfile->throttle.dynPID != 0 && currentControlRateProfile->throttle.pa_breakpoint > motorConfig()->minthrottle) {
if (throttle > motorConfig()->minthrottle) {
// Calculate TPA according to throttle
tpaFactor = 0.5f + ((float)(currentControlRateProfile->throttle.pa_breakpoint - motorConfig()->minthrottle) / (throttle - motorConfig()->minthrottle) / 2.0f);
// Limit to [0.5; 2] range
tpaFactor = constrainf(tpaFactor, 0.5f, 2.0f);
}
else {
tpaFactor = 2.0f;
}
// Attenuate TPA curve according to configured amount
tpaFactor = 1.0f + (tpaFactor - 1.0f) * (currentControlRateProfile->throttle.dynPID / 100.0f);
}
else {
tpaFactor = 1.0f;
}
return tpaFactor;
}
static float calculateMultirotorTPAFactor(void)
{
float tpaFactor;
// TPA should be updated only when TPA is actually set
if (currentControlRateProfile->throttle.dynPID == 0 || rcCommand[THROTTLE] < currentControlRateProfile->throttle.pa_breakpoint) {
tpaFactor = 1.0f;
} else if (rcCommand[THROTTLE] < motorConfig()->maxthrottle) {
tpaFactor = (100 - (uint16_t)currentControlRateProfile->throttle.dynPID * (rcCommand[THROTTLE] - currentControlRateProfile->throttle.pa_breakpoint) / (motorConfig()->maxthrottle - currentControlRateProfile->throttle.pa_breakpoint)) / 100.0f;
} else {
tpaFactor = (100 - currentControlRateProfile->throttle.dynPID) / 100.0f;
}
return tpaFactor;
}
void schedulePidGainsUpdate(void)
{
pidGainsUpdateRequired = true;
}
void FAST_CODE NOINLINE updatePIDCoefficients(void)
{
STATIC_FASTRAM uint16_t prevThrottle = 0;
// Check if throttle changed. Different logic for fixed wing vs multirotor
if (STATE(FIXED_WING) && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) {
uint16_t filteredThrottle = pt1FilterApply3(&fixedWingTpaFilter, rcCommand[THROTTLE], dT);
if (filteredThrottle != prevThrottle) {
prevThrottle = filteredThrottle;
pidGainsUpdateRequired = true;
}
}
else {
if (rcCommand[THROTTLE] != prevThrottle) {
prevThrottle = rcCommand[THROTTLE];
pidGainsUpdateRequired = true;
}
}
/*
* Compute stick position in range of [-1.0f : 1.0f] without deadband and expo
*/
for (int axis = 0; axis < 3; axis++) {
pidState[axis].stickPosition = constrain(rxGetChannelValue(axis) - PWM_RANGE_MIDDLE, -500, 500) / 500.0f;
}
// If nothing changed - don't waste time recalculating coefficients
if (!pidGainsUpdateRequired) {
return;
}
const float tpaFactor = STATE(FIXED_WING) ? calculateFixedWingTPAFactor(prevThrottle) : calculateMultirotorTPAFactor();
// PID coefficients can be update only with THROTTLE and TPA or inflight PID adjustments
//TODO: Next step would be to update those only at THROTTLE or inflight adjustments change
for (int axis = 0; axis < 3; axis++) {
if (STATE(FIXED_WING)) {
// Airplanes - scale all PIDs according to TPA
pidState[axis].kP = pidBank()->pid[axis].P / FP_PID_RATE_P_MULTIPLIER * tpaFactor;
pidState[axis].kI = pidBank()->pid[axis].I / FP_PID_RATE_I_MULTIPLIER * tpaFactor;
pidState[axis].kD = 0.0f;
pidState[axis].kFF = pidBank()->pid[axis].FF / FP_PID_RATE_FF_MULTIPLIER * tpaFactor;
pidState[axis].kT = 0.0f;
}
else {
const float axisTPA = (axis == FD_YAW) ? 1.0f : tpaFactor;
pidState[axis].kP = pidBank()->pid[axis].P / FP_PID_RATE_P_MULTIPLIER * axisTPA;
pidState[axis].kI = pidBank()->pid[axis].I / FP_PID_RATE_I_MULTIPLIER;
pidState[axis].kD = pidBank()->pid[axis].D / FP_PID_RATE_D_MULTIPLIER * axisTPA;
pidState[axis].kFF = 0.0f;
// Tracking anti-windup requires P/I/D to be all defined which is only true for MC
if ((pidBank()->pid[axis].P != 0) && (pidBank()->pid[axis].I != 0)) {
pidState[axis].kT = 2.0f / ((pidState[axis].kP / pidState[axis].kI) + (pidState[axis].kD / pidState[axis].kP));
} else {
pidState[axis].kT = 0;
}
}
}
pidGainsUpdateRequired = false;
}
static float calcHorizonRateMagnitude(void)
{
// Figure out the raw stick positions
const int32_t stickPosAil = ABS(getRcStickDeflection(FD_ROLL));
const int32_t stickPosEle = ABS(getRcStickDeflection(FD_PITCH));
const float mostDeflectedStickPos = constrain(MAX(stickPosAil, stickPosEle), 0, 500) / 500.0f;
const float modeTransitionStickPos = constrain(pidBank()->pid[PID_LEVEL].D, 0, 100) / 100.0f;
float horizonRateMagnitude;
// Calculate transition point according to stick deflection
if (mostDeflectedStickPos <= modeTransitionStickPos) {
horizonRateMagnitude = mostDeflectedStickPos / modeTransitionStickPos;
}
else {
horizonRateMagnitude = 1.0f;
}
return horizonRateMagnitude;
}
static void pidLevel(pidState_t *pidState, flight_dynamics_index_t axis, float horizonRateMagnitude)
{
// This is ROLL/PITCH, run ANGLE/HORIZON controllers
float angleTarget = pidRcCommandToAngle(rcCommand[axis], pidProfile()->max_angle_inclination[axis]);
// Automatically pitch down if the throttle is manually controlled and reduced bellow cruise throttle
if ((axis == FD_PITCH) && STATE(FIXED_WING) && FLIGHT_MODE(ANGLE_MODE) && !navigationIsControllingThrottle())
angleTarget += scaleRange(MAX(0, navConfig()->fw.cruise_throttle - rcCommand[THROTTLE]), 0, navConfig()->fw.cruise_throttle - PWM_RANGE_MIN, 0, mixerConfig()->fwMinThrottleDownPitchAngle);
const float angleErrorDeg = DECIDEGREES_TO_DEGREES(angleTarget - attitude.raw[axis]);
float angleRateTarget = constrainf(angleErrorDeg * (pidBank()->pid[PID_LEVEL].P / FP_PID_LEVEL_P_MULTIPLIER), -currentControlRateProfile->stabilized.rates[axis] * 10.0f, currentControlRateProfile->stabilized.rates[axis] * 10.0f);
// Apply simple LPF to angleRateTarget to make response less jerky
// Ideas behind this:
// 1) Attitude is updated at gyro rate, rateTarget for ANGLE mode is calculated from attitude
// 2) If this rateTarget is passed directly into gyro-base PID controller this effectively doubles the rateError.
// D-term that is calculated from error tend to amplify this even more. Moreover, this tend to respond to every
// slightest change in attitude making self-leveling jittery
// 3) Lowering LEVEL P can make the effects of (2) less visible, but this also slows down self-leveling.
// 4) Human pilot response to attitude change in RATE mode is fairly slow and smooth, human pilot doesn't
// compensate for each slightest change
// 5) (2) and (4) lead to a simple idea of adding a low-pass filter on rateTarget for ANGLE mode damping
// response to rapid attitude changes and smoothing out self-leveling reaction
if (pidBank()->pid[PID_LEVEL].I) {
// I8[PIDLEVEL] is filter cutoff frequency (Hz). Practical values of filtering frequency is 5-10 Hz
angleRateTarget = pt1FilterApply4(&pidState->angleFilterState, angleRateTarget, pidBank()->pid[PID_LEVEL].I, dT);
}
// P[LEVEL] defines self-leveling strength (both for ANGLE and HORIZON modes)
if (FLIGHT_MODE(HORIZON_MODE)) {
pidState->rateTarget = (1.0f - horizonRateMagnitude) * angleRateTarget + horizonRateMagnitude * pidState->rateTarget;
} else {
pidState->rateTarget = angleRateTarget;
}
}
/* Apply angular acceleration limit to rate target to limit extreme stick inputs to respect physical capabilities of the machine */
static void FAST_CODE pidApplySetpointRateLimiting(pidState_t *pidState, flight_dynamics_index_t axis)
{
const uint32_t axisAccelLimit = (axis == FD_YAW) ? pidProfile()->axisAccelerationLimitYaw : pidProfile()->axisAccelerationLimitRollPitch;
if (axisAccelLimit > AXIS_ACCEL_MIN_LIMIT) {
pidState->rateTarget = rateLimitFilterApply4(&pidState->axisAccelFilter, pidState->rateTarget, (float)axisAccelLimit, dT);
}
}
bool isFixedWingItermLimitActive(float stickPosition)
{
/*
* Iterm anti windup whould be active only when pilot controls the rotation
* velocity directly, not when ANGLE or HORIZON are used
*/
if (FLIGHT_MODE(ANGLE_MODE) || FLIGHT_MODE(HORIZON_MODE)) {
return false;
}
return fabsf(stickPosition) > pidProfile()->fixedWingItermLimitOnStickPosition;
}
static void FAST_CODE pidApplyFixedWingRateController(pidState_t *pidState, flight_dynamics_index_t axis)
{
const float rateError = pidState->rateTarget - pidState->gyroRate;
// Calculate new P-term and FF-term
float newPTerm = rateError * pidState->kP;
float newFFTerm = pidState->rateTarget * pidState->kFF;
// Additional P-term LPF on YAW axis
if (axis == FD_YAW && pidProfile()->yaw_lpf_hz) {
newPTerm = pt1FilterApply4(&pidState->ptermLpfState, newPTerm, pidProfile()->yaw_lpf_hz, dT);
}
// Calculate integral
pidState->errorGyroIf += rateError * pidState->kI * dT;
if (STATE(ANTI_WINDUP) || isFixedWingItermLimitActive(pidState->stickPosition)) {
pidState->errorGyroIf = constrainf(pidState->errorGyroIf, -pidState->errorGyroIfLimit, pidState->errorGyroIfLimit);
} else {
pidState->errorGyroIfLimit = fabsf(pidState->errorGyroIf);
}
if (pidProfile()->fixedWingItermThrowLimit != 0) {
pidState->errorGyroIf = constrainf(pidState->errorGyroIf, -pidProfile()->fixedWingItermThrowLimit, pidProfile()->fixedWingItermThrowLimit);
}
#ifdef USE_AUTOTUNE_FIXED_WING
if (FLIGHT_MODE(AUTO_TUNE) && !FLIGHT_MODE(MANUAL_MODE)) {
autotuneFixedWingUpdate(axis, pidState->rateTarget, pidState->gyroRate, newPTerm + newFFTerm);
}
#endif
axisPID[axis] = constrainf(newPTerm + newFFTerm + pidState->errorGyroIf, -pidProfile()->pidSumLimit, +pidProfile()->pidSumLimit);
#ifdef USE_BLACKBOX
axisPID_P[axis] = newPTerm;
axisPID_I[axis] = pidState->errorGyroIf;
axisPID_D[axis] = newFFTerm;
axisPID_Setpoint[axis] = pidState->rateTarget;
#endif
}
static void FAST_CODE applyItermRelax(const int axis, const float gyroRate, float currentPidSetpoint, float *itermErrorRate)
{
const float setpointLpf = pt1FilterApply(&windupLpf[axis], currentPidSetpoint);
const float setpointHpf = fabsf(currentPidSetpoint - setpointLpf);
if (itermRelax) {
if (axis < FD_YAW || itermRelax == ITERM_RELAX_RPY) {
const float itermRelaxFactor = MAX(0, 1 - setpointHpf / itermRelaxSetpointThreshold);
if (itermRelaxType == ITERM_RELAX_SETPOINT) {
*itermErrorRate *= itermRelaxFactor;
} else if (itermRelaxType == ITERM_RELAX_GYRO ) {
*itermErrorRate = fapplyDeadbandf(setpointLpf - gyroRate, setpointHpf);
} else {
*itermErrorRate = 0.0f;
}
if (axis == FD_ROLL) {
DEBUG_SET(DEBUG_ITERM_RELAX, 0, lrintf(setpointHpf));
DEBUG_SET(DEBUG_ITERM_RELAX, 1, lrintf(itermRelaxFactor * 100.0f));
DEBUG_SET(DEBUG_ITERM_RELAX, 2, lrintf(*itermErrorRate));
}
}
}
}
#ifdef USE_D_BOOST
static float FAST_CODE applyDBoost(float rateTarget, float gyroRate, flight_dynamics_index_t axis) {
float dBoost = 1.0f;
if (dBoostFactor > 1) {
const float dBoostGyroDelta = (gyroRate - previousRateGyro[axis]) / dT;
const float dBoostGyroAcceleration = fabsf(biquadFilterApply(&dBoostGyroLpf[axis], dBoostGyroDelta));
const float dBoostStickAcceleration = fabsf((rateTarget - previousRateTarget[axis]) / dT);
const float acceleration = MAX(dBoostGyroAcceleration, dBoostStickAcceleration);
dBoost = scaleRangef(acceleration, 0.0f, dBoostMaxAtAlleceleration, 1.0f, dBoostFactor);
dBoost = pt1FilterApply4(&dBoostLpf[axis], dBoost, D_BOOST_LPF_HZ, dT);
dBoost = constrainf(dBoost, 1.0f, dBoostFactor);
if (axis == FD_ROLL) {
DEBUG_SET(DEBUG_D_BOOST, 0, dBoostGyroAcceleration);
DEBUG_SET(DEBUG_D_BOOST, 1, dBoostStickAcceleration);
DEBUG_SET(DEBUG_D_BOOST, 2, dBoost * 100);
} else if (axis == FD_PITCH) {
DEBUG_SET(DEBUG_D_BOOST, 3, dBoost * 100);
}
previousRateTarget[axis] = rateTarget;
previousRateGyro[axis] = gyroRate;
}
return dBoost;
}
#else
static float applyDBoost(float rateTarget, float gyroRate, flight_dynamics_index_t axis) {
UNUSED(rateTarget);
UNUSED(gyroRate);
UNUSED(axis);
return 1.0f;
}
#endif
static void FAST_CODE pidApplyMulticopterRateController(pidState_t *pidState, flight_dynamics_index_t axis)
{
const float rateError = pidState->rateTarget - pidState->gyroRate;
// Calculate new P-term
float newPTerm = rateError * pidState->kP;
// Constrain YAW by yaw_p_limit value if not servo driven (in that case servo limits apply)
if (axis == FD_YAW && (getMotorCount() >= 4 && pidProfile()->yaw_p_limit)) {
newPTerm = constrain(newPTerm, -pidProfile()->yaw_p_limit, pidProfile()->yaw_p_limit);
}
// Additional P-term LPF on YAW axis
if (axis == FD_YAW && pidProfile()->yaw_lpf_hz) {
newPTerm = pt1FilterApply4(&pidState->ptermLpfState, newPTerm, pidProfile()->yaw_lpf_hz, dT);
}
// Calculate new D-term
float newDTerm;
if (pidBank()->pid[axis].D == 0) {
// optimisation for when D8 is zero, often used by YAW axis
newDTerm = 0;
} else {
// Calculate delta for Dterm calculation. Apply filters before derivative to minimize effects of dterm kick
float deltaFiltered = pidProfile()->dterm_setpoint_weight * pidState->rateTarget - pidState->gyroRate;
#ifdef USE_DTERM_NOTCH
// Apply D-term notch
deltaFiltered = notchFilterApplyFn(&pidState->deltaNotchFilter, deltaFiltered);
#endif
// Apply additional lowpass
if (pidProfile()->dterm_lpf_hz) {
deltaFiltered = biquadFilterApply(&pidState->deltaLpfState, deltaFiltered);
}
firFilterUpdate(&pidState->gyroRateFilter, deltaFiltered);
newDTerm = firFilterApply(&pidState->gyroRateFilter);
// Calculate derivative
newDTerm = newDTerm * (pidState->kD / dT) * applyDBoost(pidState->rateTarget, pidState->gyroRate, axis);
// Additionally constrain D
newDTerm = constrainf(newDTerm, -300.0f, 300.0f);
}
// TODO: Get feedback from mixer on available correction range for each axis
const float newOutput = newPTerm + newDTerm + pidState->errorGyroIf;
const float newOutputLimited = constrainf(newOutput, -pidProfile()->pidSumLimit, +pidProfile()->pidSumLimit);
// Prevent strong Iterm accumulation during stick inputs
const float motorItermWindupPoint = 1.0f - (pidProfile()->itermWindupPointPercent / 100.0f);
const float antiWindupScaler = constrainf((1.0f - getMotorMixRange()) / motorItermWindupPoint, 0.0f, 1.0f);
float itermErrorRate = rateError;
applyItermRelax(axis, pidState->gyroRate, pidState->rateTarget, &itermErrorRate);
pidState->errorGyroIf += (itermErrorRate * pidState->kI * antiWindupScaler * dT)
+ ((newOutputLimited - newOutput) * pidState->kT * antiWindupScaler * dT);
// Don't grow I-term if motors are at their limit
if (STATE(ANTI_WINDUP) || mixerIsOutputSaturated()) {
pidState->errorGyroIf = constrainf(pidState->errorGyroIf, -pidState->errorGyroIfLimit, pidState->errorGyroIfLimit);
} else {
pidState->errorGyroIfLimit = fabsf(pidState->errorGyroIf);
}
axisPID[axis] = newOutputLimited;
#ifdef USE_BLACKBOX
axisPID_P[axis] = newPTerm;
axisPID_I[axis] = pidState->errorGyroIf;
axisPID_D[axis] = newDTerm;
axisPID_Setpoint[axis] = pidState->rateTarget;
#endif
}
void updateHeadingHoldTarget(int16_t heading)
{
headingHoldTarget = heading;
}
void resetHeadingHoldTarget(int16_t heading)
{
updateHeadingHoldTarget(heading);
pt1FilterReset(&headingHoldRateFilter, 0.0f);
}
int16_t getHeadingHoldTarget() {
return headingHoldTarget;
}
static uint8_t getHeadingHoldState(void)
{
// Don't apply heading hold if overall tilt is greater than maximum angle inclination
if (calculateCosTiltAngle() < headingHoldCosZLimit) {
return HEADING_HOLD_DISABLED;
}
#if defined(USE_NAV)
int navHeadingState = navigationGetHeadingControlState();
// NAV will prevent MAG_MODE from activating, but require heading control
if (navHeadingState != NAV_HEADING_CONTROL_NONE) {
// Apply maghold only if heading control is in auto mode
if (navHeadingState == NAV_HEADING_CONTROL_AUTO) {
return HEADING_HOLD_ENABLED;
}
}
else
#endif
if (ABS(rcCommand[YAW]) == 0 && FLIGHT_MODE(HEADING_MODE)) {
return HEADING_HOLD_ENABLED;
} else {
return HEADING_HOLD_UPDATE_HEADING;
}
return HEADING_HOLD_UPDATE_HEADING;
}
/*
* HEADING_HOLD P Controller returns desired rotation rate in dps to be fed to Rate controller
*/
float pidHeadingHold(void)
{
float headingHoldRate;
int16_t error = DECIDEGREES_TO_DEGREES(attitude.values.yaw) - headingHoldTarget;
/*
* Convert absolute error into relative to current heading
*/
if (error <= -180) {
error += 360;
}
if (error >= +180) {
error -= 360;
}
/*
New MAG_HOLD controller work slightly different that previous one.
Old one mapped error to rotation speed in following way:
- on rate 0 it gave about 0.5dps for each degree of error
- error 0 = rotation speed of 0dps
- error 180 = rotation speed of 96 degrees per second
- output
- that gives about 2 seconds to correct any error, no matter how big. Of course, usually more because of inertia.
That was making him quite "soft" for small changes and rapid for big ones that started to appear
when iNav introduced real RTH and WAYPOINT that might require rapid heading changes.
New approach uses modified principle:
- manual yaw rate is not used. MAG_HOLD is decoupled from manual input settings
- instead, mag_hold_rate_limit is used. It defines max rotation speed in dps that MAG_HOLD controller can require from RateController
- computed rotation speed is capped at -mag_hold_rate_limit and mag_hold_rate_limit
- Default mag_hold_rate_limit = 40dps and default MAG_HOLD P-gain is 40
- With those values, maximum rotation speed will be required from Rate Controller when error is greater that 30 degrees
- For smaller error, required rate will be proportional.
- It uses LPF filter set at 2Hz to additionally smoothen out any rapid changes
- That makes correction of smaller errors stronger, and those of big errors softer
This make looks as very slow rotation rate, but please remember this is automatic mode.
Manual override with YAW input when MAG_HOLD is enabled will still use "manual" rates, not MAG_HOLD rates.
Highest possible correction is 180 degrees and it will take more less 4.5 seconds. It is even more than sufficient
to run RTH or WAYPOINT missions. My favourite rate range here is 20dps - 30dps that gives nice and smooth turns.
Correction for small errors is much faster now. For example, old contrioller for 2deg errors required 1dps (correction in 2 seconds).
New controller for 2deg error requires 2,6dps. 4dps for 3deg and so on up until mag_hold_rate_limit is reached.
*/
headingHoldRate = error * pidBank()->pid[PID_HEADING].P / 30;
headingHoldRate = constrainf(headingHoldRate, -pidProfile()->heading_hold_rate_limit, pidProfile()->heading_hold_rate_limit);
headingHoldRate = pt1FilterApply4(&headingHoldRateFilter, headingHoldRate, HEADING_HOLD_ERROR_LPF_FREQ, dT);
return headingHoldRate;
}
/*
* TURN ASSISTANT mode is an assisted mode to do a Yaw rotation on a ground plane, allowing one-stick turn in RATE more
* and keeping ROLL and PITCH attitude though the turn.
*/
static void pidTurnAssistant(pidState_t *pidState)
{
fpVector3_t targetRates;
targetRates.x = 0.0f;
targetRates.y = 0.0f;
if (STATE(FIXED_WING)) {
if (calculateCosTiltAngle() >= 0.173648f) {
// Ideal banked turn follow the equations:
// forward_vel^2 / radius = Gravity * tan(roll_angle)
// yaw_rate = forward_vel / radius
// If we solve for roll angle we get:
// tan(roll_angle) = forward_vel * yaw_rate / Gravity
// If we solve for yaw rate we get:
// yaw_rate = tan(roll_angle) * Gravity / forward_vel
#if defined(USE_PITOT)
float airspeedForCoordinatedTurn = sensors(SENSOR_PITOT) ?
pitot.airSpeed :
pidProfile()->fixedWingReferenceAirspeed;
#else
float airspeedForCoordinatedTurn = pidProfile()->fixedWingReferenceAirspeed;
#endif
// Constrain to somewhat sane limits - 10km/h - 216km/h
airspeedForCoordinatedTurn = constrainf(airspeedForCoordinatedTurn, 300, 6000);
// Calculate rate of turn in Earth frame according to FAA's Pilot's Handbook of Aeronautical Knowledge
float bankAngle = DECIDEGREES_TO_RADIANS(attitude.values.roll);
float coordinatedTurnRateEarthFrame = GRAVITY_CMSS * tan_approx(-bankAngle) / airspeedForCoordinatedTurn;
targetRates.z = RADIANS_TO_DEGREES(coordinatedTurnRateEarthFrame);
}
else {
// Don't allow coordinated turn calculation if airplane is in hard bank or steep climb/dive
return;
}
}
else {
targetRates.z = pidState[YAW].rateTarget;
}
// Transform calculated rate offsets into body frame and apply
imuTransformVectorEarthToBody(&targetRates);
// Add in roll and pitch
pidState[ROLL].rateTarget = constrainf(pidState[ROLL].rateTarget + targetRates.x, -currentControlRateProfile->stabilized.rates[ROLL] * 10.0f, currentControlRateProfile->stabilized.rates[ROLL] * 10.0f);
pidState[PITCH].rateTarget = constrainf(pidState[PITCH].rateTarget + targetRates.y, -currentControlRateProfile->stabilized.rates[PITCH] * 10.0f, currentControlRateProfile->stabilized.rates[PITCH] * 10.0f);
// Replace YAW on quads - add it in on airplanes
if (STATE(FIXED_WING)) {
pidState[YAW].rateTarget = constrainf(pidState[YAW].rateTarget + targetRates.z * pidProfile()->fixedWingCoordinatedYawGain, -currentControlRateProfile->stabilized.rates[YAW] * 10.0f, currentControlRateProfile->stabilized.rates[YAW] * 10.0f);
}
else {
pidState[YAW].rateTarget = constrainf(targetRates.z, -currentControlRateProfile->stabilized.rates[YAW] * 10.0f, currentControlRateProfile->stabilized.rates[YAW] * 10.0f);
}
}
static void pidApplyFpvCameraAngleMix(pidState_t *pidState, uint8_t fpvCameraAngle)
{
static uint8_t lastFpvCamAngleDegrees = 0;
static float cosCameraAngle = 1.0;
static float sinCameraAngle = 0.0;
if (lastFpvCamAngleDegrees != fpvCameraAngle) {
lastFpvCamAngleDegrees = fpvCameraAngle;
cosCameraAngle = cos_approx(DEGREES_TO_RADIANS(fpvCameraAngle));
sinCameraAngle = sin_approx(DEGREES_TO_RADIANS(fpvCameraAngle));
}
// Rotate roll/yaw command from camera-frame coordinate system to body-frame coordinate system
const float rollRate = pidState[ROLL].rateTarget;
const float yawRate = pidState[YAW].rateTarget;
pidState[ROLL].rateTarget = constrainf(rollRate * cosCameraAngle - yawRate * sinCameraAngle, -GYRO_SATURATION_LIMIT, GYRO_SATURATION_LIMIT);
pidState[YAW].rateTarget = constrainf(yawRate * cosCameraAngle + rollRate * sinCameraAngle, -GYRO_SATURATION_LIMIT, GYRO_SATURATION_LIMIT);
}
void FAST_CODE pidController(void)
{
if (!pidFiltersConfigured) {
return;
}
bool canUseFpvCameraMix = true;
uint8_t headingHoldState = getHeadingHoldState();
if (headingHoldState == HEADING_HOLD_UPDATE_HEADING) {
updateHeadingHoldTarget(DECIDEGREES_TO_DEGREES(attitude.values.yaw));
}
for (int axis = 0; axis < 3; axis++) {
// Step 1: Calculate gyro rates
pidState[axis].gyroRate = gyro.gyroADCf[axis];
// Step 2: Read target
float rateTarget;
if (axis == FD_YAW && headingHoldState == HEADING_HOLD_ENABLED) {
rateTarget = pidHeadingHold();
} else {
rateTarget = pidRcCommandToRate(rcCommand[axis], currentControlRateProfile->stabilized.rates[axis]);
}
// Limit desired rate to something gyro can measure reliably
pidState[axis].rateTarget = constrainf(rateTarget, -GYRO_SATURATION_LIMIT, +GYRO_SATURATION_LIMIT);
}
// Step 3: Run control for ANGLE_MODE, HORIZON_MODE, and HEADING_LOCK
if (FLIGHT_MODE(ANGLE_MODE) || FLIGHT_MODE(HORIZON_MODE)) {
const float horizonRateMagnitude = calcHorizonRateMagnitude();
pidLevel(&pidState[FD_ROLL], FD_ROLL, horizonRateMagnitude);
pidLevel(&pidState[FD_PITCH], FD_PITCH, horizonRateMagnitude);
canUseFpvCameraMix = false; // FPVANGLEMIX is incompatible with ANGLE/HORIZON
}
if (FLIGHT_MODE(TURN_ASSISTANT) || navigationRequiresTurnAssistance()) {
pidTurnAssistant(pidState);
canUseFpvCameraMix = false; // FPVANGLEMIX is incompatible with TURN_ASSISTANT
}
if (canUseFpvCameraMix && IS_RC_MODE_ACTIVE(BOXFPVANGLEMIX) && currentControlRateProfile->misc.fpvCamAngleDegrees) {
pidApplyFpvCameraAngleMix(pidState, currentControlRateProfile->misc.fpvCamAngleDegrees);
}
// Apply setpoint rate of change limits
for (int axis = 0; axis < 3; axis++) {
pidApplySetpointRateLimiting(&pidState[axis], axis);
}
// Step 4: Run gyro-driven control
for (int axis = 0; axis < 3; axis++) {
// Apply PID setpoint controller
if (STATE(FIXED_WING)) {
pidApplyFixedWingRateController(&pidState[axis], axis);
}
else {
pidApplyMulticopterRateController(&pidState[axis], axis);
}
}
}