Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d12265e
io: add an SRXL2 bus master for Spektrum Smart ESCs
MrScothh Sep 12, 2026
cb103f6
io: fix five SRXL2 master defects the specification does not warn about
MrScothh Sep 12, 2026
203d81a
io: report a healthy RSSI, and record why unused channels stay unsent
MrScothh Sep 12, 2026
b8c46d4
io: record why the channel scaling mirrors a receiver rather than the…
MrScothh Sep 12, 2026
7901955
io, cli: let INAV teach a Spektrum Smart ESC its throttle range
MrScothh Sep 12, 2026
5b67d30
io: record the process cadence the master state machine needs
MrScothh Sep 12, 2026
0efa3b9
io: stop requesting telemetry in failsafe, and cite the source for th…
MrScothh Sep 12, 2026
7b4ae1d
io: never announce failsafe to the ESC, because INAV owns failsafe
MrScothh Sep 12, 2026
2263ade
io: wait for the ESC powering up as an event, not as leftover state
MrScothh Sep 12, 2026
e75437f
Wire the SRXL2 ESC driver into motor output, telemetry and the RPM fi…
MrScothh Sep 12, 2026
3a0d0eb
esc_sensor: let SRXL2 telemetry be switched off, and nothing else change
MrScothh Sep 12, 2026
357340a
msp: expose the SRXL2 ESC calibration over MSP
MrScothh Sep 12, 2026
c1006c4
docs: document the Spektrum Smart ESC protocol, and enable it for SITL
MrScothh Sep 12, 2026
9842137
SRXL2: drive N ESCs on N ports
MrScothh Sep 12, 2026
b4710d4
SRXL2: make thrust reverse a mode, and fix two ways it misfired
MrScothh Sep 13, 2026
58293bf
Do not let a DSHOT range check rewrite the SRXL2 protocol
MrScothh Sep 13, 2026
a11f766
SRXL2: report why a calibration was refused
MrScothh Sep 13, 2026
d378c8f
SRXL2: report the real motor count, and open the ports on SITL
MrScothh Sep 13, 2026
e232c57
SRXL2: make the telemetry rate configurable
MrScothh Sep 13, 2026
30fd0f7
esc_sensor: do not require a serial port to report telemetry
MrScothh Sep 13, 2026
78dae82
SRXL2: drop the reversible-motors route to reverse
MrScothh Sep 13, 2026
74efc80
SRXL2: take serial function bit 29, leaving 28 to the thermal camera
MrScothh Sep 14, 2026
4096e0f
Keep the reverse channel default on boards that already had settings
MrScothh Sep 14, 2026
5a9b942
docs: say that reverse needs Brake Type as well as a channel
MrScothh Sep 14, 2026
c16aa51
docs: say what the bench measured, including what it left open
MrScothh Sep 14, 2026
b8fed40
Send a block of channels, because one does not arm an Avian
MrScothh Sep 15, 2026
5b265fb
Measure the Avian instead of reading its manual to it
MrScothh Sep 15, 2026
ed27a4f
Refuse to arm while the SRXL2 ESC has not linked
MrScothh Sep 15, 2026
b106d2f
Answer the review: ten findings, ten changes
MrScothh Sep 16, 2026
e933b37
Merge maintenance-10.x, and give up 0x2232
MrScothh Sep 16, 2026
40c2d80
Say plainly that the RPM filter cannot track throttle on this link
MrScothh Sep 16, 2026
8203fea
Do not abandon an ESC that has merely gone quiet
MrScothh Sep 16, 2026
86e4b32
Wait for the ESC's startup tones before reporting it ready
MrScothh Sep 16, 2026
3f3f129
Give the ESC's startup tones another half second
MrScothh Sep 16, 2026
6b38a54
Say what the reported current is, now that it has been measured
MrScothh Sep 17, 2026
4a47ca3
Say the RPM filter is not recommended, rather than describing why
MrScothh Sep 17, 2026
ff8fcd7
Drop the em dashes from the Smart ESC guide
MrScothh Sep 17, 2026
a844a2b
Regenerate Settings.md, which never listed SRXL2 as a motor protocol
MrScothh Sep 17, 2026
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
6 changes: 6 additions & 0 deletions docs/ESC and servo outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ INAV 7 introduced extra functionality that let you force only some outputs to be
The main restrictions is that outputs are associated with timers, which can be shared between multiple outputs and two outputs on the same timer need to have the same function.

The easiest way to modify outputs, is to use the Mixer tab in the Configurator, as it will clearly show you which timer is used by all outputs, but you can also use `timer_output_mode` on the cli.

## ESCs that are not driven from an output pin

A Spektrum Smart ESC is connected to a UART rather than to a motor output, because
its protocol is serial rather than a timer waveform. None of the output mapping
above applies to it. See [Spektrum Smart ESC](Spektrum%20Smart%20ESC.md).
33 changes: 33 additions & 0 deletions docs/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,38 @@ Enable when BLHeli32 Auto Telemetry function is used. Disable in every other cas

---

### esc_srxl2_reverse_channel

For an SRXL2 Smart ESC, the 1-based auxiliary channel its "Thrust Rev." setting selects to arm reverse. Spektrum allow channels 5 to 9 and ship channel 7 by default. Must match how the ESC was programmed, because nothing on the wire advertises it. 0 disables reverse, and anything between 1 and 4 is treated as 0 at boot - the range cannot express the hole, and a channel the ESC cannot watch would offer a mode that does nothing.

| Default | Min | Max |
| --- | --- | --- |
| 7 | 0 | 9 |

---

### esc_srxl2_telemetry

Read ESC telemetry off the SRXL2 link. Only applies when motor_pwm_protocol is SRXL2, where telemetry shares the throttle wire and so cannot be turned off by leaving a port unassigned as it would be for a conventional ESC.

| Default | Min | Max |
| --- | --- | --- |
| ON | OFF | ON |

---

### esc_srxl2_telemetry_rate

How often ESC telemetry arrives from an SRXL2 Smart ESC, in readings per second. The ESC answers about two requests in three and rotates its reply between three sensors, so it delivers roughly a ninth of what is asked for - these are the delivered rates, measured, not the request rate. The reply shares the throttle wire, so a faster rate leaves the bus less headroom; only the RPM filter benefits from it. The range is bounded at both ends by the ESC: asking on every frame makes an Avian keep the link and stop obeying the throttle, and asking slower than 1 Hz makes the link time out on a healthy ESC, because its reply is the only thing that proves it is still there.

| Allowed Values | |
| --- | --- |
| 1HZ | Default |
| 3HZ | |
| 2HZ | |

---

### ez_aggressiveness

EzTune aggressiveness
Expand Down Expand Up @@ -3520,6 +3552,7 @@ Protocol that is used to send motor updates to ESCs. Possible values - STANDARD,
| DSHOT150 | |
| DSHOT300 | |
| DSHOT600 | |
| SRXL2 | |

---

Expand Down
345 changes: 345 additions & 0 deletions docs/Spektrum Smart ESC.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ main_sources(COMMON_SRC
io/adsb.h
io/beeper.c
io/beeper.h
io/motor_srxl2.c
io/motor_srxl2.h
io/servo_sbus.c
io/servo_sbus.h
io/frsky_osd.c
Expand Down
20 changes: 20 additions & 0 deletions src/main/drivers/pwm_mapping.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "sensors/rangefinder.h"

#include "io/serial.h"
#include "io/motor_srxl2.h"
#include "io/servo_sbus.h"

enum {
Expand Down Expand Up @@ -70,6 +71,11 @@ static const motorProtocolProperties_t motorProtocolProperties[] = {
[PWM_TYPE_DSHOT150] = { .usesHwTimer = true, .isDSHOT = true },
[PWM_TYPE_DSHOT300] = { .usesHwTimer = true, .isDSHOT = true },
[PWM_TYPE_DSHOT600] = { .usesHwTimer = true, .isDSHOT = true },
/* Not a timer waveform at all: the ESC hangs off a UART. The entry has to be
* here even on a target built without the protocol, because the value is
* stored in configuration and a diff restored from a board that does have it
* would otherwise index past the end of this array. */
[PWM_TYPE_SRXL2] = { .usesHwTimer = false, .isDSHOT = false },
};

pwmInitError_e getPwmInitError(void)
Expand Down Expand Up @@ -429,6 +435,20 @@ static void pwmInitMotors(timMotorServoHardware_t * timOutputs)
// Do the pre-configuration. For motors w/o hardware timers this should be sufficient
pwmMotorPreconfigure();

#ifdef USE_MOTOR_SRXL2
/*
* SRXL2 carries one ESC per port, so the mixer's motor count has to be met by
* that many assigned ports. A motor with no port has nowhere to send its
* command and no timer output to fall back on, and dropping it silently would
* mean arming a twin that can only drive one side.
*/
if (motorConfig()->motorPwmProtocol == PWM_TYPE_SRXL2 && srxl2MotorCount() < motorCount) {
pwmInitError = PWM_INIT_ERROR_NOT_ENOUGH_MOTOR_OUTPUTS;
LOG_ERROR(PWM, "Not enough SRXL2 ports. Mixer requested %d, ports %d", motorCount, srxl2MotorCount());
return;
}
#endif

// Now if we need to configure individual motor outputs - do that
if (!motorsUseHardwareTimers()) {
LOG_INFO(PWM, "Skipped timer init for motors");
Expand Down
4 changes: 4 additions & 0 deletions src/main/drivers/pwm_mapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ typedef enum {
PWM_TYPE_DSHOT150,
PWM_TYPE_DSHOT300,
PWM_TYPE_DSHOT600,
/* Appended, not inserted: the value is stored in configuration. Unlike
* everything above it this is a UART protocol rather than a timer waveform,
* so the ESC signal goes to a serial pin and not to a motor pad. */
PWM_TYPE_SRXL2,
} motorPwmProtocolTypes_e;

typedef enum {
Expand Down
15 changes: 15 additions & 0 deletions src/main/drivers/pwm_output.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "drivers/timer.h"
#include "drivers/pwm_mapping.h"
#include "drivers/pwm_output.h"
#include "io/motor_srxl2.h"
#include "io/servo_sbus.h"
#include "sensors/esc_sensor.h"

Expand Down Expand Up @@ -616,6 +617,20 @@ void pwmMotorPreconfigure(void)
motorWritePtr = pwmWriteDigital;
break;
#endif

#ifdef USE_MOTOR_SRXL2
case PWM_TYPE_SRXL2:
/* Nothing to fall back on if this fails: the pin is a UART pin, not a
* timer output, so there is no PWM to degrade to. Leaving
* motorWritePtr null keeps the motor unwritten, which is the honest
* outcome of a port that was never assigned. */
if (srxl2MotorInitialize()) {
srxl2MotorSetReverseChannel(motorConfig()->srxl2ReverseChannel);
srxl2MotorSetTelemetryRate(motorConfig()->srxl2TelemetryRate);
motorWritePtr = srxl2MotorUpdate;
}
break;
#endif
}
}

Expand Down
86 changes: 86 additions & 0 deletions src/main/fc/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ bool cliMode = false;
#include "io/ledstrip.h"
#include "io/osd.h"
#include "io/osd/custom_elements.h"
#include "io/motor_srxl2.h"
#include "io/serial.h"

#include "fc/fc_msp_box.h"
Expand Down Expand Up @@ -4743,6 +4744,88 @@ static void cliDiff(char *cmdline)
printConfig(cmdline, true);
}

#ifdef USE_MOTOR_SRXL2
static void cliEscCalibratePrintResult(srxl2CalResult_e r)
{
switch (r) {
case SRXL2_CAL_ACCEPTED:
break;
case SRXL2_CAL_REJECT_ARMED:
cliPrintErrorLinef("Not while armed");
break;
case SRXL2_CAL_REJECT_NO_PORT:
cliPrintErrorLinef("No SRXL2 ESC port. Assign one and set motor_pwm_protocol = SRXL2");
break;
case SRXL2_CAL_REJECT_BATTERY_PRESENT:
cliPrintErrorLinef("Disconnect the battery first. The ESC only reads its");
cliPrintErrorLinef("endpoints as it powers up, and full throttle must not be");
cliPrintErrorLinef("presented to an ESC that can already act on it.");
break;
case SRXL2_CAL_REJECT_NO_VOLTAGE_SENSOR:
cliPrintErrorLinef("No battery voltage sensing, so the ESC powering up cannot be");
cliPrintErrorLinef("detected. Use 'esc_calibrate high' and 'low' by hand instead.");
break;
}
}

static void cliEscCalibrate(char *cmdline)
{
static const char * const phaseName[] = {
"off", "waiting for battery", "holding high", "holding low",
"holding high (manual)", "holding low (manual)"
};

if (isEmpty(cmdline)) {
cliPrintLinef("Phase: %s", phaseName[srxl2MotorCalibrationPhase()]);
cliPrintLine("");
cliPrintLine("Teaches a Spektrum Smart ESC its throttle endpoints. The ESC reads");
cliPrintLine("them from the signal present as it powers up, so the sequence is");
cliPrintLine("timed from the moment the battery goes in.");
cliPrintLine("");
cliPrintLine("REMOVE THE PROPELLER. This commands full throttle.");
cliPrintLine("");
cliPrintLine(" esc_calibrate start battery DISCONNECTED, then plug it in");
cliPrintLine(" when told. The rest is automatic.");
cliPrintLine(" esc_calibrate off abort");
cliPrintLine("");
cliPrintLine("By hand, for boards without battery voltage sensing:");
cliPrintLine(" esc_calibrate high then connect the battery");
cliPrintLine(" esc_calibrate low within five seconds of the two short tones");
cliPrintLine("");
cliPrintLine("Every phase ends by itself. Arming cancels it.");
return;
}

if (sl_strcasecmp(cmdline, "start") == 0) {
const srxl2CalResult_e r = srxl2MotorCalibrationBegin();
cliEscCalibratePrintResult(r);
if (r == SRXL2_CAL_ACCEPTED) {
cliPrintLine("Propeller off? Full throttle is now on the wire.");
cliPrintLine("Connect the battery. The ESC will sound its tones, and the");
cliPrintLine("throttle drops to minimum on its own about three seconds later.");
cliPrintLine("A long tone means the range was stored.");
}
} else if (sl_strcasecmp(cmdline, "high") == 0) {
const srxl2CalResult_e r = srxl2MotorCalibrationManual(SRXL2_CAL_HIGH_MANUAL);
cliEscCalibratePrintResult(r);
if (r == SRXL2_CAL_ACCEPTED) {
cliPrintLine("Full throttle on the wire. Connect the battery now.");
}
} else if (sl_strcasecmp(cmdline, "low") == 0) {
const srxl2CalResult_e r = srxl2MotorCalibrationManual(SRXL2_CAL_LOW_MANUAL);
cliEscCalibratePrintResult(r);
if (r == SRXL2_CAL_ACCEPTED) {
cliPrintLine("Low throttle on the wire. Listen for the cell count, then a long tone.");
}
} else if (sl_strcasecmp(cmdline, "off") == 0) {
srxl2MotorCalibrationAbort();
cliPrintLine("Aborted.");
} else {
cliShowParseError();
}
}
#endif

#ifdef USE_USB_MSC
static void cliMsc(char *cmdline)
{
Expand Down Expand Up @@ -5025,6 +5108,9 @@ const clicmd_t cmdTable[] = {
CLI_COMMAND_DEF("memory", "view memory usage", NULL, cliMemory),
CLI_COMMAND_DEF("mmix", "custom motor mixer", NULL, cliMotorMix),
CLI_COMMAND_DEF("motor", "get/set motor", "<index> [<value>]", cliMotor),
#ifdef USE_MOTOR_SRXL2
CLI_COMMAND_DEF("esc_calibrate", "teach a Spektrum Smart ESC its throttle range", "[start|high|low|off]", cliEscCalibrate),
#endif
#ifdef USE_USB_MSC
CLI_COMMAND_DEF("msc", "switch into msc mode", NULL, cliMsc),
#endif
Expand Down
31 changes: 30 additions & 1 deletion src/main/fc/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,40 @@ void validateAndFixConfig(void)

// Limitations of different protocols
#if !defined(USE_DSHOT)
if (motorConfig()->motorPwmProtocol > PWM_TYPE_BRUSHED) {
// Named explicitly rather than tested as "above BRUSHED". This is a DSHOT
// check, and the enum has since grown a UART protocol above DSHOT600 that a
// build without DSHOT can still drive perfectly well - a range test would
// quietly rewrite it to MULTISHOT on every boot.
if (motorConfig()->motorPwmProtocol >= PWM_TYPE_DSHOT150 &&
motorConfig()->motorPwmProtocol <= PWM_TYPE_DSHOT600) {
motorConfigMutable()->motorPwmProtocol = PWM_TYPE_MULTISHOT;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
#endif

#if !defined(USE_MOTOR_SRXL2)
// A configuration restored onto a build without the driver would keep SRXL2
// selected, and nothing would drive the motors: the branch that installs the
// SRXL2 writer is compiled out, so the writer stays null, while the protocol
// is not timer-based and so escapes the "not enough outputs" check too. The
// result is a model that arms and does nothing, which is the one outcome
// worth spending a boot-time rewrite on.
if (motorConfig()->motorPwmProtocol == PWM_TYPE_SRXL2) {
motorConfigMutable()->motorPwmProtocol = PWM_TYPE_STANDARD;
}
#else
// Reverse is either off or one of the channels a Smart ESC can be programmed
// to watch, which Spektrum document as 5 to 9. The setting's range cannot
// express "zero, or five to nine", so anything between is normalised here:
// channel 1 aliases the throttle and the driver refuses it outright, and 2
// to 4 would be sent faithfully to an ESC with no way to act on them. Both
// used to leave a reverse mode the Configurator offered and nothing
// performed. Corrected to off, where it can be seen.
const uint8_t reverseChannel = motorConfig()->srxl2ReverseChannel;
if (reverseChannel != 0 && (reverseChannel < 5 || reverseChannel > 9)) {
motorConfigMutable()->srxl2ReverseChannel = 0;
}
#endif

// Call target-specific validation function
validateAndFixTargetConfig();

Expand Down
22 changes: 21 additions & 1 deletion src/main/fc/fc_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "drivers/time.h"
#include "drivers/system.h"
#include "drivers/pwm_output.h"
#include "drivers/pwm_mapping.h"

#include "sensors/sensors.h"
#include "sensors/diagnostics.h"
Expand Down Expand Up @@ -88,6 +89,8 @@

#include "flight/mixer_profile.h"
#include "flight/mixer.h"

#include "io/motor_srxl2.h"
#include "flight/servos.h"
#include "flight/pid.h"
#include "flight/imu.h"
Expand Down Expand Up @@ -301,7 +304,24 @@ static void updateArmingStatus(void)
}

/* CHECK: */
if (!isHardwareHealthy()) {
bool escLinkMissing = false;
#ifdef USE_MOTOR_SRXL2
/*
* An SRXL2 ESC announces itself in the third of a second after it gains
* power, and is silent from then on: if that announcement is missed the
* link never forms, and the throttle reaches nothing. Arming meanwhile
* commands a motor that is not listening - the model looks armed, the
* telemetry looks sane, and the propeller does not turn.
*
* Refuse to arm until the link is actually up. Where both come up on the
* same battery this costs about a second at power-up and is invisible;
* where it does not, it is the difference between finding out on the
* bench and finding out on the takeoff roll.
*/
escLinkMissing = (motorConfig()->motorPwmProtocol == PWM_TYPE_SRXL2)
&& !srxl2MotorIsConnected();
#endif
if (!isHardwareHealthy() || escLinkMissing) {
ENABLE_ARMING_FLAG(ARMING_DISABLED_HARDWARE_FAILURE);
}
else {
Expand Down
29 changes: 29 additions & 0 deletions src/main/fc/fc_init.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
#include "io/osd.h"
#include "io/osd_dji_hd.h"
#include "io/rcdevice_cam.h"
#include "io/motor_srxl2.h"
#include "io/serial.h"
#include "io/displayport_msp.h"
#include "io/smartport_master.h"
Expand Down Expand Up @@ -340,6 +341,19 @@ void init(void)
if (motorConfig()->motorPwmProtocol == PWM_TYPE_BRUSHED) {
featureClear(FEATURE_REVERSIBLE_MOTORS);
}
#ifdef USE_MOTOR_SRXL2
/*
* A Spektrum Smart ESC reverses on a switch and goes on reading the throttle
* normally - Spektrum put it plainly: "flipping the designated switch reverses
* motor rotation, throttle will still control motor speed". Reversible motors
* means the other arrangement, where the stick centre is zero thrust, and
* enabling it here would hand the ESC roughly half throttle at the point the
* pilot expects the motor stopped. Reverse is the THRUST REVERSE mode instead.
*/
if (motorConfig()->motorPwmProtocol == PWM_TYPE_SRXL2) {
featureClear(FEATURE_REVERSIBLE_MOTORS);
}
#endif
if (!STATE(ALTITUDE_CONTROL)) {
featureClear(FEATURE_AIRMODE);
}
Expand All @@ -353,6 +367,21 @@ void init(void)
}
#else
DISABLE_ARMING_FLAG(ARMING_DISABLED_PWM_OUTPUT_ERROR);
#ifdef USE_MOTOR_SRXL2
/*
* SITL has no motor output layer - the simulator reads the mixer's motor[]
* array directly, so pwmMotorPreconfigure() never runs and nothing would open
* the SRXL2 ports. Open them here instead: SITL maps every UART onto a TCP
* port, so this is what lets a simulated ESC be attached to the real driver
* and the handshake, telemetry and calibration paths be exercised - and the
* Configurator show its ESC block - without any hardware.
*/
if (motorConfig()->motorPwmProtocol == PWM_TYPE_SRXL2) {
srxl2MotorInitialize();
srxl2MotorSetReverseChannel(motorConfig()->srxl2ReverseChannel);
srxl2MotorSetTelemetryRate(motorConfig()->srxl2TelemetryRate);
}
#endif
#endif
systemState |= SYSTEM_STATE_MOTORS_READY;

Expand Down
Loading