Skip to content

mixer: throttle boost, ported from Betaflight - #12120

Open
MrScothh wants to merge 6 commits into
iNavFlight:next-10.1from
MrScothh:feature/throttle-boost
Open

MrScothh wants to merge 6 commits into
iNavFlight:next-10.1from
MrScothh:feature/throttle-boost

Conversation

@MrScothh

@MrScothh MrScothh commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #12118 (afatfs_poll out of ITCM). This one alone would fit, but the thrust linearization PR stacked on it (#12121)
needs that room on KAKUTEF7.

Betaflight's throttle boost, for multirotors. It adds a high-pass filtered copy of the throttle to the throttle, so
the motors follow a quick throttle move sooner. It is off by default.

  • throttle_boost (0-100, default 0): the boost is this value / 10 times the throttle minus its low-pass filtered
    value. Betaflight uses 5 by default.
  • throttle_boost_cutoff (5-50 Hz, default 15): cutoff of that low-pass filter, as in Betaflight.

It works as in Betaflight's mixer.c, on the throttle after throttle_scale and battery compensation, and the
result is kept within the throttle range. Only the motors get the boost: OSD, telemetry and servos keep showing the
throttle that was asked for. It is not applied:

  • while navigation controls the throttle, where it would add to the altitude controller's output;
  • during a VTOL transition;
  • with reversible motors or the logic-condition throttle override.

The filter starts again from the current throttle after any skipped pass:

  • one where the boost itself was skipped (navigation, transition, override);
  • any gap without the mixer longer than 20 ms (disarmed, motors stopped). looptime is at most 9 ms.

It also restarts when the cutoff changes. So arming, leaving motor stop or taking the throttle back from navigation
does not kick the motors.

The math runs out of line, in flash, in one call before the motor loop: mixTable() is FAST_CODE, and ITCM is
almost full on KAKUTEF7.

Settings storage

Both fields are appended to motorConfig_t without a version bump, so nobody's motor settings are reset. pgLoad()
copies the stored bytes over the defaults. The four SRXL2 bytes are now in the struct on every target, so the layout
is the same everywhere and a target that turns USE_MOTOR_SRXL2 on or off keeps reading these fields in place. On
targets without SRXL2 the four bytes stay zero and unused, for 4 B of flash on F7.

  • The old record is 10 bytes, or 14 with USE_MOTOR_SRXL2, whose last byte is tail padding stored as zero.
  • throttle_boost (offset 13) lands in that byte on SRXL2 targets, and past the old record elsewhere; its default is 0.
  • throttle_boost_cutoff (offset 14) starts past the end of any stored record, so it keeps its default of 15, and a
    cutoff of 0 is skipped anyway.

I checked the offsets in the debug info of F7, F405 and H7 builds (16 bytes on all three), and the move between the
two layouts on a Flywoo F722, in the Qodo thread on mixer.h.

Bench

Flywoo F722 Pro V2 (FLYWOOF722PRO image), no battery, so the motors could not spin. I armed it over MSP RC with a
QUADX mixer and stepped the throttle 1200 -> 1600 -> 1200 -> 1800 -> 1200, logging the blackbox at 1 kHz:

throttle_boost mean motor output minus rcCommand throttle, max / min 90 % of a 400 us step at steady throttle
0 +1 / 0 20-22 ms 0
5 +103 / -103 12-14 ms 0
20 +410 / -378 (clipped at idle) 3-5 ms 0

The boost is linear in the setting: 10 ms into the same step the motors are 69 over the throttle with 5 and 276
with 20. With 0 the motors get exactly the throttle, as before.

Size, over #12118

Flash +252 to +396 B, and 4 B more on F7 for the SRXL2 bytes now kept on every target (MATEKF722SE +280 B,
98.59 %), RAM +24 to +40 B. ITCM +16 B on F722 targets, +24 B on
MATEKH743, +64 to +72 B on KAKUTEF7/KAKUTEF7HDV (15 360 B, 93.75 %). All F405 targets build with both mixer PRs; the
fullest RAM is SYNERDUINOSTM_F405 at 84.24 %, and the two add 24 B of RAM and 8 B of CCM.

Testing

The unit tests pass. Not flown. Flight tests on a quad would be welcome, comparing 0 and 5 on punch-outs. The values to look at are
throttle_boost and, if needed, throttle_boost_cutoff.

Log header

The blackbox header now carries throttle_boost and throttle_boost_cutoff, under the names Betaflight uses, so a
log shows whether the boost was on and how strong it was (8f33325). MAMBAF722_2022B, a log from boot with the boost
at 50 and the cutoff at 20: H throttle_boost:50, H throttle_boost_cutoff:20. +64 B of flash on MATEKF722SE
(98.60 %). blackbox_decode reads the log as before; the Blackbox Explorer's parser lists the new lines
with the other headers it does not use, such as motor_pwm_rate and acc_notch_hz.

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.
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.
@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add optional multirotor throttle boost and preserve ITCM space

✨ Enhancement ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add configurable throttle boost so multirotor motors respond sooner to rapid throttle changes.
• Keep boost out of navigation, transitions and overrides; leave displayed throttle unchanged.
• Keep filesystem polling out of ITCM to make room for mixer changes.
Diagram

graph TD
  INPUT["Scaled throttle"] --> GATE{"Boost eligible?"} -->|Yes| FILTER["PT1 boost"] --> MOTORS["Motor mix"]
  GATE -->|No| MOTORS
  INPUT --> DISPLAY["OSD telemetry servos"]
  SETTINGS["Motor settings"] --> GATE
  SETTINGS --> FILTER
Loading
High-Level Assessment

The motor-only throttle path is appropriate: modifying the shared throttle command would also change servo and displayed values. Appending settings without a version bump preserves existing motor configuration, while out-of-line calls limit ITCM use; resetting the parameter group or putting boost directly in the fast mixer path would undermine those goals.

Files changed (5) +81 / -2

Enhancement (1) +45 / -1
mixer.cApply bounded throttle boost to motor mixing +45/-1

Apply bounded throttle boost to motor mixing

• Adds a PT1-filtered high-pass throttle component for eligible multirotor motor output while preserving the shared throttle command. Skips boost for navigation control, transitions, reversible motors and throttle overrides, and restarts the filter after skipped or interrupted mixing. Calls the boost calculation out of line to limit ITCM use.

src/main/flight/mixer.c

Documentation (1) +20 / -0
Settings.mdDocument throttle boost controls +20/-0

Document throttle boost controls

• Documents boost strength and filter cutoff, including defaults, ranges, motor-only behavior and conditions that disable boost.

docs/Settings.md

Other (3) +16 / -1
settings.yamlExpose boost strength and cutoff settings +12/-0

Expose boost strength and cutoff settings

• Registers throttle_boost (0–100, default 0) and throttle_boost_cutoff (5–50 Hz, default 15) against the new motor configuration fields.

src/main/fc/settings.yaml

mixer.hAppend boost fields to motor configuration +2/-0

Append boost fields to motor configuration

• Adds strength and cutoff bytes to motorConfig_t. The disabled-by-default strength can occupy previously stored zero tail padding, while the cutoff retains its reset default for older records.

src/main/flight/mixer.h

asyncfatfs.cKeep filesystem polling out of ITCM +2/-1

Keep filesystem polling out of ITCM

• Marks afatfs_poll NOINLINE so link-time optimization cannot fold its filesystem state machine into the FAST_CODE scheduler callback path, preserving ITCM space.

src/main/io/asyncfatfs/asyncfatfs.c

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Restored motor settings can block arming ✓ Resolved
Description
The new throttleBoostCutoff field occupies the byte used by srxl2ReverseChannel in a
configuration saved by an SRXL2 build, while the motor parameter group keeps the same version.
Restoring that configuration on a build without SRXL2 copies the reverse-channel value into the
cutoff; a value of zero fails settings validation and disables arming, while a value of 5–9 silently
replaces the 15 Hz default.
Code

src/main/flight/mixer.h[125]

+    uint8_t throttleBoostCutoff;            // Hz
Evidence
The conditional SRXL2 fields precede the appended fields, so their layouts differ. The loader checks
only version and copies the stored prefix; validation rejects cutoffs below five and marks settings
invalid.

src/main/flight/mixer.h[115-125]
src/main/flight/mixer.c[92-105]
src/main/config/parameter_group.c[86-94]
src/main/fc/settings.yaml[897-915]
src/main/fc/config.c[312-317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An SRXL2 motor-config record is copied into a different non-SRXL2 layout without a version change, placing its reverse-channel byte in the new cutoff field.
## Fix Focus Areas
- src/main/flight/mixer.h[115-125]
- src/main/config/parameter_group.c[86-94]
- src/main/flight/mixer.c[92-105]
## Recommended Fix
Add a size-aware migration for this motor configuration before raw bytes are loaded, preserving existing motor settings while initializing both new fields to their defaults when the stored record has the other layout. Cover SRXL2-to-non-SRXL2 restores, including reverse channel zero.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Brief motor stops can cause a throttle jump ✓ Resolved
Description
applyThrottleBoost() resets its low-pass state only when more than 100 ms have elapsed since its
last call, but mixTable() returns without updating that state while motors are stopped. If mixing
resumes within 100 ms and the requested throttle has changed, the first motor output includes a
boost calculated against the pre-stop throttle.
Code

src/main/flight/mixer.c[R684-687]

+    if (nowMs - lastBoostMs > THROTTLE_BOOST_RESTART_MS) {
+        pt1FilterInit(&throttleLpf, motorConfig()->throttleBoostCutoff, dT);
+        pt1FilterReset(&throttleLpf, throttle);
+    }
Evidence
The stop path returns before the new update call. With no reset inside the 100 ms window, the
high-pass calculation subtracts the retained low-pass state from the new throttle.

src/main/flight/mixer.c[711-723]
src/main/flight/mixer.c[682-691]
src/main/flight/mixer.c[872-872]
src/main/flight/mixer.c[1020-1051]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A brief disarm or motor stop bypasses the boost function, leaving filter state that can affect the first output when motors run again.
## Fix Focus Areas
- src/main/flight/mixer.c[711-723]
- src/main/flight/mixer.c[668-688]
## Recommended Fix
Invalidate the boost filter on the early-return motor-stop and disarm paths, and reset it from the current throttle on the next active mix regardless of elapsed time. Retain the timeout for other gaps between mixer calls.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Cutoff edits leave the old boost response ✓ Resolved
Description
applyThrottleBoost() passes throttleBoostCutoff to pt1FilterInit() only after a restart gap,
and subsequent filter updates reuse the stored time constant. Changing the cutoff through the
settings command during continuous mixing therefore changes the displayed setting but not the motor
response until mixing pauses or boost is skipped.
Code

src/main/flight/mixer.c[R684-686]

+    if (nowMs - lastBoostMs > THROTTLE_BOOST_RESTART_MS) {
+        pt1FilterInit(&throttleLpf, motorConfig()->throttleBoostCutoff, dT);
+        pt1FilterReset(&throttleLpf, throttle);
Evidence
Initialization calculates the filter's RC from the cutoff, whereas pt1FilterApply3() recalculates
alpha using that stored RC. The settings command writes the setting directly and does not
reinitialize the filter.

src/main/flight/mixer.c[684-690]
src/main/common/filter.c[53-72]
src/main/fc/fc_msp.c[4212-4240]
src/main/fc/fc_msp.c[4833-4840]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changing the configured cutoff while mixing continues leaves the PT1 filter using its previously initialized cutoff.
## Fix Focus Areas
- src/main/flight/mixer.c[668-690]
- src/main/common/filter.c[53-85]
## Recommended Fix
Track the cutoff used by the filter and call `pt1FilterSetCutoff()` when the configuration changes, or reinitialize and reset from current throttle on a cutoff change. Preserve the existing restart behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can import rules and skills only from the repositories your team follows

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/flight/mixer.h
Comment thread src/main/flight/mixer.c Outdated
Comment thread src/main/flight/mixer.c Outdated
…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.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 13ebed9 — commit 295adc8

Target Flash Δ RAM Δ
MATEKF405 +528 B (+0.07%) CCM: +4 B (+0.01%)
RAM: +32 B (+0.03%)
MATEKF722 +368 B (+0.08%) ITCM_RAM: +16 B (+0.14%)
RAM: +48 B (+0.06%)
TCM: +4 B (+0.02%)
MATEKF765 +552 B (+0.07%) DTCM_RAM: +4 B (+0.01%)
SRAM1: +48 B (+0.04%)
MATEKH743 +412 B (+0.05%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: ±0 B (±0.00%)
ITCM_RAM: -8 B (-0.05%)
RAM: +32 B (+0.02%)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 295adc8

Download firmware for PR #12120

251 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

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.
As Betaflight writes them, so a log says whether the boost was on and
how strong it was.
# Conflicts:
#	src/main/io/asyncfatfs/asyncfatfs.c
@sensei-hacker sensei-hacker added this to the 10.1 milestone Oct 27, 2026
@sensei-hacker
sensei-hacker changed the base branch from maintenance-10.x to next-10.1 October 27, 2026 01:46
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.

2 participants