Summary
78 in-tree hwdefs declare a SAFETY_IN pin without neutralising the safety default. On any
of them, a user with no physical safety switch fitted gets no PWM or DShot output at
all — and nothing in the firmware tells them a switch is expected until they attempt to
arm. Bench work (servo travel, motor tests, ESC calibration) just silently does nothing.
On Copter there is a second problem: disabling the arming check that does mention the
switch makes the diagnostics worse rather than better.
Verified against master 582a711.
Mechanism
Declaring a SAFETY_IN pin in hwdef generates HAL_GPIO_PIN_SAFETY_IN, which sets the
compiled default of BRD_SAFETY_DEFLT to 1:
// AP_BoardConfig.cpp:96-100
#ifndef BOARD_SAFETY_ENABLE_DEFAULT
#if defined(HAL_GPIO_PIN_SAFETY_IN)
// have safety startup enabled if we have a safety pin
# define BOARD_SAFETY_ENABLE_DEFAULT 1
With BRD_SAFETY_DEFLT=1, board_init_safety() does not call force_safety_off()
(board_drivers.cpp:34-48), so safety_state stays at its initial SAFETY_DISARMED
(RCOutput.cpp:161). Every channel outside BRD_SAFETY_MASK (default 0) is then zeroed:
// RCOutput.cpp:735-738
if (safety_state == AP_HAL::Util::SAFETY_DISARMED && !(safety_mask & (1U<<chan))) {
// implement safety pwm value
period_us = 0;
}
DShot channels are skipped outright at RCOutput.cpp:1694-1699. The only way out is the
physical button poll at RCOutput.cpp:2466, and 77 of the 83 boards declare the pin
INPUT PULLDOWN, so with nothing connected it reads low forever and safety never clears.
Problem 1 — nothing indicates a switch is expected until an arm attempt
This is the main issue. The parameter and its behaviour are documented on the wiki, but
the firmware itself gives the user no signal at the time it matters.
A user setting up a new board does bench work first: check servo directions, run a motor
test, calibrate ESCs. All of it produces nothing at all, with no message, because the only
thing that reports the safety state is a pre-arm check that hasn't run yet. There is no
boot-time indication that the board expects a safety switch, so the user has no reason to
connect the behaviour to safety at all — the natural conclusion is a dead board, bad
wiring, or a bad output.
ArduCopter/esc_calibration.cpp:169-179 is the one place that does say something useful
(ESC calibration: Push safety switch every 5s), but it busy-waits with no timeout, so it
hangs forever rather than failing.
Problem 2 — on Copter, disabling the check makes diagnostics worse
When the user does try to arm they get PreArm: Hardware safety switch
(AP_Arming.cpp:813), which names hardware they don't have and doesn't name the parameter
that fixes it. The natural response is to clear that check bit in ARMING_SKIPCHK. On
Copter that:
- Silences the message entirely — not even demoted to
MAV_SEVERITY_DEBUG. The
check_enabled(Check::SWITCH) test at AP_Arming.cpp:809 wraps the whole failure
branch rather than sitting inside check_failed(), so the usual severity-demotion path
at AP_Arming.cpp:358-361 is never reached. The HUD goes clean and the aircraft looks
armable.
- Still refuses to arm, but only at the instant the user presses arm, via a second,
unskippable gate:
// AP_Arming_Copter.cpp:641-644
// check if safety switch has been pushed
if (hal.util->safety_switch_state() == AP_HAL::Util::SAFETY_DISARMED) {
check_failed(true, "Safety Switch");
return false;
}
That overload carries no Check bit, so ARMING_SKIPCHK cannot reach it. The user has
traded a continuous, correctly-worded warning for silence plus a differently worded
last-moment failure (Arm: Safety Switch vs PreArm: Hardware safety switch) from a
different code path. Copter is the only vehicle with this second gate — I grepped
safety_switch_state across every vehicle directory.
Problem 3 — bypassing arming checks arms with dead outputs (low priority)
On Plane, Rover and Blimp there is no second gate, so skipping the check bit lets the
vehicle arm normally with every output dead and no safety-related message anywhere. Rover
skips straight past the base checks (AP_Arming_Rover.cpp:87 and :121);
AP_Arming_Blimp::arm_checks():268 is a bare pass-through. A forced arm
(COMPONENT_ARM_DISARM with 21196 → do_arming_checks=false, GCS_Common.cpp:5267) runs
only mandatory_checks(), which has no safety gate on any vehicle, so it bypasses even
Copter's gate.
Noted for completeness rather than as a request — a user who has disabled arming checks
has taken that on themselves. Fixing problem 1 largely removes the motivation to get here
in the first place.
Sub is not affected — ArduSub/Parameters.h's defaults table sets BRD_SAFETY_DEFLT 0
and an ARMING_SKIPCHK that already skips this check vehicle-wide. AntennaTracker behaves
differently again (it doesn't use AP_Arming at all) but is out of scope here.
Affected boards
83 hwdefs declare an active SAFETY_IN (include chains resolved). Five neutralise it:
SkySakuraH743 and modalai_fc-v1 via define BOARD_SAFETY_ENABLE_DEFAULT 0 in hwdef;
AEROFOX-H7, QioTekAdeptF407 and luminousbee5 via BRD_SAFETY_DEFLT 0 in
defaults.parm. The remaining 78, grouped by how deliberate the pin looks:
No safety LED and no IOMCU (9) — the pin looks unintended here, possibly copy-paste:
3DRControlZeroG, CBU-H7-LC-Stamp, CBU-H7-Stamp, luminousbee4, mRoControlZeroF7,
mRoControlZeroH7(+-bdshot), mRoControlZeroOEMH7, mRoCZeroOEMH7-bdshot
Safety LED present, no IOMCU (45) — intended, but these are exactly the boards where
users fit a GPS with no switch:
ARKV6S, ARKV6X(+bdshot), Atlas-Control, CORVON_V5, CUAV-7-Nano(+ODID),
CUAV-Nora(+ODID/bdshot), CUAV-X25-EVO(+ODID), CUAV-X25-MEGA(+ODID),
CUAV-X7(+ODID/bdshot), FlyingMoonF407/F427/H743, GreenSightUltraBlue, JFB100,
JFB110, JFB200, KT-FMU-F1, NarinFC-H5/H7, PixFlamingo(+F767), Pixracer(+bdshot),
QioTekZealotF427, QioTekZealotH743(+bdshot), SIYI-UniFC-6-PICO(+ODID), VUAV-TinyV7,
VUAV-V7pro, YJUAV_A6, YJUAV_A6SE, YJUAV_A6SE_H743, YJUAV_A6Ultra,
mRoControlZeroClassic, mini-pix, rFCU
IOMCU / Pixhawk-standard (24) — long-standing intended behaviour, switch normally
supplied; listed for completeness:
AcctonGodwit_GA1, Aeromind6X, CUAV-V6X-v2(+ODID), CUAVv5Nano(+bdshot), CyberX-v10,
Lectron-Pi5-H7, PH4-mini(+bdshot), Pixhawk5X, Pixhawk6X(+ODID/PPPGW/bdshot),
SIYI_N7(+ODID), SULILGH7-P1-P2, SVehicle-E2, TBS-Colibri-F7, YARIV6X,
ZeroOneX6(+ODID), ZeroOneX6_Air
Note this scan covers the SAFETY_IN route only. BOARD_SAFETY_ENABLE_DEFAULT also
resolves to HAL_WITH_IO_MCU when there is no FMU safety pin (AP_BoardConfig.cpp:100-103),
so there may be additional IOMCU boards reaching the same state by that path.
Three pin-level anomalies found while scanning
JFB100 is the only board of the 83 declaring PE10 SAFETY_IN INPUT PULLUP. With no
switch fitted the pin reads high, i.e. permanently "pressed". safety_button_handle_pressed()
fires only at press_count == 10 exactly and the counter then runs on to saturation, so
safety is toggled off once at boot and the switch is inert thereafter. Either the polarity
is inverted relative to every other board, or it is a typo.
mRoControlZeroClassic (PA10 SAFETY_IN INPUT) and mini-pix
(PC4 SAFETY_IN INPUT) declare no pull at all, so the input floats.
Suggested fixes
-
Add a boot banner line when the board expects a safety switch.
GCS_MAVLINK::send_banner() already carries exactly this class of non-pre-arm boot
message — IOMCU:, the RCOut: output-mode line, INS fast-sampling status
(GCS_Common.cpp, send_banner()). A line there when HAL_GPIO_PIN_SAFETY_IN is
defined and BRD_SAFETY_DEFLT is 1 — naming the parameter — would put the information
in front of the user at the point they start bench testing, which is where it's
currently missing. It's re-sent on every GCS connect, so there's no init-ordering
problem.
-
Fix the Copter asymmetry. Either give the AP_Arming_Copter.cpp:642 gate the
Check::SWITCH bit so it is skippable like every other safety check, or leave it
unskippable but have hardware_safety_check() still report when the check is disabled,
so silencing the check does not silence the diagnosis.
-
Audit the 9 bucket-C boards for whether the SAFETY_IN pin reflects real hardware,
and the three pin anomalies above.
No change proposed for problem 3.
Happy to open PRs for any of these.
Summary
78 in-tree hwdefs declare a
SAFETY_INpin without neutralising the safety default. On anyof them, a user with no physical safety switch fitted gets no PWM or DShot output at
all — and nothing in the firmware tells them a switch is expected until they attempt to
arm. Bench work (servo travel, motor tests, ESC calibration) just silently does nothing.
On Copter there is a second problem: disabling the arming check that does mention the
switch makes the diagnostics worse rather than better.
Verified against master
582a711.Mechanism
Declaring a
SAFETY_INpin in hwdef generatesHAL_GPIO_PIN_SAFETY_IN, which sets thecompiled default of
BRD_SAFETY_DEFLTto 1:With
BRD_SAFETY_DEFLT=1,board_init_safety()does not callforce_safety_off()(
board_drivers.cpp:34-48), sosafety_statestays at its initialSAFETY_DISARMED(
RCOutput.cpp:161). Every channel outsideBRD_SAFETY_MASK(default 0) is then zeroed:DShot channels are skipped outright at
RCOutput.cpp:1694-1699. The only way out is thephysical button poll at
RCOutput.cpp:2466, and 77 of the 83 boards declare the pinINPUT PULLDOWN, so with nothing connected it reads low forever and safety never clears.Problem 1 — nothing indicates a switch is expected until an arm attempt
This is the main issue. The parameter and its behaviour are documented on the wiki, but
the firmware itself gives the user no signal at the time it matters.
A user setting up a new board does bench work first: check servo directions, run a motor
test, calibrate ESCs. All of it produces nothing at all, with no message, because the only
thing that reports the safety state is a pre-arm check that hasn't run yet. There is no
boot-time indication that the board expects a safety switch, so the user has no reason to
connect the behaviour to safety at all — the natural conclusion is a dead board, bad
wiring, or a bad output.
ArduCopter/esc_calibration.cpp:169-179is the one place that does say something useful(
ESC calibration: Push safety switchevery 5s), but it busy-waits with no timeout, so ithangs forever rather than failing.
Problem 2 — on Copter, disabling the check makes diagnostics worse
When the user does try to arm they get
PreArm: Hardware safety switch(
AP_Arming.cpp:813), which names hardware they don't have and doesn't name the parameterthat fixes it. The natural response is to clear that check bit in
ARMING_SKIPCHK. OnCopter that:
MAV_SEVERITY_DEBUG. Thecheck_enabled(Check::SWITCH)test atAP_Arming.cpp:809wraps the whole failurebranch rather than sitting inside
check_failed(), so the usual severity-demotion pathat
AP_Arming.cpp:358-361is never reached. The HUD goes clean and the aircraft looksarmable.
unskippable gate:
That overload carries no
Checkbit, soARMING_SKIPCHKcannot reach it. The user hastraded a continuous, correctly-worded warning for silence plus a differently worded
last-moment failure (
Arm: Safety SwitchvsPreArm: Hardware safety switch) from adifferent code path. Copter is the only vehicle with this second gate — I grepped
safety_switch_stateacross every vehicle directory.Problem 3 — bypassing arming checks arms with dead outputs (low priority)
On Plane, Rover and Blimp there is no second gate, so skipping the check bit lets the
vehicle arm normally with every output dead and no safety-related message anywhere. Rover
skips straight past the base checks (
AP_Arming_Rover.cpp:87and:121);AP_Arming_Blimp::arm_checks():268is a bare pass-through. A forced arm(
COMPONENT_ARM_DISARMwith 21196 →do_arming_checks=false,GCS_Common.cpp:5267) runsonly
mandatory_checks(), which has no safety gate on any vehicle, so it bypasses evenCopter's gate.
Noted for completeness rather than as a request — a user who has disabled arming checks
has taken that on themselves. Fixing problem 1 largely removes the motivation to get here
in the first place.
Sub is not affected —
ArduSub/Parameters.h's defaults table setsBRD_SAFETY_DEFLT 0and an
ARMING_SKIPCHKthat already skips this check vehicle-wide. AntennaTracker behavesdifferently again (it doesn't use
AP_Armingat all) but is out of scope here.Affected boards
83 hwdefs declare an active
SAFETY_IN(include chains resolved). Five neutralise it:SkySakuraH743andmodalai_fc-v1viadefine BOARD_SAFETY_ENABLE_DEFAULT 0in hwdef;AEROFOX-H7,QioTekAdeptF407andluminousbee5viaBRD_SAFETY_DEFLT 0indefaults.parm. The remaining 78, grouped by how deliberate the pin looks:No safety LED and no IOMCU (9) — the pin looks unintended here, possibly copy-paste:
3DRControlZeroG,CBU-H7-LC-Stamp,CBU-H7-Stamp,luminousbee4,mRoControlZeroF7,mRoControlZeroH7(+-bdshot),mRoControlZeroOEMH7,mRoCZeroOEMH7-bdshotSafety LED present, no IOMCU (45) — intended, but these are exactly the boards where
users fit a GPS with no switch:
ARKV6S,ARKV6X(+bdshot),Atlas-Control,CORVON_V5,CUAV-7-Nano(+ODID),CUAV-Nora(+ODID/bdshot),CUAV-X25-EVO(+ODID),CUAV-X25-MEGA(+ODID),CUAV-X7(+ODID/bdshot),FlyingMoonF407/F427/H743,GreenSightUltraBlue,JFB100,JFB110,JFB200,KT-FMU-F1,NarinFC-H5/H7,PixFlamingo(+F767),Pixracer(+bdshot),QioTekZealotF427,QioTekZealotH743(+bdshot),SIYI-UniFC-6-PICO(+ODID),VUAV-TinyV7,VUAV-V7pro,YJUAV_A6,YJUAV_A6SE,YJUAV_A6SE_H743,YJUAV_A6Ultra,mRoControlZeroClassic,mini-pix,rFCUIOMCU / Pixhawk-standard (24) — long-standing intended behaviour, switch normally
supplied; listed for completeness:
AcctonGodwit_GA1,Aeromind6X,CUAV-V6X-v2(+ODID),CUAVv5Nano(+bdshot),CyberX-v10,Lectron-Pi5-H7,PH4-mini(+bdshot),Pixhawk5X,Pixhawk6X(+ODID/PPPGW/bdshot),SIYI_N7(+ODID),SULILGH7-P1-P2,SVehicle-E2,TBS-Colibri-F7,YARIV6X,ZeroOneX6(+ODID),ZeroOneX6_AirNote this scan covers the
SAFETY_INroute only.BOARD_SAFETY_ENABLE_DEFAULTalsoresolves to
HAL_WITH_IO_MCUwhen there is no FMU safety pin (AP_BoardConfig.cpp:100-103),so there may be additional IOMCU boards reaching the same state by that path.
Three pin-level anomalies found while scanning
JFB100is the only board of the 83 declaringPE10 SAFETY_IN INPUT PULLUP. With noswitch fitted the pin reads high, i.e. permanently "pressed".
safety_button_handle_pressed()fires only at
press_count == 10exactly and the counter then runs on to saturation, sosafety is toggled off once at boot and the switch is inert thereafter. Either the polarity
is inverted relative to every other board, or it is a typo.
mRoControlZeroClassic(PA10 SAFETY_IN INPUT) andmini-pix(
PC4 SAFETY_IN INPUT) declare no pull at all, so the input floats.Suggested fixes
Add a boot banner line when the board expects a safety switch.
GCS_MAVLINK::send_banner()already carries exactly this class of non-pre-arm bootmessage —
IOMCU:, theRCOut:output-mode line, INS fast-sampling status(
GCS_Common.cpp,send_banner()). A line there whenHAL_GPIO_PIN_SAFETY_INisdefined and
BRD_SAFETY_DEFLTis 1 — naming the parameter — would put the informationin front of the user at the point they start bench testing, which is where it's
currently missing. It's re-sent on every GCS connect, so there's no init-ordering
problem.
Fix the Copter asymmetry. Either give the
AP_Arming_Copter.cpp:642gate theCheck::SWITCHbit so it is skippable like every other safety check, or leave itunskippable but have
hardware_safety_check()still report when the check is disabled,so silencing the check does not silence the diagnosis.
Audit the 9 bucket-C boards for whether the
SAFETY_INpin reflects real hardware,and the three pin anomalies above.
No change proposed for problem 3.
Happy to open PRs for any of these.