Firmware for ARM-based brushless ESC (electronic speed controllers).
ARK32 is a fork of upstream AM32 maintained by ARK Electronics. It tracks upstream capability while carrying ARK’s product line, control-path work, and test infrastructure. (This repository was previously published as ARK-Electronics/AM32; that URL redirects here.)
| Upstream AM32 | ARK32 | |
|---|---|---|
| Remote | am32-firmware/AM32 | ARK-Electronics/ARK32 |
| Product branch | main |
ark-release |
| Focus | Multi-vendor ESC firmware | ARK targets + maintainability + CI |
For stock AM32 releases, configurators, Discord, and community support, prefer am32.ca and the upstream project.
Upstream runs two commutation modes and switches between them at runtime: an interrupt-driven closed loop on the comparator zero-cross (ZC), and a slower polled ("old routine") mode that it falls back to whenever the average commutation interval gets long. On ARK targets that fallback was the failure mode — a single missed crossing under load dropped the loop back to poll mode, which is slower, so the average got worse, so it stayed there. Motors could sit in an OL↔CL oscillation instead of spooling.
ARK32 replaces the runtime mode switch with per-step extrapolation, the way BLHeli handles a missed crossing:
- Missed-ZC deadline. After each commutation,
COM_TIMERis re-armed as a deadline for the next crossing — expected arrival plus 50% grace. An accepted crossing cancels it by re-arming the timer for the normal commutation schedule. - Blind step. If the deadline fires first, the ESC commutates blind and takes the full elapsed time as the (late) interval measurement. The inflated sample pulls the running average toward slower timing — the safe direction for a decelerating rotor — and the next real crossing resyncs immediately. Blind steps do not count as zero crosses.
- Bounded. 8 consecutive blind steps means position is genuinely unknown: commutation stops and the existing
INTERVAL_TIMERstall rail restarts the motor through the normal startup ramp. - No CL→OL exit at runtime. Poll mode's enter thresholds are unchanged, but they now only apply during startup. Once the loop is on interrupts it stays there or restarts — it never degrades into polling.
- Interrupt-ZC trust rail. A closed loop can hold a stable false lock, tracking switching artifact edges below usable BEMF: crossings keep arriving on time, so neither the blind-step deadline nor the average-jump desync check can see anything wrong. If a closed loop's per-electrical-rev average stays above
polling_mode_changeover + 500for 4 consecutive revs, it is treated as a desync and restarted through startup. The 4-rev gate keeps a lagging average during spool-up from tripping it.
Net effect: a missed crossing costs one extrapolated step instead of a mode change. In SITL, racer_5inch spools to ~13.7k rpm and holds, where the previous behavior bounced between open and closed loop around ~3k. Low-throttle crawl and throttle-chop behavior are unchanged.
Source: Src/bemf_zc.c (deadline, blind step, resync), Src/commutation.c (mode entry), Src/runtime_loop.c (desync / trust rail).
Large control-path split out of a monolithic main.c into focused modules (runtime, settings, motor control helpers, and related MCU/F051 work). The goal is safer changes, clearer ownership of hot paths, and room for instrumentation without growing one file forever.
Native Linux build of the firmware against a simulated motor / bridge / battery, with DroneCAN over multicast UDP. Useful for protocol, startup, and logic tests without ESC hardware.
- Overview: Mcu/SITL/README.md
- CI:
.github/workflows/SITL.yml
make arm_sdk_install # once, for cross toolchain (SITL itself is host gcc)
make AM32_SITL_CAN
obj/AM32_AM32_SITL_CAN_*.elf --node-id 10 --verboseIn-the-loop bench automation for the ARK 4IN1 (and related F051 work): build/flash, drive the motor (Flight Stand and/or PX4 BDShot setups), read on-device performance counters over SWD, and produce metrics / pass-fail reports.
- Harness: hwci/README.md
- Bench setups: hwci/docs/BENCH_SETUPS.md
- CI:
.github/workflows/hwci.yml
Field bootloaders and app-side BL update use ARK32-bootloader (see Bootloader below and Bootloaders/README.md).
| Branch | Role |
|---|---|
ark-release |
ARK integration line — open product PRs here |
main |
Mirrors / tracks upstream AM32 more closely |
| Feature branches | Short-lived; rebase onto ark-release unless targeting pure upstream work |
IDE project trees (Keil / MRS) are not maintained in ARK32. Use the Makefile and the pinned xPack GNU Arm Embedded GCC (see make/tools.mk).
# Install the pinned xPack GNU Arm Embedded GCC 15 into tools/<os>/
# (required — distro gcc-arm-none-eabi is not used for firmware builds)
make arm_sdk_install
make arm_sdk_check # optional: confirm GCC 15.x at the pin path
# List / build targets (examples)
make targets
make -j$(nproc) ARK_4IN1_F051
# Production full-flash image (bootloader + app + factory EEPROM defaults)
make factory-image
# -> obj/AM32_ARK_4IN1_F051_<ver>.factory.bin (flash at 0x08000000)
make factory-image-check # same + layout/defaults gate (CI)Firmware objects land under obj/. MCU families supported by the build system include F051, F031, G071, E230, F415, F421, L431, G431, V203, G031, A153, and SITL — exact product names live in Inc/targets.h.
Do not hand-assemble production firmware (flash BL → flash app → configurator EEPROM → ST-Link dump). Build the full 32 KiB image from the repo:
make factory-imageThat links the release app, then runs scripts/build_factory_image.py to lay out:
| Region | Source |
|---|---|
Bootloader @ 0x08000000 |
Bootloaders/ ARK32-bootloader image |
Application @ 0x08001000 |
make ARK_4IN1_F051 |
EEPROM @ 0x08007C00 |
factory/ARK_4IN1_F051_eeprom_defaults.json |
Ship/program obj/AM32_ARK_4IN1_F051_*.factory.bin (or .factory.hex). Defaults (PWM-by-RPM, 1020 kV, 2 %/ms ramp, 15° fixed advance, PWM min/max 1020/1980 µs) are documented in factory/README.md.
Optional static analysis / size / format helpers:
make format # apply clang-format (.clang-format) to app + MCU sources
make check_format # fail if sources need formatting (used in PR CI)
make format_changed # format only files changed vs origin/ark-release
make cppcheck # static analysis of the ARK F051 control path
make size-check-ark # ARK F051 flash/RAM gate (HWCI+embed worst case, then release)Style is PX4-inspired via clang-format (Linux braces, tab indent width 8, int *p, column 140) — same make format / check_format workflow as PX4, not astyle itself. See .clang-format.
make format skips vendor trees (Mcu/**/Drivers, CMSIS, DroneCAN dsdl_generated / libcanard). Install clang-format with pip install --user 'clang-format==22.1.5' (version pinned to match CI) or your distro package.
- Firmware upgrade via Betaflight passthrough, single-wire serial, or related tools
- Servo PWM and DShot (300 / 600), including bi-directional DShot
- KISS-style ESC telemetry
- Variable PWM frequency and sinusoidal startup for larger motors
- Multi-vehicle use with a flight controller; crawler-oriented builds exist upstream
Upstream feature docs and crawler notes: AM32 wiki / crawler hardware.
An ESC has no speaker. Beeps are PWM on the motor phases so the windings act as a small transducer (same idea as other BLHeli-family ESCs). Implementation: Src/sounds.c / Inc/sounds.h. Volume is EEPROM beep_volume (0–11; DroneCAN param BEEP_VOLUME, default 5). Sounds only run when the motor is not spinning (idle / disarmed / zero throttle as applicable).
Pitch below is relative (higher PWM timer prescaler → lower pitch). Exact Hz depends on MCU clock and timer setup. The ARK signature tunes (startup and arm/beacon-4 morse) instead use fixed note frequencies via playBJNote.
| When you hear it | Pattern (pitch) | Function | Meaning |
|---|---|---|---|
| Power-up (brushless) | Morse “ARK” (·– / ·–· / –·–) rising C6 → E6 → G6 (or custom melody) | playStartupTune |
Firmware booted and is ready for input |
| Power-up (brushed build) | 4 rising beeps | playBrushedStartupTune |
Brushed-mode startup |
| Signal lost (after soft-reset) | Single short low blip on C5 (~70 ms) | playSignalLostTone |
RC/input timeout; distinct from the ARK boot tune |
| Arm / throttle zero accepted | Morse “R” (·–·) on G6 | playInputTune |
ESC armed / input lock-in (“roger”) |
| Arm + cell LVC enabled | That “R” once per cell | playInputTune × N |
Detected pack cell count (Vbat / 3.70) |
| Stick cal entered (PWM) | Descending whoop/sweep | playBeaconTune3 |
Entered servo high/low calibration |
| Stick cal high done | 2 notes, rising | playDefaultTone |
Max endpoint captured |
| Stick cal low done | 2 notes, falling | playChangedTone |
Min endpoint saved to EEPROM |
| DShot beacon 1 or 5 | Same as “default” 2-note rising | playDefaultTone |
Beacon / locate |
| DShot beacon 2 | 2 notes, falling | playChangedTone |
Beacon |
| DShot beacon 3 | Descending sweep | playBeaconTune3 |
Beacon |
| DShot beacon 4 | Morse “R” (·–·) on E6 | playInputTune2 |
Beacon |
| DShot cmd 12 (save settings) | Rising if normal dir, falling if reversed | playDefaultTone / playChangedTone |
Settings written |
| Function | When | Pattern |
|---|---|---|
playStartupTune |
Normal brushless boot (after init; also CRSF path) | If the previous run soft-reset from an RC signal timeout, plays playSignalLostTone instead (see below). Else if the previous run finished an app-side bootloader update, plays playBootloaderUpdatedTone then continues. Else if EEPROM custom tune byte 0 is programmed (not 0xFF): plays BlueJay-compatible melody from eepromBuffer.tune[] via playBlueJayTune. Otherwise default: the ARK signature tune — “ARK” in morse code (·– / ·–· / –·–), one letter per step up a C major arpeggio (C6 → E6 → G6), ~1.4 s total. |
playSignalLostTone |
Soft-reset after armed (~0.5 s) or disarmed (~2 s) input timeout (faultPollSignalTimeout → NVIC_SystemReset) |
One short low blip on C5 (≈ 523 Hz, ~70 ms). Marked via a .noinit cookie before reset so cold boot still plays the full ARK tune. Linker .noinit is provided for F051 and G431 (gcc + Keil G431 scatter). |
playBootloaderUpdatedTone |
Next boot after a successful app-side bootloader rewrite (maybe_update_bootloader → bootSoundMarkBootloaderUpdated → reset) |
Two rising beeps E6 → G6 (~90 ms + ~140 ms). Then the normal ARK/BlueJay startup continues. Cookie in .noinit so cold power-on never false-triggers. |
playBrushedStartupTune |
BRUSHED_MODE builds only |
Four rising beeps (~300 ms), phases 1–4 (prescalers 40 → 30 → 25 → 20). |
playBlueJayTune |
Custom startup only | Notes/rests encoded in EEPROM tune blob (configurator “custom startup music”). Inter-note pause can scale with tune header byte 3. |
Some AT32 F415 targets defer startup audio through play_tone_flag instead of calling the tune immediately at boot.
Played from the 20 kHz control path when the ESC transitions to armed-idle after a stable zero throttle (Src/control_loop.c).
| Function | When | Pattern |
|---|---|---|
playInputTune |
Armed with low-voltage cutoff mode 1 (cell-based) off, or as each cell beep | Morse “R” (·–·, “roger — signal received”) on G6 (≈ 1568 Hz), ~320 ms (same busy-wait budget as the old tune). |
| Cell-count beeps | Armed and low_voltage_cut_off == 1 |
cell_count = battery_voltage / 370 (≈ 3.70 V/cell), then playInputTune once per cell with ~100 ms gaps. Count the “R”s to read pack cell count. |
playInputTune2 |
DShot beacon 4; also used as deferred arm beep on some AT415 builds | Same morse “R” one arpeggio step lower (E6, ≈ 1319 Hz) so the beacon is distinguishable from the arm tune. |
Only for servo PWM input when stick calibration is not disabled. Sequence in Src/signal.c:
- Hold high stick long enough →
playBeaconTune3(descending multi-step whoop) — calibration mode entered. - Hold steady max until accepted →
playDefaultTone(two notes, rising: lower then higher). - Move to min and hold until accepted →
playChangedTone(two notes, falling: higher then lower) — endpoints saved.
DShot commands run only when armed, motor not running, and the command is repeated enough times (Src/dshot.c). Beacons 1–5 set play_tone_flag; actual audio plays on the next idle/low-throttle slot in setInput (Src/control_loop.c).
| DShot cmd | play_tone_flag |
Sound | Typical use |
|---|---|---|---|
| 1 | 1 | playDefaultTone — 2 notes rising |
Beacon 1 |
| 2 | 2 | playChangedTone — 2 notes falling |
Beacon 2 |
| 3 | 3 | playBeaconTune3 — long descending sweep |
Beacon 3 |
| 4 | 4 | playInputTune2 — morse “R” on E6 |
Beacon 4 |
| 5 | 5 | playDefaultTone — same as beacon 1 |
Beacon 5 |
| 12 | 1 + dir_reversed |
Rising if direction normal, falling if reversed | Save settings confirmation |
Other DShot commands (direction, bi-dir, EDT, programming mode, etc.) do not play a dedicated melody unless noted above. Direction set (7/8) currently has confirmation beeps commented out in source.
playBeaconTune3: stepped descending pitch with phase stepping (~10 ms steps, prescaler from high down toward lower values). Used as DShot beacon 3 and as the “entered stick calibration” cue.
| Setting | Effect |
|---|---|
beep_volume 0–11 |
Duty cycle of the beep PWM (volume * 3 compare counts). 0 is effectively silent; higher is louder (still limited so the motor barely moves). |
| Motor spinning / throttle up | Deferred tone flags wait until throttle is at idle; beeps are not mixed into normal drive. |
| No throttle signal after boot | ESC may stay in bootloader or keep waiting for input — you may only hear the startup tune, not the arm tune, until a valid zero throttle is seen. |
| Function | Status |
|---|---|
playDuskingTune |
Implemented in sounds.c (ascending then peaking melody) but not called from current application code. Kept for compatibility / possible future use. |
| File | Role |
|---|---|
Src/sounds.c |
All melody generators |
Src/main.c |
Startup tune at boot |
Src/control_loop.c |
Arm beeps, cell count, deferred DShot tones |
Src/dshot.c |
DShot command → tone flag |
Src/signal.c |
Servo stick-calibration tones |
Src/settings.c |
Applies beep_volume from EEPROM |
ARK ESCs use ARK32-bootloader (fork of upstream AM32-bootloader). Use ARK release images for ARK hardware — not the stock upstream bootloader alone when you need ARK-specific fixes (e.g. bidirectional DShot idle detection).
| Source / releases | ARK-Electronics/ARK32-bootloader · releases |
| Committed F051 image for app embed | Bootloaders/ (see Bootloaders/README.md) |
| App-side BL update | F051 builds embed the image by default (including HWCI_PERF=1) and rewrite the on-chip BL if it differs (Src/bootloader_update.c). Success soft-resets; the next boot plays playBootloaderUpdatedTone (two rising beeps) then the normal startup tune. Strip with EMBED_BOOTLOADER=0 or NO_EMBED_BL=1. |
To put ARK32 on a blank production ESC, flash the full-chip factory image (make factory-image → obj/*factory.bin at 0x08000000) so bootloader, app, and EEPROM defaults land in one step — see factory/README.md. For development or field app-only updates, flash a matching ARK32-bootloader with ST-LINK (if needed), then the application .bin/.hex at 0x08001000 (or use a configurator / one-wire serial). Later app flashes can also carry and apply a newer BL via the embed path above.
These are upstream / community tools; they are not ARK-specific:
- AM32 Configurator (web) and downloads
- esc-configurator.com
- Upstream bootloaders (non-ARK): am32-firmware/AM32-bootloader
- Target list:
Inc/targets.h(this tree) or upstream targets.h
ARK work centers on STM32F051 4-in-1 ESCs and related F051 targets, while the tree still builds the broader AM32 MCU set above. Upstream also documents STSPIN32F0, G071, GD32E230, AT32F415/F421, and others — see their hardware notes and compatibility charts.
| Topic | Where |
|---|---|
| ARK32 | ARK-Electronics/ARK32 issues and PRs on ark-release |
| Upstream AM32 | Discord, Patreon, am32.ca |
GPL-3.0 — see LICENSE. Same license family as upstream AM32.
AM32 exists because of its authors, sponsors, and community. ARK32 inherits that work; see the upstream README for the full sponsor and contributor lists.