From f8e4ce0fca543ddd59a537458bad15772007c22b Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:27:56 +0200 Subject: [PATCH 1/8] asyncfatfs: keep afatfs_poll out of the ITCM scheduler taskRunRealtimeCallbacks() calls afatfs_poll() from scheduler(), which is FAST_CODE, and LTO inlines the file system's state machine into it. On targets with an SD card that puts 800 to 980 bytes of afatfs code in ITCM, depending on the target, where KAKUTEF7 and KAKUTEF7HDV are at 99%. The poll does a little work at most once per scheduler loop, so it runs from flash like the rest of afatfs. --- src/main/io/asyncfatfs/asyncfatfs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/io/asyncfatfs/asyncfatfs.c b/src/main/io/asyncfatfs/asyncfatfs.c index cfa7da71ae8..88ebee22d3e 100644 --- a/src/main/io/asyncfatfs/asyncfatfs.c +++ b/src/main/io/asyncfatfs/asyncfatfs.c @@ -3638,8 +3638,9 @@ static void afatfs_initContinue(void) /** * Check to see if there are any pending operations on the filesystem and perform a little work (without waiting on the * sdcard). You must call this periodically. + * Out of line: the realtime callbacks call it from scheduler(), which is FAST_CODE, so LTO would inline it into ITCM. */ -void afatfs_poll(void) +NOINLINE void afatfs_poll(void) { // Only attempt to continue FS operations if the card is present & ready, otherwise we would just be wasting time if (sdcard_poll()) { From 104163ba1c6162fbcec2209578899532f54c810d Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:46:01 +0200 Subject: [PATCH 2/8] mixer: throttle boost, ported from Betaflight Adds a high-pass filtered copy of the throttle on top of it, so the motors follow quick throttle moves sooner: throttle_boost (0-100, the boost is the value / 10 times the high-passed throttle) and throttle_boost_cutoff (5-50 Hz, 15 by default), as in Betaflight. Off by default. Only the motors get the boost: mixerThrottleCommand, which OSD, telemetry and servos read, stays the throttle that was asked for. Multirotor only, and not while navigation controls the throttle, during a VTOL transition, with reversible motors or with the logic-condition throttle override. After the mixer skipped it for more than 100 ms (disarmed, motors stopped, navigation in control), the filter restarts from the current throttle. Both settings are appended to motorConfig_t without a version bump: the first one lands in the old record's tail padding, stored as zero, which is its default, and the cutoff starts past the end of any stored record, so it keeps its default of 15 Hz. --- docs/Settings.md | 20 +++++++++++++++++ src/main/fc/settings.yaml | 12 ++++++++++ src/main/flight/mixer.c | 46 ++++++++++++++++++++++++++++++++++++++- src/main/flight/mixer.h | 2 ++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/docs/Settings.md b/docs/Settings.md index a0bdc187935..daa39da2053 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7243,6 +7243,26 @@ Throttle value when the stick is set to mid-position. Used in the throttle curve --- +### throttle_boost + +Multirotor only, ported from Betaflight. Adds a high-pass filtered copy of the throttle to it, so the motors follow quick throttle moves sooner. The boost is this value divided by 10 times the difference between the throttle and its low-pass filtered value. 0 disables it; Betaflight uses 5 by default. Only the motors get the boost: OSD, telemetry and servos show the throttle that was asked for. Not applied while navigation controls the throttle, during a VTOL transition, with reversible motors or with the logic-condition throttle override. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 100 | + +--- + +### throttle_boost_cutoff + +Cutoff of the low-pass filter used by `throttle_boost` [Hz]. A higher cutoff makes each boost shorter. + +| Default | Min | Max | +| --- | --- | --- | +| 15 | 5 | 50 | + +--- + ### throttle_idle The percentage of the throttle range (`max_throttle` - `min_command`) above `min_command` used for minimum / idle throttle. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 142f5f06c9b..1ed349ccc1d 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -902,6 +902,18 @@ groups: min: 4 max: 255 default_value: 14 + - name: throttle_boost + description: "Multirotor only, ported from Betaflight. Adds a high-pass filtered copy of the throttle to it, so the motors follow quick throttle moves sooner. The boost is this value divided by 10 times the difference between the throttle and its low-pass filtered value. 0 disables it; Betaflight uses 5 by default. Only the motors get the boost: OSD, telemetry and servos show the throttle that was asked for. Not applied while navigation controls the throttle, during a VTOL transition, with reversible motors or with the logic-condition throttle override." + default_value: 0 + field: throttleBoost + min: 0 + max: 100 + - name: throttle_boost_cutoff + description: "Cutoff of the low-pass filter used by `throttle_boost` [Hz]. A higher cutoff makes each boost shorter." + default_value: 15 + field: throttleBoostCutoff + min: 5 + max: 50 - name: PG_FAILSAFE_CONFIG type: failsafeConfig_t diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index 2dccb22e3b2..aac8e094964 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -101,6 +101,8 @@ PG_RESET_TEMPLATE(motorConfig_t, motorConfig, .srxl2TelemetryRate = SETTING_ESC_SRXL2_TELEMETRY_RATE_DEFAULT, .srxl2Telemetry = SETTING_ESC_SRXL2_TELEMETRY_DEFAULT, #endif + .throttleBoost = SETTING_THROTTLE_BOOST_DEFAULT, + .throttleBoostCutoff = SETTING_THROTTLE_BOOST_CUTOFF_DEFAULT, ); PG_REGISTER_ARRAY_WITH_RESET_FN(timerOverride_t, HARDWARE_TIMER_DEFINITION_COUNT, timerOverrides, PG_TIMER_OVERRIDE_CONFIG, 0); @@ -655,6 +657,46 @@ static int getReversibleMotorsThrottleDeadband(void) return ifMotorstopFeatureEnabled() ? reversibleMotorsConfig()->neutral : directionValue; } +// The throttle the motors get; mixerThrottleCommand stays the one OSD, telemetry and servos show +static EXTENDED_FASTRAM int motorThrottleCommand; + +#define THROTTLE_BOOST_RESTART_MS 100 + +// Betaflight's throttle boost: a high-passed copy of the throttle on top of it, so the motors follow quick moves sooner +static int applyThrottleBoost(int throttle, float dT) +{ + static pt1Filter_t throttleLpf; + static timeMs_t lastBoostMs; + + // A cutoff of 0 would freeze the low-pass and turn the boost into a constant offset + if (!motorConfig()->throttleBoost || !motorConfig()->throttleBoostCutoff || !STATE(MULTIROTOR) || isMixerTransitionMixing + || feature(FEATURE_REVERSIBLE_MOTORS) || navigationRequiresAutoThrottleMode() +#ifdef USE_PROGRAMMING_FRAMEWORK + || LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_THROTTLE) +#endif + ) { + lastBoostMs = 0; + return throttle; + } + + // mixTable() does not get here while disarmed or with the motors stopped: restart from the current throttle + const timeMs_t nowMs = millis(); + if (nowMs - lastBoostMs > THROTTLE_BOOST_RESTART_MS) { + pt1FilterInit(&throttleLpf, motorConfig()->throttleBoostCutoff, dT); + pt1FilterReset(&throttleLpf, throttle); + } + lastBoostMs = nowMs; + + const float throttleHpf = throttle - pt1FilterApply3(&throttleLpf, throttle, dT); + return constrain(throttle + lrintf(motorConfig()->throttleBoost * 0.1f * throttleHpf), throttleRangeMin, throttleRangeMax); +} + +// Out of line: mixTable() is FAST_CODE and ITCM is nearly full on some F7 targets +static NOINLINE void updateMotorThrottle(float dT) +{ + motorThrottleCommand = applyThrottleBoost(mixerThrottleCommand, dT); +} + void FAST_CODE mixTable(float dT) { static float lastMixerThrottleCommand = 1000.0f; @@ -827,6 +869,8 @@ void FAST_CODE mixTable(float dT) } } + updateMotorThrottle(dT); + #define THROTTLE_CLIPPING_FACTOR 0.33f motorMixRange = (float)rpyMixRange / (float)throttleRange; if (motorMixRange > 1.0f) { @@ -851,7 +895,7 @@ void FAST_CODE mixTable(float dT) const bool fwEmergencyLanding = STATE(AIRPLANE) && !isMixerTransitionMixing && navigationIsExecutingAnEmergencyLanding(); for (int i = 0; i < motorCount; i++) { - float motorThrottle = mixerThrottleCommand * currentMixer[i].throttle; + float motorThrottle = motorThrottleCommand * currentMixer[i].throttle; #ifdef USE_AUTO_TRANSITION const motorMixer_t *targetMixer = autoTransition.active ? &autoTransition.targetMotorMixer[i] : NULL; const bool currentMotorActive = currentMixer[i].throttle > 0.0f; diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 43c31c9132d..789d9e8343c 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -121,6 +121,8 @@ typedef struct motorConfig_s { uint8_t srxl2Telemetry; // read ESC telemetry off the SRXL2 link uint8_t srxl2TelemetryRate; // how often to ask the ESC for telemetry, as srxl2TelemetryRate_e #endif + uint8_t throttleBoost; // must default to 0: it can land in the old tail padding, stored as zero + uint8_t throttleBoostCutoff; // Hz } motorConfig_t; PG_DECLARE(motorConfig_t, motorConfig); From b05bab3e7ffc52acead28fa917653103d53cdc09 Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:32:32 +0200 Subject: [PATCH 3/8] mixer: thrust linearization, ported from Betaflight A propeller's thrust grows faster than linearly with the motor command, so a multirotor responds less to the same correction at low throttle. thrust_linear (0-100, off by default) raises each motor output and lowers the throttle before the mix by the inverse amount, with the formulas of Betaflight 2026.6 (#15226): a second-order expansion of ArduPilot's MOT_THST_EXPO model, so the value is about MOT_THST_EXPO * 100 and hover stays where it was (within 8 us up to 50, 80 us at 100). Betaflight allows 150, but above 100 its compensation goes negative at low throttle and from about 130 the curve stops being monotonic, so the range stops at 100. The formulas live in flight/thrust_linear.h with their own unit test. Unlike Betaflight, outputs at or below idle are left alone, so the failsafe clamp to min_command is not pushed further down, and the setting is global rather than per PID profile. As with the throttle boost, only the motors get the lowered throttle. Multirotor only. Off on VTOL setups (any mixer profile that is not a multirotor), so it never switches at a transition; also off with reversible motors and with the logic-condition throttle override. Built on the throttle boost commit, so the three settings share one layout in motorConfig_t: thrustLinear lands in the tail padding of that record, stored as zero, which is its default. --- docs/Settings.md | 10 +++ src/main/fc/settings.yaml | 6 ++ src/main/flight/mixer.c | 47 ++++++++++++- src/main/flight/mixer.h | 1 + src/main/flight/thrust_linear.h | 33 ++++++++++ src/test/unit/thrust_linear_unittest.cc | 88 +++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/main/flight/thrust_linear.h create mode 100644 src/test/unit/thrust_linear_unittest.cc diff --git a/docs/Settings.md b/docs/Settings.md index daa39da2053..73a24bd44ee 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7293,6 +7293,16 @@ Can be used in ANGLE and HORIZON mode and will automatically boost throttle when --- +### thrust_linear + +Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), with reversible motors and with the logic-condition throttle override. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 100 | + +--- + ### tpa_breakpoint See tpa_rate. diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 1ed349ccc1d..695af388c94 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -914,6 +914,12 @@ groups: field: throttleBoostCutoff min: 5 max: 50 + - name: thrust_linear + description: "Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), with reversible motors and with the logic-condition throttle override." + default_value: 0 + field: thrustLinear + min: 0 + max: 100 - name: PG_FAILSAFE_CONFIG type: failsafeConfig_t diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index aac8e094964..abb831bc553 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -52,6 +52,7 @@ #include "flight/mixer_transition_logic.h" #include "flight/pid.h" #include "flight/servos.h" +#include "flight/thrust_linear.h" #include "navigation/navigation.h" @@ -103,6 +104,7 @@ PG_RESET_TEMPLATE(motorConfig_t, motorConfig, #endif .throttleBoost = SETTING_THROTTLE_BOOST_DEFAULT, .throttleBoostCutoff = SETTING_THROTTLE_BOOST_CUTOFF_DEFAULT, + .thrustLinear = SETTING_THRUST_LINEAR_DEFAULT, ); PG_REGISTER_ARRAY_WITH_RESET_FN(timerOverride_t, HARDWARE_TIMER_DEFINITION_COUNT, timerOverrides, PG_TIMER_OVERRIDE_CONFIG, 0); @@ -691,10 +693,50 @@ static int applyThrottleBoost(int throttle, float dT) return constrain(throttle + lrintf(motorConfig()->throttleBoost * 0.1f * throttleHpf), throttleRangeMin, throttleRangeMax); } +static EXTENDED_FASTRAM float thrustLinear; // 0 when off + +// A VTOL switching it in flight would step the motors, so it stays off on any setup with a non-multirotor profile +static bool isThrustLinearizationAllowed(void) +{ + for (int i = 0; i < MAX_MIXER_PROFILE_COUNT; i++) { + if (!isMultirotorTypePlatform(mixerConfigByIndex(i)->platformType)) { + return false; + } + } + return STATE(MULTIROTOR) && !feature(FEATURE_REVERSIBLE_MOTORS) +#ifdef USE_PROGRAMMING_FRAMEWORK + && !LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_THROTTLE) +#endif + ; +} + +static int compensateThrustLinearization(int throttle) +{ + thrustLinear = 0.0f; + if (!motorConfig()->thrustLinear || !isThrustLinearizationAllowed()) { + return throttle; + } + + thrustLinear = motorConfig()->thrustLinear / 100.0f; + const float range = throttleRangeMax - throttleRangeMin; + return throttleRangeMin + lrintf(range * thrustLinearCompensate((throttle - throttleRangeMin) / range, thrustLinear)); +} + +static NOINLINE int applyThrustLinearization(int motorOutput) +{ + // Not in Betaflight: at or below idle nothing changes, so the failsafe clamp to min_command is not pushed further + if (motorOutput <= throttleRangeMin) { + return motorOutput; + } + + const float range = throttleRangeMax - throttleRangeMin; + return throttleRangeMin + lrintf(range * thrustLinearCurve((motorOutput - throttleRangeMin) / range, thrustLinear)); +} + // Out of line: mixTable() is FAST_CODE and ITCM is nearly full on some F7 targets static NOINLINE void updateMotorThrottle(float dT) { - motorThrottleCommand = applyThrottleBoost(mixerThrottleCommand, dT); + motorThrottleCommand = compensateThrustLinearization(applyThrottleBoost(mixerThrottleCommand, dT)); } void FAST_CODE mixTable(float dT) @@ -930,6 +972,9 @@ void FAST_CODE mixTable(float dT) motor[i] = constrain(motorThrottle, throttleRangeMin, throttleRangeMax); } else { motor[i] = rpyMix[i] + constrain(motorThrottle, throttleMin, throttleMax); + if (thrustLinear) { + motor[i] = applyThrustLinearization(motor[i]); + } } if (failsafeIsActive()) { diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 789d9e8343c..c43d3ca35cc 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -123,6 +123,7 @@ typedef struct motorConfig_s { #endif uint8_t throttleBoost; // must default to 0: it can land in the old tail padding, stored as zero uint8_t throttleBoostCutoff; // Hz + uint8_t thrustLinear; // must default to 0: it can land in the old tail padding, stored as zero } motorConfig_t; PG_DECLARE(motorConfig_t, motorConfig); diff --git a/src/main/flight/thrust_linear.h b/src/main/flight/thrust_linear.h new file mode 100644 index 00000000000..64d665f002a --- /dev/null +++ b/src/main/flight/thrust_linear.h @@ -0,0 +1,33 @@ +/* + * This file is part of INAV. + * + * INAV 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. + * + * INAV 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 INAV. If not, see . + */ + +#pragma once + +// Betaflight 2026.6 (#15226), on 0-1 over the throttle range with e = thrust_linear / 100; sound only for e <= 1 + +// Raises low motor outputs, where thrust grows more slowly than the command +static inline float thrustLinearCurve(float u, float e) +{ + const float inv = 1.0f - u; + return u * (1.0f + e * inv * (1.0f + e * (inv - u))); +} + +// Inverse of the curve to second order in e, applied to the throttle so hover stays where it was +static inline float thrustLinearCompensate(float t, float e) +{ + return t * (1.0f - e * (1.0f - t)); +} diff --git a/src/test/unit/thrust_linear_unittest.cc b/src/test/unit/thrust_linear_unittest.cc new file mode 100644 index 00000000000..3705571721b --- /dev/null +++ b/src/test/unit/thrust_linear_unittest.cc @@ -0,0 +1,88 @@ +/* + * This file is part of INAV. + * + * INAV 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. + * + * INAV 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 INAV. If not, see . + */ + +#include + +extern "C" { + #include "flight/thrust_linear.h" +} + +#include "unittest_macros.h" +#include "gtest/gtest.h" + +// thrust_linear 0-100 in steps of 10 +static float strength(int step) +{ + return step / 10.0f; +} + +TEST(ThrustLinearTest, OffIsIdentity) +{ + for (int i = 0; i <= 100; i++) { + const float x = i / 100.0f; + EXPECT_FLOAT_EQ(thrustLinearCurve(x, 0.0f), x); + EXPECT_FLOAT_EQ(thrustLinearCompensate(x, 0.0f), x); + } +} + +TEST(ThrustLinearTest, MatchesBetaflight) +{ + // Betaflight 2026.6 pidApplyThrustLinearization() / pidCompensateThrustLinearization(), worked by hand + EXPECT_NEAR(thrustLinearCurve(0.3f, 0.5f), 0.426f, 1e-5f); + EXPECT_NEAR(thrustLinearCompensate(0.3f, 0.5f), 0.195f, 1e-5f); + EXPECT_NEAR(thrustLinearCurve(0.8f, 1.0f), 0.864f, 1e-5f); +} + +TEST(ThrustLinearTest, CurveKeepsEndsAndOrder) +{ + for (int s = 0; s <= 10; s++) { + const float e = strength(s); + EXPECT_NEAR(thrustLinearCurve(0.0f, e), 0.0f, 1e-6f); + EXPECT_NEAR(thrustLinearCurve(1.0f, e), 1.0f, 1e-6f); + float previous = 0.0f; + for (int i = 1; i <= 1000; i++) { + const float u = i / 1000.0f; + const float out = thrustLinearCurve(u, e); + EXPECT_GE(out, previous) << "e " << e << " u " << u; + EXPECT_GE(out, u - 1e-6f) << "raises, never lowers: e " << e << " u " << u; + EXPECT_LE(out, 1.0f + 1e-6f) << "e " << e << " u " << u; + previous = out; + } + } +} + +TEST(ThrustLinearTest, CompensationNeverNegative) +{ + for (int s = 0; s <= 10; s++) { + for (int i = 0; i <= 1000; i++) { + EXPECT_GE(thrustLinearCompensate(i / 1000.0f, strength(s)), 0.0f); + } + } +} + +TEST(ThrustLinearTest, HoverStaysWhereItWas) +{ + // Worst cases near 15 % throttle: 0.81 % of the range at 50, 2.5 % at 70, 8.7 % at 100 + for (int s = 0; s <= 10; s++) { + const float e = strength(s); + const float tolerance = e <= 0.5f ? 0.0085f : (e <= 0.7f ? 0.026f : 0.088f); + for (int i = 100; i <= 1000; i++) { + const float t = i / 1000.0f; + EXPECT_NEAR(thrustLinearCurve(thrustLinearCompensate(t, e), e), t, tolerance) << "e " << e << " t " << t; + } + } +} From 0d96c54f9e0720abd9f9863b657304e6c4a1bf1e Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:36:12 +0200 Subject: [PATCH 4/8] mixer: restart the throttle boost after any skipped pass or a cutoff change The low-pass restarted only after 100 ms without the mixer, so a shorter motor stop resumed against the throttle from before the stop, and a cutoff changed while mixing kept the old one. Restart from the current throttle whenever the gap is longer than any mixer period (20 ms; looptime is at most 9 ms) or the cutoff differs from the one in use. --- src/main/flight/mixer.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index aac8e094964..40a64b1430c 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -660,13 +660,15 @@ static int getReversibleMotorsThrottleDeadband(void) // The throttle the motors get; mixerThrottleCommand stays the one OSD, telemetry and servos show static EXTENDED_FASTRAM int motorThrottleCommand; -#define THROTTLE_BOOST_RESTART_MS 100 +// Longer than any mixer period, so a gap this long means the mixer skipped the boost at least once +#define THROTTLE_BOOST_RESTART_US 20000 // Betaflight's throttle boost: a high-passed copy of the throttle on top of it, so the motors follow quick moves sooner static int applyThrottleBoost(int throttle, float dT) { static pt1Filter_t throttleLpf; - static timeMs_t lastBoostMs; + static timeUs_t lastBoostUs; + static uint8_t boostCutoff; // A cutoff of 0 would freeze the low-pass and turn the boost into a constant offset if (!motorConfig()->throttleBoost || !motorConfig()->throttleBoostCutoff || !STATE(MULTIROTOR) || isMixerTransitionMixing @@ -675,17 +677,18 @@ static int applyThrottleBoost(int throttle, float dT) || LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_THROTTLE) #endif ) { - lastBoostMs = 0; + lastBoostUs = 0; return throttle; } // mixTable() does not get here while disarmed or with the motors stopped: restart from the current throttle - const timeMs_t nowMs = millis(); - if (nowMs - lastBoostMs > THROTTLE_BOOST_RESTART_MS) { - pt1FilterInit(&throttleLpf, motorConfig()->throttleBoostCutoff, dT); + const timeUs_t nowUs = micros(); + if (nowUs - lastBoostUs > THROTTLE_BOOST_RESTART_US || boostCutoff != motorConfig()->throttleBoostCutoff) { + boostCutoff = motorConfig()->throttleBoostCutoff; + pt1FilterInit(&throttleLpf, boostCutoff, dT); pt1FilterReset(&throttleLpf, throttle); } - lastBoostMs = nowMs; + lastBoostUs = nowUs; const float throttleHpf = throttle - pt1FilterApply3(&throttleLpf, throttle, dT); return constrain(throttle + lrintf(motorConfig()->throttleBoost * 0.1f * throttleHpf), throttleRangeMin, throttleRangeMax); From 8d50e9f949800b716c81d5deeb5ace268d64918f Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:36:27 +0200 Subject: [PATCH 5/8] mixer: thrust linearization only with a throttle weight of 1 on every motor INAV multiplies the absolute throttle command by each motor's mixer weight, so the throttle compensation, worked out on the common throttle, is not the inverse of the curve for any other weight: with 0.8, a 1500 command and thrust_linear 100, a motor went from 1200 down to idle. The standard multirotor mixes all use 1; with any other weight the feature stays off. --- docs/Settings.md | 2 +- src/main/fc/settings.yaml | 2 +- src/main/flight/mixer.c | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/Settings.md b/docs/Settings.md index 73a24bd44ee..e6cc2c61eed 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7295,7 +7295,7 @@ Can be used in ANGLE and HORIZON mode and will automatically boost throttle when ### thrust_linear -Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), with reversible motors and with the logic-condition throttle override. +Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), when any motor's throttle weight in the mixer is not 1, with reversible motors and with the logic-condition throttle override. | Default | Min | Max | | --- | --- | --- | diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 695af388c94..8c6550ff7b3 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -915,7 +915,7 @@ groups: min: 5 max: 50 - name: thrust_linear - description: "Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), with reversible motors and with the logic-condition throttle override." + description: "Multirotor only, ported from Betaflight 2026.6. A propeller's thrust grows faster than linearly with the motor command, so the quad responds less to the same correction at low throttle. This raises low motor outputs and lowers the throttle before the mix by the inverse amount, so hover stays where it was (within 8 us up to 50, about 23 us at 70 and 80 us at 100, at worst near 15 % throttle). The value is about ArduPilot's `MOT_THST_EXPO` times 100. Only the motors get the change: OSD, telemetry and servos show the throttle that was asked for. 0 disables it. Above about 70 check `nav_mc_hover_thr` again. Betaflight allows up to 150, but its formulas drive the throttle negative above 100 and stop being monotonic from about 130. Off on VTOL setups (any mixer profile that is not a multirotor), when any motor's throttle weight in the mixer is not 1, with reversible motors and with the logic-condition throttle override." default_value: 0 field: thrustLinear min: 0 diff --git a/src/main/flight/mixer.c b/src/main/flight/mixer.c index f19a2364548..ccd79ebd8ec 100644 --- a/src/main/flight/mixer.c +++ b/src/main/flight/mixer.c @@ -706,6 +706,12 @@ static bool isThrustLinearizationAllowed(void) return false; } } + // INAV scales the absolute command by the weight: with any other weight this is not the curve's inverse + for (int i = 0; i < motorCount; i++) { + if (currentMixer[i].throttle != 1.0f) { + return false; + } + } return STATE(MULTIROTOR) && !feature(FEATURE_REVERSIBLE_MOTORS) #ifdef USE_PROGRAMMING_FRAMEWORK && !LOGIC_CONDITION_GLOBAL_FLAG(LOGIC_CONDITION_GLOBAL_FLAG_OVERRIDE_THROTTLE) From a21ec129697dbef5c53ffe636431d7cff151d822 Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:01:09 +0200 Subject: [PATCH 6/8] mixer: keep the SRXL2 motor fields on every target The throttle boost fields follow the SRXL2 ones, which existed only where USE_MOTOR_SRXL2 is defined. A target that turned it on or off would then read its stored boost cutoff as the SRXL2 reverse channel, or the other way round, and a cutoff of 0 blocks arming as an invalid setting. With the fields always there the layout is the same everywhere; on targets without SRXL2 they stay zero and unused. --- src/main/flight/mixer.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 789d9e8343c..450174eec7e 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -95,7 +95,7 @@ typedef struct motorConfig_s { uint8_t motorPwmProtocol; uint16_t digitalIdleOffsetValue; uint8_t motorPoleCount; // Magnetic poles in the motors for calculating actual RPM from eRPM provided by ESC telemetry -#ifdef USE_MOTOR_SRXL2 + // On every target, so turning USE_MOTOR_SRXL2 on or off never moves the fields stored after these /* * Appended at the end, but not immediately after motorPoleCount. * @@ -120,7 +120,6 @@ typedef struct motorConfig_s { uint8_t srxl2ReverseChannel; // 1-based aux channel an SRXL2 ESC uses to arm reverse; 0 disables uint8_t srxl2Telemetry; // read ESC telemetry off the SRXL2 link uint8_t srxl2TelemetryRate; // how often to ask the ESC for telemetry, as srxl2TelemetryRate_e -#endif uint8_t throttleBoost; // must default to 0: it can land in the old tail padding, stored as zero uint8_t throttleBoostCutoff; // Hz } motorConfig_t; From 8f333255c705a00c29fadedfb9b01eb2dbe8f03a Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:33:44 +0200 Subject: [PATCH 7/8] blackbox: throttle_boost and throttle_boost_cutoff in the log header As Betaflight writes them, so a log says whether the boost was on and how strong it was. --- src/main/blackbox/blackbox.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/blackbox/blackbox.c b/src/main/blackbox/blackbox.c index fc6b8c11d3b..a8f051f2de6 100644 --- a/src/main/blackbox/blackbox.c +++ b/src/main/blackbox/blackbox.c @@ -2153,6 +2153,8 @@ static bool blackboxWriteSysinfo(void) BLACKBOX_PRINT_HEADER_LINE("serialrx_provider", "%d", rxConfig()->serialrx_provider); BLACKBOX_PRINT_HEADER_LINE("motor_pwm_protocol", "%d", motorConfig()->motorPwmProtocol); BLACKBOX_PRINT_HEADER_LINE("motor_pwm_rate", "%d", getEscUpdateFrequency()); + BLACKBOX_PRINT_HEADER_LINE("throttle_boost", "%d", motorConfig()->throttleBoost); + BLACKBOX_PRINT_HEADER_LINE("throttle_boost_cutoff", "%d", motorConfig()->throttleBoostCutoff); BLACKBOX_PRINT_HEADER_LINE("debug_mode", "%d", systemConfig()->debug_mode); BLACKBOX_PRINT_HEADER_LINE("features", "%d", featureConfig()->enabledFeatures); BLACKBOX_PRINT_HEADER_LINE("waypoints", "%d,%d", getWaypointCount(),isWaypointListValid()); From b2fd306abf7f35d6b25daafb9fc138ec6ea7a256 Mon Sep 17 00:00:00 2001 From: MrScothh <167884257+MrScothh@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:33:59 +0200 Subject: [PATCH 8/8] blackbox: thrust_linear in the log header As Betaflight writes it, so a log says whether the linearization was on and how strong it was. --- src/main/blackbox/blackbox.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/blackbox/blackbox.c b/src/main/blackbox/blackbox.c index a8f051f2de6..348a47085b9 100644 --- a/src/main/blackbox/blackbox.c +++ b/src/main/blackbox/blackbox.c @@ -2155,6 +2155,7 @@ static bool blackboxWriteSysinfo(void) BLACKBOX_PRINT_HEADER_LINE("motor_pwm_rate", "%d", getEscUpdateFrequency()); BLACKBOX_PRINT_HEADER_LINE("throttle_boost", "%d", motorConfig()->throttleBoost); BLACKBOX_PRINT_HEADER_LINE("throttle_boost_cutoff", "%d", motorConfig()->throttleBoostCutoff); + BLACKBOX_PRINT_HEADER_LINE("thrust_linear", "%d", motorConfig()->thrustLinear); BLACKBOX_PRINT_HEADER_LINE("debug_mode", "%d", systemConfig()->debug_mode); BLACKBOX_PRINT_HEADER_LINE("features", "%d", featureConfig()->enabledFeatures); BLACKBOX_PRINT_HEADER_LINE("waypoints", "%d,%d", getWaypointCount(),isWaypointListValid());