Skip to content

perf(advance): key the auto-advance schedule on erpm, not duty cycle#55

Draft
AlexKlimaj wants to merge 2 commits into
ark-releasefrom
claude/timing-advance
Draft

perf(advance): key the auto-advance schedule on erpm, not duty cycle#55
AlexKlimaj wants to merge 2 commits into
ark-releasefrom
claude/timing-advance

Conversation

@AlexKlimaj

Copy link
Copy Markdown
Member

Problem

auto_advance_level was a single linear map of duty cycle (Src/runtime_loop.c:440):

auto_advance_level = map(duty_cycle, 100, 2000, 13, 23);

Commutation lag is dominated by the phase lag of current behind applied voltage, atan(w*L/R), so what the schedule wants on its x axis is a measure of speed. Duty is only a proxy for speed at one load, and the error runs in the harmful direction: under load rpm sits well below what duty implies, so the real lag is lower and the duty curve over-advances. More advance means more current — so the old curve pushed the wrong way in exactly the condition that is already thermally worst.

Change

Two commits, separable:

1. build(size-check) — raise the F051 flash gate 95% → 99.8% (26265 → 27592 B of the 27648 B RX region). The F051 app region is the binding constraint on this fork and holding back 5% was blocking feature work for margin the project doesn't need; the gate's job is to catch an overflow, not reserve headroom. Percentages are now evaluated with awk so a fractional limit works at all — shell integer arithmetic can't express 99.8. RAM stays at 90% deliberately: what's left there is stack headroom, and exhausting it fails at runtime rather than at link time.

For scale, ark-release sits at 26156 B, so the old gate left 109 bytes — and commit 2 needs 104 of them.

2. perf(advance) — the schedule itself. Input becomes measured k_erpm normalized to what this motor reaches at full duty on the present pack (kv * V * poles/2). Same 13..23 output range, same curve shape, no retuned constants — only the x axis changes. settings.c derives a Q12 "kerpm per centivolt" scale from kV and pole count once per settings load, so the hot path adds a multiply and a shift and no division to the main loop.

Verified behaviour

Transcribed both schedules plus the real map() from Src/functions.c and swept them (6S / 2000 kV / 14 poles):

duty k_erpm old new
200 31 13 13
800 124 15 15
1400 217 19 19
2000 310 23 23

Under load at duty 1200 — the correction this PR is for:

rpm vs no-load k_erpm old new
100% 186 18 18
80% 148 18 17
70% 130 18 16
60% 111 18 15

Fallback paths, each checked individually:

condition result
erased eeprom (kv 20, poles 255) duty proxy
poles 0 duty proxy
poles 64, 12S erpm schedule, level 13
boot, no pack reading duty proxy
kV entered at 55% (1100) duty proxy
kv 250 (below limiter floor) duty proxy
kv 300 (at floor) erpm schedule
healthy 6S hover erpm schedule, level 17

Overflow probed at extremes: kv 10220 / poles 64 / 12S → scale 13395, ceiling 16482 kerpm, all within uint16_t.

What this does NOT fix

Pack voltage. Stating it plainly because my first draft of this change claimed otherwise and the sweep disproved it: under no load k_erpm scales with V and so does the ceiling, so the ratio — and therefore this schedule — is voltage-invariant, exactly like the curve it replaces. Verified identical level at 4S/6S/12S for equal duty.

The physics says L/R falls roughly as 1/kv, which makes the lag a function of BEMF (~rpm/kv) and leaves a residual voltage term. Keying on BEMF would be the fuller correction, but it changes full-throttle advance on every pack size at once and needs bench data to pick endpoints. Deliberately out of scope here.

So this is the conservative half: identical to today wherever duty was a valid proxy, different only under load, in the direction that lowers current.

Interaction with #53

The >25% past ceiling fallback exists because entering 50-60% of the real motor kV is a known field workaround for the throttle-limiter bug (am32-firmware/AM32#405, fixed in #53). Normalizing against a ceiling that low would peg advance at the top of the range through most of the throttle band. The duty proxy is immune to a wrong kV, so it stays the fallback rather than being deleted. #53 should land first — it removes the reason anyone would misconfigure kV.

Cost

ARK_4IN1_F051, HWCI_PERF=1, make size-check-ark:

baseline this PR
FLASH 26156 B 26260 B (+104)
RAM 6872 B 6872 B (+0)

Gate passes at 94.98% of 99.8%.

Verification status — read before merging

  • make AM32_SITL_CAN and make ARK_4IN1_F051 build clean under -Wall -Wextra -Werror.
  • make size-check-ark PASS.
  • make check_format passes with clang-format 22.1.5 (the version CI pins; with 18.x it reports 4 pre-existing failures in Mcu/f415, Mcu/g431, Mcu/v203 — version skew, those files are untouched here).
  • make cppcheck not run — cppcheck isn't installed in this environment.
  • The SITL suite could not gate this change. All 13 tests that require the motor to spin currently fail in my container, and they fail identically on a pristine origin/ark-release worktree with none of this PR's code — so it is not a regression from this change. The pattern is exactly "everything that must spin fails, everything that must-not-spin passes"; rpm stays 0 for 11+ s after throttle, with or without --nosleep, at normal load, no orphaned processes, ark-release unmoved at 4ab7b5f. These same tests passed 29/29 earlier in the same session, so something in the environment degraded rather than the code. Worth a look on a clean runner — if CI is green there, this is purely local and can be ignored; if CI is red on ark-release too, it wants its own issue.

Draft because the behaviour is bench-verifiable and not bench-verified. The numbers above prove the schedule computes what it's designed to compute and that the fallbacks hold — they do not prove less current on a real motor. The efficiency_sweep gate against hwci/baselines/ is the check that matters, and the load rows are where any gain should appear.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HoD65bRT7unXhETnZ13WEq


Generated by Claude Code

claude added 2 commits July 25, 2026 04:58
The F051 app region is the binding constraint on this fork, and holding
back 5% of it was blocking feature work for margin the project does not
need. The gate's job is to catch a PR that overflows the chip, not to
reserve headroom. Flash limit goes 95% -> 99.8%, i.e. 26265 -> 27592 of
the 27648 byte RX image region.

For scale: ark-release currently sits at 26156 B, so the old gate left
109 bytes. The advance-schedule change in the next commit needs 104 of
them.

Percentages are now evaluated with awk rather than shell integer
arithmetic so a fractional limit like 99.8 works at all; truncation is
toward zero, and an over-budget image still fails (verified with
FLASH_MAX_PCT=1).

RAM stays at 90% deliberately. Unlike flash, what is left is stack
headroom, and exhausting it fails at runtime instead of at link time, so
the gate is doing real work there.

This is a size decision only. Global -O3 previously filled ~99% AND
regressed hold100 free-run on F051 (armed/input drop near ~97%
throttle); that regression is functional, not a size problem, so the
higher limit is explicitly not permission to enable global -O3. Noted in
the script next to the limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoD65bRT7unXhETnZ13WEq
auto_advance_level was a single linear map of duty cycle:

    auto_advance_level = map(duty_cycle, 100, 2000, 13, 23);

Commutation lag is dominated by the phase lag of current behind applied
voltage, atan(w*L/R), so what the schedule wants on its x axis is a
measure of speed. Duty is only a proxy for speed at one load, and the
error is in the harmful direction: under load rpm sits well below what
duty implies, so the real lag is lower and the duty curve over-advances.
More advance means more current, so the old curve pushed the wrong way in
exactly the condition that is already thermally worst.

Replace the input with measured k_erpm normalized to what this motor
reaches at full duty on the present pack (kv * V * poles/2). Same 13..23
output range, same curve shape, no retuned constants - only the x axis
changes. settings.c derives a Q12 "kerpm per centivolt" scale from kv and
pole count once per settings load, so the hot path is a multiply and a
shift with no division added to the main loop.

Verified against a transcription of both schedules plus the real map():
  - no load, 6S/2000kV/14P: new matches old across the throttle range
    (13,14,15,18,19,20,23 at duty 200..2000)
  - loaded at duty 1200: 70% of no-load rpm moves the level 18 -> 16,
    which is the intended correction
  - 4S/6S/12S at equal duty: identical level, see below

What this does NOT fix, stated plainly because the first draft of this
change claimed otherwise: pack voltage. Under no load k_erpm scales with
V and so does the ceiling, so the ratio - and this schedule - is
voltage-invariant, exactly like the curve it replaces. The physics says
L/R falls roughly as 1/kv, which makes the lag a function of BEMF
(~rpm/kv) and leaves a residual voltage term. Keying on BEMF would be
the fuller correction but it changes full-throttle advance on every pack
size at once and needs bench data to pick endpoints, so it is left out.
This commit is the conservative half: identical to today wherever duty
was a valid proxy, different only under load, in the direction that
lowers current.

Falls back to the duty proxy when the normalization cannot be trusted,
each path unit-checked:
  - implausible kV (<300, where the throttle limiter already gives up) or
    pole count outside 2..64 (an erased eeprom reads 0 or 0xff)
  - no usable pack reading yet, before the ADC settles at boot
  - measured erpm more than 25% past the configured ceiling, the
    signature of a mis-entered motor kV. That case matters because
    entering 50-60% of the real kV is a known field workaround for the
    throttle-limiter bug (am32-firmware#405), and normalizing
    against a ceiling that low would peg advance at the top of the range
    through most of the throttle band. The duty proxy is immune to a
    wrong kV, so it stays the fallback rather than being removed.

Costs 104 bytes of flash and no RAM on ARK_4IN1_F051.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoD65bRT7unXhETnZ13WEq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants