From 74b4aef2c918912cbc111f74fdd002579a6fa6f2 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 28 Feb 2026 14:36:07 -0600 Subject: [PATCH 01/10] PINIO: add PWM duty control and unify output mode handling - pinio: auto-detect timer capability in pinioInit(); configure PWM when timer is free (OWNER_FREE), fall back to GPIO for occupied timers - pinio: add pinioSetDuty(channel, duty) for 0-100% PWM control at 24 kHz - Remove LED strip PWM code path (ledConfigurePWM/ledPinStartPWM/StopPWM); these functions are superseded by pinioSetDuty() - osd_joystick: migrate from led pin PWM to pinioSetDuty(); add osd_joystick_pinio_channel setting; bump PG version to 1 - Guard osd_joystick and rcdevice_cam with USE_PINIO instead of USE_LED_STRIP - programming: rename LOGIC_CONDITION_LED_PIN_PWM to LOGIC_CONDITION_PINIO_PWM (value 52 preserved for backward compat); operandA=channel, operandB=duty - mixer: add OUTPUT_MODE_PINIO=4; clears MOTOR/SERVO/LED flags so pinioInit() can claim the timer output - MSP OUTPUT_MAPPING_EXT2: set specialLabels=PIN_LABEL_PINIO_BASE+j for PINIO channel j, enabling configurator to display correct USER number - Fix missing bounds check in pinioSet() (index < 0 || >= pinioHardwareCount) - Validate cliLedPinPWM duty input range (0-100) --- src/main/drivers/light_ws2811strip.c | 80 +++++--------------------- src/main/drivers/light_ws2811strip.h | 4 -- src/main/drivers/pinio.c | 54 ++++++++++++++++- src/main/drivers/pinio.h | 2 + src/main/drivers/pwm_mapping.c | 4 ++ src/main/drivers/pwm_mapping.h | 3 +- src/main/fc/cli.c | 26 +++++++-- src/main/fc/fc_msp.c | 27 +++++++-- src/main/fc/fc_tasks.c | 2 +- src/main/fc/settings.yaml | 8 ++- src/main/flight/mixer.h | 3 +- src/main/io/osd_joystick.c | 20 ++++--- src/main/io/osd_joystick.h | 5 +- src/main/io/rcdevice_cam.c | 10 ++-- src/main/programming/logic_condition.c | 16 ++---- src/main/programming/logic_condition.h | 2 +- 16 files changed, 154 insertions(+), 112 deletions(-) diff --git a/src/main/drivers/light_ws2811strip.c b/src/main/drivers/light_ws2811strip.c index fe5f405d032..e20e3cec953 100644 --- a/src/main/drivers/light_ws2811strip.c +++ b/src/main/drivers/light_ws2811strip.c @@ -62,7 +62,6 @@ static DMA_RAM timerDMASafeType_t ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE]; static IO_t ws2811IO = IO_NONE; static TCH_t * ws2811TCH = NULL; static bool ws2811Initialised = false; -static bool pwmMode = false; static hsvColor_t ledColorBuffer[WS2811_LED_STRIP_LENGTH]; @@ -112,14 +111,6 @@ bool ledConfigureDMA(void) { return timerPWMConfigChannelDMA(ws2811TCH, ledStripDMABuffer, sizeof(ledStripDMABuffer[0]), WS2811_DMA_BUFFER_SIZE); } -void ledConfigurePWM(void) { - timerConfigBase(ws2811TCH, 100, WS2811_TIMER_HZ ); - timerPWMConfigChannel(ws2811TCH, 0); - timerPWMStart(ws2811TCH); - timerEnable(ws2811TCH); - pwmMode = true; -} - void ws2811LedStripInit(void) { const timerHardware_t * timHw = timerGetByTag(IO_TAG(WS2811_PIN), TIM_USE_ANY); @@ -141,28 +132,20 @@ void ws2811LedStripInit(void) IOInit(ws2811IO, OWNER_LED_STRIP, RESOURCE_OUTPUT, 0); IOConfigGPIOAF(ws2811IO, IOCFG_AF_PP_FAST, timHw->alternateFunction); - if (ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_LOW) { - ledConfigurePWM(); - *timerCCR(ws2811TCH) = 0; - } else if (ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_HIGH) { - ledConfigurePWM(); - *timerCCR(ws2811TCH) = 100; - } else { - if (!ledConfigureDMA()) { - // If DMA failed - abort - ws2811Initialised = false; - return; - } - - // Zero out DMA buffer - memset(&ledStripDMABuffer, 0, sizeof(ledStripDMABuffer)); - if ( ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_SHARED_HIGH ) { - ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE-1] = 255; - } - ws2811Initialised = true; - - ws2811UpdateStrip(); + if (!ledConfigureDMA()) { + // If DMA failed - abort + ws2811Initialised = false; + return; + } + + // Zero out DMA buffer + memset(&ledStripDMABuffer, 0, sizeof(ledStripDMABuffer)); + if (ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_SHARED_HIGH) { + ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE-1] = 255; } + ws2811Initialised = true; + + ws2811UpdateStrip(); } bool isWS2811LedStripReady(void) @@ -191,7 +174,7 @@ void ws2811UpdateStrip(void) static rgbColor24bpp_t *rgb24; // don't wait - risk of infinite block, just get an update next time round - if (pwmMode || timerPWMDMAInProgress(ws2811TCH)) { + if (timerPWMDMAInProgress(ws2811TCH)) { return; } @@ -216,40 +199,5 @@ void ws2811UpdateStrip(void) timerPWMStartDMA(ws2811TCH); } -//value -void ledPinStartPWM(uint16_t value) { - if (ws2811TCH == NULL) { - return; - } - - if ( !pwmMode ) { - timerPWMStopDMA(ws2811TCH); - //FIXME: implement method to release DMA - ws2811TCH->dma->owner = OWNER_FREE; - - ledConfigurePWM(); - } - *timerCCR(ws2811TCH) = value; -} - -void ledPinStopPWM(void) { - if (ws2811TCH == NULL || !pwmMode ) { - return; - } - - if ( ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_HIGH ) { - *timerCCR(ws2811TCH) = 100; - return; - } else if ( ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_LOW ) { - *timerCCR(ws2811TCH) = 0; - return; - } - pwmMode = false; - - if (!ledConfigureDMA()) { - ws2811Initialised = false; - } -} - #endif diff --git a/src/main/drivers/light_ws2811strip.h b/src/main/drivers/light_ws2811strip.h index 94c36445ec7..1a434946c96 100644 --- a/src/main/drivers/light_ws2811strip.h +++ b/src/main/drivers/light_ws2811strip.h @@ -48,10 +48,6 @@ void ws2811LedStripHardwareInit(void); void ws2811LedStripDMAEnable(void); bool ws2811LedStripDMAInProgress(void); -//value 0...100 -void ledPinStartPWM(uint16_t value); -void ledPinStopPWM(void); - void ws2811UpdateStrip(void); void setLedHsv(uint16_t index, const hsvColor_t *color); diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index 3e89e5fccca..5b2619fcb43 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -65,6 +65,7 @@ const int pinioHardwareCount = ARRAYLEN(pinioHardware); /*** Runtime configuration ***/ typedef struct pinioRuntime_s { IO_t io; + TCH_t *tch; // Non-NULL when pin is configured in PWM mode bool inverted; bool state; } pinioRuntime_t; @@ -84,6 +85,32 @@ void pinioInit(void) continue; } + // If the pin has a timer and is unclaimed, configure it as a PWM output. + // pwmMotorAndServoInit() runs before pinioInit(), so claimed motor/servo pins + // are already owned and the OWNER_FREE check correctly skips them. + const timerHardware_t *timHw = timerGetByTag(pinioHardware[i].ioTag, TIM_USE_ANY); + if (timHw && IOGetOwner(io) == OWNER_FREE) { + TCH_t *tch = timerGetTCH(timHw); + if (tch) { + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(i)); + IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); + // period=100 means CCR value is directly the duty percentage (0–100); + // 2.4 MHz / 100 = 24 kHz PWM, above audible range + timerConfigBase(tch, 100, 2400000); + timerPWMConfigChannel(tch, 0); + timerPWMStart(tch); + timerEnable(tch); + pinioRuntime[i].tch = tch; + pinioRuntime[i].io = io; + pinioRuntime[i].inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; + pinioRuntime[i].state = false; + // Start in the "off" state: HIGH if inverted, LOW if normal + *timerCCR(tch) = pinioRuntime[i].inverted ? 100 : 0; + continue; + } + } + + // GPIO fallback: no timer available or pin already claimed IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(i)); IOConfigGPIO(io, pinioHardware[i].ioMode); @@ -102,13 +129,38 @@ void pinioInit(void) void pinioSet(int index, bool newState) { + if (index < 0 || index >= pinioHardwareCount) { + return; + } + if (!pinioRuntime[index].io) { return; } if (newState != pinioRuntime[index].state) { - IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); + if (pinioRuntime[index].tch) { + *timerCCR(pinioRuntime[index].tch) = (newState ^ pinioRuntime[index].inverted) ? 100 : 0; + } else { + IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); + } pinioRuntime[index].state = newState; } } + +void pinioSetDuty(int index, uint8_t duty) +{ + if (index < 0 || index >= pinioHardwareCount) { + return; + } + + if (!pinioRuntime[index].tch) { + return; + } + + // Clamp to valid range and apply inversion + if (duty > 100) { + duty = 100; + } + *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; +} #endif diff --git a/src/main/drivers/pinio.h b/src/main/drivers/pinio.h index a1de21c12de..2b15c9633b2 100644 --- a/src/main/drivers/pinio.h +++ b/src/main/drivers/pinio.h @@ -24,6 +24,7 @@ #include #include "drivers/io_types.h" +#include "drivers/timer.h" #define PINIO_COUNT 4 #define PINIO_FLAGS_INVERTED 0x80 @@ -39,3 +40,4 @@ extern const int pinioHardwareCount; void pinioInit(void); void pinioSet(int index, bool newState); +void pinioSetDuty(int index, uint8_t duty); diff --git a/src/main/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index 2d01127a504..4ef3d5fea6d 100644 --- a/src/main/drivers/pwm_mapping.c +++ b/src/main/drivers/pwm_mapping.c @@ -232,6 +232,10 @@ static void timerHardwareOverride(timerHardware_t * timer) { timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO); timer->usageFlags |= TIM_USE_LED; break; + case OUTPUT_MODE_PINIO: + // Clear motor/servo/LED flags so pinioInit() can claim this timer + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED); + break; } } diff --git a/src/main/drivers/pwm_mapping.h b/src/main/drivers/pwm_mapping.h index 3f08d9b500a..c860166bc74 100644 --- a/src/main/drivers/pwm_mapping.h +++ b/src/main/drivers/pwm_mapping.h @@ -55,7 +55,8 @@ typedef enum { typedef enum { PIN_LABEL_NONE = 0, - PIN_LABEL_LED + PIN_LABEL_LED, + PIN_LABEL_PINIO_BASE = 2 // values 2..5 = USER1..USER4 (add channel index 0-3) } pinLabel_e; typedef enum { diff --git a/src/main/fc/cli.c b/src/main/fc/cli.c index 3e8165dcf32..a648f4f5d77 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -58,6 +58,7 @@ bool cliMode = false; #include "drivers/flash.h" #include "drivers/io.h" #include "drivers/io_impl.h" +#include "drivers/pinio.h" #include "drivers/osd_symbols.h" #include "drivers/persistent.h" #include "drivers/sdcard/sdcard.h" @@ -173,6 +174,7 @@ static const char * outputModeNames[] = { "MOTORS", "SERVOS", "LED", + "PINIO", NULL }; @@ -2168,20 +2170,28 @@ static void cliModeColor(char *cmdline) } } + +#endif // USE_LED_STRIP + +#ifdef USE_PINIO static void cliLedPinPWM(char *cmdline) { int i; if (isEmpty(cmdline)) { - ledPinStopPWM(); + pinioSetDuty(0, 0); cliPrintLine("PWM stopped"); } else { i = fastA2I(cmdline); - ledPinStartPWM(i); - cliPrintLinef("PWM started: %d%%",i); + if (i < 0 || i > 100) { + cliPrintLine("Error: duty must be 0-100"); + return; + } + pinioSetDuty(0, (uint8_t)i); + cliPrintLinef("PWM started: %d%%", i); } } -#endif +#endif // USE_PINIO static void cliDelay(char* cmdLine) { int ms = 0; @@ -3191,6 +3201,8 @@ static void cliTimerOutputMode(char *cmdline) mode = OUTPUT_MODE_SERVOS; } else if(!sl_strcasecmp("LED", tok)) { mode = OUTPUT_MODE_LED; + } else if(!sl_strcasecmp("PINIO", tok)) { + mode = OUTPUT_MODE_PINIO; } else { cliShowParseError(); return; @@ -4919,7 +4931,9 @@ const clicmd_t cmdTable[] = { CLI_COMMAND_DEF("help", NULL, NULL, cliHelp), #ifdef USE_LED_STRIP CLI_COMMAND_DEF("led", "configure leds", NULL, cliLed), - CLI_COMMAND_DEF("ledpinpwm", "start/stop PWM on LED pin, 0..100 duty ratio", "[]\r\n", cliLedPinPWM), +#endif +#ifdef USE_PINIO + CLI_COMMAND_DEF("ledpinpwm", "set PINIO PWM duty on channel 0, 0..100", "[]\r\n", cliLedPinPWM), #endif CLI_COMMAND_DEF("map", "configure rc channel order", "[]", cliMap), CLI_COMMAND_DEF("memory", "view memory usage", NULL, cliMemory), @@ -4980,7 +4994,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 45379d1caa4..4b97d6df8f8 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -55,6 +55,9 @@ #include "drivers/osd.h" #include "drivers/osd_symbols.h" #include "drivers/pwm_mapping.h" +#ifdef USE_PINIO +#include "drivers/pinio.h" +#endif #include "drivers/sdcard/sdcard.h" #include "drivers/serial.h" #include "drivers/system.h" @@ -1681,12 +1684,28 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF sbufWriteU8(dst, timer2id(timerHardware[i].tim)); #endif sbufWriteU32(dst, timerHardware[i].usageFlags); - #if defined(SITL_BUILD) || !defined(WS2811_PIN) + #if defined(SITL_BUILD) sbufWriteU8(dst, 0); #else - // Extra label to help identify repurposed PINs. - // Eventually, we can try to add more labels for PPM pins, etc. - sbufWriteU8(dst, timerHardware[i].tag == led_tag ? PIN_LABEL_LED : PIN_LABEL_NONE); + { + uint8_t specialLabel = PIN_LABEL_NONE; + #if defined(WS2811_PIN) + if (timerHardware[i].tag == led_tag) { + specialLabel = PIN_LABEL_LED; + } + #endif + #ifdef USE_PINIO + if (specialLabel == PIN_LABEL_NONE) { + for (int j = 0; j < pinioHardwareCount; j++) { + if (timerHardware[i].tag == pinioHardware[j].ioTag) { + specialLabel = PIN_LABEL_PINIO_BASE + j; + break; + } + } + } + #endif + sbufWriteU8(dst, specialLabel); + } #endif } } diff --git a/src/main/fc/fc_tasks.c b/src/main/fc/fc_tasks.c index 4ca125d5c6a..5cfb7dec86d 100755 --- a/src/main/fc/fc_tasks.c +++ b/src/main/fc/fc_tasks.c @@ -420,7 +420,7 @@ void fcTasksInit(void) #endif #endif #ifdef USE_RCDEVICE -#ifdef USE_LED_STRIP +#ifdef USE_PINIO setTaskEnabled(TASK_RCDEVICE, rcdeviceIsEnabled() || osdJoystickEnabled()); #else setTaskEnabled(TASK_RCDEVICE, rcdeviceIsEnabled()); diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 476dfe09ff7..6d00fc67994 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -4279,13 +4279,19 @@ groups: - name: PG_OSD_JOYSTICK_CONFIG type: osdJoystickConfig_t headers: ["io/osd_joystick.h"] - condition: USE_RCDEVICE && USE_LED_STRIP + condition: USE_RCDEVICE && USE_PINIO members: - name: osd_joystick_enabled description: "Enable OSD Joystick emulation" default_value: OFF field: osd_joystick_enabled type: bool + - name: osd_joystick_pinio_channel + description: "PINIO channel index (0-3) for the camera OSD control pin" + default_value: 0 + field: pinio_channel + min: 0 + max: 3 - name: osd_joystick_down description: "PWM value for DOWN key" default_value: 0 diff --git a/src/main/flight/mixer.h b/src/main/flight/mixer.h index 12688bd2c09..6c4370d4176 100644 --- a/src/main/flight/mixer.h +++ b/src/main/flight/mixer.h @@ -48,7 +48,8 @@ typedef enum { OUTPUT_MODE_AUTO = 0, OUTPUT_MODE_MOTORS, OUTPUT_MODE_SERVOS, - OUTPUT_MODE_LED + OUTPUT_MODE_LED, + OUTPUT_MODE_PINIO } outputMode_e; typedef struct motorAxisCorrectionLimits_s { diff --git a/src/main/io/osd_joystick.c b/src/main/io/osd_joystick.c index c1d9dee5a5a..def03c66590 100644 --- a/src/main/io/osd_joystick.c +++ b/src/main/io/osd_joystick.c @@ -17,7 +17,7 @@ #include "fc/runtime_config.h" #include "drivers/time.h" -#include "drivers/light_ws2811strip.h" +#include "drivers/pinio.h" #include "io/serial.h" #include "io/rcdevice.h" @@ -25,13 +25,14 @@ #include "osd_joystick.h" #ifdef USE_RCDEVICE -#ifdef USE_LED_STRIP +#ifdef USE_PINIO -PG_REGISTER_WITH_RESET_TEMPLATE(osdJoystickConfig_t, osdJoystickConfig, PG_OSD_JOYSTICK_CONFIG, 0); +PG_REGISTER_WITH_RESET_TEMPLATE(osdJoystickConfig_t, osdJoystickConfig, PG_OSD_JOYSTICK_CONFIG, 1); PG_RESET_TEMPLATE(osdJoystickConfig_t, osdJoystickConfig, .osd_joystick_enabled = SETTING_OSD_JOYSTICK_ENABLED_DEFAULT, + .pinio_channel = SETTING_OSD_JOYSTICK_PINIO_CHANNEL_DEFAULT, .osd_joystick_down = SETTING_OSD_JOYSTICK_DOWN_DEFAULT, .osd_joystick_up = SETTING_OSD_JOYSTICK_UP_DEFAULT, .osd_joystick_left = SETTING_OSD_JOYSTICK_LEFT_DEFAULT, @@ -45,28 +46,29 @@ bool osdJoystickEnabled(void) { void osdJoystickSimulate5KeyButtonPress(uint8_t operation) { + const int ch = osdJoystickConfig()->pinio_channel; switch (operation) { case RCDEVICE_CAM_KEY_ENTER: - ledPinStartPWM( osdJoystickConfig()->osd_joystick_enter ); + pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_enter); break; case RCDEVICE_CAM_KEY_LEFT: - ledPinStartPWM( osdJoystickConfig()->osd_joystick_left ); + pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_left); break; case RCDEVICE_CAM_KEY_UP: - ledPinStartPWM( osdJoystickConfig()->osd_joystick_up ); + pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_up); break; case RCDEVICE_CAM_KEY_RIGHT: - ledPinStartPWM( osdJoystickConfig()->osd_joystick_right ); + pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_right); break; case RCDEVICE_CAM_KEY_DOWN: - ledPinStartPWM( osdJoystickConfig()->osd_joystick_down ); + pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_down); break; } } void osdJoystickSimulate5KeyButtonRelease(void) { - ledPinStopPWM(); + pinioSetDuty(osdJoystickConfig()->pinio_channel, 0); } diff --git a/src/main/io/osd_joystick.h b/src/main/io/osd_joystick.h index 574b8e3b776..15f52134692 100644 --- a/src/main/io/osd_joystick.h +++ b/src/main/io/osd_joystick.h @@ -2,11 +2,12 @@ #include "config/parameter_group.h" -#ifdef USE_RCDEVICE -#ifdef USE_LED_STRIP +#ifdef USE_RCDEVICE +#ifdef USE_PINIO typedef struct osdJoystickConfig_s { bool osd_joystick_enabled; + uint8_t pinio_channel; // PINIO index for the cam-control output pin uint8_t osd_joystick_down; uint8_t osd_joystick_up; uint8_t osd_joystick_left; diff --git a/src/main/io/rcdevice_cam.c b/src/main/io/rcdevice_cam.c index 0edbc8bf979..1388ca170ee 100644 --- a/src/main/io/rcdevice_cam.c +++ b/src/main/io/rcdevice_cam.c @@ -49,7 +49,7 @@ bool waitingDeviceResponse = false; static bool isFeatureSupported(uint8_t feature) { #ifndef UNIT_TEST -#ifdef USE_LED_STRIP +#ifdef USE_PINIO if (!rcdeviceIsEnabled() && osdJoystickEnabled() ) { return true; } @@ -117,7 +117,7 @@ static void rcdeviceCameraControlProcess(void) switchStates[switchIndex].isActivated = true; } #ifndef UNIT_TEST -#ifdef USE_LED_STRIP +#ifdef USE_PINIO else if ((behavior1 != RCDEVICE_PROTOCOL_CAM_CTRL_UNKNOWN_CAMERA_OPERATION) && osdJoystickEnabled()) { switch (behavior1) { case RCDEVICE_PROTOCOL_CAM_CTRL_SIMULATE_WIFI_BTN: @@ -137,7 +137,7 @@ static void rcdeviceCameraControlProcess(void) UNUSED(behavior1); } else { #ifndef UNIT_TEST -#ifdef USE_LED_STRIP +#ifdef USE_PINIO if (osdJoystickEnabled() && switchStates[switchIndex].isActivated) { osdJoystickSimulate5KeyButtonRelease(); } @@ -275,7 +275,7 @@ static void rcdevice5KeySimulationProcess(timeUs_t currentTimeUs) waitingDeviceResponse = true; } #ifndef UNIT_TEST -#ifdef USE_LED_STRIP +#ifdef USE_PINIO else if (osdJoystickEnabled()) { osdJoystickSimulate5KeyButtonRelease(); isButtonPressed = false; @@ -320,7 +320,7 @@ static void rcdevice5KeySimulationProcess(timeUs_t currentTimeUs) waitingDeviceResponse = true; } #ifndef UNIT_TEST -#ifdef USE_LED_STRIP +#ifdef USE_PINIO else if (osdJoystickEnabled()) { if ( key == RCDEVICE_CAM_KEY_CONNECTION_OPEN ) { rcdeviceInMenu = true; diff --git a/src/main/programming/logic_condition.c b/src/main/programming/logic_condition.c index edba68b8e94..d44a17c0020 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -58,7 +58,7 @@ #include "io/vtx.h" #include "drivers/vtx_common.h" -#include "drivers/light_ws2811strip.h" +#include "drivers/pinio.h" PG_REGISTER_ARRAY_WITH_RESET_FN(logicCondition_t, MAX_LOGIC_CONDITIONS, logicConditions, PG_LOGIC_CONDITIONS, 4); @@ -506,15 +506,11 @@ static int logicConditionCompute( } break; -#ifdef USE_LED_STRIP - case LOGIC_CONDITION_LED_PIN_PWM: - - if (operandA >=0 && operandA <= 100) { - ledPinStartPWM((uint8_t)operandA); - } else { - ledPinStopPWM(); - } - return operandA; +#ifdef USE_PINIO + case LOGIC_CONDITION_PINIO_PWM: + // operandA = PINIO channel index (0-3), operandB = duty cycle (0-100) + pinioSetDuty(operandA, (uint8_t)constrain(operandB, 0, 100)); + return operandB; break; #endif #ifdef USE_GPS_FIX_ESTIMATION diff --git a/src/main/programming/logic_condition.h b/src/main/programming/logic_condition.h index 74ea96c98ee..859096aeee5 100644 --- a/src/main/programming/logic_condition.h +++ b/src/main/programming/logic_condition.h @@ -81,7 +81,7 @@ typedef enum { LOGIC_CONDITION_TIMER = 49, LOGIC_CONDITION_DELTA = 50, LOGIC_CONDITION_APPROX_EQUAL = 51, - LOGIC_CONDITION_LED_PIN_PWM = 52, + LOGIC_CONDITION_PINIO_PWM = 52, LOGIC_CONDITION_DISABLE_GPS_FIX = 53, LOGIC_CONDITION_RESET_MAG_CALIBRATION = 54, LOGIC_CONDITION_SET_GIMBAL_SENSITIVITY = 55, From 4e29b1d81371b4caf1b7af7ba96c1050df000661 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 28 Feb 2026 17:12:18 -0600 Subject: [PATCH 02/10] Remove legacy LED pin PWM modes, consolidate PINIO PWM interface Remove the led_pin_pwm_mode setting (SHARED_LOW/SHARED_HIGH/LOW/HIGH) and PG_LEDPIN_CONFIG parameter group. Dedicated PWM is now handled by PINIO channels; LED strip idle level is controllable at runtime via programming framework operation 52 (channel=PINIO_COUNT) or CLI command `piniopwm [channel] `. - Remove led_pin_pwm_mode_e enum, ledPinConfig_t, 3 dead declarations - Add ws2811SetIdleHigh(bool) for binary LED idle level control - Rename CLI command ledpinpwm -> piniopwm with channel parameter - Route operation 52 channel==PINIO_COUNT to LED strip idle level - Rename docs/LED pin PWM.md -> docs/PINIO PWM.md, rewrite - Update OSD Joystick.md and Programming Framework.md references Co-Authored-By: Claude Opus 4.6 --- docs/LED pin PWM.md | 96 -------------------------- docs/OSD Joystick.md | 18 ++--- docs/PINIO PWM.md | 71 +++++++++++++++++++ docs/Programming Framework.md | 2 +- src/main/drivers/light_ws2811strip.c | 17 ++--- src/main/drivers/light_ws2811strip.h | 21 ++---- src/main/fc/cli.c | 51 ++++++++++---- src/main/fc/settings.yaml | 14 ---- src/main/programming/logic_condition.c | 11 ++- 9 files changed, 138 insertions(+), 163 deletions(-) delete mode 100644 docs/LED pin PWM.md create mode 100644 docs/PINIO PWM.md diff --git a/docs/LED pin PWM.md b/docs/LED pin PWM.md deleted file mode 100644 index 8ed96a357e2..00000000000 --- a/docs/LED pin PWM.md +++ /dev/null @@ -1,96 +0,0 @@ -# LED pin PWM - -Normally LED pin is used to drive WS2812 led strip. LED pin is held low, and every 10ms or 20ms a set of pulses is sent to change color of the 32 LEDs: - -![alt text](/docs/assets/images/ws2811_packets.png "ws2811 packets") -![alt text](/docs/assets/images/ws2811_data.png "ws2811 data") - -As alternative function, it is possible to generate PWM signal with specified duty ratio on the LED pin. - -Feature can be used to drive external devices such as a VTX power switch. Setting the PWM duty cycle to 100% or 0% can -provide an extra PINIO pin. It is also used to simulate [OSD joystick](OSD%20Joystick.md) to control cameras. - -PWM frequency is fixed to 24kHz with duty ratio between 0 and 100%: - -![alt text](/docs/assets/images/led_pin_pwm.png "led pin pwm") - -Note that the LED feature needs to be enabled when using the PIN in this mode (feature LED_STRIP). - -There are four modes of operation: -- low -- high -- shared_low -- shared_high - -Mode is configured using ```led_pin_pwm_mode``` setting: ```LOW```, ```HIGH```, ```SHARED_LOW```, ```SHARED_HIGH``` - -*Note that in any mode, there will be ~2 seconds LOW pulse on boot.* - -## LOW -LED Pin is initialized to output low level by default and can be used to generate PWM signal. - -ws2812 strip can not be controlled. - -## HIGH -LED Pin is initialized to output high level by default and can be used to generate PWM signal. - -ws2812 strip can not be controlled. - -## SHARED_LOW (default) -LED Pin is used to drive WS2812 strip. Pauses between pulses are low: - -![alt text](/docs/assets/images/ws2811_packets.png "ws2811 packets") - -It is possible to generate PWM signal with duty ratio >0...100%. - -While PWM signal is generated, ws2811 strip is not updated. - -When PWM generation is disabled, LED pin is used to drive ws2812 strip. - -Total ws2812 pulses duration is ~1ms with ~9ms pauses. Thus connected device should ignore PWM signal with duty ratio < ~10%. - -## SHARED_HIGH -LED Pin is used to drive WS2812 strip. Pauses between pulses are high. ws2812 pulses are prefixed with 50us low 'reset' pulse: - -![alt text](/docs/assets/images/ws2811_packets_high.png "ws2811 packets_high") -![alt text](/docs/assets/images/ws2811_data_high.png "ws2811 data_high") - - It is possible to generate PWM signal with duty ratio 0...<100%. - - While PWM signal is generated, ws2811 strip is not updated. - - When PWM generation is disabled, LED pin is used to drive ws2812 strip. Total ws2812 pulses duration is ~1ms with ~9ms pauses. Thus connected device should ignore PWM signal with duty ratio > ~90%. - - After sending ws2812 protocol pulses for 32 LEDS, we held line high for 9ms, then send 50us low 'reset' pulse. Datasheet for ws2812 protocol does not describe behavior for long high pulse, but in practice it works the same as 'reset' pulse. To be safe, we also send correct low 'reset' pulse before starting next LEDs update sequence. - - This mode is used to simulate OSD joystick. It is Ok that effectively voltage level is held >90% while driving LEDs, because OSD joystick keypress voltages are below 90%. - - See [OSD Joystick](OSD%20Joystick.md) for more information. - -# Generating PWM signal with programming framework - -See "LED Pin PWM" operation in [Programming Framework](Programming%20Framework.md) - - -# Generating PWM signal from CLI - -```ledpinpwm ``` - value = 0...100 - enable PWM generation with specified duty cycle - -```ledpinpwm``` - disable PWM generation ( disable to allow ws2812 LEDs updates in shared modes ) - - -# Example of driving LED - -It is possible to drive single color LED with brightness control. Current consumption should not be greater then 1-2ma, thus LED can be used for indication only. - -![alt text](/docs/assets/images/ledpinpwmled.png "led pin pwm led") - -# Example of driving powerfull white LED - -To drive power LED with brightness control, Mosfet should be used: - -![alt text](/docs/assets/images/ledpinpwmpowerled.png "led pin pwm power_led") - -# Programming tab example for using the LED pin as a PINIO, such as for turning a VTX or camera on and off -![screenshot of programming tab using led as pinio](/docs/assets/images/led-as-pinio.png) - diff --git a/docs/OSD Joystick.md b/docs/OSD Joystick.md index 9e0f677e455..9c62eb850d3 100644 --- a/docs/OSD Joystick.md +++ b/docs/OSD Joystick.md @@ -1,8 +1,8 @@ # OSD joystick -LED pin can be used to emulate 5key OSD joystick for OSD camera pin, while still driving ws2812 LEDs (shared functionality). +A PINIO channel can be used to emulate a 5-key OSD joystick for OSD camera control. -See [LED pin PWM](LED%20pin%20PWM.md) for more details. +See [PINIO PWM](PINIO%20PWM.md) for more details. Note that for cameras which support RuncamDevice protocol, there is alternative functionality using serial communication: [Runcam device](Runcam%20device.md) @@ -22,17 +22,17 @@ To simulate 5key joystick, it is sufficient to generate correct voltage on camer # Enabling OSD Joystick emulation -```set led_pin_pwm_mode=shared_high``` - ```set osd_joystick_enabled=on``` -Also enable "Multi-color RGB LED Strip support" in Configuration tab. +```set osd_joystick_pinio_channel=``` + +Where `` is the PINIO channel (0-3) connected to the camera OSD pin. # Connection diagram -We use LED pin PWM functionality with RC filter to generate voltage: +We use PINIO PWM with an RC filter to generate voltage: -![alt text](/docs/assets/images/ledpinpwmfilter.png "led pin pwm filter") +![alt text](/docs/assets/images/ledpinpwmfilter.png "PINIO PWM filter") # Example PCB layout (SMD components) @@ -48,7 +48,7 @@ If default voltages does not work with your camera model, then you have to measu 2. Measure voltages on OSD pin while each key is pressed. 3. Connect camera to FC throught RC filter as shown on schematix above. 4. Enable OSD Joystick emulation (see "Enabling OSD Joystick emulation" above) -4. Use cli command ```led_pin_pwm ```, value = 0...100 to find out PWM values for each voltage. +4. Use CLI command `piniopwm `, value = 0...100 to find out PWM values for each voltage. 5. Specify PWM values in configuration and save: ```set osd_joystick_down=0``` @@ -87,7 +87,7 @@ There are 3 RC Boxes which can be used in armed and unarmed state: - Camera 2 - Up - Camera 3 - Down -Other keys can be emulated using Programming framework ( see [LED pin PWM](LED%20pin%20PWM.md) for more details ). +Other keys can be emulated using the Programming framework (see [PINIO PWM](PINIO%20PWM.md) for more details). # Behavior on boot diff --git a/docs/PINIO PWM.md b/docs/PINIO PWM.md new file mode 100644 index 00000000000..fbc9b80a335 --- /dev/null +++ b/docs/PINIO PWM.md @@ -0,0 +1,71 @@ +# PINIO PWM + +INAV provides two mechanisms for generating output signals on GPIO pins: + +1. **PINIO channels (0-3)** — Any PWM-capable timer output defined as `PINIOx_PIN` in the target. Supports full 0-100% duty cycle PWM at 24 kHz. +2. **LED strip idle level (channel 4)** — The WS2812 LED strip pin can be switched between idle-LOW and idle-HIGH between LED update bursts. Binary on/off only. + +## PINIO PWM channels + +PINIO channels are configured per-target in `target.h` using `PINIO1_PIN` through `PINIO4_PIN`. When a PINIO pin has a timer, it is automatically configured as a 24 kHz PWM output. + +PWM duty cycle can be controlled via: +- **CLI:** `piniopwm [channel] ` (duty = 0-100) +- **Programming framework:** Operation 52, Operand A = channel (0-3), Operand B = duty (0-100) + +Setting duty to 0 stops PWM generation (pin goes LOW, or HIGH if `PINIO_FLAGS_INVERTED` is set in target.h). + +Feature can be used to drive external devices such as a VTX power switch. Setting the PWM duty cycle to 100% or 0% effectively provides a digital on/off output. It is also used to simulate [OSD joystick](OSD%20Joystick.md) to control cameras. + +PWM frequency is fixed to 24kHz with duty ratio between 0 and 100%: + +![alt text](/docs/assets/images/led_pin_pwm.png "PINIO PWM signal") + +## LED strip idle level (channel 4) + +When the LED strip feature is enabled, the WS2812 pin sends data bursts (~1 ms) every 10-20 ms. Between bursts, the pin idles at a configurable level. + +The LED strip idle level is accessible as channel `4` (the next channel after PINIO hardware channels 0-3): + +- **CLI:** `piniopwm 4 ` — value > 0 sets idle HIGH, 0 sets idle LOW +- **Programming framework:** Operation 52, Operand A = 4, Operand B = value (>0 = HIGH, 0 = LOW) + +This can be used to drive a MOSFET or similar device connected to the LED pin, toggled by the programming framework based on flight mode, RC channel, GPS state, etc. + +*Note: there will be a ~2 second LOW pulse on the LED pin during boot.* + +### LED strip idle level timing + +Normally LED pin is held low between WS2812 updates: + +![alt text](/docs/assets/images/ws2811_packets.png "ws2811 packets") +![alt text](/docs/assets/images/ws2811_data.png "ws2811 data") + +When idle is set HIGH, the pin is held high between updates. Total ws2812 pulse duration is ~1ms with ~9ms pauses. Connected devices should be tolerant of these brief transients. + +# Generating PWM/output signals with programming framework + +See operation 52 "PINIO PWM" in [Programming Framework](Programming%20Framework.md) + +# Generating PWM/output signals from CLI + +`piniopwm [channel] ` — channel = 0-4, duty = 0-100 + +- One argument: sets duty on channel 0 (backward compatible) +- Two arguments: first is channel, second is duty +- No arguments: stops PWM on channel 0 + +# Example of driving LED + +It is possible to drive single color LED with brightness control. Current consumption should not be greater then 1-2ma, thus LED can be used for indication only. + +![alt text](/docs/assets/images/ledpinpwmled.png "PINIO PWM LED") + +# Example of driving powerful white LED + +To drive power LED with brightness control, a MOSFET should be used: + +![alt text](/docs/assets/images/ledpinpwmpowerled.png "PINIO PWM power LED") + +# Programming tab example for using a PINIO channel to switch a VTX or camera on and off +![screenshot of programming tab using PINIO](/docs/assets/images/led-as-pinio.png) diff --git a/docs/Programming Framework.md b/docs/Programming Framework.md index 4233f78487d..9a374a07e6a 100644 --- a/docs/Programming Framework.md +++ b/docs/Programming Framework.md @@ -114,7 +114,7 @@ for complete documentation on using JavaScript to program your flight controller | 49 | Timer | A simple on - off timer. `true` for the duration of `Operand A` [ms]. Then `false` for the duration of `Operand B` [ms]. | | 50 | Delta | This returns `true` when the value of `Operand A` has changed by the value of `Operand B` or greater within 100ms. ( \|ΔA\| >= B ) | | 51 | Approx Equals (A ~ B) | `true` if `Operand B` is within 1% of `Operand A`. | -| 52 | LED Pin PWM | Value `Operand A` from [`0` : `100`] PWM / PINIO generation on LED Pin. See [LED pin PWM](LED%20pin%20PWM.md). Any other value stops PWM generation (stop to allow ws2812 LEDs updates in shared modes). | +| 52 | PINIO PWM | `Operand A` = channel (0-3 for PINIO hardware, 4 for LED strip idle level). `Operand B` = duty cycle (0-100). Channels 0-3 support full PWM; channel 4 is binary (>0 = HIGH). See [PINIO PWM](PINIO%20PWM.md). | | 53 | Disable GPS Sensor Fix | Disables the GNSS sensor fix. For testing GNSS failure. | | 54 | Mag calibration | Trigger a magnetometer calibration. | | 55 | Set Gimbal Sensitivity | Scales `Operand A` from [`-16` : `15`] diff --git a/src/main/drivers/light_ws2811strip.c b/src/main/drivers/light_ws2811strip.c index e20e3cec953..cc052fcd872 100644 --- a/src/main/drivers/light_ws2811strip.c +++ b/src/main/drivers/light_ws2811strip.c @@ -43,20 +43,12 @@ #include "drivers/timer.h" #include "drivers/light_ws2811strip.h" -#include "config/parameter_group_ids.h" -#include "fc/settings.h" #include "fc/runtime_config.h" #define WS2811_PERIOD (WS2811_TIMER_HZ / WS2811_CARRIER_HZ) #define WS2811_BIT_COMPARE_1 ((WS2811_PERIOD * 2) / 3) #define WS2811_BIT_COMPARE_0 (WS2811_PERIOD / 3) -PG_REGISTER_WITH_RESET_TEMPLATE(ledPinConfig_t, ledPinConfig, PG_LEDPIN_CONFIG, 0); - -PG_RESET_TEMPLATE(ledPinConfig_t, ledPinConfig, - .led_pin_pwm_mode = SETTING_LED_PIN_PWM_MODE_DEFAULT -); - static DMA_RAM timerDMASafeType_t ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE]; static IO_t ws2811IO = IO_NONE; @@ -138,11 +130,8 @@ void ws2811LedStripInit(void) return; } - // Zero out DMA buffer + // Zero out DMA buffer — LED pin idles LOW between WS2812 bursts memset(&ledStripDMABuffer, 0, sizeof(ledStripDMABuffer)); - if (ledPinConfig()->led_pin_pwm_mode == LED_PIN_PWM_MODE_SHARED_HIGH) { - ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE-1] = 255; - } ws2811Initialised = true; ws2811UpdateStrip(); @@ -199,5 +188,9 @@ void ws2811UpdateStrip(void) timerPWMStartDMA(ws2811TCH); } +void ws2811SetIdleHigh(bool high) +{ + ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE - 1] = high ? 255 : 0; +} #endif diff --git a/src/main/drivers/light_ws2811strip.h b/src/main/drivers/light_ws2811strip.h index 1a434946c96..d0edcc276ea 100644 --- a/src/main/drivers/light_ws2811strip.h +++ b/src/main/drivers/light_ws2811strip.h @@ -16,8 +16,10 @@ */ #pragma once + +#include + #include "common/color.h" -#include "config/parameter_group.h" #define WS2811_LED_STRIP_LENGTH 128 #define WS2811_BITS_PER_LED 24 @@ -30,23 +32,8 @@ #define WS2811_TIMER_HZ 2400000 #define WS2811_CARRIER_HZ 800000 -typedef enum { - LED_PIN_PWM_MODE_SHARED_LOW = 0, - LED_PIN_PWM_MODE_SHARED_HIGH = 1, - LED_PIN_PWM_MODE_LOW = 2, - LED_PIN_PWM_MODE_HIGH = 3 -} led_pin_pwm_mode_e; - -typedef struct ledPinConfig_s { - uint8_t led_pin_pwm_mode; //led_pin_pwm_mode_e -} ledPinConfig_t; - -PG_DECLARE(ledPinConfig_t, ledPinConfig); - void ws2811LedStripInit(void); -void ws2811LedStripHardwareInit(void); -void ws2811LedStripDMAEnable(void); -bool ws2811LedStripDMAInProgress(void); +void ws2811SetIdleHigh(bool high); void ws2811UpdateStrip(void); diff --git a/src/main/fc/cli.c b/src/main/fc/cli.c index a648f4f5d77..ce49099613a 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -58,6 +58,7 @@ bool cliMode = false; #include "drivers/flash.h" #include "drivers/io.h" #include "drivers/io_impl.h" +#include "drivers/light_ws2811strip.h" #include "drivers/pinio.h" #include "drivers/osd_symbols.h" #include "drivers/persistent.h" @@ -69,8 +70,6 @@ bool cliMode = false; #include "drivers/time.h" #include "drivers/usb_msc.h" #include "drivers/vtx_common.h" -#include "drivers/light_ws2811strip.h" - #include "fc/fc_core.h" #include "fc/cli.h" #include "fc/config.h" @@ -2174,22 +2173,48 @@ static void cliModeColor(char *cmdline) #endif // USE_LED_STRIP #ifdef USE_PINIO -static void cliLedPinPWM(char *cmdline) +static void cliPinioPwm(char *cmdline) { - int i; + int channel = 0; + int duty; if (isEmpty(cmdline)) { pinioSetDuty(0, 0); - cliPrintLine("PWM stopped"); + cliPrintLine("PWM stopped on channel 0"); + return; + } + + // Find second argument (space-separated) + char *dutyStr = strchr(cmdline, ' '); + if (dutyStr) { + // Two args: channel duty + channel = fastA2I(cmdline); + dutyStr++; + duty = fastA2I(dutyStr); } else { - i = fastA2I(cmdline); - if (i < 0 || i > 100) { - cliPrintLine("Error: duty must be 0-100"); - return; - } - pinioSetDuty(0, (uint8_t)i); - cliPrintLinef("PWM started: %d%%", i); + // One arg: duty on channel 0 + duty = fastA2I(cmdline); } + + if (channel < 0 || channel > PINIO_COUNT) { + cliPrintLinef("Error: channel must be 0-%d", PINIO_COUNT); + return; + } + if (duty < 0 || duty > 100) { + cliPrintLine("Error: duty must be 0-100"); + return; + } + +#ifdef USE_LED_STRIP + if (channel == PINIO_COUNT) { + ws2811SetIdleHigh(duty > 0); + cliPrintLinef("LED idle %s", duty > 0 ? "HIGH" : "LOW"); + return; + } +#endif + + pinioSetDuty(channel, (uint8_t)duty); + cliPrintLinef("PWM ch %d: %d%%", channel, duty); } #endif // USE_PINIO @@ -4933,7 +4958,7 @@ const clicmd_t cmdTable[] = { CLI_COMMAND_DEF("led", "configure leds", NULL, cliLed), #endif #ifdef USE_PINIO - CLI_COMMAND_DEF("ledpinpwm", "set PINIO PWM duty on channel 0, 0..100", "[]\r\n", cliLedPinPWM), + CLI_COMMAND_DEF("piniopwm", "set PINIO PWM duty cycle", "[] \r\n", cliPinioPwm), #endif CLI_COMMAND_DEF("map", "configure rc channel order", "[]", cliMap), CLI_COMMAND_DEF("memory", "view memory usage", NULL, cliMemory), diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 6d00fc67994..89cc479e3a8 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -189,9 +189,6 @@ tables: - name: nav_mc_althold_throttle values: ["STICK", "MID_STICK", "HOVER"] enum: navMcAltHoldThrottle_e - - name: led_pin_pwm_mode - values: ["SHARED_LOW", "SHARED_HIGH", "LOW", "HIGH"] - enum: led_pin_pwm_mode_e - name: gyro_filter_mode values: ["OFF", "STATIC", "DYNAMIC", "ADAPTIVE"] enum: gyroFilterType_e @@ -4265,17 +4262,6 @@ groups: field: attnFilterCutoff max: 100 - - name: PG_LEDPIN_CONFIG - type: ledPinConfig_t - headers: ["drivers/light_ws2811strip.h"] - members: - - name: led_pin_pwm_mode - condition: USE_LED_STRIP - description: "PWM mode of LED pin." - default_value: "SHARED_LOW" - field: led_pin_pwm_mode - table: led_pin_pwm_mode - - name: PG_OSD_JOYSTICK_CONFIG type: osdJoystickConfig_t headers: ["io/osd_joystick.h"] diff --git a/src/main/programming/logic_condition.c b/src/main/programming/logic_condition.c index d44a17c0020..5336a54ad12 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -58,6 +58,7 @@ #include "io/vtx.h" #include "drivers/vtx_common.h" +#include "drivers/light_ws2811strip.h" #include "drivers/pinio.h" PG_REGISTER_ARRAY_WITH_RESET_FN(logicCondition_t, MAX_LOGIC_CONDITIONS, logicConditions, PG_LOGIC_CONDITIONS, 4); @@ -508,7 +509,15 @@ static int logicConditionCompute( #ifdef USE_PINIO case LOGIC_CONDITION_PINIO_PWM: - // operandA = PINIO channel index (0-3), operandB = duty cycle (0-100) + // operandA = channel, operandB = duty cycle (0-100) + // Channels 0..PINIO_COUNT-1 = hardware PINIO (PWM capable) + // Channel PINIO_COUNT = LED strip idle level (binary: >0 = HIGH) +#ifdef USE_LED_STRIP + if (operandA == PINIO_COUNT) { + ws2811SetIdleHigh(operandB > 0); + return operandB; + } +#endif pinioSetDuty(operandA, (uint8_t)constrain(operandB, 0, 100)); return operandB; break; From 5097e9eed4984a2d77eef09c4ca8e9925aba06bc Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 7 Mar 2026 11:04:44 -0600 Subject: [PATCH 03/10] PINIO: fix operand order for backward compat, unify pinioSet/pinioSetDuty Swap PINIO_PWM operands so operandA=duty, operandB=pin, matching the old LED_PIN_PWM behavior (operandA=duty only) for backward compatibility. Pin numbering: 0=LED pin, 1=USER1, 2=USER2, etc. Consolidate pinioSet into pinioSetDuty: on/off is just PWM at 0% or 100% duty. pinioSetDuty now handles both timer and GPIO pins; pinioSet becomes a one-liner wrapper. Removes redundant state tracking field. --- src/main/drivers/pinio.c | 40 ++++++++++---------------- src/main/programming/logic_condition.c | 17 ++++++----- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index 5b2619fcb43..31514cf28a4 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -67,7 +67,6 @@ typedef struct pinioRuntime_s { IO_t io; TCH_t *tch; // Non-NULL when pin is configured in PWM mode bool inverted; - bool state; } pinioRuntime_t; static pinioRuntime_t pinioRuntime[PINIO_COUNT]; @@ -103,7 +102,6 @@ void pinioInit(void) pinioRuntime[i].tch = tch; pinioRuntime[i].io = io; pinioRuntime[i].inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; - pinioRuntime[i].state = false; // Start in the "off" state: HIGH if inverted, LOW if normal *timerCCR(tch) = pinioRuntime[i].inverted ? 100 : 0; continue; @@ -123,11 +121,10 @@ void pinioInit(void) } pinioRuntime[i].io = io; - pinioRuntime[i].state = false; } } -void pinioSet(int index, bool newState) +void pinioSetDuty(int index, uint8_t duty) { if (index < 0 || index >= pinioHardwareCount) { return; @@ -137,30 +134,23 @@ void pinioSet(int index, bool newState) return; } - if (newState != pinioRuntime[index].state) { - if (pinioRuntime[index].tch) { - *timerCCR(pinioRuntime[index].tch) = (newState ^ pinioRuntime[index].inverted) ? 100 : 0; - } else { - IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); - } - pinioRuntime[index].state = newState; - } -} - -void pinioSetDuty(int index, uint8_t duty) -{ - if (index < 0 || index >= pinioHardwareCount) { - return; + // Clamp to valid range + if (duty > 100) { + duty = 100; } - if (!pinioRuntime[index].tch) { - return; + if (pinioRuntime[index].tch) { + // Timer-capable pin: set PWM duty cycle directly + *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; + } else { + // GPIO pin: treat as on/off (0 = off, any non-zero = on) + IOWrite(pinioRuntime[index].io, (duty > 0) ^ pinioRuntime[index].inverted); } +} - // Clamp to valid range and apply inversion - if (duty > 100) { - duty = 100; - } - *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; +// pinioSet is a convenience wrapper: on/off is just PWM at 100% or 0% duty +void pinioSet(int index, bool newState) +{ + pinioSetDuty(index, newState ? 100 : 0); } #endif diff --git a/src/main/programming/logic_condition.c b/src/main/programming/logic_condition.c index 5336a54ad12..3adfb178f62 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -509,17 +509,16 @@ static int logicConditionCompute( #ifdef USE_PINIO case LOGIC_CONDITION_PINIO_PWM: - // operandA = channel, operandB = duty cycle (0-100) - // Channels 0..PINIO_COUNT-1 = hardware PINIO (PWM capable) - // Channel PINIO_COUNT = LED strip idle level (binary: >0 = HIGH) + // operandA = duty cycle (0-100), operandB = pin (0=LED pin, 1=USER1, 2=USER2, ...) + // operandB=0 preserves backward compatibility with old LED_PIN_PWM behavior + if (operandB == 0) { #ifdef USE_LED_STRIP - if (operandA == PINIO_COUNT) { - ws2811SetIdleHigh(operandB > 0); - return operandB; - } + ws2811SetIdleHigh(operandA > 0); #endif - pinioSetDuty(operandA, (uint8_t)constrain(operandB, 0, 100)); - return operandB; + return operandA; + } + pinioSetDuty(operandB - 1, (uint8_t)constrain(operandA, 0, 100)); + return operandA; break; #endif #ifdef USE_GPS_FIX_ESTIMATION From 5ca5b499b221aa1b9411f0408d9c6ad386953155 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 8 Mar 2026 15:45:27 -0500 Subject: [PATCH 04/10] pinio pwm: Settings.md small update for osd_joystick_pinio_channel --- docs/Settings.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/Settings.md b/docs/Settings.md index 4e577bbedd5..7cd00c000ee 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -2332,16 +2332,6 @@ Used to prevent Iterm accumulation on during maneuvers. Iterm will be dampened w --- -### led_pin_pwm_mode - -PWM mode of LED pin. - -| Default | Min | Max | -| --- | --- | --- | -| SHARED_LOW | | | - ---- - ### limit_attn_filter_cutoff Throttle attenuation PI control output filter cutoff frequency @@ -5072,6 +5062,16 @@ PWM value for LEFT key --- +### osd_joystick_pinio_channel + +PINIO channel index (0-3) for the camera OSD control pin + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 3 | + +--- + ### osd_joystick_right PWM value for RIGHT key From cdc42c77d96bca7195d1860aa77de843060ebcb3 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 9 Mar 2026 19:47:06 -0500 Subject: [PATCH 05/10] Add TIM_USE_PINIO flag and label LED/PINIO outputs in output mapping - Add TIM_USE_PINIO usage flag so PINIO timer overrides are visible in the output mapping sent to configurator (previously all flags were cleared, causing pins to vanish from the mapping table) - Set TIM_USE_PINIO in timerHardwareOverride for OUTPUT_MODE_PINIO - Assign sequential USER labels to timer-override PINIO pins in MSP2_INAV_OUTPUT_MAPPING_EXT2, continuing from pinioHardwareCount and capped at PINIO_COUNT --- src/main/drivers/pwm_mapping.c | 2 +- src/main/drivers/timer.h | 2 ++ src/main/fc/fc_msp.c | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index 4ef3d5fea6d..32f279ba117 100644 --- a/src/main/drivers/pwm_mapping.c +++ b/src/main/drivers/pwm_mapping.c @@ -233,8 +233,8 @@ static void timerHardwareOverride(timerHardware_t * timer) { timer->usageFlags |= TIM_USE_LED; break; case OUTPUT_MODE_PINIO: - // Clear motor/servo/LED flags so pinioInit() can claim this timer timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED); + timer->usageFlags |= TIM_USE_PINIO; break; } } diff --git a/src/main/drivers/timer.h b/src/main/drivers/timer.h index d87e0400d52..217dea9ab18 100644 --- a/src/main/drivers/timer.h +++ b/src/main/drivers/timer.h @@ -117,6 +117,7 @@ typedef enum { //TIM_USE_FW_SERVO = (1 << 6), TIM_USE_LED = (1 << 24), // Remapping needs to be in the lower 8 bits. TIM_USE_BEEPER = (1 << 25), + TIM_USE_PINIO = (1 << 26), } timerUsageFlag_e; #define TIM_USE_OUTPUT_AUTO (TIM_USE_MOTOR | TIM_USE_SERVO) @@ -124,6 +125,7 @@ typedef enum { #define TIM_IS_MOTOR(flags) ((flags) & TIM_USE_MOTOR) #define TIM_IS_SERVO(flags) ((flags) & TIM_USE_SERVO) #define TIM_IS_LED(flags) ((flags) & TIM_USE_LED) +#define TIM_IS_PINIO(flags) ((flags) & TIM_USE_PINIO) #define TIM_IS_MOTOR_ONLY(flags) (TIM_IS_MOTOR(flags) && !TIM_IS_SERVO(flags)) #define TIM_IS_SERVO_ONLY(flags) (!TIM_IS_MOTOR(flags) && TIM_IS_SERVO(flags)) diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index 4b97d6df8f8..09ccd7eaee5 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -1675,6 +1675,9 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF #if !defined(SITL_BUILD) && defined(WS2811_PIN) ioTag_t led_tag = IO_TAG(WS2811_PIN); #endif + #ifdef USE_PINIO + int nextPinioIndex = pinioHardwareCount; + #endif for (uint8_t i = 0; i < timerHardwareCount; ++i) if (!(timerHardware[i].usageFlags & (TIM_USE_PPM | TIM_USE_PWM))) { @@ -1703,6 +1706,11 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF } } } + // Timer-override PINIO pins: assign next USER index (up to PINIO_COUNT) + if (specialLabel == PIN_LABEL_NONE && (timerHardware[i].usageFlags & TIM_USE_PINIO) && nextPinioIndex < PINIO_COUNT) { + specialLabel = PIN_LABEL_PINIO_BASE + nextPinioIndex; + nextPinioIndex++; + } #endif sbufWriteU8(dst, specialLabel); } From ec835c4c528f03046dedc519bc16bfcf5f5699e2 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 11 Mar 2026 22:53:59 -0500 Subject: [PATCH 06/10] pinio/pwm: Share user-assigned pins between box modes and programming tab --- src/main/drivers/pinio.c | 98 +++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index 31514cf28a4..ad9e9671600 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -65,21 +65,24 @@ const int pinioHardwareCount = ARRAYLEN(pinioHardware); /*** Runtime configuration ***/ typedef struct pinioRuntime_s { IO_t io; - TCH_t *tch; // Non-NULL when pin is configured in PWM mode + TCH_t *tch; // Non-NULL when pin is configured in PWM mode bool inverted; + uint8_t duty; // Timer mode: duty level (0–100) applied by pinioSet(true); + // updated by pinioSetDuty(). Defaults to 100 so a mode box + // activating with no programming framework condition gives full on. } pinioRuntime_t; static pinioRuntime_t pinioRuntime[PINIO_COUNT]; +static int pinioRuntimeCount = 0; void pinioInit(void) { - if (pinioHardwareCount == 0) { - return; - } + int runtimeCount = 0; - for (int i = 0; i < pinioHardwareCount; i++) { + // Pass 1: target-defined PINIO pins from pinioHardware[] (PINIO1_PIN–PINIO4_PIN). + // These may be GPIO-only pads or timer-capable pads; timer is preferred when available. + for (int i = 0; i < pinioHardwareCount && runtimeCount < PINIO_COUNT; i++) { IO_t io = IOGetByTag(pinioHardware[i].ioTag); - if (!io) { continue; } @@ -91,7 +94,7 @@ void pinioInit(void) if (timHw && IOGetOwner(io) == OWNER_FREE) { TCH_t *tch = timerGetTCH(timHw); if (tch) { - IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(i)); + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); // period=100 means CCR value is directly the duty percentage (0–100); // 2.4 MHz / 100 = 24 kHz PWM, above audible range @@ -99,34 +102,71 @@ void pinioInit(void) timerPWMConfigChannel(tch, 0); timerPWMStart(tch); timerEnable(tch); - pinioRuntime[i].tch = tch; - pinioRuntime[i].io = io; - pinioRuntime[i].inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; + pinioRuntime[runtimeCount].tch = tch; + pinioRuntime[runtimeCount].io = io; + pinioRuntime[runtimeCount].inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; + pinioRuntime[runtimeCount].duty = 100; // default: mode box on = full on // Start in the "off" state: HIGH if inverted, LOW if normal - *timerCCR(tch) = pinioRuntime[i].inverted ? 100 : 0; + *timerCCR(tch) = pinioRuntime[runtimeCount].inverted ? 100 : 0; + runtimeCount++; continue; } } - // GPIO fallback: no timer available or pin already claimed - IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(i)); + // GPIO fallback: no timer available or pin already claimed by another subsystem + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); IOConfigGPIO(io, pinioHardware[i].ioMode); - if (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) { - pinioRuntime[i].inverted = true; + pinioRuntime[runtimeCount].inverted = true; IOHi(io); } else { - pinioRuntime[i].inverted = false; + pinioRuntime[runtimeCount].inverted = false; IOLo(io); } + pinioRuntime[runtimeCount].io = io; + runtimeCount++; + } + + // Pass 2: timer outputs assigned to PINIO mode via the mixer (TIM_USE_PINIO flag). + // These pins are NOT pre-defined in target.h; the user assigns them in the configurator. + // pwmMotorAndServoInit() left them unclaimed; we pick them up here in timerHardware[] order. + for (int i = 0; i < timerHardwareCount && runtimeCount < PINIO_COUNT; i++) { + const timerHardware_t *timHw = &timerHardware[i]; + if (!TIM_IS_PINIO(timHw->usageFlags)) { + continue; + } + + IO_t io = IOGetByTag(timHw->tag); + if (!io || IOGetOwner(io) != OWNER_FREE) { + // Skip invalid pins and pins already claimed by Pass 1 + continue; + } - pinioRuntime[i].io = io; + TCH_t *tch = timerGetTCH(timHw); + if (!tch) { + continue; + } + + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); + IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); + timerConfigBase(tch, 100, 2400000); + timerPWMConfigChannel(tch, 0); + timerPWMStart(tch); + timerEnable(tch); + pinioRuntime[runtimeCount].tch = tch; + pinioRuntime[runtimeCount].io = io; + pinioRuntime[runtimeCount].inverted = false; + pinioRuntime[runtimeCount].duty = 100; // default: mode box on = full on + *timerCCR(tch) = 0; + runtimeCount++; } + + pinioRuntimeCount = runtimeCount; } void pinioSetDuty(int index, uint8_t duty) { - if (index < 0 || index >= pinioHardwareCount) { + if (index < 0 || index >= pinioRuntimeCount) { return; } @@ -140,7 +180,7 @@ void pinioSetDuty(int index, uint8_t duty) } if (pinioRuntime[index].tch) { - // Timer-capable pin: set PWM duty cycle directly + pinioRuntime[index].duty = duty; *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; } else { // GPIO pin: treat as on/off (0 = off, any non-zero = on) @@ -148,9 +188,25 @@ void pinioSetDuty(int index, uint8_t duty) } } -// pinioSet is a convenience wrapper: on/off is just PWM at 100% or 0% duty +// pinioSet is called by PINIOBOX when an RC mode is assigned to this channel. +// For GPIO channels: drives the pin high or low directly. +// For timer channels: active = output at stored duty level (set by pinioSetDuty, +// defaults to 100%); inactive = output at 0%. This integrates mode-box on/off +// with programming-framework duty control: the mode box gates the output, and +// pinioSetDuty() sets the level applied when the gate is open. +// Channels with no mode box assigned are never called from PINIOBOX, so the +// programming framework retains exclusive uninterrupted control in that case. void pinioSet(int index, bool newState) { - pinioSetDuty(index, newState ? 100 : 0); + if (index < 0 || index >= pinioRuntimeCount || !pinioRuntime[index].io) { + return; + } + + if (pinioRuntime[index].tch) { + uint8_t duty = newState ? pinioRuntime[index].duty : 0; + *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; + } else { + IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); + } } #endif From 15f08278b05087c0f44d4a5162262ded49e7558c Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 11 Mar 2026 23:11:59 -0500 Subject: [PATCH 07/10] pinio/pwm: First round simplify most recent commits --- src/main/drivers/pinio.c | 130 ++++++++++++++++---------------------- src/main/fc/settings.yaml | 20 +++--- 2 files changed, 63 insertions(+), 87 deletions(-) diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index ad9e9671600..09fcbe1257e 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -29,6 +29,15 @@ #include "drivers/io.h" #include "drivers/pinio.h" +// CCR = duty% directly; 2.4 MHz / 100 = 24 kHz PWM, above audible range +#define PINIO_PWM_PERIOD 100 +#define PINIO_PWM_BASE_HZ 2400000 + +static inline uint8_t pinioEffectiveDuty(uint8_t duty, bool inverted) +{ + return inverted ? (100 - duty) : duty; +} + /*** Hardware definitions ***/ const pinioHardware_t pinioHardware[] = { #if defined(PINIO1_PIN) @@ -75,90 +84,72 @@ typedef struct pinioRuntime_s { static pinioRuntime_t pinioRuntime[PINIO_COUNT]; static int pinioRuntimeCount = 0; +// Configure one PINIO runtime slot in PWM mode. Returns false if no TCH available. +static bool pinioInitTimerPWM(int slot, IO_t io, const timerHardware_t *timHw, bool inverted) +{ + TCH_t *tch = timerGetTCH(timHw); + if (!tch) { + return false; + } + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(slot)); + IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); + timerConfigBase(tch, PINIO_PWM_PERIOD, PINIO_PWM_BASE_HZ); + timerPWMConfigChannel(tch, 0); + timerPWMStart(tch); + timerEnable(tch); + pinioRuntime[slot].tch = tch; + pinioRuntime[slot].io = io; + pinioRuntime[slot].inverted = inverted; + pinioRuntime[slot].duty = 100; // default: mode box on = full on + *timerCCR(tch) = pinioEffectiveDuty(0, inverted); // start off + return true; +} + void pinioInit(void) { int runtimeCount = 0; - // Pass 1: target-defined PINIO pins from pinioHardware[] (PINIO1_PIN–PINIO4_PIN). - // These may be GPIO-only pads or timer-capable pads; timer is preferred when available. + // Pass 1: target-defined PINIO pins (PINIO1_PIN–PINIO4_PIN in target.h). + // Timer-capable pins are configured as PWM; GPIO-only pins fall back to IOWrite. + // pwmMotorAndServoInit() runs before pinioInit(), so motor/servo pins are already + // owned and the OWNER_FREE check correctly skips dual-assigned pads. for (int i = 0; i < pinioHardwareCount && runtimeCount < PINIO_COUNT; i++) { IO_t io = IOGetByTag(pinioHardware[i].ioTag); if (!io) { continue; } - // If the pin has a timer and is unclaimed, configure it as a PWM output. - // pwmMotorAndServoInit() runs before pinioInit(), so claimed motor/servo pins - // are already owned and the OWNER_FREE check correctly skips them. + bool inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; const timerHardware_t *timHw = timerGetByTag(pinioHardware[i].ioTag, TIM_USE_ANY); - if (timHw && IOGetOwner(io) == OWNER_FREE) { - TCH_t *tch = timerGetTCH(timHw); - if (tch) { - IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); - IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); - // period=100 means CCR value is directly the duty percentage (0–100); - // 2.4 MHz / 100 = 24 kHz PWM, above audible range - timerConfigBase(tch, 100, 2400000); - timerPWMConfigChannel(tch, 0); - timerPWMStart(tch); - timerEnable(tch); - pinioRuntime[runtimeCount].tch = tch; - pinioRuntime[runtimeCount].io = io; - pinioRuntime[runtimeCount].inverted = (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) != 0; - pinioRuntime[runtimeCount].duty = 100; // default: mode box on = full on - // Start in the "off" state: HIGH if inverted, LOW if normal - *timerCCR(tch) = pinioRuntime[runtimeCount].inverted ? 100 : 0; - runtimeCount++; - continue; - } + if (timHw && IOGetOwner(io) == OWNER_FREE && pinioInitTimerPWM(runtimeCount, io, timHw, inverted)) { + runtimeCount++; + continue; } - // GPIO fallback: no timer available or pin already claimed by another subsystem + // GPIO fallback: no timer available or pin already claimed IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); IOConfigGPIO(io, pinioHardware[i].ioMode); - if (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) { - pinioRuntime[runtimeCount].inverted = true; - IOHi(io); - } else { - pinioRuntime[runtimeCount].inverted = false; - IOLo(io); - } + pinioRuntime[runtimeCount].inverted = inverted; pinioRuntime[runtimeCount].io = io; + inverted ? IOHi(io) : IOLo(io); runtimeCount++; } - // Pass 2: timer outputs assigned to PINIO mode via the mixer (TIM_USE_PINIO flag). - // These pins are NOT pre-defined in target.h; the user assigns them in the configurator. - // pwmMotorAndServoInit() left them unclaimed; we pick them up here in timerHardware[] order. + // Pass 2: timer outputs assigned PINIO mode via the mixer (TIM_USE_PINIO flag). + // These pins have no PINIO_N_PIN target definition; the user assigns them in the + // configurator. pwmMotorAndServoInit() left them unclaimed; pick them up here. for (int i = 0; i < timerHardwareCount && runtimeCount < PINIO_COUNT; i++) { const timerHardware_t *timHw = &timerHardware[i]; if (!TIM_IS_PINIO(timHw->usageFlags)) { continue; } - IO_t io = IOGetByTag(timHw->tag); if (!io || IOGetOwner(io) != OWNER_FREE) { - // Skip invalid pins and pins already claimed by Pass 1 continue; } - - TCH_t *tch = timerGetTCH(timHw); - if (!tch) { - continue; + if (pinioInitTimerPWM(runtimeCount, io, timHw, false)) { + runtimeCount++; } - - IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); - IOConfigGPIOAF(io, IOCFG_AF_PP, timHw->alternateFunction); - timerConfigBase(tch, 100, 2400000); - timerPWMConfigChannel(tch, 0); - timerPWMStart(tch); - timerEnable(tch); - pinioRuntime[runtimeCount].tch = tch; - pinioRuntime[runtimeCount].io = io; - pinioRuntime[runtimeCount].inverted = false; - pinioRuntime[runtimeCount].duty = 100; // default: mode box on = full on - *timerCCR(tch) = 0; - runtimeCount++; } pinioRuntimeCount = runtimeCount; @@ -166,45 +157,34 @@ void pinioInit(void) void pinioSetDuty(int index, uint8_t duty) { - if (index < 0 || index >= pinioRuntimeCount) { + if ((unsigned)index >= (unsigned)pinioRuntimeCount) { return; } - - if (!pinioRuntime[index].io) { - return; - } - - // Clamp to valid range if (duty > 100) { duty = 100; } - if (pinioRuntime[index].tch) { pinioRuntime[index].duty = duty; - *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; + *timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); } else { - // GPIO pin: treat as on/off (0 = off, any non-zero = on) IOWrite(pinioRuntime[index].io, (duty > 0) ^ pinioRuntime[index].inverted); } } -// pinioSet is called by PINIOBOX when an RC mode is assigned to this channel. +// pinioSet is called by PINIOBOX when an RC mode box is assigned to this channel. // For GPIO channels: drives the pin high or low directly. -// For timer channels: active = output at stored duty level (set by pinioSetDuty, -// defaults to 100%); inactive = output at 0%. This integrates mode-box on/off -// with programming-framework duty control: the mode box gates the output, and -// pinioSetDuty() sets the level applied when the gate is open. -// Channels with no mode box assigned are never called from PINIOBOX, so the -// programming framework retains exclusive uninterrupted control in that case. +// For timer channels: active = output at stored duty (set by pinioSetDuty, default 100%); +// inactive = 0%. The stored duty is NOT modified, so deactivating and reactivating the +// mode box restores the programmed level. Channels with no mode box assigned are never +// called from PINIOBOX, giving the programming framework exclusive uninterrupted control. void pinioSet(int index, bool newState) { - if (index < 0 || index >= pinioRuntimeCount || !pinioRuntime[index].io) { + if ((unsigned)index >= (unsigned)pinioRuntimeCount) { return; } - if (pinioRuntime[index].tch) { uint8_t duty = newState ? pinioRuntime[index].duty : 0; - *timerCCR(pinioRuntime[index].tch) = pinioRuntime[index].inverted ? (100 - duty) : duty; + *timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); } else { IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); } diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 89cc479e3a8..ede01910544 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -4104,35 +4104,31 @@ groups: members: - name: pinio_box1 field: permanentId[0] - description: "Mode assignment for PINIO#1" - default_value: "target specific" + description: "Mode box assignment for PINIO channel 1" min: 0 max: 255 - default_value: :BOX_PERMANENT_ID_NONE + default_value: :BOX_PERMANENT_ID_USER1 type: uint8_t - name: pinio_box2 field: permanentId[1] - default_value: "target specific" - description: "Mode assignment for PINIO#1" + description: "Mode box assignment for PINIO channel 2" min: 0 max: 255 - default_value: :BOX_PERMANENT_ID_NONE + default_value: :BOX_PERMANENT_ID_USER2 type: uint8_t - name: pinio_box3 field: permanentId[2] - default_value: "target specific" - description: "Mode assignment for PINIO#1" + description: "Mode box assignment for PINIO channel 3" min: 0 max: 255 - default_value: :BOX_PERMANENT_ID_NONE + default_value: :BOX_PERMANENT_ID_USER3 type: uint8_t - name: pinio_box4 field: permanentId[3] - default_value: "target specific" - description: "Mode assignment for PINIO#1" + description: "Mode box assignment for PINIO channel 4" min: 0 max: 255 - default_value: :BOX_PERMANENT_ID_NONE + default_value: :BOX_PERMANENT_ID_USER4 type: uint8_t - name: PG_LOG_CONFIG From 39ebba5e27bf95143ab5d694e7b3c59eae37f15b Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Fri, 13 Mar 2026 16:33:39 -0500 Subject: [PATCH 08/10] pinio/pwm: better integrate LED pin, simplify --- docs/Cli.md | 1 + docs/PINIO PWM.md | 50 ++++++++++++++++++-------- docs/Programming Framework.md | 2 +- docs/Settings.md | 16 ++++----- src/main/drivers/pinio.c | 34 ++++++++++++++---- src/main/drivers/pinio.h | 1 + src/main/fc/cli.c | 27 +++++--------- src/main/io/osd_joystick.c | 4 +-- src/main/io/piniobox.c | 3 +- src/main/programming/logic_condition.c | 13 ++----- 10 files changed, 89 insertions(+), 62 deletions(-) diff --git a/docs/Cli.md b/docs/Cli.md index d949937a61b..9399bef6a65 100644 --- a/docs/Cli.md +++ b/docs/Cli.md @@ -100,6 +100,7 @@ While connected to the CLI, all Logical Switches are temporarily disabled (5.1.0 | `msc` | Enter USB Mass storage mode. See [USB MSC documentation](USB_Mass_Storage_(MSC)_mode.md) for usage information. | | `osd_layout` | Get or set the layout of OSD items | | `pid` | Configurable PID controllers | +| `piniopwm` | Set PINIO PWM duty cycle. See [PINIO PWM](PINIO%20PWM.md) | | `play_sound` | ``, or none for next item | | `control_profile` | Change profile | | `resource` | View currently used resources | diff --git a/docs/PINIO PWM.md b/docs/PINIO PWM.md index fbc9b80a335..b1be27cc046 100644 --- a/docs/PINIO PWM.md +++ b/docs/PINIO PWM.md @@ -2,16 +2,22 @@ INAV provides two mechanisms for generating output signals on GPIO pins: -1. **PINIO channels (0-3)** — Any PWM-capable timer output defined as `PINIOx_PIN` in the target. Supports full 0-100% duty cycle PWM at 24 kHz. -2. **LED strip idle level (channel 4)** — The WS2812 LED strip pin can be switched between idle-LOW and idle-HIGH between LED update bursts. Binary on/off only. +1. **PINIO channels (1-4)** — PWM-capable timer outputs, either defined in the target (`PINIOx_PIN`) or assigned by the user in the configurator's Output tab. Supports full 0-100% duty cycle PWM at 24 kHz. +2. **LED strip idle level (channel 0)** — The WS2812 LED strip pin can be switched between idle-LOW and idle-HIGH between LED update bursts. Binary on/off only. ## PINIO PWM channels -PINIO channels are configured per-target in `target.h` using `PINIO1_PIN` through `PINIO4_PIN`. When a PINIO pin has a timer, it is automatically configured as a 24 kHz PWM output. +PINIO channels can come from two sources: + +1. **Target-defined:** `PINIO1_PIN` through `PINIO4_PIN` in `target.h`. These are always available on supported boards. +2. **User-assigned:** Any timer output pad can be set to PINIO mode in the configurator's Output tab (timer_output_mode = PINIO). No target.h changes needed. + +When a PINIO pin has a hardware timer, it is automatically configured as a 24 kHz PWM output. Pins without a timer fall back to GPIO on/off. PWM duty cycle can be controlled via: -- **CLI:** `piniopwm [channel] ` (duty = 0-100) -- **Programming framework:** Operation 52, Operand A = channel (0-3), Operand B = duty (0-100) +- **CLI:** `piniopwm ` (channel = 1-4, duty = 0-100) +- **Programming framework:** Operation 52, Operand A = duty (0-100), Operand B = channel (1-4) +- **Mode boxes:** USER1-USER4 in the Modes tab toggle the channel on/off Setting duty to 0 stops PWM generation (pin goes LOW, or HIGH if `PINIO_FLAGS_INVERTED` is set in target.h). @@ -21,14 +27,25 @@ PWM frequency is fixed to 24kHz with duty ratio between 0 and 100%: ![alt text](/docs/assets/images/led_pin_pwm.png "PINIO PWM signal") -## LED strip idle level (channel 4) +## Mode box and programming framework interaction + +PINIO channels can be controlled by both mode boxes (Modes tab) and the programming framework (Programming tab). When both are used: + +- The **programming framework** sets the duty cycle (0-100%). +- The **mode box** gates the output on or off. When the mode is active, the programmed duty is output. When inactive, the pin is driven to 0%. + +If no RC channel range is assigned to the USERx mode in the Modes tab, the programming framework has exclusive uninterrupted control — the mode box does not interfere. + +The default duty (before the programming framework sets a value) is 100%, so toggling a mode box without any programming gives full on/off behavior. + +## LED strip idle level (channel 0) When the LED strip feature is enabled, the WS2812 pin sends data bursts (~1 ms) every 10-20 ms. Between bursts, the pin idles at a configurable level. -The LED strip idle level is accessible as channel `4` (the next channel after PINIO hardware channels 0-3): +The LED strip idle level is accessible as channel `0`: -- **CLI:** `piniopwm 4 ` — value > 0 sets idle HIGH, 0 sets idle LOW -- **Programming framework:** Operation 52, Operand A = 4, Operand B = value (>0 = HIGH, 0 = LOW) +- **CLI:** `piniopwm 0 ` — value > 0 sets idle HIGH, 0 sets idle LOW +- **Programming framework:** Operation 52, Operand A = value (>0 = HIGH, 0 = LOW), Operand B = 0 This can be used to drive a MOSFET or similar device connected to the LED pin, toggled by the programming framework based on flight mode, RC channel, GPS state, etc. @@ -43,17 +60,22 @@ Normally LED pin is held low between WS2812 updates: When idle is set HIGH, the pin is held high between updates. Total ws2812 pulse duration is ~1ms with ~9ms pauses. Connected devices should be tolerant of these brief transients. -# Generating PWM/output signals with programming framework +# Channel numbering + +| Channel | Target | Description | +|---------|--------|-------------| +| 0 | LED strip | Binary idle level (HIGH/LOW) | +| 1-4 | PINIO 1-4 | Full 0-100% PWM at 24 kHz | -See operation 52 "PINIO PWM" in [Programming Framework](Programming%20Framework.md) +This numbering is consistent across the CLI (`piniopwm`), programming framework (operation 52), and the `pinio_box1`-`pinio_box4` settings. # Generating PWM/output signals from CLI `piniopwm [channel] ` — channel = 0-4, duty = 0-100 -- One argument: sets duty on channel 0 (backward compatible) -- Two arguments: first is channel, second is duty -- No arguments: stops PWM on channel 0 +- One argument: sets LED idle level (channel 0, backward compatible) +- Two arguments: first is channel (0 = LED idle, 1-4 = PINIO), second is duty +- No arguments: stops PWM on PINIO 1 # Example of driving LED diff --git a/docs/Programming Framework.md b/docs/Programming Framework.md index 9a374a07e6a..c30f5a69aea 100644 --- a/docs/Programming Framework.md +++ b/docs/Programming Framework.md @@ -114,7 +114,7 @@ for complete documentation on using JavaScript to program your flight controller | 49 | Timer | A simple on - off timer. `true` for the duration of `Operand A` [ms]. Then `false` for the duration of `Operand B` [ms]. | | 50 | Delta | This returns `true` when the value of `Operand A` has changed by the value of `Operand B` or greater within 100ms. ( \|ΔA\| >= B ) | | 51 | Approx Equals (A ~ B) | `true` if `Operand B` is within 1% of `Operand A`. | -| 52 | PINIO PWM | `Operand A` = channel (0-3 for PINIO hardware, 4 for LED strip idle level). `Operand B` = duty cycle (0-100). Channels 0-3 support full PWM; channel 4 is binary (>0 = HIGH). See [PINIO PWM](PINIO%20PWM.md). | +| 52 | PINIO PWM | `Operand A` = duty cycle (0-100). `Operand B` = channel (0 for LED strip idle level, 1-4 for PINIO channels). Channels 1-4 support full PWM; channel 0 is binary (>0 = HIGH). See [PINIO PWM](PINIO%20PWM.md). | | 53 | Disable GPS Sensor Fix | Disables the GNSS sensor fix. For testing GNSS failure. | | 54 | Mag calibration | Trigger a magnetometer calibration. | | 55 | Set Gimbal Sensitivity | Scales `Operand A` from [`-16` : `15`] diff --git a/docs/Settings.md b/docs/Settings.md index 7cd00c000ee..4b6ae28160e 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -5574,41 +5574,41 @@ Pilot name ### pinio_box1 -Mode assignment for PINIO#1 +Mode box assignment for PINIO channel 1 | Default | Min | Max | | --- | --- | --- | -| `BOX_PERMANENT_ID_NONE` | 0 | 255 | +| `BOX_PERMANENT_ID_USER1` | 0 | 255 | --- ### pinio_box2 -Mode assignment for PINIO#1 +Mode box assignment for PINIO channel 2 | Default | Min | Max | | --- | --- | --- | -| `BOX_PERMANENT_ID_NONE` | 0 | 255 | +| `BOX_PERMANENT_ID_USER2` | 0 | 255 | --- ### pinio_box3 -Mode assignment for PINIO#1 +Mode box assignment for PINIO channel 3 | Default | Min | Max | | --- | --- | --- | -| `BOX_PERMANENT_ID_NONE` | 0 | 255 | +| `BOX_PERMANENT_ID_USER3` | 0 | 255 | --- ### pinio_box4 -Mode assignment for PINIO#1 +Mode box assignment for PINIO channel 4 | Default | Min | Max | | --- | --- | --- | -| `BOX_PERMANENT_ID_NONE` | 0 | 255 | +| `BOX_PERMANENT_ID_USER4` | 0 | 255 | --- diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index 09fcbe1257e..04f29c5cf59 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -28,6 +28,9 @@ #include "common/memory.h" #include "drivers/io.h" #include "drivers/pinio.h" +#ifdef USE_LED_STRIP +#include "drivers/light_ws2811strip.h" +#endif // CCR = duty% directly; 2.4 MHz / 100 = 24 kHz PWM, above audible range #define PINIO_PWM_PERIOD 100 @@ -74,8 +77,9 @@ const int pinioHardwareCount = ARRAYLEN(pinioHardware); /*** Runtime configuration ***/ typedef struct pinioRuntime_s { IO_t io; - TCH_t *tch; // Non-NULL when pin is configured in PWM mode + volatile timCCR_t *ccr; // Cached CCR register pointer (NULL for GPIO-only pins) bool inverted; + bool active; // Mode box state; defaults to true (no RC channel = always active) uint8_t duty; // Timer mode: duty level (0–100) applied by pinioSet(true); // updated by pinioSetDuty(). Defaults to 100 so a mode box // activating with no programming framework condition gives full on. @@ -97,11 +101,12 @@ static bool pinioInitTimerPWM(int slot, IO_t io, const timerHardware_t *timHw, b timerPWMConfigChannel(tch, 0); timerPWMStart(tch); timerEnable(tch); - pinioRuntime[slot].tch = tch; + pinioRuntime[slot].ccr = timerCCR(tch); pinioRuntime[slot].io = io; pinioRuntime[slot].inverted = inverted; + pinioRuntime[slot].active = true; pinioRuntime[slot].duty = 100; // default: mode box on = full on - *timerCCR(tch) = pinioEffectiveDuty(0, inverted); // start off + *pinioRuntime[slot].ccr = pinioEffectiveDuty(0, inverted); // start off return true; } @@ -155,17 +160,31 @@ void pinioInit(void) pinioRuntimeCount = runtimeCount; } +int pinioGetRuntimeCount(void) +{ + return pinioRuntimeCount; +} + void pinioSetDuty(int index, uint8_t duty) { +#ifdef USE_LED_STRIP + if (index == 0) { + ws2811SetIdleHigh(duty > 0); + return; + } +#endif + index--; // user-facing 1-4 → runtime 0-3 if ((unsigned)index >= (unsigned)pinioRuntimeCount) { return; } if (duty > 100) { duty = 100; } - if (pinioRuntime[index].tch) { + if (pinioRuntime[index].ccr) { pinioRuntime[index].duty = duty; - *timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); + if (pinioRuntime[index].active) { + *pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); + } } else { IOWrite(pinioRuntime[index].io, (duty > 0) ^ pinioRuntime[index].inverted); } @@ -182,9 +201,10 @@ void pinioSet(int index, bool newState) if ((unsigned)index >= (unsigned)pinioRuntimeCount) { return; } - if (pinioRuntime[index].tch) { + if (pinioRuntime[index].ccr) { + pinioRuntime[index].active = newState; uint8_t duty = newState ? pinioRuntime[index].duty : 0; - *timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); + *pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); } else { IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); } diff --git a/src/main/drivers/pinio.h b/src/main/drivers/pinio.h index 2b15c9633b2..cc55fad00d8 100644 --- a/src/main/drivers/pinio.h +++ b/src/main/drivers/pinio.h @@ -41,3 +41,4 @@ extern const int pinioHardwareCount; void pinioInit(void); void pinioSet(int index, bool newState); void pinioSetDuty(int index, uint8_t duty); +int pinioGetRuntimeCount(void); diff --git a/src/main/fc/cli.c b/src/main/fc/cli.c index ce49099613a..8778255adba 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -2173,46 +2173,37 @@ static void cliModeColor(char *cmdline) #endif // USE_LED_STRIP #ifdef USE_PINIO +// Channel numbering: 0 = LED strip idle level, 1-4 = PINIO channels (matches programming framework) static void cliPinioPwm(char *cmdline) { int channel = 0; int duty; if (isEmpty(cmdline)) { - pinioSetDuty(0, 0); - cliPrintLine("PWM stopped on channel 0"); + pinioSetDuty(1, 0); + cliPrintLine("PWM stopped on PINIO 1"); return; } - // Find second argument (space-separated) - char *dutyStr = strchr(cmdline, ' '); + const char *dutyStr = nextArg(cmdline); if (dutyStr) { - // Two args: channel duty channel = fastA2I(cmdline); - dutyStr++; duty = fastA2I(dutyStr); } else { - // One arg: duty on channel 0 + // One arg: duty on channel 0 (LED idle, backward compat with old LED_PIN_PWM) duty = fastA2I(cmdline); } - if (channel < 0 || channel > PINIO_COUNT) { - cliPrintLinef("Error: channel must be 0-%d", PINIO_COUNT); + const int maxChannel = MAX(pinioGetRuntimeCount(), PINIO_COUNT); + if (channel < 0 || channel > maxChannel) { + cliShowArgumentRangeError("channel", 0, maxChannel); return; } if (duty < 0 || duty > 100) { - cliPrintLine("Error: duty must be 0-100"); + cliShowArgumentRangeError("duty", 0, 100); return; } -#ifdef USE_LED_STRIP - if (channel == PINIO_COUNT) { - ws2811SetIdleHigh(duty > 0); - cliPrintLinef("LED idle %s", duty > 0 ? "HIGH" : "LOW"); - return; - } -#endif - pinioSetDuty(channel, (uint8_t)duty); cliPrintLinef("PWM ch %d: %d%%", channel, duty); } diff --git a/src/main/io/osd_joystick.c b/src/main/io/osd_joystick.c index def03c66590..9fba216fe6d 100644 --- a/src/main/io/osd_joystick.c +++ b/src/main/io/osd_joystick.c @@ -46,7 +46,7 @@ bool osdJoystickEnabled(void) { void osdJoystickSimulate5KeyButtonPress(uint8_t operation) { - const int ch = osdJoystickConfig()->pinio_channel; + const int ch = osdJoystickConfig()->pinio_channel + 1; // setting is 0-indexed, pinioSetDuty is 1-indexed switch (operation) { case RCDEVICE_CAM_KEY_ENTER: pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_enter); @@ -68,7 +68,7 @@ void osdJoystickSimulate5KeyButtonPress(uint8_t operation) { void osdJoystickSimulate5KeyButtonRelease(void) { - pinioSetDuty(osdJoystickConfig()->pinio_channel, 0); + pinioSetDuty(osdJoystickConfig()->pinio_channel + 1, 0); } diff --git a/src/main/io/piniobox.c b/src/main/io/piniobox.c index 4fbcc657e4a..e9942a58d0b 100644 --- a/src/main/io/piniobox.c +++ b/src/main/io/piniobox.c @@ -63,7 +63,8 @@ void pinioBoxInit(void) void pinioBoxUpdate(void) { for (int i = 0; i < PINIO_COUNT; i++) { - if (pinioBoxRuntimeConfig.boxId[i] != BOXID_NONE) { + if (pinioBoxRuntimeConfig.boxId[i] != BOXID_NONE && + isModeActivationConditionPresent(pinioBoxRuntimeConfig.boxId[i])) { pinioSet(i, IS_RC_MODE_ACTIVE(pinioBoxRuntimeConfig.boxId[i])); } } diff --git a/src/main/programming/logic_condition.c b/src/main/programming/logic_condition.c index 3adfb178f62..500070316b9 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -58,7 +58,6 @@ #include "io/vtx.h" #include "drivers/vtx_common.h" -#include "drivers/light_ws2811strip.h" #include "drivers/pinio.h" PG_REGISTER_ARRAY_WITH_RESET_FN(logicCondition_t, MAX_LOGIC_CONDITIONS, logicConditions, PG_LOGIC_CONDITIONS, 4); @@ -509,17 +508,9 @@ static int logicConditionCompute( #ifdef USE_PINIO case LOGIC_CONDITION_PINIO_PWM: - // operandA = duty cycle (0-100), operandB = pin (0=LED pin, 1=USER1, 2=USER2, ...) - // operandB=0 preserves backward compatibility with old LED_PIN_PWM behavior - if (operandB == 0) { -#ifdef USE_LED_STRIP - ws2811SetIdleHigh(operandA > 0); -#endif - return operandA; - } - pinioSetDuty(operandB - 1, (uint8_t)constrain(operandA, 0, 100)); + // operandA = duty cycle (0-100), operandB = channel (0=LED idle, 1-4=PINIO) + pinioSetDuty(operandB, (uint8_t)constrain(operandA, 0, 100)); return operandA; - break; #endif #ifdef USE_GPS_FIX_ESTIMATION case LOGIC_CONDITION_DISABLE_GPS_FIX: From 7eebdf634563aa3dd8ed2ff6973b88c770a9fb82 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 3 May 2026 22:29:44 -0500 Subject: [PATCH 09/10] pwm_mapping: guard beeper timer from timerHardwareOverride() If a user sets a timer_output_mode override on the same timer as the PWM beeper, timerHardwareOverride() would modify TIM_USE_BEEPER flags. A subsequent pwmAssignOutput() call then strips all non-assigned flags (e.g. usageFlags &= TIM_USE_SERVO), silently clearing TIM_USE_BEEPER so beeperPwmInit() can no longer find its timer. Fix: return early from timerHardwareOverride() for any timer that has TIM_USE_BEEPER set, leaving it untouched by all output mode overrides including OUTPUT_MODE_MOTORS, SERVOS, LED, and PINIO. Fixes beeper_pwm_mode regression on MATEKH743 (TIM2/PA15) and MATEKF405CAN (TIM1/PA8) as reported in issue #11492. --- src/main/drivers/pwm_mapping.c | 112 ++++++++++++++++----------------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/src/main/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index 32f279ba117..7bb4b3477e3 100644 --- a/src/main/drivers/pwm_mapping.c +++ b/src/main/drivers/pwm_mapping.c @@ -219,6 +219,10 @@ 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); @@ -274,73 +278,84 @@ uint8_t pwmClaimTimer(HAL_Timer_t *tim, uint32_t usageFlags) { return changed; } -void pwmEnsureEnoughtMotors(uint8_t motorCount) +static void pwmAssignOutput(timMotorServoHardware_t *timOutputs, timerHardware_t *timHw, int type) { - uint8_t motorOnlyOutputs = 0; - - for (int idx = 0; idx < timerHardwareCount; idx++) { - timerHardware_t *timHw = &timerHardware[idx]; + switch (type) { + case MAP_TO_MOTOR_OUTPUT: + timHw->usageFlags &= TIM_USE_MOTOR; + timOutputs->timMotors[timOutputs->maxTimMotorCount++] = timHw; + pwmClaimTimer(timHw->tim, timHw->usageFlags); + break; + case MAP_TO_SERVO_OUTPUT: + timHw->usageFlags &= TIM_USE_SERVO; + timOutputs->timServos[timOutputs->maxTimServoCount++] = timHw; + pwmClaimTimer(timHw->tim, timHw->usageFlags); + break; + case MAP_TO_LED_OUTPUT: + timHw->usageFlags &= TIM_USE_LED; + pwmClaimTimer(timHw->tim, timHw->usageFlags); + break; + default: + break; + } +} - timerHardwareOverride(timHw); +void pwmBuildTimerOutputList(timMotorServoHardware_t *timOutputs, bool isMixerUsingServos) +{ + UNUSED(isMixerUsingServos); + timOutputs->maxTimMotorCount = 0; + timOutputs->maxTimServoCount = 0; - if (checkPwmTimerConflicts(timHw)) { - continue; - } + uint8_t motorCount = getMotorCount(); + uint8_t motorIdx = 0; - if (TIM_IS_MOTOR_ONLY(timHw->usageFlags)) { - motorOnlyOutputs++; - motorOnlyOutputs += pwmClaimTimer(timHw->tim, timHw->usageFlags); - } + // Apply all timerOverrides upfront so flag state is stable for both passes + for (int idx = 0; idx < timerHardwareCount; idx++) { + timerHardwareOverride(&timerHardware[idx]); } + // Pass 0: Dedicated timers — explicitly assigned OUTPUT_MODE_MOTORS or OUTPUT_MODE_SERVOS + // These take priority over AUTO timers regardless of array position for (int idx = 0; idx < timerHardwareCount; idx++) { timerHardware_t *timHw = &timerHardware[idx]; + uint8_t outputMode = timerOverrides(timer2id(timHw->tim))->outputMode; if (checkPwmTimerConflicts(timHw)) { continue; } - if (TIM_IS_MOTOR(timHw->usageFlags) && !TIM_IS_MOTOR_ONLY(timHw->usageFlags)) { - if (motorOnlyOutputs < motorCount) { - timHw->usageFlags &= ~TIM_USE_SERVO; - timHw->usageFlags |= TIM_USE_MOTOR; - motorOnlyOutputs++; - motorOnlyOutputs += pwmClaimTimer(timHw->tim, timHw->usageFlags); - } else { - timHw->usageFlags &= ~TIM_USE_MOTOR; - pwmClaimTimer(timHw->tim, timHw->usageFlags); - } + if (outputMode == OUTPUT_MODE_MOTORS && TIM_IS_MOTOR(timHw->usageFlags) && + !pwmHasServoOnTimer(timOutputs, timHw->tim) && motorIdx < motorCount) { + pwmAssignOutput(timOutputs, timHw, MAP_TO_MOTOR_OUTPUT); + motorIdx++; + } else if (outputMode == OUTPUT_MODE_SERVOS && TIM_IS_SERVO(timHw->usageFlags) && + !pwmHasMotorOnTimer(timOutputs, timHw->tim)) { + pwmAssignOutput(timOutputs, timHw, MAP_TO_SERVO_OUTPUT); } } -} - -void pwmBuildTimerOutputList(timMotorServoHardware_t * timOutputs, bool isMixerUsingServos) -{ - UNUSED(isMixerUsingServos); - timOutputs->maxTimMotorCount = 0; - timOutputs->maxTimServoCount = 0; - - uint8_t motorCount = getMotorCount(); - uint8_t motorIdx = 0; - - pwmEnsureEnoughtMotors(motorCount); + // Pass 1: AUTO timers — fill remaining motors, servos, and LEDs in array order for (int idx = 0; idx < timerHardwareCount; idx++) { timerHardware_t *timHw = &timerHardware[idx]; + uint8_t outputMode = timerOverrides(timer2id(timHw->tim))->outputMode; - int type = MAP_TO_NONE; - - // Check for known conflicts (i.e. UART, LEDSTRIP, Rangefinder and ADC) if (checkPwmTimerConflicts(timHw)) { LOG_WARNING(PWM, "Timer output %d skipped", idx); continue; } + // Dedicated timers already handled in Pass 0 + if (outputMode == OUTPUT_MODE_MOTORS || outputMode == OUTPUT_MODE_SERVOS) { + continue; + } + + int type = MAP_TO_NONE; + // Make sure first motorCount motor outputs get assigned to motor if (TIM_IS_MOTOR(timHw->usageFlags) && (motorIdx < motorCount)) { timHw->usageFlags &= ~TIM_USE_SERVO; pwmClaimTimer(timHw->tim, timHw->usageFlags); - motorIdx += 1; + motorIdx++; } if (TIM_IS_SERVO(timHw->usageFlags) && !pwmHasMotorOnTimer(timOutputs, timHw->tim)) { @@ -351,24 +366,7 @@ void pwmBuildTimerOutputList(timMotorServoHardware_t * timOutputs, bool isMixerU type = MAP_TO_LED_OUTPUT; } - switch(type) { - case MAP_TO_MOTOR_OUTPUT: - timHw->usageFlags &= TIM_USE_MOTOR; - timOutputs->timMotors[timOutputs->maxTimMotorCount++] = timHw; - pwmClaimTimer(timHw->tim, timHw->usageFlags); - break; - case MAP_TO_SERVO_OUTPUT: - timHw->usageFlags &= TIM_USE_SERVO; - timOutputs->timServos[timOutputs->maxTimServoCount++] = timHw; - pwmClaimTimer(timHw->tim, timHw->usageFlags); - break; - case MAP_TO_LED_OUTPUT: - timHw->usageFlags &= TIM_USE_LED; - pwmClaimTimer(timHw->tim, timHw->usageFlags); - break; - default: - break; - } + pwmAssignOutput(timOutputs, timHw, type); } } From ecb590a518f706ac49d5050a0578546ea9cdbf2a Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 3 May 2026 22:46:09 -0500 Subject: [PATCH 10/10] test: add pwm_mapping beeper timer guard unit tests 11 tests covering: - Bug reproduction: OUTPUT_MODE_SERVOS/MOTORS/LED/PINIO override on a beeper-flagged timer corrupts TIM_USE_BEEPER via pwmAssignOutput() - Fix verification: guard in timerHardwareOverride() leaves beeper timers untouched for all 4 output mode overrides - Regression: normal (non-beeper) timer overrides still applied correctly - Edge case: timer with only TIM_USE_BEEPER always protected --- src/test/unit/pwm_mapping_beeper_unittest.cc | 375 +++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 src/test/unit/pwm_mapping_beeper_unittest.cc diff --git a/src/test/unit/pwm_mapping_beeper_unittest.cc b/src/test/unit/pwm_mapping_beeper_unittest.cc new file mode 100644 index 00000000000..1ce0754a618 --- /dev/null +++ b/src/test/unit/pwm_mapping_beeper_unittest.cc @@ -0,0 +1,375 @@ +/* + * Unit test: timerHardwareOverride() must not corrupt TIM_USE_BEEPER flags. + * + * Bug (pre-fix): + * When a user applies OUTPUT_MODE_SERVOS to a timer that has TIM_USE_BEEPER + * set (e.g. MATEKH743 TIM2/PA15), timerHardwareOverride() would apply the + * servo-mode mask without first checking for the beeper flag. The subsequent + * pwmAssignOutput(servo) call strips everything except TIM_USE_SERVO, leaving + * beeperPwmInit() unable to find its timer. + * + * Fix (commit 551bce85d6): + * Guard added at the top of timerHardwareOverride(): + * if (timer->usageFlags & TIM_USE_BEEPER) { return; } + * + * This file contains: + * 1. A minimal inline reproduction of both the BUGGY and FIXED versions of + * timerHardwareOverride() so that the test is fully self-contained + * (pwm_mapping.c is excluded from the SITL/unit build by its own guard). + * 2. TEST BugReproduction — demonstrates that the bug corrupts TIM_USE_BEEPER. + * This test would FAIL on pre-fix code and PASSES on the fixed code. + * 3. TEST FixVerification — positive assertion that TIM_USE_BEEPER survives + * after a servo-mode override on a beeper timer. + * 4. Negative / regression tests covering normal (non-beeper) timers to + * confirm that the guard does not break the existing override behaviour. + */ + +#include +#include + +#include "gtest/gtest.h" +#include "unittest_macros.h" + +/* ------------------------------------------------------------------------- + * Minimal type/flag definitions — mirrors the real firmware headers. + * We avoid pulling in the real headers because they drag in hundreds of + * platform-specific includes that cannot be satisfied in a host build. + * ------------------------------------------------------------------------- */ + +/* timerUsageFlag_e — must match src/main/drivers/timer.h exactly */ +typedef enum { + TIM_USE_ANY = 0, + TIM_USE_PPM = (1 << 0), + TIM_USE_PWM = (1 << 1), + TIM_USE_MOTOR = (1 << 2), + TIM_USE_SERVO = (1 << 3), + TIM_USE_LED = (1 << 24), + TIM_USE_BEEPER = (1 << 25), +} timerUsageFlag_e; + +/* outputMode_e — must match src/main/flight/mixer.h exactly */ +typedef enum { + OUTPUT_MODE_AUTO = 0, + OUTPUT_MODE_MOTORS, + OUTPUT_MODE_SERVOS, + OUTPUT_MODE_LED, + OUTPUT_MODE_PINIO +} outputMode_e; + +/* Minimal timer hardware entry — only the fields exercised by the function. */ +typedef struct timerHardware_s { + uint32_t usageFlags; + /* All other fields (tim, tag, channel, …) are unused in this test. */ +} timerHardware_t; + +/* ------------------------------------------------------------------------- + * Inline reproductions of timerHardwareOverride(). + * + * We inline these rather than linking pwm_mapping.c because: + * a) pwm_mapping.c is wrapped in #if !defined(SITL_BUILD) and the unit-test + * target.h defines SITL_BUILD, so the real file compiles to nothing. + * b) The function depends on timerOverrides() (parameter-group accessor) and + * timer2id(), which are platform / PG infrastructure that is not present in + * a host unit-test build. + * + * The functions below are literal translations of the C source with the PG + * indirection collapsed into a simple `outputMode` parameter. + * ------------------------------------------------------------------------- */ + +/* + * BUGGY version (pre-fix, equivalent to the code BEFORE commit 551bce85d6). + * + * The OUTPUT_MODE_SERVOS case clears TIM_USE_MOTOR and TIM_USE_LED but does + * NOT clear TIM_USE_BEEPER. The resulting combined flags + * (TIM_USE_SERVO | TIM_USE_BEEPER) will later be stripped by pwmAssignOutput() + * to just TIM_USE_SERVO, silently destroying the beeper timer entry. + */ +static void timerHardwareOverride_buggy(timerHardware_t *timer, + outputMode_e outputMode) +{ + /* NOTE: no beeper guard here — this is intentionally the buggy version */ + switch (outputMode) { + case OUTPUT_MODE_MOTORS: + timer->usageFlags &= ~((uint32_t)(TIM_USE_SERVO | TIM_USE_LED)); + timer->usageFlags |= (uint32_t)TIM_USE_MOTOR; + break; + case OUTPUT_MODE_SERVOS: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_LED)); + timer->usageFlags |= (uint32_t)TIM_USE_SERVO; + break; + case OUTPUT_MODE_LED: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO)); + timer->usageFlags |= (uint32_t)TIM_USE_LED; + break; + case OUTPUT_MODE_PINIO: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO | TIM_USE_LED)); + break; + default: + break; + } +} + +/* + * FIXED version (post-fix, matches commit 551bce85d6 exactly). + * + * The beeper guard causes the function to return immediately when + * TIM_USE_BEEPER is set, leaving the flags completely untouched. + */ +static void timerHardwareOverride_fixed(timerHardware_t *timer, + outputMode_e outputMode) +{ + /* Guard added by the fix: never modify a beeper timer. */ + if (timer->usageFlags & (uint32_t)TIM_USE_BEEPER) { + return; + } + switch (outputMode) { + case OUTPUT_MODE_MOTORS: + timer->usageFlags &= ~((uint32_t)(TIM_USE_SERVO | TIM_USE_LED)); + timer->usageFlags |= (uint32_t)TIM_USE_MOTOR; + break; + case OUTPUT_MODE_SERVOS: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_LED)); + timer->usageFlags |= (uint32_t)TIM_USE_SERVO; + break; + case OUTPUT_MODE_LED: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO)); + timer->usageFlags |= (uint32_t)TIM_USE_LED; + break; + case OUTPUT_MODE_PINIO: + timer->usageFlags &= ~((uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO | TIM_USE_LED)); + break; + default: + break; + } +} + +/* ========================================================================= + * Helper — simulate what pwmAssignOutput(MAP_TO_SERVO_OUTPUT) does to flags. + * In real code: timHw->usageFlags &= TIM_USE_SERVO; + * We need this to show the full two-step corruption path. + * ========================================================================= */ +static void pwmAssignServo_stripFlags(timerHardware_t *timHw) +{ + timHw->usageFlags &= (uint32_t)TIM_USE_SERVO; +} + +/* ========================================================================= + * TEST SUITE: BeeperTimerProtection + * ========================================================================= */ + +class BeeperTimerProtectionTest : public ::testing::Test { +protected: + timerHardware_t timer; + + void SetUp() override { + /* Simulate MATEKH743 TIM2/PA15: beeper shares a servo-capable timer */ + timer.usageFlags = (uint32_t)(TIM_USE_BEEPER | TIM_USE_SERVO); + } +}; + +/* + * TEST 1 — BugReproduction + * + * This test demonstrates the pre-fix bug. On the BUGGY code path: + * 1. timerHardwareOverride_buggy() applies OUTPUT_MODE_SERVOS, which does + * NOT clear TIM_USE_BEEPER → flags become TIM_USE_SERVO | TIM_USE_BEEPER. + * 2. pwmAssignOutput(servo) strips everything except TIM_USE_SERVO + * → TIM_USE_BEEPER is lost. + * + * The final assertion checks that TIM_USE_BEEPER was lost, which is the + * observable symptom of the bug. This test PASSES (i.e. the bug reproduces), + * which is what we want from the reproduction test. + */ +TEST_F(BeeperTimerProtectionTest, BugReproduction_BeeperFlagLostAfterOverride) +{ + /* Step 1: buggy override — does not protect beeper */ + timerHardwareOverride_buggy(&timer, OUTPUT_MODE_SERVOS); + + /* After the buggy override: both SERVO and BEEPER flags should be set + * because OUTPUT_MODE_SERVOS only clears MOTOR and LED, not BEEPER. */ + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_SERVO) + << "SERVO flag should be set by the OUTPUT_MODE_SERVOS override"; + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER) + << "BEEPER flag should still be present at this point (intermediate state)"; + + /* Step 2: simulate pwmAssignOutput(servo) which strips everything + * except TIM_USE_SERVO — this is the second half of the corruption. */ + pwmAssignServo_stripFlags(&timer); + + /* The bug: TIM_USE_BEEPER is now gone. beeperPwmInit() will fail silently. */ + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER) + << "BUG CONFIRMED: TIM_USE_BEEPER was stripped by pwmAssignOutput " + "because timerHardwareOverride() did not protect it. " + "beeperPwmInit() would fail to find its timer."; +} + +/* + * TEST 2 — FixVerification + * + * The fixed timerHardwareOverride() returns immediately when TIM_USE_BEEPER is + * set, so the servo override is a no-op for beeper timers. TIM_USE_BEEPER must + * survive, allowing beeperPwmInit() to find the timer later. + * + * This test PASSES on the fixed code and would FAIL on the buggy code. + */ +TEST_F(BeeperTimerProtectionTest, FixVerification_BeeperFlagSurvivesOverride) +{ + const uint32_t originalFlags = timer.usageFlags; + + /* Fixed override: should be a no-op because TIM_USE_BEEPER is set */ + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_SERVOS); + + /* Flags must be completely unchanged — the guard must have returned early. */ + EXPECT_EQ(timer.usageFlags, originalFlags) + << "FIX VERIFIED: timerHardwareOverride() must not modify a timer " + "that has TIM_USE_BEEPER set."; + + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER) + << "TIM_USE_BEEPER must still be set after the override attempt"; +} + +/* + * TEST 3 — FixVerification for OUTPUT_MODE_MOTORS + * + * Confirm the beeper guard works for all output-mode variants, not just SERVOS. + */ +TEST_F(BeeperTimerProtectionTest, FixVerification_BeeperProtectedFromMotorOverride) +{ + const uint32_t originalFlags = timer.usageFlags; + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_MOTORS); + + EXPECT_EQ(timer.usageFlags, originalFlags) + << "OUTPUT_MODE_MOTORS must not modify a beeper timer"; + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER); +} + +/* + * TEST 4 — FixVerification for OUTPUT_MODE_LED + */ +TEST_F(BeeperTimerProtectionTest, FixVerification_BeeperProtectedFromLedOverride) +{ + const uint32_t originalFlags = timer.usageFlags; + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_LED); + + EXPECT_EQ(timer.usageFlags, originalFlags) + << "OUTPUT_MODE_LED must not modify a beeper timer"; + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER); +} + +/* + * TEST 5 — FixVerification for OUTPUT_MODE_PINIO + */ +TEST_F(BeeperTimerProtectionTest, FixVerification_BeeperProtectedFromPinioOverride) +{ + const uint32_t originalFlags = timer.usageFlags; + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_PINIO); + + EXPECT_EQ(timer.usageFlags, originalFlags) + << "OUTPUT_MODE_PINIO must not modify a beeper timer"; + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER); +} + +/* ========================================================================= + * Negative / regression tests — normal (non-beeper) timers must still be + * overridden correctly. The guard must NOT prevent normal overrides. + * ========================================================================= */ + +class NormalTimerOverrideTest : public ::testing::Test { +protected: + timerHardware_t timer; + + /* No TIM_USE_BEEPER: both motor and servo flags set (auto-mode timer). */ + void SetUp() override { + timer.usageFlags = (uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO); + } +}; + +TEST_F(NormalTimerOverrideTest, MotorOverride_SetsMotorClearsServoAndLed) +{ + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_MOTORS); + + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_MOTOR) + << "TIM_USE_MOTOR must be set after OUTPUT_MODE_MOTORS override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_SERVO) + << "TIM_USE_SERVO must be cleared by OUTPUT_MODE_MOTORS override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_LED) + << "TIM_USE_LED must be cleared by OUTPUT_MODE_MOTORS override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_BEEPER) + << "TIM_USE_BEEPER must remain clear on a non-beeper timer"; +} + +TEST_F(NormalTimerOverrideTest, ServoOverride_SetsServoClearsMotorAndLed) +{ + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_SERVOS); + + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_SERVO) + << "TIM_USE_SERVO must be set after OUTPUT_MODE_SERVOS override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_MOTOR) + << "TIM_USE_MOTOR must be cleared by OUTPUT_MODE_SERVOS override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_LED) + << "TIM_USE_LED must be cleared by OUTPUT_MODE_SERVOS override"; +} + +TEST_F(NormalTimerOverrideTest, LedOverride_SetsLedClearsMotorAndServo) +{ + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_LED); + + EXPECT_TRUE(timer.usageFlags & (uint32_t)TIM_USE_LED) + << "TIM_USE_LED must be set after OUTPUT_MODE_LED override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_MOTOR) + << "TIM_USE_MOTOR must be cleared by OUTPUT_MODE_LED override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_SERVO) + << "TIM_USE_SERVO must be cleared by OUTPUT_MODE_LED override"; +} + +TEST_F(NormalTimerOverrideTest, PinioOverride_ClearsAllOutputFlags) +{ + timer.usageFlags = (uint32_t)(TIM_USE_MOTOR | TIM_USE_SERVO | TIM_USE_LED); + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_PINIO); + + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_MOTOR) + << "TIM_USE_MOTOR must be cleared by OUTPUT_MODE_PINIO override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_SERVO) + << "TIM_USE_SERVO must be cleared by OUTPUT_MODE_PINIO override"; + EXPECT_FALSE(timer.usageFlags & (uint32_t)TIM_USE_LED) + << "TIM_USE_LED must be cleared by OUTPUT_MODE_PINIO override"; +} + +TEST_F(NormalTimerOverrideTest, AutoMode_LeavesTimerUnchanged) +{ + const uint32_t originalFlags = timer.usageFlags; + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_AUTO); + + EXPECT_EQ(timer.usageFlags, originalFlags) + << "OUTPUT_MODE_AUTO (default case) must not change flags"; +} + +/* ========================================================================= + * Edge case: timer has ONLY TIM_USE_BEEPER (no motor/servo capability) + * ========================================================================= */ + +TEST(BeeperOnlyTimer, OverrideIgnoredForPureBeeper) +{ + timerHardware_t timer; + timer.usageFlags = (uint32_t)TIM_USE_BEEPER; + + const uint32_t originalFlags = timer.usageFlags; + + /* All override modes must be no-ops for a pure beeper timer. */ + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_MOTORS); + EXPECT_EQ(timer.usageFlags, originalFlags); + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_SERVOS); + EXPECT_EQ(timer.usageFlags, originalFlags); + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_LED); + EXPECT_EQ(timer.usageFlags, originalFlags); + + timerHardwareOverride_fixed(&timer, OUTPUT_MODE_PINIO); + EXPECT_EQ(timer.usageFlags, originalFlags); +}