Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/Multirotor Chirp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Experimental multirotor chirp measurement

Chirp generates a repeatable, single-axis rate excitation for Blackbox analysis.
It is a measurement tool: it does **not** select or change PID gains, filters,
rates or EZ-Tune settings. The initial implementation is intended for developer
validation; automated recommendations and a Configurator workflow are separate
work. It has not been validated in real flight.

## Relationship to existing work

[Q-Tune #8546](https://github.com/iNavFlight/inav/pull/8546) detects oscillations
from flight samples. This feature supplies a known excitation signal for
identification experiments instead. It neither copies nor replaces Q-Tune's
detector. Both relate to [the multirotor autotuning request
#10433](https://github.com/iNavFlight/inav/issues/10433); this measurement feature
alone does not resolve that request.

## Scope and prerequisites

- Conventional multirotor mixer and multirotor PID controller; no tricopters,
helicopters, VTOL transitions, fixed wings or reversible motors.
- Manual-throttle ANGLE flight only. Navigation modes, heading hold, horizon,
failsafe and programming overrides inhibit the test.
- A working, already flyable tune. This is not a way to make an unstable aircraft
flyable. Maintain visual control and space to recover from the excitation.
- Blackbox must actually be recording with `debug_mode = CHIRP`, at least
600 samples/second. The PID loop must also run at least at 600 Hz. For example,
a 1 kHz PID loop with `blackbox_rate_num = 1` and `blackbox_rate_denom = 1`
meets this requirement. A 500 Hz log does not.
- A dedicated receiver switch channel between **5 and 12** (5 = AUX1), with no
other function assigned to it. Channels 13-32 can be overlaid by MSP, so are
deliberately excluded. The selected channel must exist on the receiver.

The feature is built when `USE_CHIRP` and `USE_BLACKBOX` are available, and is
disabled by default (`chirp_axis = OFF`). It uses a receiver channel directly,
without adding or reusing a flight-mode ID or changing MSP status payloads.

## Configuration

Save a `diff all` backup first. Set up and verify normal Blackbox recording before
enabling the test. For example, to prepare a roll measurement using CH8 (AUX4):

```text
set chirp_axis = ROLL
set chirp_trigger_channel = 8
set chirp_amplitude = 10
set debug_mode = CHIRP
save
```

`chirp_amplitude` is the peak rate perturbation in degrees/second (1-30, default
10). It describes the commanded excitation, not the maximum aircraft response.
Use small excitation appropriate to the airframe. Change the axis to `PITCH` or
`YAW` while disarmed to measure the other axes. Do not change controller settings
or profiles during a run.

## Measurement sequence

1. Leave the trigger low, take off normally, and establish a level hover in
ANGLE with manual throttle. This implementation does not hold position or
altitude for the pilot.
2. With roll/pitch/yaw sticks centred and recording running, the test observes
the low trigger (900-1300). Switching high (1700-2100) starts a two-second
settling period followed by one 20-second logarithmic sweep from 2 to 60 Hz.
The amplitude fades in and out over one second.
3. Keep the aircraft under control. Moving a stick, moving the trigger out of
its high range, or changing to an incompatible mode stops excitation. After
an abort the switch must be observed low again under valid conditions before
another high transition can start a test. A held-high switch never repeats
a completed run and cannot start a run at arming or after receiver recovery.
4. Switch low, land and inspect the log. Repeat for another axis if needed.
5. Disable the experiment afterwards with `set chirp_axis = OFF` and `save`.

During settling and excitation, any of the following stops the test:

- Disarm, loss of valid receiver data, failsafe, wrong controller/platform/mode,
landing detection, mixer transition, MSP RC override or programming override.
- Roll/pitch/yaw channel deviation above 50 from centre, processed stick command
above 50, processed throttle outside (1200, 1800), or throttle change greater
than 100 from its value at test start.
- Roll or pitch exceeding 20 degrees, measured gyro rate above 200 degrees/sec
on any axis, non-finite gyro data or motor mix range at/above 0.9.
- Blackbox stopping/pausing, inadequate logging rate, missing CHIRP debug mode,
changed chirp configuration/control profile/mixer profile, or a PID update gap
exceeding 10 ms.

These limits are conservative experimental checks, not a guarantee of safe
flight. Receiver values are processed by INAV's normal RX path. Mixer saturation
feedback is from the preceding mixer update, and storage-full/write-error
detection follows the normal Blackbox state machine. A short output clipping
event or dropped log record can still occur; inspect the recorded output and
timestamps and discard invalid runs. There is no automatic airborne detection
beyond the normal landing flag: deliberately operating the switch while armed
on the ground can start the test if the other conditions are met.

## Blackbox contract

The new `CHIRP` debug enum is appended; existing debug IDs are unchanged. All
eight debug channels are populated on each PID iteration. Phase values are:
0 idle/disabled, 1 ready, 2 settling, 3 running, 4 complete, 5 aborted.

| Field | Meaning | Scale |
| --- | --- | --- |
| `debug[0]` | Phase | enum above |
| `debug[1]` | Selected axis | 0 off, 1 roll, 2 pitch, 3 yaw |
| `debug[2]` | Injected rate perturbation | degrees/sec × 100 |
| `debug[3]` | Instantaneous sweep frequency | Hz × 100 |
| `debug[4]` | Inhibit/abort bitmask | below |
| `debug[5]` | Actual selected-axis rate-controller setpoint, including chirp | degrees/sec × 100 |
| `debug[6]` | Selected-axis gyro feedback used by the controller | degrees/sec × 100 |
| `debug[7]` | Selected-axis limited PID sum before mixing | PID output × 100 |

Inhibit bits: 1 flight conditions, 2 pilot/override input, 4 logging, 8 motion,
16 mixer saturation, 32 configuration/profile, 64 timing, 128 trigger state.
Several bits may be present. Aborted runs retain their last reason until another
inhibit occurs, a valid low trigger resets them or the feature is disabled.

Excitation is added **after** angle control, rate acceleration limiting and
programming overrides, immediately before the multicopter rate controller. The
normal P, I, D and CD paths all receive the perturbed setpoint. The standard
Blackbox setpoint is updated accordingly. `debug[6]` is the actual controller
feedback; it can differ from the standard `gyroADC` field when the Smith predictor
is enabled. No changes are made to the predictor or filters for this test.

Use `debug[2]` as the known external excitation, and retain the actual setpoint,
feedback and actuator output. ANGLE feedback and the CD path remain active, so
the gyro/excitation ratio alone is **not** the open-loop plant or the isolated
rate-controller response. Do not derive PID recommendations from that ratio
without modelling the closed loop and validating the estimate. A completed
sweep is not a quality or stability certificate.

## Developer validation

`chirp_unittest` exercises the production generator, single-shot state machine,
inhibits, switch reset, amplitude limits, sweep frequency, timing gaps and the
32-bit microsecond wrap. `chirp_flight_unittest` links the production firmware
adapter with stubbed flight inputs and exercises axis selection, logging,
controller readiness, invalid channels and the flight abort paths.

Before release, validate the full control loop in a flight-dynamics simulator,
measure CPU cost and flash/RAM use on supported boards, verify Blackbox storage
throughput, and perform staged flight tests. Unit tests and a successful SITL
build do not substitute for those checks.
34 changes: 34 additions & 0 deletions docs/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,39 @@ Blackbox logging rate numerator. Use num/denom settings to decide if a frame sho

---

### chirp_amplitude

Peak chirp perturbation in degrees/second. Select a small excitation appropriate to the aircraft; the test does not change PID gains.

| Default | Min | Max |
| --- | --- | --- |
| 10 | 1 | 30 |

---

### chirp_axis

Experimental multirotor rate chirp axis. OFF disables excitation. Requires manual-throttle ANGLE flight and CHIRP Blackbox logging. See Multirotor Chirp.md.

| Allowed Values | |
| --- | --- |
| OFF | Default |
| ROLL | |
| PITCH | |
| YAW | |

---

### chirp_trigger_channel

Dedicated receiver switch channel for the experimental chirp test (5 = AUX1). Observe low (900-1300) in valid flight conditions, then high (1700-2100) to start one sweep. Do not share with other functions.

| Default | Min | Max |
| --- | --- | --- |
| 5 | 5 | 12 |

---

### crsf_use_legacy_baro_packet

CRSF telemetry: If `ON`, send altitude about start point in GPS telemetry packet. If `OFF`, GPS has ASL altitude, altitude about start point in separate packet. Default: 'OFF'
Expand Down Expand Up @@ -803,6 +836,7 @@ Defines debug values exposed in debug variables (developer / debugging setting)
| ESC | |
| FW_TURN | |
| MAG | |
| CHIRP | |

---

Expand Down
5 changes: 5 additions & 0 deletions src/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ main_sources(COMMON_SRC
flight/secondary_dynamic_gyro_notch.h
flight/dynamic_lpf.c
flight/dynamic_lpf.h
flight/chirp.c
flight/chirp.h
flight/chirp_flight.c
flight/chirp_flight.h

flight/ez_tune.c
flight/ez_tune.h
flight/adaptive_filter.c
Expand Down
1 change: 1 addition & 0 deletions src/main/build/debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ typedef enum {
DEBUG_ESC,
DEBUG_FW_TURN,
DEBUG_MAG,
DEBUG_CHIRP,
DEBUG_COUNT // also update debugModeNames in cli.c
} debugType_e;

Expand Down
3 changes: 2 additions & 1 deletion src/main/config/parameter_group_ids.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@
#define PG_DRONECAN_CONFIG 1045
#define PG_TERRAIN_NAV_CONFIG 1046
#define PG_DRONECAN_DNA_SERVER 1047 // Separate PG so we don't wipe user settings if the allocation table changes
#define PG_INAV_END PG_DRONECAN_DNA_SERVER
#define PG_CHIRP_CONFIG 1048
#define PG_INAV_END PG_CHIRP_CONFIG

// OSD configuration (subject to change)
//#define PG_OSD_FONT_CONFIG 2047
Expand Down
3 changes: 2 additions & 1 deletion src/main/fc/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ static const char *debugModeNames[DEBUG_COUNT] = {
"TERRAIN_NAV",
"ESC",
"FW_TURN",
"MAG"
"MAG",
"CHIRP"
};

/* Sensor names (used in lookup tables for *_hardware settings and in status
Expand Down
30 changes: 29 additions & 1 deletion src/main/fc/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ tables:
"AUTOTRIM", "AUTOTUNE", "RATE_DYNAMICS", "LANDING", "POS_EST",
"ADAPTIVE_FILTER", "HEADTRACKER", "GPS", "LULU", "SBUS2", "OSD_REFRESH",
"MAG_CALIB", "VTOL_TRANSITION", "VTOL_MC_PROTECT", "TERRAIN_NAV", "ESC",
"FW_TURN", "MAG"]
"FW_TURN", "MAG", "CHIRP"]
- name: chirp_axis
values: ["OFF", "ROLL", "PITCH", "YAW"]
- name: vtol_mc_protection_mode
values: ["OFF", "NAV", "NAV_AND_STABILIZED"]
- name: aux_operator
Expand Down Expand Up @@ -1724,6 +1726,32 @@ groups:
min: 0
max: 99

- name: PG_CHIRP_CONFIG
headers: ["flight/chirp_flight.h"]
type: chirpConfig_t
condition: USE_CHIRP
members:
- name: chirp_axis
description: "Experimental multirotor rate chirp axis. OFF disables excitation. Requires manual-throttle ANGLE flight and CHIRP Blackbox logging. See Multirotor Chirp.md."
field: axis
type: uint8_t
table: chirp_axis
default_value: "OFF"
- name: chirp_trigger_channel
description: "Dedicated receiver switch channel for the experimental chirp test (5 = AUX1). Observe low (900-1300) in valid flight conditions, then high (1700-2100) to start one sweep. Do not share with other functions."
field: triggerChannel
type: uint8_t
min: 5
max: 12
default_value: 5
- name: chirp_amplitude
description: "Peak chirp perturbation in degrees/second. Select a small excitation appropriate to the aircraft; the test does not change PID gains."
field: amplitude
type: uint8_t
min: 1
max: 30
default_value: 10

- name: PG_EZ_TUNE
headers: ["flight/ez_tune.h"]
type: ezTuneSettings_t
Expand Down
94 changes: 94 additions & 0 deletions src/main/flight/chirp.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* This file is part of INAV.
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#include <math.h>

#include "flight/chirp.h"

#define CHIRP_TWO_PI 6.28318530718f
#define CHIRP_LOG_FREQUENCY_RATIO 3.40119738166f // log(60 / 2)
#define CHIRP_FADE_US 1000000U

void chirpUpdate(chirpState_t *state, uint32_t now, bool enabled, bool switchLow,
bool switchHigh, uint16_t inhibit, float amplitude)
{
state->output = 0;

if (!enabled) {
*state = (chirpState_t){0};
return;
}

if (!isfinite(amplitude) || amplitude <= 0 || amplitude > 30) {
inhibit |= CHIRP_INHIBIT_CONFIG;
}
if (state->phase == CHIRP_SETTLING || state->phase == CHIRP_RUNNING) {
if (now - state->updatedAt > CHIRP_MAX_INTERVAL_US) {
inhibit |= CHIRP_INHIBIT_TIMING;
}
}

if (inhibit) {
state->phase = CHIRP_ABORTED;
state->inhibit = inhibit;
return;
}

if (switchLow) {
*state = (chirpState_t){ .phase = CHIRP_READY, .updatedAt = now };
return;
}

if (!switchHigh) {
state->phase = CHIRP_ABORTED;
state->inhibit = CHIRP_INHIBIT_SWITCH;
return;
}

if (state->phase == CHIRP_READY) {
state->phase = CHIRP_SETTLING;
state->startedAt = now;
state->updatedAt = now;
} else if (state->phase == CHIRP_IDLE) {
state->phase = CHIRP_ABORTED;
state->inhibit = CHIRP_INHIBIT_SWITCH;
}

if (state->phase == CHIRP_SETTLING) {
state->updatedAt = now;
if (now - state->startedAt < CHIRP_SETTLE_US) {
return;
}
state->phase = CHIRP_RUNNING;
state->startedAt = now;
state->frequency = CHIRP_START_HZ;
state->angle = 0;
}

if (state->phase != CHIRP_RUNNING) {
return;
}

const uint32_t elapsed = now - state->startedAt;
if (elapsed >= CHIRP_DURATION_US) {
state->phase = CHIRP_DONE;
return;
}

const float frequency = CHIRP_START_HZ * expf(CHIRP_LOG_FREQUENCY_RATIO * elapsed / CHIRP_DURATION_US);
const float dt = (now - state->updatedAt) * 1e-6f;
// Integrate with bounded phase to avoid loss of precision late in the sweep.
state->angle = fmodf(state->angle + CHIRP_TWO_PI * 0.5f * (state->frequency + frequency) * dt, CHIRP_TWO_PI);
state->updatedAt = now;
state->frequency = frequency;

float envelope = 1;
if (elapsed < CHIRP_FADE_US) {
envelope = (float)elapsed / CHIRP_FADE_US;
} else if (CHIRP_DURATION_US - elapsed < CHIRP_FADE_US) {
envelope = (float)(CHIRP_DURATION_US - elapsed) / CHIRP_FADE_US;
}
state->output = amplitude * envelope * sinf(state->angle);
}
Loading
Loading