Skip to content

Instantly share code, notes, and snippets.

@hakimio
Created May 12, 2026 19:11
Show Gist options
  • Select an option

  • Save hakimio/28066d000c63f0e3122e42ce4bcfe14c to your computer and use it in GitHub Desktop.

Select an option

Save hakimio/28066d000c63f0e3122e42ce4bcfe14c to your computer and use it in GitHub Desktop.
Anycubic ACE 2 Pro Feed Check Analysis
ACE2 check_length / error_length — Complete Analysis
1. Validation limits (SET_FEED_CHECK handler, sub_800B3AC)
The MCU rejects the command (returns error code 1) if:
┌──────────────┬─────┬──────────────┬─────────────────────────────────────────────────────┐
│ Parameter │ Min │ Max │ Notes │
├──────────────┼─────┼──────────────┼─────────────────────────────────────────────────────┤
│ check_length │ 3 │ 254 │ 0–2 and 255+ → rejected │
├──────────────┼─────┼──────────────┼─────────────────────────────────────────────────────┤
│ error_length │ 3 │ check_length │ must also be ≠ 255 (auto-excluded since max is 254) │
└──────────────┴─────┴──────────────┴─────────────────────────────────────────────────────┘
The guard logic in ARM:
# check_length: SUB R3, R0, #0xFF → CMN R3, #0xFC → BCS (carry set = R0 ∈ [3, 254])
# error_length: CMP R3, #3 + CMP R3, #0xFF + CMP R3, R0 (must be ≤ check_length)
2. Firmware boot-time clamp (filament task init, 0x80157DA)
When the task initialises (loop counter reaches 0x100), the MCU re-validates SRAM and corrects bad values:
if check_length < 3:
check_length ← 3
error_length ← 10 ← hard reset to boot defaults
if error_length < 3:
error_length ← 10
if error_length > check_length:
error_length ← 0xFF ← sentinel that disables the deficit check
3. The encoder-deficit check algorithm (sub_8009D74)
Called on every feed tick. Variables:
- S0 = commanded position (running odometer, from function pointer at R4+0x10)
- S16 = encoder position (running odometer, from function pointer at R4+0x14)
- S20 = S0 − prev_S0 = commanded delta since last checkpoint
Guard conditions — skip the check if any of these are true:
S0 < 30.0 → skip (session startup, not enough data)
slot == ASSISTING
AND S0 < 300.0 → skip (assist warm-up zone, built in)
S0 < 30.0 → skip (same, re-checked after the 300 branch)
S20 ≤ check_length × 1.2342 → skip (checkpoint interval not yet reached)
The actual deficit check (fires when S20 crosses the check_length threshold):
encoder_delta = | S16 − prev_encoder |
deficit = | S20 − encoder_delta | ← how far motor commanded vs encoder saw
if deficit > error_length × 1.2342:
slot_error ← 0x85 (or 0x86 if byte_20001A55 == 1)
The factor 1.2342 is a fixed unit-conversion scalar hard-coded in flash at 0x8009EAC.
4. gklib hardcoded defaults (initializeDevice, 0x6292B0)
SetFilamentFeedCheckParam(ace, check_length=100, error_length=90)
// logged: "ACEProxyV2 device %d set feed check params: check_length=100, error_length=90"
handle_set_feed_check (web API at 0x64C6B0) accepts the same params as check_len / error_len floats — allowing runtime override without reconnecting.
5. Why assist errors trigger and how to tune
What the numbers actually mean:
┌─────────────────────────────────┬────────────────────────────────────────┬───────────────┐
│ Quantity │ Formula │ Default │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Check fires every… │ check_length × 1.2342 units commanded │ 123.4 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Deficit to trigger error │ > error_length × 1.2342 units │ > 111.1 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Min encoder movement per window │ (check_length − error_length) × 1.2342 │ 12.3 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Slip tolerance │ error_length / check_length │ 90 % │
│ Quantity │ Formula │ Default │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Check fires every… │ check_length × 1.2342 units commanded │ 123.4 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Deficit to trigger error │ > error_length × 1.2342 units │ > 111.1 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Min encoder movement per window │ (check_length − error_length) × 1.2342 │ 12.3 units │
├─────────────────────────────────┼────────────────────────────────────────┼───────────────┤
│ Slip tolerance │ error_length / check_length │ 90 % │
└─────────────────────────────────┴────────────────────────────────────────┴───────────────┘
During buffer assist the encoder legitimately lags the motor (slack, sensor delay, filament flex). The built-in 300-unit assist warmup helps at the start, but once it expires, any consistent lag exceeding (check_length − error_length) units per
check window triggers 0x85/0x86.
6. Recommended values
Rule: to reduce false assist errors, (a) widen the window (check_length ↑) so transient slips average out, and (b) increase the tolerance (error_length closer to check_length).
┌──────────────────────────────────┬──────────────┬──────────────┬───────────┬───────────────────────────────────────────────────────────────────────┐
│ Goal │ check_length │ error_length │ Tolerance │ Notes │
├──────────────────────────────────┼──────────────┼──────────────┼───────────┼───────────────────────────────────────────────────────────────────────┤
│ gklib default │ 100 │ 90 │ 90 % │ catches ~10-unit lag │
├──────────────────────────────────┼──────────────┼──────────────┼───────────┼───────────────────────────────────────────────────────────────────────┤
│ Recommended: fewer assist errors │ 200 │ 185 │ 92.5 % │ wider window, same ratio + some extra slack │
├──────────────────────────────────┼──────────────┼──────────────┼───────────┼───────────────────────────────────────────────────────────────────────┤
│ Aggressive: only real jams │ 200 │ 196 │ 98 % │ fires only if encoder shows < 2 % of commanded movement │
├──────────────────────────────────┼──────────────┼──────────────┼───────────┼───────────────────────────────────────────────────────────────────────┤
│ Effectively disabled │ 254 │ 254 │ ~100 % │ error_length = check_length → deficit always ≤ threshold, never fires │
└──────────────────────────────────┴──────────────┴──────────────┴───────────┴───────────────────────────────────────────────────────────────────────┘
The 200 / 185 setting is the practical sweet spot: it doubles the measurement window (so a brief encoder dip during buffer reversals doesn't accumulate to the threshold), and raises the slip tolerance to 92.5 %, while still catching a genuinely
jammed or runaway motor (encoder would have to show < 15 units out of 200 to fire).
If the encoder deficit is structurally large throughout the entire assist cycle (e.g., the ACE 2 encoder is not well-coupled to the assist motor at your filament type/diameter), use 200 / 196 instead — that only fires if the encoder is almost
entirely stationary while the motor runs.
@Simon-CR

Copy link
Copy Markdown

A follow-up with evidence, and an apology in advance for the length.

First, a correction to my own earlier comment. I said the feed check was a slip comparator "rather
than an absolute threshold" as though that were news — but your feed-check gist already has
that exactly right (deficit = |S20 − encoder_delta|, > error_length × 1.2342, 0x85 / 0x86,
and the 0xFF sentinel). I had been reading the older model in the main analysis and did not
notice you had already superseded it. Sorry for the noise.

What follows is only what I verified byte-by-byte myself in V1.1.31, with the disassembly quoted
so you can check it rather than take my word.


1. SET_FEED_CHECK bounds — and the tuning example in the main analysis would be rejected

The main analysis says "Valid range is 1–255 per field", and recommends
check_length=80, error_length=90.

sub_800B3AC:

800b3ac: ldr   r0,[r1,#0]        ; check_length
800b3ae: sub.w r3, r0, #255
800b3b2: cmn.w r3, #252          ; carry set only for check_length in [3, 254]
800b3b6: bcs   0x800b3bc
800b3b8: movs  r1, #1            ; PARAM_ERROR
800b3bc: ldr   r3,[r1,#4]        ; error_length
800b3c0: cmp   r3, #3
800b3c2: bcc   0x800b3fa         ; error_length < 3  -> PARAM_ERROR
800b3c4: cmp   r3, #255
800b3c6: beq   0x800b3fa         ; error_length == 255 -> PARAM_ERROR
800b3c8: cmp   r3, r0
800b3ca: bhi   0x800b3b8         ; error_length > check_length -> PARAM_ERROR

So the accepted set is check ∈ [3,254], error ∈ [3,254], error ≠ 255, and error ≤ check.

check_length=80, error_length=90 fails the last test (90 > 80) and is rejected outright. Your
feed-check gist's recommendations (200/185, 254/254) all satisfy error ≤ check, so only the older
document is affected.

2. The boot clamp's second branch sets 255, not 10

feed-check gist:

if error_length < 3: error_length ← 10

The clamp at 0x080157DA:

80157e2: ldrb r2,[r0,#66]   ; check_length
80157e6: adds r1, r2, #1
80157e8: uxtb r1, r1
80157ea: cmp  r1, #4
80157ec: bcs  0x801580e     ; (check+1) >= 4  -> go test error_length
80157ee: movs r1, #3
80157f0: strb r1,[r0,#66]   ; check_length = 3
80157f4: movs r1, #10
80157f6: strb r1,[r0,#67]   ; <-- shared store site
80157fa: b    0x8015826

801580e: ldrb r3,[r0,#67]   ; error_length
8015812: adds r1, r3, #1
8015814: uxtb r1, r1
8015816: cmp  r1, #4
8015818: mov.w r1, #255     ; <-- r1 = 255, set BEFORE the branch
801581c: bcc  0x80157f6     ; jumps to the shared store, with r1 = 255
801581e: cmp  r3, r2
8015820: it   hi
8015822: strbhi r1,[r0,#67] ; error_length > check_length -> 255

The branch at 0x801581C reuses the store at 0x80157F6, but r1 was reloaded with 255 two
instructions earlier — so a bad error_length becomes 255 (the disabling sentinel), not 10.
Only the first branch reaches that store with r1 = 10.

Also, the adds #1 ; uxtb ; cmp #4 idiom means both tests catch {0, 1, 2, 255}, not just
< 3 — so 255 is treated as invalid on entry as well.

3. The 300 mm warm-up guard is PRELOADING, not ASSISTING

feed-check gist:

slot == ASSISTING AND S0 < 300.0 → skip (assist warm-up zone)

sub_8009D74:

8009e1e: movw r3, #0x1000
8009e22: movt r3, #0x2000        ; 0x20001000
8009e26: add.w r3, r3, r0, lsl #2
8009e2a: ldr  r3,[r3,#4]         ; slot status at 0x20001004 + slot*4
8009e2c: subs r3, #5             ; compare with 5
8009e2e: clz  r3, r3
8009e32: lsrs r3, r3, #5         ; r3 = (status == 5)
8009e34: vldr s4, [pc,#120]      ; 0x08009EB0 = 00 00 96 43 = 300.0f
8009e38: vcmp.f32 s0, s4
8009e40: it lt
8009e42: movlt r2, #1
8009e44: tst  r3, r2
8009e46: bne  0x8009e9a          ; skip if (status == 5 && commanded < 300)

Status 5 is preloading; assisting is 3 (per the ACEPRO driver's
ACE2_SLOT_STATUS_DETAIL_BY_CODE, which matches everything I have observed live). So it is a
preload warm-up. That matters practically: it means feed assist gets no warm-up allowance —
and, per the next item, no check at all.

4. A bypass flag disables the check entirely for rollback and for assist

I do not think this appears in either document, and it changes what the feature protects.

sub_8009D74 begins:

8009d7c: ldrb  r0,[r0,#4]        ; enabled?
8009d80: beq.w 0x8009ea2         ; no -> return
8009d84: ldrb.w r0,[r4,#60]      ; obj+0x3C  = bypass
8009d88: cmp   r0, #0
8009d8a: bne.w 0x8009ea2         ; set -> return immediately

obj+0x3C is written by sub_800B22C from its 6th argument (0x0800B262), and
FEED_OR_ROLLBACK computes that argument as (mode == 1) at 0x0800B62C–0x0800B63A — i.e.
rollback. The assist handler sets it directly at 0x0800A438. It is cleared only when the
operation ends.

So the deficit check is live for mode 0 feeds and auto-preload only. It is switched off for the
entire duration of any rollback and any feed assist. Since printing runs on assist, the check
provides no jam protection during a print — which seems worth stating explicitly, because the
tuning discussion naturally reads as though it were protecting the print.

(A side effect: 0x82 ROLLBACK_ERROR looks unreachable, since the only thing that sets the
object's error field is the check that rollback disables.)

5. 0x85 / 0x86 never reach the slot status

feed-check gist:

slot_error ← 0x85 (or 0x86 if byte_20001A55 == 1)

The store is to the feed-check object, not the slot status:

8009e84: ldrb.w r1,[r1,#65]      ; 0x20001A55  (CHN_BUF_FEED)
8009e88: movs r2, #133           ; 0x85
8009e8a: cmp  r1, #1
8009e8e: moveq r2, #134          ; 0x86
8009e90: ldr  r1,[r4,#12]        ; on_error callback
8009e92: str  r2,[r4,#48]        ; obj+0x30  <-- object's error field
8009e94: blx  r1

The FEED handler then reads that field back through vtable+24 and substitutes its own code:

800a26c: ldr  r1,[r0,#24]        ; get_error()
800a272: blx  r1
800a274: cmp  r0, #0
800a276: beq.w 0x800ae3c         ; no error -> status 0
800a282: movw r0, #0x1000
800a286: movt r0, #0x2000
800a28a: add.w r0, r0, r8, lsl #2
800a28e: movs r1, #129           ; 0x81 FEED_ERROR
800a290: b.w  0x800ae52          ; store into the slot status

The preload path does the same with 132. So the STUCK/TANGLED distinction is computed and then
discarded: a slot status can never read 133 or 134 in V1.1.31, and host logic branching on
those codes is dead. The CHN_BUF_FEED bit that distinguishes them is not exposed anywhere.


Separately: protobuf descriptors

I also decoded the nanopb 0.4.x descriptors in the image, which turned up a handful of shape
differences against ace2-pro.proto — GetTempResponse has 7 float fields rather than 6 (so the
existing names shift by one), cmd 72's request is 2 fields not 4, cmd 78 returns 16 {u32,u32}
pairs (key/linear calibration, not motor status), GetMaterialInfoResponse field 2 is a nested
message, and SetPrinterStatusRequest.status is a bool. I have not hand-verified every one of
those the way I did the items above, so I offer them as "please check" rather than as corrections.
The decode is reproducible — script and full descriptor dump are in the repo, and the derived
.proto carries addresses in comments:

https://github.com/Simon-CR/ace2-pro-firmware-research

Thanks again — the fact that any of this was checkable at all is down to your map.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment