diff --git a/docs/Cli.md b/docs/Cli.md index d27029ee1b5..931ab71d5f4 100644 --- a/docs/Cli.md +++ b/docs/Cli.md @@ -102,6 +102,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/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..b1be27cc046 --- /dev/null +++ b/docs/PINIO PWM.md @@ -0,0 +1,93 @@ +# PINIO PWM + +INAV provides two mechanisms for generating output signals on GPIO pins: + +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 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 = 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). + +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") + +## 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 `0`: + +- **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. + +*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. + +# 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 | + +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 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 + +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 595f8a5e1d7..a6b546c54cf 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` = 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 d49067357bc..9a054c02a85 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -2499,19 +2499,6 @@ Used to prevent Iterm accumulation on during maneuvers. Iterm will be dampened w --- -### led_pin_pwm_mode - -PWM mode of LED pin. - -| Allowed Values | | -| --- | --- | -| SHARED_LOW | Default | -| SHARED_HIGH | | -| LOW | | -| HIGH | | - ---- - ### limit_attn_filter_cutoff Throttle attenuation PI control output filter cutoff frequency @@ -5322,6 +5309,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 @@ -5854,41 +5851,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/light_ws2811strip.c b/src/main/drivers/light_ws2811strip.c index fe5f405d032..cc052fcd872 100644 --- a/src/main/drivers/light_ws2811strip.c +++ b/src/main/drivers/light_ws2811strip.c @@ -43,26 +43,17 @@ #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; static TCH_t * ws2811TCH = NULL; static bool ws2811Initialised = false; -static bool pwmMode = false; static hsvColor_t ledColorBuffer[WS2811_LED_STRIP_LENGTH]; @@ -112,14 +103,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 +124,17 @@ 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 — LED pin idles LOW between WS2812 bursts + memset(&ledStripDMABuffer, 0, sizeof(ledStripDMABuffer)); + ws2811Initialised = true; + + ws2811UpdateStrip(); } bool isWS2811LedStripReady(void) @@ -191,7 +163,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 +188,9 @@ 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; - } +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 94c36445ec7..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,27 +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); - -//value 0...100 -void ledPinStartPWM(uint16_t value); -void ledPinStopPWM(void); +void ws2811SetIdleHigh(bool high); void ws2811UpdateStrip(void); diff --git a/src/main/drivers/pinio.c b/src/main/drivers/pinio.c index 3e89e5fccca..04f29c5cf59 100644 --- a/src/main/drivers/pinio.c +++ b/src/main/drivers/pinio.c @@ -28,6 +28,18 @@ #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 +#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[] = { @@ -65,50 +77,136 @@ const int pinioHardwareCount = ARRAYLEN(pinioHardware); /*** Runtime configuration ***/ typedef struct pinioRuntime_s { IO_t io; + volatile timCCR_t *ccr; // Cached CCR register pointer (NULL for GPIO-only pins) bool inverted; - bool state; + 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. } pinioRuntime_t; static pinioRuntime_t pinioRuntime[PINIO_COUNT]; +static int pinioRuntimeCount = 0; -void pinioInit(void) +// 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) { - if (pinioHardwareCount == 0) { - return; + 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].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 + *pinioRuntime[slot].ccr = pinioEffectiveDuty(0, inverted); // start off + return true; +} - for (int i = 0; i < pinioHardwareCount; i++) { - IO_t io = IOGetByTag(pinioHardware[i].ioTag); +void pinioInit(void) +{ + int runtimeCount = 0; + // 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; } - IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(i)); + 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 && pinioInitTimerPWM(runtimeCount, io, timHw, inverted)) { + runtimeCount++; + continue; + } + + // GPIO fallback: no timer available or pin already claimed + IOInit(io, OWNER_PINIO, RESOURCE_OUTPUT, RESOURCE_INDEX(runtimeCount)); IOConfigGPIO(io, pinioHardware[i].ioMode); + pinioRuntime[runtimeCount].inverted = inverted; + pinioRuntime[runtimeCount].io = io; + inverted ? IOHi(io) : IOLo(io); + runtimeCount++; + } - if (pinioHardware[i].flags & PINIO_FLAGS_INVERTED) { - pinioRuntime[i].inverted = true; - IOHi(io); - } else { - pinioRuntime[i].inverted = false; - IOLo(io); + // 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) { + continue; + } + if (pinioInitTimerPWM(runtimeCount, io, timHw, false)) { + runtimeCount++; + } + } + + pinioRuntimeCount = runtimeCount; +} - pinioRuntime[i].io = io; - pinioRuntime[i].state = false; +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].ccr) { + pinioRuntime[index].duty = duty; + if (pinioRuntime[index].active) { + *pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); + } + } else { + IOWrite(pinioRuntime[index].io, (duty > 0) ^ pinioRuntime[index].inverted); } } +// 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 (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 (!pinioRuntime[index].io) { + if ((unsigned)index >= (unsigned)pinioRuntimeCount) { return; } - - if (newState != pinioRuntime[index].state) { + if (pinioRuntime[index].ccr) { + pinioRuntime[index].active = newState; + uint8_t duty = newState ? pinioRuntime[index].duty : 0; + *pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted); + } else { IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted); - pinioRuntime[index].state = newState; } } #endif diff --git a/src/main/drivers/pinio.h b/src/main/drivers/pinio.h index a1de21c12de..cc55fad00d8 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,5 @@ 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/drivers/pwm_mapping.c b/src/main/drivers/pwm_mapping.c index 120b3d4fc91..c37ae1775a7 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); @@ -232,6 +236,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: + timer->usageFlags &= ~(TIM_USE_MOTOR|TIM_USE_SERVO|TIM_USE_LED); + timer->usageFlags |= TIM_USE_PINIO; + break; } } @@ -287,10 +295,12 @@ static void pwmAssignOutput(timMotorServoHardware_t *timOutputs, timerHardware_t timHw->usageFlags &= TIM_USE_LED; pwmClaimTimer(timHw->tim, timHw->usageFlags); break; + default: + break; } } -void pwmBuildTimerOutputList(timMotorServoHardware_t * timOutputs, bool isMixerUsingServos) +void pwmBuildTimerOutputList(timMotorServoHardware_t *timOutputs, bool isMixerUsingServos) { UNUSED(isMixerUsingServos); timOutputs->maxTimMotorCount = 0; @@ -298,7 +308,7 @@ void pwmBuildTimerOutputList(timMotorServoHardware_t * timOutputs, bool isMixerU const uint8_t motorCount = getMotorCount(); - // Apply configurator overrides to all timer outputs + // Apply all timerOverrides upfront so flag state is stable for both passes for (int idx = 0; idx < timerHardwareCount; idx++) { timerHardwareOverride(&timerHardware[idx]); } 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/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/cli.c b/src/main/fc/cli.c index 6f809432648..b1491bbe55f 100644 --- a/src/main/fc/cli.c +++ b/src/main/fc/cli.c @@ -58,6 +58,8 @@ 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" #include "drivers/sdcard/sdcard.h" @@ -68,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" @@ -173,6 +173,7 @@ static const char * outputModeNames[] = { "MOTORS", "SERVOS", "LED", + "PINIO", NULL }; @@ -2169,20 +2170,45 @@ static void cliModeColor(char *cmdline) } } -static void cliLedPinPWM(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 i; + int channel = 0; + int duty; if (isEmpty(cmdline)) { - ledPinStopPWM(); - cliPrintLine("PWM stopped"); + pinioSetDuty(1, 0); + cliPrintLine("PWM stopped on PINIO 1"); + return; + } + + const char *dutyStr = nextArg(cmdline); + if (dutyStr) { + channel = fastA2I(cmdline); + duty = fastA2I(dutyStr); } else { - i = fastA2I(cmdline); - ledPinStartPWM(i); - cliPrintLinef("PWM started: %d%%",i); + // One arg: duty on channel 0 (LED idle, backward compat with old LED_PIN_PWM) + duty = fastA2I(cmdline); } + + const int maxChannel = MAX(pinioGetRuntimeCount(), PINIO_COUNT); + if (channel < 0 || channel > maxChannel) { + cliShowArgumentRangeError("channel", 0, maxChannel); + return; + } + if (duty < 0 || duty > 100) { + cliShowArgumentRangeError("duty", 0, 100); + return; + } + + pinioSetDuty(channel, (uint8_t)duty); + cliPrintLinef("PWM ch %d: %d%%", channel, duty); } -#endif +#endif // USE_PINIO static void cliDelay(char* cmdLine) { int ms = 0; @@ -3192,6 +3218,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; @@ -4921,7 +4949,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("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), @@ -4982,7 +5012,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 d17512964a4..bef7c54de40 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" @@ -1680,6 +1683,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))) { @@ -1689,12 +1695,33 @@ 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; + } + } + } + // 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); + } #endif } } diff --git a/src/main/fc/fc_tasks.c b/src/main/fc/fc_tasks.c index 4d72c59f1dc..bffcd05ecdf 100755 --- a/src/main/fc/fc_tasks.c +++ b/src/main/fc/fc_tasks.c @@ -428,7 +428,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 c53ef66852f..995988ce692 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 @@ -4122,35 +4119,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 @@ -4280,27 +4273,22 @@ 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"] - 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..9fba216fe6d 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 + 1; // setting is 0-indexed, pinioSetDuty is 1-indexed 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 + 1, 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/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/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 801239f4851..3f32fcee861 100644 --- a/src/main/programming/logic_condition.c +++ b/src/main/programming/logic_condition.c @@ -59,7 +59,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); @@ -525,16 +525,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(); - } +#ifdef USE_PINIO + case LOGIC_CONDITION_PINIO_PWM: + // 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: diff --git a/src/main/programming/logic_condition.h b/src/main/programming/logic_condition.h index 34155ea082e..183c0cfa054 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, 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); +}