Skip to content

Conversation

@shota3527
Copy link
Contributor

@shota3527 shota3527 commented Sep 27, 2025

User description

add a cli command to let fixed wing use airspeed based tpa.
It calculates the corresponding throttle value based on the airspeed. Ensure a seamless integration with the current throttle-based TPA. Turn it on with cli command and no more tunning is needed, fallback to old throttle based tpa if airspeed is not avaliable

tpa_breakpoint + (airspeed - fw_reference_airspeed)/fw_reference_airspeed * (tpa_breakpoint - ThrottleIdleValue(default:1150))
sim test ok

recommend to raise pitot_lpf_milli_hz on vitual pitot


PR Type

Enhancement


Description

This description is generated by an AI tool. It may have inaccuracies

  • Add airspeed-based TPA calculation for fixed-wing aircraft

  • New airspeed_tpa setting to enable airspeed TPA mode

  • Refactor TPA calculation to support both throttle and airspeed

  • Add airspeed validity check function for sensor health


Diagram Walkthrough

flowchart LR
  A["Throttle Input"] --> B["TPA Calculation"]
  C["Airspeed Sensor"] --> D["Airspeed Valid Check"]
  D --> E["Airspeed TPA Mode"]
  E --> B
  B --> F["PID Coefficients Update"]
Loading

File Walkthrough

Relevant files
Configuration changes
controlrate_profile.c
Initialize airspeed TPA setting                                                   

src/main/fc/controlrate_profile.c

  • Add airspeed_tpa field initialization to control rate profile reset
    function
+2/-1     
controlrate_profile_config_struct.h
Add airspeed TPA configuration field                                         

src/main/fc/controlrate_profile_config_struct.h

  • Add airspeed_tpa boolean field to throttle configuration structure
+1/-0     
settings.yaml
Configure airspeed TPA setting                                                     

src/main/fc/settings.yaml

  • Add airspeed_tpa setting configuration with description
  • Set default value to OFF
+5/-0     
Enhancement
pid.c
Implement airspeed-based TPA calculation                                 

src/main/flight/pid.c

  • Refactor TPA calculation to support airspeed-based mode
  • Add airspeed TPA calculation using reference airspeed
  • Modify calculateMultirotorTPAFactor to accept throttle parameter
  • Update PID coefficient calculation logic
+20/-17 
pitotmeter.c
Add airspeed sensor validity check                                             

src/main/sensors/pitotmeter.c

  • Add pitotValidForAirspeed function to check sensor validity
  • Include GPS fix requirement for virtual pitot sensors
+9/-0     
pitotmeter.h
Expose airspeed validity function                                               

src/main/sensors/pitotmeter.h

  • Add function declaration for pitotValidForAirspeed
+1/-0     
Documentation
Settings.md
Document airspeed TPA setting                                                       

docs/Settings.md

  • Add documentation for airspeed_tpa setting
  • Include formula and usage description
+10/-0   

@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
⚡ Recommended focus areas for review

Possible Issue

The new multirotor TPA now takes a throttle parameter but updatePIDCoefficients passes prevThrottle, which may represent airspeed-derived or filtered values in fixed-wing mode. Ensure that for multirotor mode the raw throttle is used so attenuation matches actual RC throttle, and verify integer vs float math does not cause unintended clamping.

static float calculateMultirotorTPAFactor(uint16_t throttle)
{
    float tpaFactor;

    // TPA should be updated only when TPA is actually set
    if (currentControlRateProfile->throttle.dynPID == 0 || throttle < currentControlRateProfile->throttle.pa_breakpoint) {
        tpaFactor = 1.0f;
    } else if (throttle < getMaxThrottle()) {
        tpaFactor = (100 - (uint16_t)currentControlRateProfile->throttle.dynPID * (throttle - currentControlRateProfile->throttle.pa_breakpoint) / (float)(getMaxThrottle() - currentControlRateProfile->throttle.pa_breakpoint)) / 100.0f;
    } else {
        tpaFactor = (100 - currentControlRateProfile->throttle.dynPID) / 100.0f;
    }
Edge Conditions

The airspeed-to-throttle mapping can produce values below idle or above max; while comments say limits are applied in fixed-wing TPA, validate calculateFixedWingTPAFactor safely handles tpaThrottle outside [idle, max] to avoid negative or >1 factors.

if (usedPidControllerType == PID_TYPE_PIFF && pitotValidForAirspeed() && currentControlRateProfile->throttle.airspeed_tpa) {
    // Use airspeed instead of throttle for TPA calculation
    const float airspeed = getAirspeedEstimate(); // in cm/s
    const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
    tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
    //upper and lower limits will be applied in calculateFixedWingTPAFactor()
}
else if (usedPidControllerType == PID_TYPE_PIFF && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) {
    tpaThrottle = pt1FilterApply(&fixedWingTpaFilter, rcCommand[THROTTLE]);
Maintainability

prevThrottle now serves as a generic TPA driver (raw, filtered, or airspeed-derived). Consider renaming to a neutral term and documenting its semantics to prevent future misuse across flight modes.

void updatePIDCoefficients(void)
{
    STATIC_FASTRAM uint16_t prevThrottle = 0;
    STATIC_FASTRAM uint16_t tpaThrottle = 0;

    if (usedPidControllerType == PID_TYPE_PIFF && pitotValidForAirspeed() && currentControlRateProfile->throttle.airspeed_tpa) {
        // Use airspeed instead of throttle for TPA calculation
        const float airspeed = getAirspeedEstimate(); // in cm/s
        const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
        tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
        //upper and lower limits will be applied in calculateFixedWingTPAFactor()
    }
    else if (usedPidControllerType == PID_TYPE_PIFF && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) {
        tpaThrottle = pt1FilterApply(&fixedWingTpaFilter, rcCommand[THROTTLE]);
    }
    else {
        tpaThrottle = rcCommand[THROTTLE];
    }
    if (tpaThrottle != prevThrottle) {
        prevThrottle = tpaThrottle;
        pidGainsUpdateRequired = true;
    }

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Sep 27, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
The TPA implementation is overly complex
Suggestion Impact:The commit removes the virtual throttle approach for airspeed-based TPA and introduces a direct airspeed-based TPA factor calculation (calculateFixedWingAirspeedTPAFactor), using a power of referenceAirspeed/(airspeed+epsilon) with user-configurable exponent and constraints, and integrates it into updatePIDCoefficients. This aligns with the suggestion's intent to directly scale via airspeed rather than converting to a throttle surrogate, though the exact formula (power law with configurable exponent) differs from the proposed 1/(airspeed^2).

code diff:

+static float calculateFixedWingAirspeedTPAFactor(void){
+    const float airspeed = getAirspeedEstimate(); // in cm/s
+    const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
+    float tpaFactor= powf(referenceAirspeed/(airspeed+0.01f), currentControlRateProfile->throttle.apa_pow/100.0f);
+    tpaFactor= constrainf(tpaFactor, 0.3f, 2.0f);
+    return tpaFactor;
+}
+
 static float calculateFixedWingTPAFactor(uint16_t throttle)
 {
     float tpaFactor;
@@ -451,9 +460,6 @@
         if (throttle > getThrottleIdleValue()) {
             // Calculate TPA according to throttle
             tpaFactor = 0.5f + ((float)(currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()) / (throttle - getThrottleIdleValue()) / 2.0f);
-
-            // Limit to [0.5; 2] range
-            tpaFactor = constrainf(tpaFactor, 0.5f, 2.0f);
         }
         else {
             tpaFactor = 2.0f;
@@ -461,6 +467,8 @@
 
         // Attenuate TPA curve according to configured amount
         tpaFactor = 1.0f + (tpaFactor - 1.0f) * (currentControlRateProfile->throttle.dynPID / 100.0f);
+        // Limit to [0.5; 2] range
+        tpaFactor = constrainf(tpaFactor, 0.3f, 2.0f);
     }
     else {
         tpaFactor = 1.0f;
@@ -479,12 +487,31 @@
     } else if (throttle < getMaxThrottle()) {
         tpaFactor = (100 - (uint16_t)currentControlRateProfile->throttle.dynPID * (throttle - currentControlRateProfile->throttle.pa_breakpoint) / (float)(getMaxThrottle() - currentControlRateProfile->throttle.pa_breakpoint)) / 100.0f;
     } else {
-        tpaFactor = (100 - currentControlRateProfile->throttle.dynPID) / 100.0f;
+        tpaFactor = (100 - constrain(currentControlRateProfile->throttle.dynPID, 0, 100)) / 100.0f;
     }
 
     return tpaFactor;
 }
 
+static float calculateTPAThtrottle(void)
+{
+    uint16_t tpaThrottle = 0;
+    static const fpVector3_t vDown = { .v = { 0.0f, 0.0f, 1.0f } };
+
+    if (usedPidControllerType == PID_TYPE_PIFF && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) { //fixed wing TPA with filtering
+        fpVector3_t vForward = { .v = { HeadVecEFFiltered.x, -HeadVecEFFiltered.y, -HeadVecEFFiltered.z } };
+        float groundCos = vectorDotProduct(&vForward, &vDown);
+        int16_t throttleAdjustment =  currentControlRateProfile->throttle.tpa_pitch_compensation * groundCos * 90.0f / (PI/2); //when 1deg pitch up, increase throttle by pitch(deg)_to_throttle. cos(89 deg)*90/(pi/2)=0.99995,cos(80 deg)*90/(pi/2)=9.9493,
+        throttleAdjustment= throttleAdjustment<0? throttleAdjustment/2:throttleAdjustment; //reduce throttle compensation when pitch down
+        uint16_t throttleAdjusted = rcCommand[THROTTLE] + constrain(throttleAdjustment, -1000, 1000);
+        tpaThrottle = pt1FilterApply(&fixedWingTpaFilter, constrain(throttleAdjusted, 1000, 2000));
+    }
+    else {
+        tpaThrottle = rcCommand[THROTTLE]; //multirotor TPA without filtering
+    }
+    return tpaThrottle;
+}
+
 void schedulePidGainsUpdate(void)
 {
     pidGainsUpdateRequired = true;
@@ -492,26 +519,7 @@
 
 void updatePIDCoefficients(void)
 {
-    STATIC_FASTRAM uint16_t prevThrottle = 0;
-    STATIC_FASTRAM uint16_t tpaThrottle = 0;
-
-    if (usedPidControllerType == PID_TYPE_PIFF && pitotValidForAirspeed() && currentControlRateProfile->throttle.airspeed_tpa) {
-        // Use airspeed instead of throttle for TPA calculation
-        const float airspeed = getAirspeedEstimate(); // in cm/s
-        const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
-        tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
-        //upper and lower limits will be applied in calculateFixedWingTPAFactor()
-    }
-    else if (usedPidControllerType == PID_TYPE_PIFF && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) {
-        tpaThrottle = pt1FilterApply(&fixedWingTpaFilter, rcCommand[THROTTLE]);
-    }
-    else {
-        tpaThrottle = rcCommand[THROTTLE];
-    }
-    if (tpaThrottle != prevThrottle) {
-        prevThrottle = tpaThrottle;
-        pidGainsUpdateRequired = true;
-    }
+    STATIC_FASTRAM float tpaFactorprev=-1.0f;
 
 #ifdef USE_ANTIGRAVITY
     if (usedPidControllerType == PID_TYPE_PID) {
@@ -526,13 +534,29 @@
     for (int axis = 0; axis < 3; axis++) {
         pidState[axis].stickPosition = constrain(rxGetChannelValue(axis) - PWM_RANGE_MIDDLE, -500, 500) / 500.0f;
     }
-
+    
+    float tpaFactor=1.0f;
+    if(usedPidControllerType == PID_TYPE_PIFF){ // Fixed wing TPA calculation
+        if(currentControlRateProfile->throttle.apa_pow>0 && pitotValidForAirspeed()){
+            tpaFactor = calculateFixedWingAirspeedTPAFactor();
+        }else{
+            tpaFactor = calculateFixedWingTPAFactor(calculateTPAThtrottle());
+        }
+    } else {
+        tpaFactor = calculateMultirotorTPAFactor(calculateTPAThtrottle());
+    }
+    if (tpaFactor != tpaFactorprev) {
+        pidGainsUpdateRequired = true;
+    }
+    tpaFactorprev = tpaFactor;
+
+    
     // If nothing changed - don't waste time recalculating coefficients
     if (!pidGainsUpdateRequired) {
         return;
     }
 
-    const float tpaFactor = usedPidControllerType == PID_TYPE_PIFF ? calculateFixedWingTPAFactor(prevThrottle) : calculateMultirotorTPAFactor(prevThrottle);
+    

The current TPA implementation is overly complex, creating a "virtual throttle"
from airspeed. It should be replaced with a more direct approach that scales PID
gains inversely with dynamic pressure (airspeed^2).

Examples:

src/main/flight/pid.c [498-503]
    if (usedPidControllerType == PID_TYPE_PIFF && pitotValidForAirspeed() && currentControlRateProfile->throttle.airspeed_tpa) {
        // Use airspeed instead of throttle for TPA calculation
        const float airspeed = getAirspeedEstimate(); // in cm/s
        const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
        tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
        //upper and lower limits will be applied in calculateFixedWingTPAFactor()

Solution Walkthrough:

Before:

function updatePIDCoefficients() {
  // ...
  tpaThrottle = 0;
  if (airspeed_tpa_enabled && airspeed_is_valid) {
    // Convert airspeed to a "virtual throttle" value
    airspeed = getAirspeedEstimate();
    referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed;
    tpaThrottle = breakpoint + (airspeed - referenceAirspeed) / referenceAirspeed * (breakpoint - idle_throttle);
  } else {
    tpaThrottle = rcCommand[THROTTLE];
  }

  // Feed virtual throttle into the old TPA formula
  tpaFactor = calculateFixedWingTPAFactor(tpaThrottle);

  // Scale PIDs
  pidState.kP = baseP * tpaFactor;
  // ...
}

After:

function updatePIDCoefficients() {
  // ...
  tpaFactor = 1.0;
  if (airspeed_tpa_enabled && airspeed_is_valid) {
    // Directly calculate scaling factor based on dynamic pressure (airspeed^2)
    airspeed = getAirspeedEstimate();
    referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed;
    if (airspeed > 0) {
      tpaFactor = (referenceAirspeed * referenceAirspeed) / (airspeed * airspeed);
    }
    // Apply user-configured TPA amount and limits
    tpaFactor = 1.0 + (tpaFactor - 1.0) * (tpa_rate / 100.0);
    tpaFactor = constrain(tpaFactor, 0.5, 2.0);
  } else {
    tpaFactor = calculateFixedWingTPAFactor(rcCommand[THROTTLE]);
  }
  // Scale PIDs
  pidState.kP = baseP * tpaFactor;
  // ...
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a convoluted design choice and proposes a more direct, physically-grounded, and standard industry approach, significantly improving the feature's clarity and tunability.

High
Possible issue
Prevent division-by-zero in TPA calculation
Suggestion Impact:The commit removed the direct division by referenceAirspeed and introduced a new airspeed-based TPA factor calculation using referenceAirspeed/(airspeed+0.01f), which avoids division-by-zero without needing a fallback throttle path. It thus mitigates the division-by-zero risk the suggestion identified, albeit via a different implementation.

code diff:

+static float calculateFixedWingAirspeedTPAFactor(void){
+    const float airspeed = getAirspeedEstimate(); // in cm/s
+    const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
+    float tpaFactor= powf(referenceAirspeed/(airspeed+0.01f), currentControlRateProfile->throttle.airspeed_tpa_pow/100.0f);
+    tpaFactor= constrainf(tpaFactor, 0.3f, 2.0f);
+    return tpaFactor;
+}

Add a check to prevent division-by-zero when referenceAirspeed is zero in the
tpaThrottle calculation, falling back to the standard throttle value if
necessary.

src/main/flight/pid.c [501-502]

 const float referenceAirspeed = pidProfile()->fixedWingReferenceAirspeed; // in cm/s
-tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
+if (referenceAirspeed > 0) {
+    tpaThrottle = currentControlRateProfile->throttle.pa_breakpoint + (uint16_t)((airspeed - referenceAirspeed) / referenceAirspeed * (currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()));
+} else {
+    // Fallback to regular throttle if reference airspeed is not configured
+    tpaThrottle = rcCommand[THROTTLE];
+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical division-by-zero risk in the new TPA calculation, which could cause a flight controller crash or loss of control.

High
Ensure a pitot sensor is detected

Add a check in pitotValidForAirspeed to ensure a pitot sensor is detected
(PITOT_NONE) before proceeding with other validity checks.

src/main/sensors/pitotmeter.c [308-316]

 bool pitotValidForAirspeed(void)
 {
-    bool ret = false;
-    ret = pitotIsHealthy() && pitotIsCalibrationComplete();
+    if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_NONE) {
+        return false;
+    }
+
+    bool ret = pitotIsHealthy() && pitotIsCalibrationComplete();
     if (detectedSensors[SENSOR_INDEX_PITOT] == PITOT_VIRTUAL) {
         ret = ret && STATE(GPS_FIX);
     }
     return ret;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential edge case where the system might incorrectly use airspeed TPA if no pitot sensor is configured, improving the robustness of the check.

Medium
  • Update

@shota3527
Copy link
Contributor Author

throttle position vs tpa factor
image

@sensei-hacker
Copy link
Member

sensei-hacker commented Oct 15, 2025

This does look kinda over-complicated for historical reasons. But I'm not volunteering to refactor it, so I'm not really complaining.

We originally saw we need to reduce the PID when airspeed increases. Because the force/lift from the control surface is proportional to ( 0.5 * airspeed) ².

What we really wanted originally when TPA was introduced was the airspeed. But we often don't have the airspeed. So we used the throttle value to make a rough estimate of what the airspeed might be, in order to reduce the PID appropriately.

Now when we DO have the airspeed (the value we wanted in the first place), we're using the airspeed to simulate a throttle value, in order to estimate how much the throttle value might affect the airspeed and therefore the PID.

Kinda like:
We want to know how much Mike weighs.
The scale says he weighs 140 pounds.
Therefore we can estimate he's about six feet tall.
On average, people who are six feet tall weigh 155 pounds.
Therefore Mike probably weighs 155 pounds (an estimate originally based on the fact that he weighs 140 pounds).

Given we already know the airspeed, there's no need to mess around with estimating a "virtual throttle", then estimating how much that will affect the velocity and therefore the force.
The number we need is simply ( 0.5 * airspeed ) ² , from the lift equation.

@shota3527
Copy link
Contributor Author

shota3527 commented Oct 16, 2025

We originally saw we need to reduce the PID when airspeed increases. Because the force/lift from the control surface is proportional to ( 0.5 * airspeed) ².

I think this only explain the first half of the theory and it is not proportial but inverse propotional, The following are my thoughts, in short the angular velocity of the plane is propotional to the control surface multiplied by airspeed. If the old TPA assumes throttle is propotional to airspeed. then it is on same page with the following. And airspeed gain reduction(TPA) will need some internal variable to calculate, then why dont we use the virtual throttle value as the internal variable to maintain the compability, reducing the workload of users to shift from throttle based to airspeed based.

Relationship Between Airspeed, Control Surface Deflection, and Angular Velocity in Model Aircraft

1. Fundamental Dynamic Equation

For a single rotational axis (roll, pitch, or yaw):

dot(ω) + A·V·ω = B·V²·δ

  • ω: angular velocity (rad/s)
  • dot(ω): angular acceleration (rad/s²)
  • V: airspeed (m/s)
  • δ: control surface deflection angle (rad)
  • A, B: lumped constants (geometry, aero derivatives, inertia, air density)

Interpretation:
Angular acceleration + rate damping = airspeed² × control surface deflection


2. Steady-State Condition

In our small plane, the plane enters Steady-State quily, that is why the plane is mainly driven by FF gain
When angular acceleration is negligible:

0 + A·V·ω = B·V²·δ

Therefore:

ω = (B/A)·V·δ


3. Key Proportional Relationships

  • At fixed δ: angular velocity increases linearly with airspeed.
  • At fixed ω: required control deflection decreases as 1/V.

4. Quick Reference

  1. Constant angular rate control law
    δ ≈ K·(ω_cmd / V), where K = A / B

  2. Two-speed comparison
    For the same target angular rate ω:
    δ₂ / δ₁ = V₁ / V₂


Summary

Condition Relationship
General dynamics dot(ω) + A·V·ω = B·V²·δ
Steady state ω = (B/A)·V·δ
Constant rate control δ ∝ 1/V
Two-speed comparison δ₂ / δ₁ = V₁ / V₂

High-speed flight requires smaller control deflections to achieve the same rotation rate due to aerodynamic damping and increased control effectiveness.

@sensei-hacker
Copy link
Member

sensei-hacker commented Oct 16, 2025

I think this only explain the first half of the theory and it is not proportial but inverse propotional,
...
High-speed flight requires smaller control deflections to achieve the same rotation rate due to aerodynamic damping and increased control effectiveness.

Agreed.
The lift force from the control surface (the dynamic pressure) at a given AoA is proportional to (0.5 * V)² .
(Assuming barometric pressure etc doesn't change)

Therefore the deflection needed to generate the force is ~ inversely proportional. (Given assumptions).

So no need to bring a simulated throttle into it.

THAT said, you actually wrote the dang code. And it works, presumably. I didn't write it and probably wouldn't, so whichever way you want to write it is fine by me. I'm somewhat regretting having said anything because you do great work and my comment wasn't pointing out a real problem that needed to be fixed. It just felt awkward to do a "virtual throttle".

Ps I had mentioned inversely proportional to (0.5 * V)², I see why you said inversely proportional to V itself. I see V is probably more correct for correcting for external disturbances, whereas V² would be for initiating maneuver, for starting a a turn. (If I understand correctly).

On an unrelated topic - I wanted to let you know I'm hoping to cut an INAV 9.0RC1 in the next couple of weeks. Hopefully with airspeed PID attenuation included. I sent a build with this plus some other PRs over to James Downing for testing.

@shota3527
Copy link
Contributor Author

shota3527 commented Oct 16, 2025

I see V is probably more correct for correcting for external disturbances, whereas V² would be for initiating a maneuver, for starting a turn. (If I understand correctly).

I think V is more important for continuous control stick input , and V² is more important for everything else including disturbances and starting a turn.

I am changing my mind. considering only V, then the TPA formula can be much simplier
δ = δref * Vref / V

but have some promlems.

  1. no uperlimit, hard code or config will introduce complexity
  2. the real situation might be some where between V² and V, and the formula could not mimic that
  3. there might be deflect in the control link

While the virtual throttle method has some extra parameters to toggle, or add a new parameter/modify δ = δref * Vref / V

curiousto know how others think

@sensei-hacker
Copy link
Member

sensei-hacker commented Oct 16, 2025

I love that I have someone to discuss this with who understands it better than I do. :)

the real situation might be some where between V² and V, and the formula could not mimic that

It occurs to me that V^1.3 or V^1.4 will always be in between. I think V^1.3 would probably be a good compromise between the two.

Of course, if you didn't want to do a decimal exponent, something like (0.5 * V) ^ 2 would also give a compromise value in between V and V² 😜
(Assuming the velocity is faster than a snail).

no uperlimit, hard code or config will introduce complexity

The baseline is not having any airspeed-based attenuation at all, so IMHO capping the attenuation at 4 * vref or so would be pretty good. It's not like it explodes at the cap. It just doesn't attenuate any further. I don't think the exact cap is critical. Of course one could smooth out the saturation by multiplying by (10 - Vfactor) / (Vfactor + 8) or whatever. (Where Vfactor = current airspeed / Vref).

@sensei-hacker
Copy link
Member

sensei-hacker commented Oct 16, 2025

there might be deflect in the control link

Yeah I expect the deflection will be approximately linear to the force (0.5V)^2 given the material is reasonably sized for the application.

In the end, I suspect that's about the difference between V^1.3 and V^1.35.

@trailx
Copy link
Contributor

trailx commented Oct 16, 2025

I got to do a preliminary test flight of this today. I am particularly interested in this because I've wanted it for a couple years. Currently I fly the three PID profiles based on custom programming logic that auto changes control profile based on airspeed. I have tuned these breakpoints as shown below (ignore the best-fit curves). These profiles are tuned fairly high to provide the best performance and stabilization without inducing oscillations. I've put many hours of flight on these settings without issues, and these PIDs are well established. Also I am using the Matek digital airspeed sensor.

Airspeed TPA Program

To test this PR, I applied airspeed TPA to profile 2, and added a switch that would force that profile over the airspeed-based changes. Profile 2 was tuned for use between 45 and 55 mph with no TPA in effect. So I applied the reference airspeed at 55mph, and the TPA throttle reference (BP) at 1500, which is roughly the throttle that achieves 55mph.

I'll talk through my flight today. I set up the TPA % at 35 to start.
Airspeed TPA Initial Setup
Above is the view as I started flight. I increased throttle to understand how the current setting operated, and I ran into roll oscillations around 66 mph airspeed. In flight, I adjusted the TPA number to try to push the oscillation point to a higher airspeed. I increased it up to its maximum, 100, and it didn't seem to have much affect on the airspeed in which I encountered oscillations. Oscillations always seemed to occur around 66-70 mph.

I expected increasing TPA to progressively attenuate PIDs and delay oscillation onset to higher speeds. However, changing it from 35% to 100% didn’t significantly affect the oscillation airspeed, suggesting the attenuation scaling or mapping may not be strong enough.

I also tried moving the BP or TPA throttle reference up and down, from 1380 to 1605 and it always seemed to oscillate around the same airspeed, 69 mph-ish. I was hesitant to change this a whole lot because I didn't really understand what effects it would have on flight performance.

I never ran into any oscillations or issues at lower airspeeds, only higher airspeeds. It was hard to tell, but it seemed like it may have a little more stability when in my profile 1, than when I locked in profile 2.

Because TPA is largely an invisible adjustment behind the scenes, its difficult to fully understand and visualize what's going on inside it while flying, and because airspeed is mapped on top of throttle, it makes it difficult to understand the attenuation curve when looking at the current graphs.

If you've got any advice for how I should adjust the "TPA levers", I can do another test flight tomorrow to test anything different. My takeaway is that 100% TPA doesn't seen to be enough. I'd love to get a better idea of what the formula is that I applied, and what that looks like on top of the stepped profile program that I use today.

BTW, I am James Downing FPV on discord.

@trailx
Copy link
Contributor

trailx commented Oct 20, 2025

I did some further digging on this, and I attempted to visualize the airspeed TPA math directly to my stepped curve. I think I uncovered something.

First, I realized I had something wrong in my earlier graph. In my second step profile, my Roll-P (which is what was likely causing my roll oscillation) is actually higher than I have depicted. I have it set at 46. So I marked that on the graph below with the green dot. That's at the reference airspeed, 55 mph.

I then graphed in red and orange the two curves I flew. Starting with 35% TPA factor, and then again at 100% TPA factor. I encountered an oscillation at roughly 65mph and 70 mph. Those two points are shown purple and annotated. A rough "local max" slope line can be inferred by these two points. It shows that the ideal curve needs to be steeper than what is provided.

I found what look to be two potential solutions, both graphed below.

The preferred solution I believe is to remove the reference to idle throttle (or more accurately pull that down to 0). This is shown in the blue line. Unless I did something wrong in my math, its not doing what we want it to. Its really the cause of the slope limitation. When I set that to 0, I get a curve that looks much more like what is intended, and its very close to the "local max" slope in that airspeed range.

An alternative approach could be to increase the TPA %. I increased it to 300% and was able to create the grey dashed line. I don't think this is as ideal, but it would be preferred to at least have greater control on the effective slope.

There's also a non-zero potential I got my math wrong and translated this incorrectly, but this matches my experience. Hope this helps shed some light on what I saw.

Airspeed TPA Curve Graph

@trailx
Copy link
Contributor

trailx commented Oct 21, 2025

I need to apologize, the graph I originally produced didn’t sit right with me. The curves didn’t match the logic I expected and saw from the code and other reference material, they were far too linear looking. After re-checking the math carefully, I found some errors. The overall takeaway about why oscillation occurs was still correct, but my earlier recommendation was not.

Below is the corrected airspeed-TPA graph based on the exact formulas pulled from the code (double checked this time). The red and orange lines and purple points still represent the same things in my prior post, but they show the correct overall shape. 100% TPA attenuates more than I showed before, but not significantly more. This increased the slope of the "local max" roll-P term calculated based on the oscillation points I encountered. The low-speed is boosted much more than I calculated last time, and I show the 2x cap on the graph rather than leaving it unbounded.

image

My conclusion has changed. We simply need a higher dynamic TPA range in order to better match the curve of the dynamic pressure effects. Planes with looser tuning may not reveal these effects when testing. However on my tightly tuned Penguin, the mismatch is noticeable. If I had to make an educated guess at the best dynamic pressure curve based on the data I have available, I think it would be in the range of 200-250% TPA.

I recommend opening the bounds for the dynamic TPA factor. Instead of limiting it to 0-100%, it needs to be 0-300% at minimum, but it may make sense to open this up further to accommodate and anticipate future needs. I also recommend the removal of the lower bound of 1/2 attenuation factor. If I had a lower bound of 1/2 TPA factor, there's a chance I'd still run into an oscillation at or above 100 mph. Difficult to extrapolate this, but I'd recommend lowering the lower scaler bound to 1/4 TPA. I think the upper bound of 2X can remain.

Thanks for your time on this. I think with these small tweaks it will be ideal.

Recommendations:

  • Increase allowable TPA dynamic factor from 0-100% to 0-300%, or even as much as 500%.
  • Remove the lower attenuation limit of 1/2, replace with 1/4.
  • Keep current upper bound of 2x.

@trailx
Copy link
Contributor

trailx commented Oct 22, 2025

In speaking with Jetrell, I realized some minor clarification would be helpful.

In my last post, I referred to the "TPA dynamic factor", for lack of the correct term. The correct term is tpa_rate. So to clarify my recommendation is to raise the upper limit of tpa_rate from 100 to at least 300, with 500 giving more headroom as needed to accommodate different unforeseen setups.

Second, I recommended moving the lower attenuation limit from 1/2 to 1/4. This is a hard coded sanity limit. I added an annotation on the graph with the dotted blue lines, showing where my "theoretically ideal" 225% tpa_rate line would be otherwise truncated at 1/2 with today's limit. This could potentially create oscillations at very high speeds, 100mph+ because it didn't allow enough attenuation.

The recommendation is to adjust the following line in pid.c from:
tpaFactor = constrainf(tpaFactor, 0.5f, 2.0f);
to:
tpaFactor = constrainf(tpaFactor, 0.25f, 2.0f);

image

@sensei-hacker
Copy link
Member

sensei-hacker commented Nov 3, 2025

I'm looking to get INAV 9RC1 cut very soon so that 9.0 can be released in time to flash Christmas gifts with it.

Based on the above discussion about using an exponent of around ^1.3 or ^1.5, and the testing by @trailx , do you expect to make further refinements on this soon?

@Jetrell
Copy link

Jetrell commented Nov 3, 2025

My tests showed the current tpafactor constraint of 0.5 attenuation to be okay on planes with lower control surface throw limits, and at speeds up to around 150km/h.. But beyond that speed, or if the airplane has larger control surface throws. Then attenuating the gains by 50% was not enough.
I'd agree with James. It could do with the gains being attenuated by 75%. Which lowers them to only 25% of their tuned value, when at the TPA_breakpoint.

And due to this. I also think it might be good to have the tpafactor broken out as a user adjustable setting, within that new constraint.

Just to add a bit more. I think it would also aid attenuation/boost response time if Down Pitch was accounted for in the calculations. I noticed there was occasionally a little bit of lag in the virtual airspeed. Likely GPS velocity related.
But if it knew an attitude change was commanded. That could maybe bring on attenuation/boost faster.
When testing this. It should also be noted that fw_tpa_time_constant ideally should be set to zero, Otherwise it will add to this lag.

Another thing that would be helpful for tuning. Is if the TPA gain changes could be shown on the OSD Outputs. So the user can see the attenuated or boosted gains being adjusted real-time while in flight. This would certainly help while trying to get the base gain tune more accurate etc.

@shota3527
Copy link
Contributor Author

shota3527 commented Nov 6, 2025

@sensei-hacker
I have update the codes to use ^1.2 as default with 2x(low speed)~0.3x(high speed) limiter. and seems ok in SITL.

@shota3527
Copy link
Contributor Author

Update the pitot LPF Hz default value to 1hz, default 0.35hz is lagging

@trailx
Copy link
Contributor

trailx commented Nov 6, 2025

Looks like you changed the code to no longer map to throttle. If I followed the math right, the proposed ^1.2 default looks like its effectively really close to my proposal of 225%. Purple line is new power function. Is this power value adjustable in the air? I didn't look that far into the code, but that's typically how I tune.

image

Thanks! Looking forward to flying it. Hopefully tomorrow.

@Jetrell
Copy link

Jetrell commented Nov 6, 2025

@shota3527 Thanks for the extra commits.. You've ticked a lot more boxes with its handling.

However, I don't mean to come across ungrateful for your work here. But integrating pitch angle based on + or - from gravity vector, would help in a GNSS failure or if a reduction in airspeed accuracy occurs in aerobatic maneuvers, which Turnrate can't detect either... Or the pitot tube gets partially blocked.
My reasoning is this. With this addition, users will now likely have the base PID tune at the TPA_breakpoint, set much tighter than before. Which would cause a much higher probability of control surface oscillations, if airspeed sensor inaccuracy occurs.

It's in the case of a dive condition that the current fixedwing TPA doesn't work correctly. Because throttle can be low and airspeed will still rise in a dive.
So due to this fact. If the plane either goes into a nose downward dive, either aerobatically by a stick command, whether from right way up or inverted flight. Or a command that comes from the nav controller. Or even a loss of control leading to a nose down dive,.
This detection of pitch angle with either an increase in acceleration or a timer, should certainly indicate that airspeed will be rising. Which should be a safe fallback for sensor failure, and also fix the limitations of the current fixedwing TPA implementation. What is your view on this ?

@trailx
Copy link
Contributor

trailx commented Nov 7, 2025

I got two flights in today on this PR. I'll try to keep my notes short. Wind was between 10-15 mph according to both feel and windspeed estimator in INAV. Once again I took off into my airspeed-driven 3-profile setup. I had a switch that would lock the middle profile, which is the one that had airspeed-pid-attenuation power at 1.20. Reference airspeed was set to 55 mph, where profile 2 maxed out previously.

First flight, overall everything performed as expected. Low speeds worked flawlessly, showing excellent control at low speeds even in the wind. I did run into some oscillations in the 75+ mph range, but I noted that my pitot was under-reporting airspeed. I have both GPS 3D speed and airspeed on the OSD, and airspeed was consistently low. I've had a consistent struggle with this and am working on tuning the scale properly. Assuming this was the issue, I landed and adjusted the scale.

Second flight, the airspeed was more inline with expectations at normal flight speed, while over-estimated at low speeds. I think the FC restart in windy conditions disturbed the zero offset. That said, the oscillations were gone. I did a few steep power off and power on dives without issue.

Overall, this flew extremely well, and I'm looking forward to getting my pitot sensor dialed in a little better, and further adjusting things as needed. I'd like to try flying with the virtual pitot too.

TLDR:

  1. Ensure airspeed sensors are calibrated well, otherwise if its under-reporting airspeed, it may not attenuate well enough at higher speeds, but this isn't an issue with this PR, just a general implementation note.
  2. Being able to adjust the airspeed PID attenuation power, reference airspeed, and pitot scale in-flight via the OSD adjustments tab would be useful improvements.

Thanks for all your work on this!

@shota3527
Copy link
Contributor Author

In case of climb/dive situation, how about based on current throttle based tpa, offset the throttle value by "pitch to throttle value"(i forget the variable name but it is in navigation settings). Then hopefully no additional parameters needed.

I am also thinking about raise the default tpa value from 0, if it is also 0 in the default presets.
Then user just need to set the fw_reference_airplspeed. Then it is mostly ok.

@trailx
Copy link
Contributor

trailx commented Nov 8, 2025

pitch2thr is typically only tuned for climb performance up to the max angles because this is only used for automated flight modes. I've got pitch2thr set to 10 (not sure if that's the default or not, I don't remember). If we use pitch2thr as an unconstrained multiplier, and assume the ref throttle is set to 1500, I'd essentially max out the TPA effect at 50 degrees (50*10=500, gets you to 1000 or 2000). Or potentially get as much as a 900us effect with a vertical dive or climb. That seems excessive to me and would get truncated in many cases to max boost or min attenuation levels - for instance a power off 90deg dive would be equivalent to almost full power level? I mean, maybe, but it really depends on the plane and how well its tuned.

Notable is the physics as you dive steeper - the effect that gravity has isn't linear, especially as you get to very steep dive angles - its added effect is minimal for every additional degree of tilt. It may make sense to apply a sine function over top of the pitch2thr, roughly shown in the screen shot below (the 57.29 is just there to turn radians into degrees). This would naturally taper the pitch2thr effect at steeper angles while still compensating linearly as expected in the typical range.

It would end up something like: effective_pitch = sin(pitch_angle / 57.29) * 57.29 (I'm sure theres a better way to do the radian/deg math) which then gets to multiplied by pitch2thr.

image

@sensei-hacker
Copy link
Member

sensei-hacker commented Nov 9, 2025

It may make sense to apply a sine function over top of the pitch2thr, roughly shown in the screen shot below (the 57.29 is just there to turn radians into degrees). T

Agreed. The force of gravity accelerating the aircraft (replacing motor thrust) is proportional to the sine of the angle.
Linear is close enough up to about 30-40 degrees, which is fine for adjusting the actual throttle with nav_fw_pitch2thr - you aren't going to reduce it below zero. :)

@shota3527
Copy link
Contributor Author

Yes, throttleAdjusted will decrease when the pitch is up. A 2000us throttle will decrease to 1100us for calculation with the current default value, and then the PID might be too high.
I think for such a powful plane, first should be tuning nav_fw_pitch2thr. 10 or 11 is too much for such a powerful plane

@Jetrell
Copy link

Jetrell commented Nov 18, 2025

I think for such a powful plane, first should be tuning nav_fw_pitch2thr. 10 or 11 is too much for such a powerful plane

Due to this being a shared variable. We have to be cautions of how much this setting is altered, without it effecting the navigation climb/dive throttle. Meaning that the user would also have to retune all other setting related to pitch2thr for navigation.

@trailx
Copy link
Contributor

trailx commented Nov 18, 2025

My penguin is overpowered, and the default pitch2thr would result in it exiting automated flight climbs at a very high rate of speed. Right now its at 8 and climbs well, if maybe still a little too strong. (In RTH I like it to pop up pretty fast) The difficulty is the non-linearity of throttle % vs airspeed, and the users just need to be aware that the addition of the pitch calculation is probably limited somewhat in its effectivity to the automated flight pitch limits (its not, but it effectively may be).

Tuning pitch2thr so that when in automated flight the plane does high angle climbs and dives roughly at a constant speed may be a good prerequisite to tuning TPA.

@rts18
Copy link

rts18 commented Nov 19, 2025

@shota3527 After reading yesterdays conversation. I took one of my planes with a 2:1 thrust to weight out to test it with commit 00c9db0.
I specifically focused on testing the "TPA with pitch angle awareness" modification.

As it would appear in the logic and raised by Jet. The PIDs are boosted at high climb speeds, when the throttle is significantly above the tpa_breakpoint, using a high thrust output plane. This lead to control surface oscillations.

I proceeded to alter the nav_fw_pitch2thr. It showed minimal influence over this situation, as did the fw_tpa_time_constant filter.

I'm aware setting variables are a limited hardware resource. However cutting corners on such an important feature as this is not the way to go.
This feature requires its own "throttle_pitch angle" setting.
The way nav_fw_pitch2thr is used for navigation throttle, is not at all compatible with the requirements of the "TPA pitch angle" function, because of the different ways both can be tuned. As I believe @MrD-RC would agree.

@shota3527 I would like to encourage your efforts towards implementing such an overdue feature. ❤️
It makes an extreme difference to fixedwing stabilization performance. However please don't leave this feature with edge logic holes in it. Obi-wan, your our only hope.

What about adding a tpa pitch scaler variable that could be adjusted by the user if their plane has a high thrust to weight. It could reduce the effect PID boost would have on such aircraft at a high climb speed/angle, without altering the dive angle attenuation function.
You could also use barometric vertical airspeed as a secondary reference. Both the air speed sources, if GPS virtual and the barometer are unlikely to fail at the same time. If they do, the altitude estimation will also be stuffed.

I have not forgotten that APA is still the best approach. But this method is also required to work well if the air speed source fails.

How about this extrapolation? Adding fw_tpa_pitch_scaler for over powered planes. And BaroVerticalSpeedas the speed source.

{ float getBaroVerticalSpeed(void); 
static float calculateTPAThtrottle(void)
{
    uint16_t tpaThrottle = 0;
    static const fpVector3_t vDown = { .v = { 0.0f, 0.0f, 1.0f } };

    if (usedPidControllerType == PID_TYPE_PIFF && (currentControlRateProfile->throttle.fixedWingTauMs > 0)) {
        fpVector3_t vForward = { .v = { HeadVecEFFiltered.x, -HeadVecEFFiltered.y, -HeadVecEFFiltered.z } };
        float groundCos = vectorDotProduct(&vForward, &vDown); 
        
        float pitchAdjustmentFactor = currentBatteryProfile->nav.fw.pitch_to_throttle * groundCos * 90.0f; 

        float verticalSpeed = getBaroVerticalSpeed(); 
       
        float baroAdjustment = 0.0f;

        if (fabsf(verticalSpeed) > 100.0f) 
           baroAdjustment = verticalSpeed / 10.0f; 
        }

        int16_t combinedAdjustment = (int16_t)constrainf(pitchAdjustmentFactor + baroAdjustment, -1000, 1000);
        
        uint16_t throttleAdjusted = rcCommand[THROTTLE] + combinedAdjustment;
        tpaThrottle = pt1FilterApply(&fixedWingTpaFilter, constrain(throttleAdjusted, 1000, 2000));
    }
    else {
        tpaThrottle = rcCommand[THROTTLE]; 
    }
    return tpaThrottle;
}


@trailx
Copy link
Contributor

trailx commented Nov 19, 2025

@rts18 , I don't know how tightly you have your PIDs adjusted, but did TPA+pitch work for a certain range of pitch angles? At what angles, speeds, and throttles did you run into oscillations?

How far did try you adjusting pitch2thr? I have to assume that at 0, it would not create this oscillation. I also assume that at 1, it will likely not create the oscillation. Academically, at some point, you should be able adjust the pitch-up-TPA-gain oscillation out of the system by adjusting the P2T.

But at that point - where it no longer creates oscillations on a climb - does it create other issues from pitch2thr being too low?
Does it start oscillating in a pitch-down attitude?
Does it no longer maintain airspeed when climbing in automatic flight?

Rather than going on a hunt for new solutions to fixing TPA, at what point do we try to adopt something more akin to the "basic TPA" solution that was developed by betaflight?
betaflight/betaflight#13895

I don't think we need to consider betaflight's advanced method, because APA fills that gap, arguably better.

I'm also a bit concerned that we won't find a solution to TPA that works in all situations - and APA gets held up. I don't want perfecting TPA to hold up the implementation of APA, and at least getting that improvement out to people in 9.0.

@rts18
Copy link

rts18 commented Nov 20, 2025

At what angles, speeds, and throttles did you run into oscillations?

Climbing vertical at 80 km/h @ 85% throttle. It has a 25.2v 700w power system.
Read my last post again. The goal was to find if the issue @Jetrell raised in the wiki was of concern to some users. Isn't that the reason for testing. Not just to prove your own model works. But to ensure the logic works for others as well.

Does it start oscillating in a pitch-down attitude?

No, why would it. Have you not read how the logic works. Down pitch works as it should. pitch angle overrides raw throttle and attenuates the gains if the plane is in a low throttle dive. Instead of boosting them as the old fixedwing tpa used to.

Does it no longer maintain airspeed when climbing in automatic flight?

No it won't. This was the whole point of my last post.
It will practically stall when commencing a RTH climb when nav_fw_ptch2thr is set lower than 8. And I wasn't going below 6 in case I required RTH in that flight.
This was my whole point, when using nav_fw_pitch2thr for this function as well as using it for navigation climb/dive. The functions it is being used to adjust are incompatible.
For example. If nav_fw_pitch2thr is set to a low value like 8, at a 22° climb angle. That only provides 176us of throttle increase between nav_fw_cruise_thr and nav_fw_max_thr. Which is far too little to be useful. Using nav_fw_pitch2thr at default is the minimum I would select for tuning its use for RTH or WP for most users.

I'm also a bit concerned that we won't find a solution to TPA that works in all situations - and APA gets held up. I don't want perfecting TPA to hold up the implementation of APA, and at least getting that improvement out to people in 9.0.

Dude you worry too much. What's the good of merging something that has a known and real life tested bug.
As has been said by myself and others in this thread. APA works fine if you don't encounter an airspeed source failure. Then your plane will go into the worst control surface flutter you could imagine, if the TPA and pitch angle isn't sorted. Don't forget that base gains will now be tuned far tighter then before.

When things don't get cleaned up before a merge, you end up with bugs everywhere that never get addressed.

@MrD-RC
Copy link
Member

MrD-RC commented Nov 20, 2025

My 2 cents. nav_fw_pitch2thr should not be used as an APA tuning parameter. It is a navigational tuning parameter, which is separate from the APA system.

APA can be effected by the results of nav_fw_pitch2thr. But it should have its own apa_pitch_compensation (or whatever the appropriate name is) parameter for turning the APA. It should not be tied to the navigation.

@trailx
Copy link
Contributor

trailx commented Nov 21, 2025

Does it start oscillating in a pitch-down attitude?

No, why would it. Have you not read how the logic works. Down pitch works as it should.

You're right, I wasn't thinking. And yes I fully understand how the logic works. I was really trying to understand the details of what you experienced. Down-pitch may over-attenuate, but you're right over-attenuation isn't the concern, it just becomes less stabilized. The concern is over-boost where it can generate oscillation.

I think there's another potential issue when switching to TPA and simply flying level at high throttle because it may not attenuate enough because TPA's curve is flatter than APA.

Does it no longer maintain airspeed when climbing in automatic flight?

No it won't. This was the whole point of my last post. It will practically stall when commencing a RTH climb when nav_fw_ptch2thr is set lower than 8.

It wasn't completely clear in your last post, so I wanted to understand. I've got my P2T down to 7 now and it still maintains speed in climb. I think I could probably go lower without issue. I may have my cruise throttle set proportionally higher than you to stay further away from stall.

Dude you worry too much. What's the good of merging something that has a known and real life tested bug. As has been said by myself and others in this thread. APA works fine if you don't encounter an airspeed source failure.

Maybe you're right, its my engineering brain mitigating schedule risk. To me, APA is the single most important fundamental fixed-wing feature that I've been wanting and advocating for over the last couple years. That said, TPA has never controlled gains appropriately on a fixed wing, and I don't think it will ever function as well as APA. If it did, we wouldn't even need an APA solution.

So that’s why I’m concerned about a fundamental mismatch here that may not have a solution:
APA enables tuning much higher PIDs than TPA can safely handle, but when TPA suddenly takes over during an airspeed dropout, it may lead to either over-boost at high angles of attack, or under-attenuation at high throttles. Both leading to oscillations.

But, I realized last night that there may be a simple workaround for anyone using APA. I think you can avoid almost all TPA-fallback oscillation issues by setting tpa_breakpoint to a throttle value significantly below the throttle required to maintain the reference airspeed used by APA. For example, on my penguin, looking at the graphs, I’m thinking I'll actually set it to 1000, while it flies at the ref airspeed at about 1450.

Doing this accomplishes several things at once:

  • If APA fails, throttle-TPA immediately drops into the attenuation region.
  • This prevents TPA from trying to boost already-high APA-tuned PIDs.
  • It attempts to address the known issue that TPA does not attenuate strongly enough at high throttle.
  • It also limits the boost region that would cause issues during high pitch-up flight.
  • It guarantees that fallback behavior is “safeish” (attenuated vs APA) rather than destabilizing by effectively applying scaled down PIDs across the throttle spectrum.

This basically “chops” the PID gains if APA drops out. No code changes needed, just documentation of best practices, and this only becomes an issue if people start tuning higher PIDs, I doubt theres any issue when flying defaults.

@trailx
Copy link
Contributor

trailx commented Nov 21, 2025

The following graph overlays APA’s adjustment factor with TPA’s adjustment factor on roughly equivalent X-scales between throttle (top) and airspeed (bottom) for my particular airframe. Obviously this will vary from plane to plane (and airspeed and throttle are not linear) but this gives a reasonable visual comparison and shows the fundamental flaw.

If you tune your PIDs to be very stable (but not oscillatory) at ~50 mph / 1500 μs, you can see the mismatch that appears between APA (green) and TPA (dotted red), even with 100% TPA. At any point beyond their common setpoint. TPA is simply unable to attenuate gains enough. Simply divorcing pitch2thr from TPA+pitch doesn't solve this.

On the opposite end, if you pitch up hard enough to drive throttle into a region where TPA-boost would exceed APA’s factor, then you hit the other issue we’ve been discussing, its just not as visually evident.

If you lower the TPA breakpoint to 1020 μs, you get the yellow curve. As far as I can tell, it still bottoms out at 0.5, due to the 0.5f term in the code:
tpaFactor = 0.5f + ((float)(currentControlRateProfile->throttle.pa_breakpoint - getThrottleIdleValue()) / (throttle - getThrottleIdleValue()) / 2.0f);

(Oh yeah, and I dropped the idlethrottle to 1000 in this graph, which may not be ideal... when breakpoint and idle throttle invert, the graph does strange things.)

Here's the reference graph, with what I think is a decent workaround, and the way I will set up my plane for now (yellow) to mitigate the issues we've discussed:
image

@rts18
Copy link

rts18 commented Nov 21, 2025

I think you can avoid almost all TPA-fallback oscillation issues by setting tpa_breakpoint to a throttle value significantly below

That won't have any effect on the reported condition because the throttle is still above the breakpoint no matter how far.
The inverse throttle value used when climb pitch angle is added to the equation, will still cause the same result on powerful planes that can climb at a high pitch angle, higher throttle and therefor higher speed, leading to the PIDs being boosted.
Such planes as these require this fix more than others, due to their wide flight speed envelope.

@trailx Please stop distracting from the problem with workarounds. Open a chat in the Discussions section. shota3527 is a capable developer. He will fix the issue when he has time.

There are two issues.

  • pitch2thr is not compatible for tuning of both tpa pitch angle and navigation throttle. This function requires its own setting variable.
  • TPA assisted pitch angle requires a secondary means to determine vertical velocity, as I originally mentioned.

@trailx
Copy link
Contributor

trailx commented Nov 22, 2025

Let's not discourage active participation... I agree with you that the workaround I mentioned isn't the final solution, but openly talking through the issues is part of the process of solving it. I think if we put it simply, if TPA is the fallback, the solution needs to ensure TPA's gains are never above APA's.

After more thought, I think I can boil my thoughts down to two code recommendations, and one general setup recommendation:

  • The TPA curve shouldn't bottom out at 0.5. The curve is simply too flat and thats part of the basic problem with TPA, but it only ends up with inadequate attenuation at high throttle. When I graphed it out with a low breakpoint, I realized its actually asymptotic at 0.5. Unless I read it wrong and graphed it incorrectly, I believe this is a core deficiency within TPA.
  • TPA+pitch should ignore pitch-up effects entirely and only attenuate pitch-down angles. The goal is not perfecting TPA, but to ensure its flyable as a fallback in all conditions without inducing oscillations. Boosting gains at pitch up has minimal benefit, and has the biggest chance of inducing oscillations. I don't think its needed.
  • Setup recommendation: If flying APA, I think a clean solution is still to set the TPA breakpoint low just to automatically attenuate the PIDs if airspeed source was to fail, and even it TPA is tuned poorly, the hope is simply that it won't induce oscillations.

I'd like to test the fallback functionality directly... is there a clever way to force APA to dropout via a switch? Best I can think is setting up two identical control profiles, and setting APA_pow = 0 on one of them?

@trailx
Copy link
Contributor

trailx commented Nov 26, 2025

I flew the "workaround" that I had mentioned, setting tpa_breakpoint to 1150 (idle was 1100). I put a second profile on a switch with apa_pow = 0 so I could toggle between APA and a TPA+pitch setup with two otherwise identical profiles. Flipping the switch mimics an APA dropout. Testing in this manner, TPA+pitch flew reasonably well. The reduced PIDs were somewhat noticeable, but it was controllable and felt fairly stable in the gusty conditions today.

The only issue I saw was about 1 second of roll oscillation at high throttle during a pitch-up maneuver, but damped out quickly as the airspeed dropped. Considering this plane is tuned very tight, the workaround seems viable for flying APA currently and concerned about inducing oscillations in the case TPA is engaged. Plus, I still don't know of another way to functionally and cleanly reduce PIDs when it falls back from APA to TPA.

In the case of a fall-back to TPA, it may make sense to issue a system warning on the OSD, so the pilot can have situational awareness of the error and know to fly more conservatively. Something like "AIRSPD FAIL - TPA" could be displayed, just to alert to you an otherwise invisible function that could cause issues if not carefully configured and tested.

@sensei-hacker
Copy link
Member

@shota3527 do you have any thoughts on the last few comments?

@shota3527
Copy link
Contributor Author

shota3527 commented Dec 10, 2025

I have read comments and made some updates

  1. Add tpa_pitch_compensation
  2. Reduce the effect of tpa_pitch_compensation by about 36%. now at 1deg pitch angle, throttle for calculation is reduced by tpa_pitch_compensation, it was 90*tpa_pitch_compensation at 90deg pitch angle
  3. Allow tpa_rate aka throttle.dynPID to have a maximum of 200 for fixed wing for stronger tpa, it can reach 0.5 now. And constraints of the tpaFactor have been changed to [0.3,2.0]

I think BaroVerticalSpeed is not a good idea because it is speed, not acceleration/trust or force, so it is not appropriate to add or convert it to the throttle value.
I think in theory, boosting the pitch-up gain is more necessary than reducing the pitch-down gain. Assuming the plane flies at 150 at level, 180 (+30) when diving (might be 200 without the exponential air drag), it is likely to fly at 100 (-50) when climbing. Then, a greater speed difference compared with flying level needs more adjustment in the TPA. I hope the separate parameter, tpa_pitch_compensation will ensure it's flyable as a fallback in all conditions

@Jetrell
Copy link

Jetrell commented Dec 11, 2025

Thanks for making the changes @shota3527
Some preliminary tests show it to work okay. But it still needs TPA/pitch angle testing with a more powerful plane. And that by reducing tpa_pitch_compensation to see the result in a fast climb... Or even a steep dive, if the gains response might seem a little weak in some cases.

I updated the Wiki to fall inline with your last commits. To help other testers understand how to tune it.

@Jetrell
Copy link

Jetrell commented Dec 15, 2025

I did more in depth testing of TPA + pitch angle with high speed vertical climbs and dives. The results were good, no oscillations where seen on this model at over 100km/h sustained climb. I had tpa_pitch_compensation = 5 .
Testing was done with apa_pow = 0 . And I later increased it again, to check how it worked with GNSS fix disabled for virtual pitot use.

I could leave my tuning results here. But in reality, this plane is at the high end of FPV performance. So its tune is unlikley to be used by most people..
TPA + pitch angle tuning will vary depending on -

  • The planes performance.
  • How tight the tune is at the breakpoint.
  • How much TPA_rate is required.

I also tested APA again. It still works as before.
IMO, it looks good to go.

@rts18
Copy link

rts18 commented Dec 16, 2025

Tested with new tpa_pitch_compensation setting. Lowering it prevented control surface oscillations at high climb speeds, while not interfering with navigation throttle as adjusting pitch2thr did.

This TPA_APA feature works fine out of the box for most models. However it's good that it now accounts for faster airplanes as well if the air speed source fails.
The only default setting that required changing is TPA_rate. Because it's a joint variable used for both MC and FW. However I seen Jetrell made a note in the wiki, to increase it before tuning.

Thanks again @shota3527
I guess the implementation of this feature will now make room to reintroduce proper fixedwing autotune. Like in the old iNAV days when it did PI as well. But it was removed because it often gave an incorrect tune without airspeed governing of the PIFF.

@sensei-hacker sensei-hacker merged commit 2747993 into iNavFlight:master Dec 21, 2025
22 checks passed
sensei-hacker added a commit that referenced this pull request Dec 22, 2025
Add airspeed TPA support (backport from #11042)  via merge from master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants