From 34044cc4c4a10d784d09a93c551e9a09269bec90 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 7 Jun 2026 15:03:25 -0500 Subject: [PATCH 1/4] Add BUZZER as a runtime-assignable timer output mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main/drivers/pwm_mapping.c | 4 ++++ src/main/drivers/pwm_mapping.h | 5 +++-- src/main/drivers/sound_beeper.c | 13 +++++++++++++ src/main/fc/cli.c | 5 ++++- src/main/fc/fc_msp.c | 16 ++++++++++++++++ src/main/flight/mixer.h | 3 ++- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/main/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index a0239642abb..1a5d5f56cd8 100644 --- a/src/main/drivers/pwm_mapping.c +++ b/src/main/drivers/pwm_mapping.c @@ -233,6 +233,10 @@ static void timerHardwareOverride(timerHardware_t * timer) { timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED); timer->usageFlags |= TIM_USE_PINIO; break; + case OUTPUT_MODE_BEEPER: + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED|TIM_USE_PINIO); + timer->usageFlags |= TIM_USE_BEEPER; + break; } } diff --git a/src/main/drivers/pwm_mapping.h b/src/main/drivers/pwm_mapping.h index 4a6722046c5..3212375c247 100644 --- a/src/main/drivers/pwm_mapping.h +++ b/src/main/drivers/pwm_mapping.h @@ -91,8 +91,9 @@ typedef struct { // Output assignment types for MSP2_INAV_OUTPUT_ASSIGNMENT response // LED outputs are not reported here; they are already identified by TIM_USE_LED // in the MSP2_INAV_OUTPUT_MAPPING_EXT2 usageFlags response. -#define OUTPUT_ASSIGNMENT_TYPE_MOTOR 1 -#define OUTPUT_ASSIGNMENT_TYPE_SERVO 2 +#define OUTPUT_ASSIGNMENT_TYPE_MOTOR 1 +#define OUTPUT_ASSIGNMENT_TYPE_SERVO 2 +#define OUTPUT_ASSIGNMENT_TYPE_BUZZER 3 #endif // SITL_BUILD bool pwmMotorAndServoInit(void); diff --git a/src/main/drivers/sound_beeper.c b/src/main/drivers/sound_beeper.c index bc76117d180..8bfe179bcf1 100644 --- a/src/main/drivers/sound_beeper.c +++ b/src/main/drivers/sound_beeper.c @@ -78,6 +78,19 @@ void beeperInit(const beeperDevConfig_t *config) #if !defined(BEEPER) UNUSED(config); #else + // Runtime output assignment takes precedence over the compile-time BEEPER pin. + // pwmBuildTimerOutputList() runs before beeperInit(), so TIM_USE_BEEPER is already + // set on the runtime-assigned pad by the time we get here. + 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; + } + } + beeperIO = IOGetByTag(config->ioTag); beeperInverted = config->isInverted; diff --git a/src/main/fc/cli.c b/src/main/fc/cli.c index 27713297c86..8ed295c2d3f 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -177,6 +177,7 @@ static const char * outputModeNames[] = { "SERVOS", "LED", "PINIO", + "BEEPER", NULL }; @@ -3225,6 +3226,8 @@ static void cliTimerOutputMode(char *cmdline) mode = OUTPUT_MODE_LED; } else if(!sl_strcasecmp("PINIO", tok)) { mode = OUTPUT_MODE_PINIO; + } else if(!sl_strcasecmp("BEEPER", tok)) { + mode = OUTPUT_MODE_BEEPER; } else { cliShowParseError(); return; @@ -5037,7 +5040,7 @@ const clicmd_t cmdTable[] = { #ifdef USE_OSD CLI_COMMAND_DEF("osd_layout", "get or set the layout of OSD items", "[ [ [ []]]]", cliOsdLayout), #endif - CLI_COMMAND_DEF("timer_output_mode", "get or set the outputmode for a given timer.", "[ []]", cliTimerOutputMode), + CLI_COMMAND_DEF("timer_output_mode", "get or set the outputmode for a given timer.", "[ []]", cliTimerOutputMode), }; static void cliHelp(char *cmdline) diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index e5cfe7e81e8..5afe2f9247a 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -1820,6 +1820,14 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_SERVO); sbufWriteU8(dst, (uint8_t)(s + 1)); } + for (int idx = 0; idx < timerHardwareCount; idx++) { + if (timerOverrides(timer2id(timerHardware[idx].tim))->outputMode == OUTPUT_MODE_BEEPER) { + sbufWriteU8(dst, (uint8_t)idx); + sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_BUZZER); + sbufWriteU8(dst, 1); + break; + } + } } break; #endif @@ -4919,6 +4927,14 @@ bool mspFCProcessInOutCommand(uint16_t cmdMSP, sbuf_t *dst, sbuf_t *src, mspResu sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_SERVO); sbufWriteU8(dst, (uint8_t)(s + 1)); } + for (int idx = 0; idx < timerHardwareCount; idx++) { + if (proposedModes[timer2id(timerHardware[idx].tim)] == OUTPUT_MODE_BEEPER) { + sbufWriteU8(dst, (uint8_t)idx); + sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_BUZZER); + sbufWriteU8(dst, 1); + break; + } + } *ret = MSP_RESULT_ACK; } break; diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 6c4370d4176..91f40406e6f 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -49,7 +49,8 @@ typedef enum { OUTPUT_MODE_MOTORS, OUTPUT_MODE_SERVOS, OUTPUT_MODE_LED, - OUTPUT_MODE_PINIO + OUTPUT_MODE_PINIO, + OUTPUT_MODE_BEEPER } outputMode_e; typedef struct motorAxisCorrectionLimits_s { From 8d703ead2312c7e4aeec09f0e970cb96d1869dfb Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 7 Jun 2026 17:07:07 -0500 Subject: [PATCH 2/4] refactor(msp): use TIM_USE_* bit indices as MSP2_INAV_OUTPUT_ASSIGNMENT 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. --- src/main/drivers/pwm_mapping.h | 9 +++------ src/main/fc/fc_msp.c | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/drivers/pwm_mapping.h b/src/main/drivers/pwm_mapping.h index 3212375c247..afe9301f7e7 100644 --- a/src/main/drivers/pwm_mapping.h +++ b/src/main/drivers/pwm_mapping.h @@ -88,12 +88,9 @@ typedef struct { const timerHardware_t * timServos[MAX_PWM_OUTPUTS]; } timMotorServoHardware_t; -// Output assignment types for MSP2_INAV_OUTPUT_ASSIGNMENT response -// LED outputs are not reported here; they are already identified by TIM_USE_LED -// in the MSP2_INAV_OUTPUT_MAPPING_EXT2 usageFlags response. -#define OUTPUT_ASSIGNMENT_TYPE_MOTOR 1 -#define OUTPUT_ASSIGNMENT_TYPE_SERVO 2 -#define OUTPUT_ASSIGNMENT_TYPE_BUZZER 3 +// MSP2_INAV_OUTPUT_ASSIGNMENT type byte: bit index of the TIM_USE_* flag. +// Matches the JavaScript TIM_USE_* constants in outputMapping.js (which are bit indices). +// Use __builtin_ctz(TIM_USE_x) to derive these from the bit-mask definitions in timer.h. #endif // SITL_BUILD bool pwmMotorAndServoInit(void); diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index 5afe2f9247a..50ddc2d9f09 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -1812,18 +1812,18 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF const timMotorServoHardware_t *hw = pwmGetOutputAssignment(); for (int m = 0; m < hw->maxTimMotorCount; m++) { sbufWriteU8(dst, (uint8_t)(hw->timMotors[m] - timerHardware)); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_MOTOR); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_MOTOR)); sbufWriteU8(dst, (uint8_t)(m + 1)); } for (int s = 0; s < hw->maxTimServoCount; s++) { sbufWriteU8(dst, (uint8_t)(hw->timServos[s] - timerHardware)); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_SERVO); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_SERVO)); sbufWriteU8(dst, (uint8_t)(s + 1)); } for (int idx = 0; idx < timerHardwareCount; idx++) { if (timerOverrides(timer2id(timerHardware[idx].tim))->outputMode == OUTPUT_MODE_BEEPER) { sbufWriteU8(dst, (uint8_t)idx); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_BUZZER); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_BEEPER)); sbufWriteU8(dst, 1); break; } @@ -4919,18 +4919,18 @@ bool mspFCProcessInOutCommand(uint16_t cmdMSP, sbuf_t *dst, sbuf_t *src, mspResu for (int m = 0; m < tempOut.maxTimMotorCount; m++) { sbufWriteU8(dst, (uint8_t)(tempOut.timMotors[m] - timerHardware)); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_MOTOR); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_MOTOR)); sbufWriteU8(dst, (uint8_t)(m + 1)); } for (int s = 0; s < tempOut.maxTimServoCount; s++) { sbufWriteU8(dst, (uint8_t)(tempOut.timServos[s] - timerHardware)); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_SERVO); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_SERVO)); sbufWriteU8(dst, (uint8_t)(s + 1)); } for (int idx = 0; idx < timerHardwareCount; idx++) { if (proposedModes[timer2id(timerHardware[idx].tim)] == OUTPUT_MODE_BEEPER) { sbufWriteU8(dst, (uint8_t)idx); - sbufWriteU8(dst, OUTPUT_ASSIGNMENT_TYPE_BUZZER); + sbufWriteU8(dst, __builtin_ctz(TIM_USE_BEEPER)); sbufWriteU8(dst, 1); break; } From 49148fc5d45ce179b51dd5c314d26c892a845e97 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 7 Jun 2026 19:16:29 -0500 Subject: [PATCH 3/4] fix(beeper): allow compile-time beeper pads to be reassigned at runtime 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. --- src/main/drivers/pwm_mapping.c | 12 ++++-------- src/main/drivers/sound_beeper.c | 12 +++++++++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index 1a5d5f56cd8..acc427ee48e 100644 --- a/src/main/drivers/pwm_mapping.c +++ b/src/main/drivers/pwm_mapping.c @@ -212,25 +212,21 @@ static bool checkPwmTimerConflicts(const timerHardware_t *timHw) } static void timerHardwareOverride(timerHardware_t * timer) { - // Never modify a beeper timer — beeperPwmInit() must find TIM_USE_BEEPER intact - if (timer->usageFlags & TIM_USE_BEEPER) { - return; - } switch (timerOverrides(timer2id(timer->tim))->outputMode) { case OUTPUT_MODE_MOTORS: - timer->usageFlags &= ~(TIM_USE_SERVO|TIM_USE_LED); + timer->usageFlags &= ~(TIM_USE_SERVO|TIM_USE_LED|TIM_USE_BEEPER); timer->usageFlags |= TIM_USE_MOTOR; break; case OUTPUT_MODE_SERVOS: - timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_LED); + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_LED|TIM_USE_BEEPER); timer->usageFlags |= TIM_USE_SERVO; break; case OUTPUT_MODE_LED: - timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO); + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_BEEPER); timer->usageFlags |= TIM_USE_LED; break; case OUTPUT_MODE_PINIO: - timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED); + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED|TIM_USE_BEEPER); timer->usageFlags |= TIM_USE_PINIO; break; case OUTPUT_MODE_BEEPER: diff --git a/src/main/drivers/sound_beeper.c b/src/main/drivers/sound_beeper.c index 8bfe179bcf1..ecc998706d8 100644 --- a/src/main/drivers/sound_beeper.c +++ b/src/main/drivers/sound_beeper.c @@ -78,7 +78,7 @@ void beeperInit(const beeperDevConfig_t *config) #if !defined(BEEPER) UNUSED(config); #else - // Runtime output assignment takes precedence over the compile-time BEEPER pin. + // Runtime output assignment: scan for any pad explicitly set to OUTPUT_MODE_BEEPER. // pwmBuildTimerOutputList() runs before beeperInit(), so TIM_USE_BEEPER is already // set on the runtime-assigned pad by the time we get here. for (int idx = 0; idx < timerHardwareCount; idx++) { @@ -91,6 +91,16 @@ void beeperInit(const beeperDevConfig_t *config) } } + // Skip compile-time beeper pad if the user has overridden it to another function. + for (int idx = 0; idx < timerHardwareCount; idx++) { + if (timerHardware[idx].tag == config->ioTag) { + if (!(timerHardware[idx].usageFlags & TIM_USE_BEEPER)) { + return; + } + break; + } + } + beeperIO = IOGetByTag(config->ioTag); beeperInverted = config->isInverted; From bfe680a4fc280ddc44f0928be8e6aee403bbe331 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 2 Jul 2026 14:26:08 -0500 Subject: [PATCH 4/4] fix(beeper): report PWM init failure instead of failing silently beeperPwmInit() now returns bool. beeperInit() checks it on both the runtime-assigned pad and the compile-time fallback pad, logging an error via LOG_ERROR(PWM, ...) on failure. The runtime path does not fall through to the compile-time pin on failure, since that pin may have been reassigned to another function. Addresses Qodo review finding on PR #11675. --- src/main/drivers/pwm_output.c | 7 +++++-- src/main/drivers/pwm_output.h | 2 +- src/main/drivers/sound_beeper.c | 10 ++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/drivers/pwm_output.c b/src/main/drivers/pwm_output.c index a4efb9e3008..a0e983fcf19 100644 --- a/src/main/drivers/pwm_output.c +++ b/src/main/drivers/pwm_output.c @@ -764,7 +764,7 @@ void pwmWriteBeeper(bool onoffBeep) } } -void beeperPwmInit(ioTag_t tag, uint16_t frequency) +bool beeperPwmInit(ioTag_t tag, uint16_t frequency) { beeperPwm = NULL; @@ -774,14 +774,17 @@ void beeperPwmInit(ioTag_t tag, uint16_t frequency) // Attempt to allocate TCH TCH_t * tch = timerGetTCH(timHw); if (tch == NULL) { - return; + return false; } beeperPwm = &beeperPwmPort; beeperFrequency = frequency; IOConfigGPIOAF(IOGetByTag(tag), IOCFG_AF_PP, timHw->alternateFunction); pwmOutConfigTimer(beeperPwm, tch, PWM_TIMER_HZ, 1000000 / beeperFrequency, (1000000 / beeperFrequency) / 2); + return true; } + + return false; } #endif diff --git a/src/main/drivers/pwm_output.h b/src/main/drivers/pwm_output.h index 1dc644f7f4e..0aba4b142ef 100644 --- a/src/main/drivers/pwm_output.h +++ b/src/main/drivers/pwm_output.h @@ -59,7 +59,7 @@ bool pwmMotorConfig(const struct timerHardware_s *timerHardware, uint8_t motorIn void pwmServoPreconfigure(void); bool pwmServoConfig(const struct timerHardware_s *timerHardware, uint8_t servoIndex, uint16_t servoPwmRate, uint16_t servoCenterPulse, bool enableOutput); void pwmWriteBeeper(bool onoffBeep); -void beeperPwmInit(ioTag_t tag, uint16_t frequency); +bool beeperPwmInit(ioTag_t tag, uint16_t frequency); void sendDShotCommand(dshotCommands_e cmd); void initDShotCommands(void); diff --git a/src/main/drivers/sound_beeper.c b/src/main/drivers/sound_beeper.c index ecc998706d8..19ae57ae97b 100644 --- a/src/main/drivers/sound_beeper.c +++ b/src/main/drivers/sound_beeper.c @@ -23,6 +23,7 @@ #include "drivers/time.h" #include "drivers/io.h" +#include "common/log.h" #include "drivers/timer.h" #include "drivers/pwm_mapping.h" #include "drivers/pwm_output.h" @@ -84,7 +85,10 @@ void beeperInit(const beeperDevConfig_t *config) 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); + if (!beeperPwmInit(timHw->tag, BEEPER_PWM_FREQUENCY)) { + LOG_ERROR(PWM, "Beeper PWM init failed on assigned pad, beeper disabled"); + return; + } beeperConfigMutable()->pwmMode = true; systemBeep(false); return; @@ -107,7 +111,9 @@ void beeperInit(const beeperDevConfig_t *config) if (beeperIO) { IOInit(beeperIO, OWNER_BEEPER, RESOURCE_OUTPUT, 0); if (beeperConfig()->pwmMode) { - beeperPwmInit(config->ioTag, BEEPER_PWM_FREQUENCY); + if (!beeperPwmInit(config->ioTag, BEEPER_PWM_FREQUENCY)) { + LOG_ERROR(PWM, "Beeper PWM init failed on compile-time pad, beeper disabled"); + } } else { IOConfigGPIO(beeperIO, config->isOD ? IOCFG_OUT_OD : IOCFG_OUT_PP); }