Skip to content

Commit 39ebba5

Browse files
committed
pinio/pwm: better integrate LED pin, simplify
1 parent 15f0827 commit 39ebba5

10 files changed

Lines changed: 89 additions & 62 deletions

File tree

docs/Cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ While connected to the CLI, all Logical Switches are temporarily disabled (5.1.0
100100
| `msc` | Enter USB Mass storage mode. See [USB MSC documentation](USB_Mass_Storage_(MSC)_mode.md) for usage information. |
101101
| `osd_layout` | Get or set the layout of OSD items |
102102
| `pid` | Configurable PID controllers |
103+
| `piniopwm` | Set PINIO PWM duty cycle. See [PINIO PWM](PINIO%20PWM.md) |
103104
| `play_sound` | `<index>`, or none for next item |
104105
| `control_profile` | Change profile |
105106
| `resource` | View currently used resources |

docs/PINIO PWM.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
INAV provides two mechanisms for generating output signals on GPIO pins:
44

5-
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.
6-
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.
5+
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.
6+
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.
77

88
## PINIO PWM channels
99

10-
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.
10+
PINIO channels can come from two sources:
11+
12+
1. **Target-defined:** `PINIO1_PIN` through `PINIO4_PIN` in `target.h`. These are always available on supported boards.
13+
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.
14+
15+
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.
1116

1217
PWM duty cycle can be controlled via:
13-
- **CLI:** `piniopwm [channel] <duty>` (duty = 0-100)
14-
- **Programming framework:** Operation 52, Operand A = channel (0-3), Operand B = duty (0-100)
18+
- **CLI:** `piniopwm <channel> <duty>` (channel = 1-4, duty = 0-100)
19+
- **Programming framework:** Operation 52, Operand A = duty (0-100), Operand B = channel (1-4)
20+
- **Mode boxes:** USER1-USER4 in the Modes tab toggle the channel on/off
1521

1622
Setting duty to 0 stops PWM generation (pin goes LOW, or HIGH if `PINIO_FLAGS_INVERTED` is set in target.h).
1723

@@ -21,14 +27,25 @@ PWM frequency is fixed to 24kHz with duty ratio between 0 and 100%:
2127

2228
![alt text](/docs/assets/images/led_pin_pwm.png "PINIO PWM signal")
2329

24-
## LED strip idle level (channel 4)
30+
## Mode box and programming framework interaction
31+
32+
PINIO channels can be controlled by both mode boxes (Modes tab) and the programming framework (Programming tab). When both are used:
33+
34+
- The **programming framework** sets the duty cycle (0-100%).
35+
- 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%.
36+
37+
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.
38+
39+
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.
40+
41+
## LED strip idle level (channel 0)
2542

2643
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.
2744

28-
The LED strip idle level is accessible as channel `4` (the next channel after PINIO hardware channels 0-3):
45+
The LED strip idle level is accessible as channel `0`:
2946

30-
- **CLI:** `piniopwm 4 <value>` — value > 0 sets idle HIGH, 0 sets idle LOW
31-
- **Programming framework:** Operation 52, Operand A = 4, Operand B = value (>0 = HIGH, 0 = LOW)
47+
- **CLI:** `piniopwm 0 <value>` — value > 0 sets idle HIGH, 0 sets idle LOW
48+
- **Programming framework:** Operation 52, Operand A = value (>0 = HIGH, 0 = LOW), Operand B = 0
3249

3350
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.
3451

@@ -43,17 +60,22 @@ Normally LED pin is held low between WS2812 updates:
4360

4461
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.
4562

46-
# Generating PWM/output signals with programming framework
63+
# Channel numbering
64+
65+
| Channel | Target | Description |
66+
|---------|--------|-------------|
67+
| 0 | LED strip | Binary idle level (HIGH/LOW) |
68+
| 1-4 | PINIO 1-4 | Full 0-100% PWM at 24 kHz |
4769

48-
See operation 52 "PINIO PWM" in [Programming Framework](Programming%20Framework.md)
70+
This numbering is consistent across the CLI (`piniopwm`), programming framework (operation 52), and the `pinio_box1`-`pinio_box4` settings.
4971

5072
# Generating PWM/output signals from CLI
5173

5274
`piniopwm [channel] <duty>` — channel = 0-4, duty = 0-100
5375

54-
- One argument: sets duty on channel 0 (backward compatible)
55-
- Two arguments: first is channel, second is duty
56-
- No arguments: stops PWM on channel 0
76+
- One argument: sets LED idle level (channel 0, backward compatible)
77+
- Two arguments: first is channel (0 = LED idle, 1-4 = PINIO), second is duty
78+
- No arguments: stops PWM on PINIO 1
5779

5880
# Example of driving LED
5981

docs/Programming Framework.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ for complete documentation on using JavaScript to program your flight controller
114114
| 49 | Timer | A simple on - off timer. `true` for the duration of `Operand A` [ms]. Then `false` for the duration of `Operand B` [ms]. |
115115
| 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 ) |
116116
| 51 | Approx Equals (A ~ B) | `true` if `Operand B` is within 1% of `Operand A`. |
117-
| 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). |
117+
| 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). |
118118
| 53 | Disable GPS Sensor Fix | Disables the GNSS sensor fix. For testing GNSS failure. |
119119
| 54 | Mag calibration | Trigger a magnetometer calibration. |
120120
| 55 | Set Gimbal Sensitivity | Scales `Operand A` from [`-16` : `15`]

docs/Settings.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5574,41 +5574,41 @@ Pilot name
55745574

55755575
### pinio_box1
55765576

5577-
Mode assignment for PINIO#1
5577+
Mode box assignment for PINIO channel 1
55785578

55795579
| Default | Min | Max |
55805580
| --- | --- | --- |
5581-
| `BOX_PERMANENT_ID_NONE` | 0 | 255 |
5581+
| `BOX_PERMANENT_ID_USER1` | 0 | 255 |
55825582

55835583
---
55845584

55855585
### pinio_box2
55865586

5587-
Mode assignment for PINIO#1
5587+
Mode box assignment for PINIO channel 2
55885588

55895589
| Default | Min | Max |
55905590
| --- | --- | --- |
5591-
| `BOX_PERMANENT_ID_NONE` | 0 | 255 |
5591+
| `BOX_PERMANENT_ID_USER2` | 0 | 255 |
55925592

55935593
---
55945594

55955595
### pinio_box3
55965596

5597-
Mode assignment for PINIO#1
5597+
Mode box assignment for PINIO channel 3
55985598

55995599
| Default | Min | Max |
56005600
| --- | --- | --- |
5601-
| `BOX_PERMANENT_ID_NONE` | 0 | 255 |
5601+
| `BOX_PERMANENT_ID_USER3` | 0 | 255 |
56025602

56035603
---
56045604

56055605
### pinio_box4
56065606

5607-
Mode assignment for PINIO#1
5607+
Mode box assignment for PINIO channel 4
56085608

56095609
| Default | Min | Max |
56105610
| --- | --- | --- |
5611-
| `BOX_PERMANENT_ID_NONE` | 0 | 255 |
5611+
| `BOX_PERMANENT_ID_USER4` | 0 | 255 |
56125612

56135613
---
56145614

src/main/drivers/pinio.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
#include "common/memory.h"
2929
#include "drivers/io.h"
3030
#include "drivers/pinio.h"
31+
#ifdef USE_LED_STRIP
32+
#include "drivers/light_ws2811strip.h"
33+
#endif
3134

3235
// CCR = duty% directly; 2.4 MHz / 100 = 24 kHz PWM, above audible range
3336
#define PINIO_PWM_PERIOD 100
@@ -74,8 +77,9 @@ const int pinioHardwareCount = ARRAYLEN(pinioHardware);
7477
/*** Runtime configuration ***/
7578
typedef struct pinioRuntime_s {
7679
IO_t io;
77-
TCH_t *tch; // Non-NULL when pin is configured in PWM mode
80+
volatile timCCR_t *ccr; // Cached CCR register pointer (NULL for GPIO-only pins)
7881
bool inverted;
82+
bool active; // Mode box state; defaults to true (no RC channel = always active)
7983
uint8_t duty; // Timer mode: duty level (0–100) applied by pinioSet(true);
8084
// updated by pinioSetDuty(). Defaults to 100 so a mode box
8185
// 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
97101
timerPWMConfigChannel(tch, 0);
98102
timerPWMStart(tch);
99103
timerEnable(tch);
100-
pinioRuntime[slot].tch = tch;
104+
pinioRuntime[slot].ccr = timerCCR(tch);
101105
pinioRuntime[slot].io = io;
102106
pinioRuntime[slot].inverted = inverted;
107+
pinioRuntime[slot].active = true;
103108
pinioRuntime[slot].duty = 100; // default: mode box on = full on
104-
*timerCCR(tch) = pinioEffectiveDuty(0, inverted); // start off
109+
*pinioRuntime[slot].ccr = pinioEffectiveDuty(0, inverted); // start off
105110
return true;
106111
}
107112

@@ -155,17 +160,31 @@ void pinioInit(void)
155160
pinioRuntimeCount = runtimeCount;
156161
}
157162

163+
int pinioGetRuntimeCount(void)
164+
{
165+
return pinioRuntimeCount;
166+
}
167+
158168
void pinioSetDuty(int index, uint8_t duty)
159169
{
170+
#ifdef USE_LED_STRIP
171+
if (index == 0) {
172+
ws2811SetIdleHigh(duty > 0);
173+
return;
174+
}
175+
#endif
176+
index--; // user-facing 1-4 → runtime 0-3
160177
if ((unsigned)index >= (unsigned)pinioRuntimeCount) {
161178
return;
162179
}
163180
if (duty > 100) {
164181
duty = 100;
165182
}
166-
if (pinioRuntime[index].tch) {
183+
if (pinioRuntime[index].ccr) {
167184
pinioRuntime[index].duty = duty;
168-
*timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted);
185+
if (pinioRuntime[index].active) {
186+
*pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted);
187+
}
169188
} else {
170189
IOWrite(pinioRuntime[index].io, (duty > 0) ^ pinioRuntime[index].inverted);
171190
}
@@ -182,9 +201,10 @@ void pinioSet(int index, bool newState)
182201
if ((unsigned)index >= (unsigned)pinioRuntimeCount) {
183202
return;
184203
}
185-
if (pinioRuntime[index].tch) {
204+
if (pinioRuntime[index].ccr) {
205+
pinioRuntime[index].active = newState;
186206
uint8_t duty = newState ? pinioRuntime[index].duty : 0;
187-
*timerCCR(pinioRuntime[index].tch) = pinioEffectiveDuty(duty, pinioRuntime[index].inverted);
207+
*pinioRuntime[index].ccr = pinioEffectiveDuty(duty, pinioRuntime[index].inverted);
188208
} else {
189209
IOWrite(pinioRuntime[index].io, newState ^ pinioRuntime[index].inverted);
190210
}

src/main/drivers/pinio.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ extern const int pinioHardwareCount;
4141
void pinioInit(void);
4242
void pinioSet(int index, bool newState);
4343
void pinioSetDuty(int index, uint8_t duty);
44+
int pinioGetRuntimeCount(void);

src/main/fc/cli.c

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,46 +2173,37 @@ static void cliModeColor(char *cmdline)
21732173
#endif // USE_LED_STRIP
21742174

21752175
#ifdef USE_PINIO
2176+
// Channel numbering: 0 = LED strip idle level, 1-4 = PINIO channels (matches programming framework)
21762177
static void cliPinioPwm(char *cmdline)
21772178
{
21782179
int channel = 0;
21792180
int duty;
21802181

21812182
if (isEmpty(cmdline)) {
2182-
pinioSetDuty(0, 0);
2183-
cliPrintLine("PWM stopped on channel 0");
2183+
pinioSetDuty(1, 0);
2184+
cliPrintLine("PWM stopped on PINIO 1");
21842185
return;
21852186
}
21862187

2187-
// Find second argument (space-separated)
2188-
char *dutyStr = strchr(cmdline, ' ');
2188+
const char *dutyStr = nextArg(cmdline);
21892189
if (dutyStr) {
2190-
// Two args: channel duty
21912190
channel = fastA2I(cmdline);
2192-
dutyStr++;
21932191
duty = fastA2I(dutyStr);
21942192
} else {
2195-
// One arg: duty on channel 0
2193+
// One arg: duty on channel 0 (LED idle, backward compat with old LED_PIN_PWM)
21962194
duty = fastA2I(cmdline);
21972195
}
21982196

2199-
if (channel < 0 || channel > PINIO_COUNT) {
2200-
cliPrintLinef("Error: channel must be 0-%d", PINIO_COUNT);
2197+
const int maxChannel = MAX(pinioGetRuntimeCount(), PINIO_COUNT);
2198+
if (channel < 0 || channel > maxChannel) {
2199+
cliShowArgumentRangeError("channel", 0, maxChannel);
22012200
return;
22022201
}
22032202
if (duty < 0 || duty > 100) {
2204-
cliPrintLine("Error: duty must be 0-100");
2203+
cliShowArgumentRangeError("duty", 0, 100);
22052204
return;
22062205
}
22072206

2208-
#ifdef USE_LED_STRIP
2209-
if (channel == PINIO_COUNT) {
2210-
ws2811SetIdleHigh(duty > 0);
2211-
cliPrintLinef("LED idle %s", duty > 0 ? "HIGH" : "LOW");
2212-
return;
2213-
}
2214-
#endif
2215-
22162207
pinioSetDuty(channel, (uint8_t)duty);
22172208
cliPrintLinef("PWM ch %d: %d%%", channel, duty);
22182209
}

src/main/io/osd_joystick.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ bool osdJoystickEnabled(void) {
4646

4747

4848
void osdJoystickSimulate5KeyButtonPress(uint8_t operation) {
49-
const int ch = osdJoystickConfig()->pinio_channel;
49+
const int ch = osdJoystickConfig()->pinio_channel + 1; // setting is 0-indexed, pinioSetDuty is 1-indexed
5050
switch (operation) {
5151
case RCDEVICE_CAM_KEY_ENTER:
5252
pinioSetDuty(ch, osdJoystickConfig()->osd_joystick_enter);
@@ -68,7 +68,7 @@ void osdJoystickSimulate5KeyButtonPress(uint8_t operation) {
6868

6969

7070
void osdJoystickSimulate5KeyButtonRelease(void) {
71-
pinioSetDuty(osdJoystickConfig()->pinio_channel, 0);
71+
pinioSetDuty(osdJoystickConfig()->pinio_channel + 1, 0);
7272
}
7373

7474

src/main/io/piniobox.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ void pinioBoxInit(void)
6363
void pinioBoxUpdate(void)
6464
{
6565
for (int i = 0; i < PINIO_COUNT; i++) {
66-
if (pinioBoxRuntimeConfig.boxId[i] != BOXID_NONE) {
66+
if (pinioBoxRuntimeConfig.boxId[i] != BOXID_NONE &&
67+
isModeActivationConditionPresent(pinioBoxRuntimeConfig.boxId[i])) {
6768
pinioSet(i, IS_RC_MODE_ACTIVE(pinioBoxRuntimeConfig.boxId[i]));
6869
}
6970
}

src/main/programming/logic_condition.c

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858

5959
#include "io/vtx.h"
6060
#include "drivers/vtx_common.h"
61-
#include "drivers/light_ws2811strip.h"
6261
#include "drivers/pinio.h"
6362

6463
PG_REGISTER_ARRAY_WITH_RESET_FN(logicCondition_t, MAX_LOGIC_CONDITIONS, logicConditions, PG_LOGIC_CONDITIONS, 4);
@@ -509,17 +508,9 @@ static int logicConditionCompute(
509508

510509
#ifdef USE_PINIO
511510
case LOGIC_CONDITION_PINIO_PWM:
512-
// operandA = duty cycle (0-100), operandB = pin (0=LED pin, 1=USER1, 2=USER2, ...)
513-
// operandB=0 preserves backward compatibility with old LED_PIN_PWM behavior
514-
if (operandB == 0) {
515-
#ifdef USE_LED_STRIP
516-
ws2811SetIdleHigh(operandA > 0);
517-
#endif
518-
return operandA;
519-
}
520-
pinioSetDuty(operandB - 1, (uint8_t)constrain(operandA, 0, 100));
511+
// operandA = duty cycle (0-100), operandB = channel (0=LED idle, 1-4=PINIO)
512+
pinioSetDuty(operandB, (uint8_t)constrain(operandA, 0, 100));
521513
return operandA;
522-
break;
523514
#endif
524515
#ifdef USE_GPS_FIX_ESTIMATION
525516
case LOGIC_CONDITION_DISABLE_GPS_FIX:

0 commit comments

Comments
 (0)