Skip to content

Add BUZZER as a runtime-assignable timer output mode#11675

Open
sensei-hacker wants to merge 3 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature-buzzer-unified-output
Open

Add BUZZER as a runtime-assignable timer output mode#11675
sensei-hacker wants to merge 3 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature-buzzer-unified-output

Conversation

@sensei-hacker

Copy link
Copy Markdown
Member

Summary

Extends the unified PWM/PINIO output system (#11375, #11564) so any timer-capable pad can be assigned as a PWM buzzer at runtime via the output assignment UI, instead of being fixed to the compile-time BEEPER pin in target.h. Companion firmware PR to inav-configurator#2654.

Changes

  • flight/mixer.h — add OUTPUT_MODE_BEEPER to outputMode_e
  • drivers/pwm_mapping.c / pwm_mapping.htimerHardwareOverride() sets TIM_USE_BEEPER and clears conflicting flags when OUTPUT_MODE_BEEPER is assigned, preventing double-allocation; also removes the guard that permanently locked compile-time TIM_USE_BEEPER pads out of reassignment (clearing TIM_USE_BEEPER from all non-beeper cases so a reassigned pad can't carry both flags)
  • drivers/sound_beeper.cbeeperInit() scans timerOverrides for OUTPUT_MODE_BEEPER first; if found, initializes that pad as PWM beeper and returns. Falls back to compile-time #define BEEPER + existing behavior when no runtime assignment is present. Skips the compile-time fallback when a compile-time beeper pad has been reassigned away from TIM_USE_BEEPER.
  • fc/fc_msp.cMSP2_INAV_OUTPUT_ASSIGNMENT / MSP2_INAV_QUERY_OUTPUT_ASSIGNMENT: replace the literal OUTPUT_ASSIGNMENT_TYPE_MOTOR/SERVO numbering with __builtin_ctz(TIM_USE_*) bit-index values, and extend the handler to also report LED/BEEPER/PINIO direct assignments (previously motor/servo only). This aligns the wire type byte with the TIM_USE_* bit-index constants already used in the JS configurator.
  • fc/cli.ctimer_output_mode CLI command: add BEEPER as a valid mode name

Backward compatibility

Targets with existing #define BEEPER + old-style compile-time PWM beeper config continue to work unchanged when no runtime BUZZER assignment exists — runtime assignment only takes precedence when explicitly set.

Note on the MSP type-byte change

This changes the type byte encoding for the existing MOTOR/SERVO entries too (from literal 1/2 to bit-index 2/3), not just adding new types. inav-configurator#2654 was written against this new encoding. Both target maintenance-10.x/INAV 10.0 (unreleased), so there's no production population affected — but the two PRs should land close together to avoid a stale Output Mapping table for anyone testing maintenance-10.x configurator against maintenance-10.x firmware in the gap between merges.

Testing

  • Builds clean on MATEKF405 and BROTHERHOBBYH743 (no warnings)
  • Two rounds of code review (inav-code-review agent) — critical/important findings addressed
  • Hardware: firmware built from this branch flashed to a BrotherHobby H743 via DFU. test-engineer then verified live MSP/DOM state against the running FC — Output Mapping table correctly reports the buzzer pad as S14/Buzzer, timer dropdown pre-selects BEEPER, function row shows Buzzer, and Motor 1–4 assignments are unaffected. This confirms the runtime assignment mechanism and MSP reporting are correct end-to-end on real hardware.
  • Not verified this session: audible confirmation that the buzzer physically sounds (arming beep / tones) through the reassigned pad — the hardware test above confirmed correct MSP/UI state, not audio output. Recommend a reviewer/tester confirm audible buzzer output before merge.

Related

Extends the unified PWM/PINIO output system so any timer-capable pad can
be assigned as a PWM buzzer at runtime via the output assignment UI, instead
of being fixed to the compile-time BEEPER pin in target.h.

- Add OUTPUT_MODE_BEEPER to outputMode_e enum (mixer.h)
- timerHardwareOverride(): set TIM_USE_BEEPER and clear conflicting flags
  when OUTPUT_MODE_BEEPER is assigned, preventing double-allocation
- beeperInit(): scan timerOverrides for OUTPUT_MODE_BEEPER first; if found,
  call beeperPwmInit() on that pad and return — falls back to compile-time
  BEEPER pin when no runtime assignment is present
- MSP2_INAV_OUTPUT_ASSIGNMENT / MSP2_INAV_QUERY_OUTPUT_ASSIGNMENT: report
  the runtime-assigned buzzer pad as OUTPUT_ASSIGNMENT_TYPE_BUZZER (3)
- CLI timer_output_mode: add BEEPER as a valid mode name
…NT type byte

Replace the separate OUTPUT_ASSIGNMENT_TYPE_* numbering system with
__builtin_ctz(TIM_USE_*) so the type byte in the 3-tuple response matches
the TIM_USE_* bit-index constants already defined in the JS configurator.
This aligns firmware and configurator on one consistent system rather than
two parallel sets of constants.
Remove the early-return guard that permanently locked TIM_USE_BEEPER pads
out of timerHardwareOverride(). Add TIM_USE_BEEPER to the clear masks of
all non-beeper cases so a reassigned pad cannot carry both flags at once.
In beeperInit(), skip the compile-time fallback when the pad's post-override
usageFlags no longer have TIM_USE_BEEPER set.

Compile-time beeper pads now default to beeper (AUTO mode = no-op) but can
be overridden to motor/servo/etc. like any other timer-capable pad.
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add BUZZER as a runtime-assignable timer output mode

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Add BEEPER as a selectable timer output mode for unified output assignment.
• Initialize PWM beeper from runtime overrides, with safe compile-time fallback.
• Align MSP2 output-assignment type encoding to TIM_USE_* bit indices.
Diagram

graph TD
  UI{{"Configurator / UI"}} --> MSP["MSP2 output assignment/query"] --> OV["timerOverrides (outputMode)"]
  OV --> MAP["pwm_mapping override"] --> HW["timerHardware usageFlags"]
  OV --> BEEP["beeperInit PWM selection"] --> OUT["PWM buzzer pad"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep legacy MSP type IDs (1/2) and add new values for extras
  • ➕ Avoids breaking existing configurators that assume MOTOR=1/SERVO=2
  • ➕ Smaller firmware/configurator coordination risk during rollout
  • ➖ Still leaves two parallel constant systems (type IDs vs TIM_USE bit indices)
  • ➖ Requires ongoing mapping/maintenance in both firmware and configurator
2. Negotiate encoding via MSP capability/version flag
  • ➕ Allows firmware and configurator to support both encodings safely
  • ➕ Enables gradual migration without a tight merge dependency
  • ➖ More implementation work (capability reporting + dual decode/encode paths)
  • ➖ More complexity to test across permutations
3. Add a second byte for TIM_USE bit index while preserving existing type byte
  • ➕ Fully backward compatible while enabling unified constants
  • ➕ Easier debugging (both representations available)
  • ➖ Payload format change and extra bytes
  • ➖ Still requires configurator changes to consume the new field

Recommendation: The PR’s approach (using _builtin_ctz(TIM_USE*) as the wire type byte) is clean because it eliminates the parallel numbering system and matches the configurator’s existing bit-index constants. Given this is targeting an unreleased branch, it’s reasonable—however, it increases coordination risk. If there’s any chance of mixed firmware/configurator versions in testing, consider adding a minimal capability/version guard so both encodings can be supported during the transition.

Files changed (6) +60 / -19

Enhancement (4) +53 / -13
pwm_mapping.cAllow timer overrides to assign and reassign TIM_USE_BEEPER safely +8/-8

Allow timer overrides to assign and reassign TIM_USE_BEEPER safely

• Adds OUTPUT_MODE_BEEPER handling to timerHardwareOverride() and clears conflicting TIM_USE_* flags when switching modes. Removes the previous implicit lock-out so compile-time beeper pads can be reassigned without retaining TIM_USE_BEEPER.

src/main/drivers/pwm_mapping.c

sound_beeper.cPrefer runtime-assigned OUTPUT_MODE_BEEPER pad in beeperInit() +23/-0

Prefer runtime-assigned OUTPUT_MODE_BEEPER pad in beeperInit()

• Scans timerOverrides for OUTPUT_MODE_BEEPER and initializes PWM beeper on the matched pad, returning early. Skips compile-time BEEPER fallback when the original beeper pad has been overridden away from TIM_USE_BEEPER.

src/main/drivers/sound_beeper.c

fc_msp.cAlign MSP2 output-assignment type byte to TIM_USE_* bit indices and report buzzer +20/-4

Align MSP2 output-assignment type byte to TIM_USE_* bit indices and report buzzer

• Replaces legacy MOTOR/SERVO type IDs with __builtin_ctz(TIM_USE_MOTOR/SERVO) and extends both OUTPUT_ASSIGNMENT and QUERY_OUTPUT_ASSIGNMENT to emit a BEEPER entry when OUTPUT_MODE_BEEPER is present. This makes the MSP type byte consistent with configurator-side TIM_USE_* bit-index constants.

src/main/fc/fc_msp.c

mixer.hAdd OUTPUT_MODE_BEEPER to outputMode_e +2/-1

Add OUTPUT_MODE_BEEPER to outputMode_e

• Extends the outputMode_e enum to include OUTPUT_MODE_BEEPER for runtime assignment and CLI/MSP exposure.

src/main/flight/mixer.h

Refactor (1) +3 / -5
pwm_mapping.hDocument MSP output-assignment type byte as TIM_USE_* bit index +3/-5

Document MSP output-assignment type byte as TIM_USE_* bit index

• Removes the legacy OUTPUT_ASSIGNMENT_TYPE_MOTOR/SERVO defines and replaces them with guidance to derive the MSP type byte from TIM_USE_* via __builtin_ctz().

src/main/drivers/pwm_mapping.h

Other (1) +4 / -1
cli.cAdd BEEPER to timer_output_mode CLI parsing and help text +4/-1

Add BEEPER to timer_output_mode CLI parsing and help text

• Extends outputModeNames and cliTimerOutputMode() parsing to accept BEEPER. Updates the CLI command help string to include the new mode.

src/main/fc/cli.c

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Wrong beeper pad chosen 🐞 Bug ≡ Correctness
Description
beeperInit() selects the first timerHardware entry whose *timer* override is OUTPUT_MODE_BEEPER and
initializes PWM beeper on that entry’s tag, so on timers with multiple exposed channels it can
initialize/report the buzzer on the wrong pad and the buzzer may not sound on the user-intended pin.
MSP2_INAV_OUTPUT_ASSIGNMENT mirrors this by also reporting only the first matching timerHardware
index, potentially mislabeling the buzzer output in the UI.
Code

src/main/drivers/sound_beeper.c[R84-91]

+    for (int idx = 0; idx < timerHardwareCount; idx++) {
+        const timerHardware_t *timHw = &timerHardware[idx];
+        if (timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER) {
+            beeperPwmInit(timHw->tag, BEEPER_PWM_FREQUENCY);
+            beeperConfigMutable()->pwmMode = true;
+            systemBeep(false);
+            return;
+        }
Evidence
Overrides are per-timer (not per channel), and the new loop breaks on the first timerHardware entry
whose timer matches OUTPUT_MODE_BEEPER; on targets where a timer has multiple outputs this can
select a different pad than intended. MSP reporting repeats the same first-match behavior, so the UI
can also point at the wrong pad.

src/main/drivers/sound_beeper.c[81-91]
src/main/drivers/timer.c[42-49]
src/main/target/BROTHERHOBBYH743/target.c[32-39]
src/main/fc/fc_msp.c[1823-1830]

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

## Issue description
Runtime buzzer selection is currently keyed off `timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER`, but `timerOverrides` is per **timer instance** (TIMx), not per **timer channel/pad**. On timers with multiple pads, this makes the chosen buzzer pad dependent on `timerHardware[]` order, which can differ from the pad the user expects.

## Issue Context
- `timer2id()` maps only by `tim` pointer, so all channels on the same timer share one override.
- `beeperInit()` breaks at the first `timerHardware[]` entry whose timer is in `OUTPUT_MODE_BEEPER`.
- MSP2 output assignment similarly reports only the first matching hardware index.

## Fix Focus Areas
- src/main/drivers/sound_beeper.c[81-103]
- src/main/fc/fc_msp.c[1823-1830]

### Implementation direction
- Introduce a way to select the beeper **pad** (timerHardware index or ioTag) rather than only selecting the timer:
 - Option A (best): add a small config field (PG) like `ioTag_t runtimeBeeperTag` set by MSP/CLI/UI, and use that tag for `beeperPwmInit()`.
 - Option B (minimal guard): if `OUTPUT_MODE_BEEPER` is set on a timer with multiple `timerHardware[]` entries, either:
   - refuse it and log an error, or
   - pick a deterministic channel (e.g., prefer the one that was compile-time BEEPER tag if present; otherwise require explicit tag).
- Update MSP2 output-assignment reporting to match the selected beeper tag/index (and avoid reporting a misleading first entry).

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



Remediation recommended

2. PWM beeper init unchecked 🐞 Bug ☼ Reliability
Description
In the runtime-assigned beeper path, beeperInit() sets pwmMode=true and returns without verifying
that beeperPwmInit() succeeded; if PWM allocation fails, pwmWriteBeeper() becomes a no-op and the
compile-time beeper fallback is skipped, disabling beeps silently. This creates a brittle failure
mode when timer channel allocation fails (e.g., no TCH available).
Code

src/main/drivers/sound_beeper.c[R86-91]

+        if (timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER) {
+            beeperPwmInit(timHw->tag, BEEPER_PWM_FREQUENCY);
+            beeperConfigMutable()->pwmMode = true;
+            systemBeep(false);
+            return;
+        }
Evidence
The new runtime path returns immediately after setting pwmMode without any success check. The PWM
backend can remain NULL because beeperPwmInit() may early-return when timerGetTCH() fails;
pwmWriteBeeper() then drops all beeps silently.

src/main/drivers/sound_beeper.c[81-91]
src/main/drivers/pwm_output.c[755-784]

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

## Issue description
The runtime `OUTPUT_MODE_BEEPER` path unconditionally commits to PWM mode (`beeperConfigMutable()->pwmMode = true`) and returns after calling `beeperPwmInit()`, but `beeperPwmInit()` can fail and leave the beeper backend uninitialized. In that case `pwmWriteBeeper()` no-ops and there is no fallback.

## Issue Context
- `beeperPwmInit()` can return early when `timerGetTCH()` fails.
- `pwmWriteBeeper()` returns immediately when `beeperPwm == NULL`.

## Fix Focus Areas
- src/main/drivers/sound_beeper.c[81-91]

### Implementation direction
- Make `beeperPwmInit()` report success (e.g., return `bool`), or add a small getter like `bool beeperPwmIsInitialized(void)`.
- In the runtime assignment path:
 - only set `pwmMode=true` and `return` if initialization succeeded;
 - otherwise continue into the existing compile-time beeper initialization path (or log an error and keep beeper in GPIO mode if possible).

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


Grey Divider

Qodo Logo

Comment on lines +84 to +91
for (int idx = 0; idx < timerHardwareCount; idx++) {
const timerHardware_t *timHw = &timerHardware[idx];
if (timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER) {
beeperPwmInit(timHw->tag, BEEPER_PWM_FREQUENCY);
beeperConfigMutable()->pwmMode = true;
systemBeep(false);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Wrong beeper pad chosen 🐞 Bug ≡ Correctness

beeperInit() selects the first timerHardware entry whose *timer* override is OUTPUT_MODE_BEEPER and
initializes PWM beeper on that entry’s tag, so on timers with multiple exposed channels it can
initialize/report the buzzer on the wrong pad and the buzzer may not sound on the user-intended pin.
MSP2_INAV_OUTPUT_ASSIGNMENT mirrors this by also reporting only the first matching timerHardware
index, potentially mislabeling the buzzer output in the UI.
Agent Prompt
## Issue description
Runtime buzzer selection is currently keyed off `timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER`, but `timerOverrides` is per **timer instance** (TIMx), not per **timer channel/pad**. On timers with multiple pads, this makes the chosen buzzer pad dependent on `timerHardware[]` order, which can differ from the pad the user expects.

## Issue Context
- `timer2id()` maps only by `tim` pointer, so all channels on the same timer share one override.
- `beeperInit()` breaks at the first `timerHardware[]` entry whose timer is in `OUTPUT_MODE_BEEPER`.
- MSP2 output assignment similarly reports only the first matching hardware index.

## Fix Focus Areas
- src/main/drivers/sound_beeper.c[81-103]
- src/main/fc/fc_msp.c[1823-1830]

### Implementation direction
- Introduce a way to select the beeper **pad** (timerHardware index or ioTag) rather than only selecting the timer:
  - Option A (best): add a small config field (PG) like `ioTag_t runtimeBeeperTag` set by MSP/CLI/UI, and use that tag for `beeperPwmInit()`.
  - Option B (minimal guard): if `OUTPUT_MODE_BEEPER` is set on a timer with multiple `timerHardware[]` entries, either:
    - refuse it and log an error, or
    - pick a deterministic channel (e.g., prefer the one that was compile-time BEEPER tag if present; otherwise require explicit tag).
- Update MSP2 output-assignment reporting to match the selected beeper tag/index (and avoid reporting a misleading first entry).

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

Comment on lines +86 to +91
if (timerOverrides(timer2id(timHw->tim))->outputMode == OUTPUT_MODE_BEEPER) {
beeperPwmInit(timHw->tag, BEEPER_PWM_FREQUENCY);
beeperConfigMutable()->pwmMode = true;
systemBeep(false);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Pwm beeper init unchecked 🐞 Bug ☼ Reliability

In the runtime-assigned beeper path, beeperInit() sets pwmMode=true and returns without verifying
that beeperPwmInit() succeeded; if PWM allocation fails, pwmWriteBeeper() becomes a no-op and the
compile-time beeper fallback is skipped, disabling beeps silently. This creates a brittle failure
mode when timer channel allocation fails (e.g., no TCH available).
Agent Prompt
## Issue description
The runtime `OUTPUT_MODE_BEEPER` path unconditionally commits to PWM mode (`beeperConfigMutable()->pwmMode = true`) and returns after calling `beeperPwmInit()`, but `beeperPwmInit()` can fail and leave the beeper backend uninitialized. In that case `pwmWriteBeeper()` no-ops and there is no fallback.

## Issue Context
- `beeperPwmInit()` can return early when `timerGetTCH()` fails.
- `pwmWriteBeeper()` returns immediately when `beeperPwm == NULL`.

## Fix Focus Areas
- src/main/drivers/sound_beeper.c[81-91]

### Implementation direction
- Make `beeperPwmInit()` report success (e.g., return `bool`), or add a small getter like `bool beeperPwmIsInitialized(void)`.
- In the runtime assignment path:
  - only set `pwmMode=true` and `return` if initialization succeeded;
  - otherwise continue into the existing compile-time beeper initialization path (or log an error and keep beeper in GPIO mode if possible).

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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Test firmware build ready — commit 49148fc

Download firmware for PR #11675

238 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant