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/5] 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/5] 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 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 3/5] 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 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 4/5] 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 5/5] 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());