Klipper sensorless homing: false triggers, repeatability, and why one working threshold isn't tuned (CoreXY, TMC5160)

Correction, added the day after posting. My measuring tool had a bug: it
inferred the axis position from where it believed the head was rather than
from actual rail contact, so a wrong belief produced a wrong correction and
compounded across runs. Points 1-4 are unaffected, as is the conclusion in
point 5 that raising homing_speed widened the working window - the threshold
sweep is self-referencing and did not go through the broken path. The rail
measurement scatter
row in that table did, and should not be trusted; with
the bug fixed the rail measures 300.00mm with 0.00mm spread over five runs. I
also withdrew a claim about double-homing that I made further down. The full
correction is post #4 in this thread.

I spent this week getting sensorless homing working properly on a Voron 2.4
(TMC5160, 48V on XY) and hit several things I could not find written down
anywhere. Posting them because four of the five would have saved me hours, and
one of them is a genuine footgun.

I also ended up writing a tool that does the measuring, which is at the end.
The findings matter more than the tool.

This partly follows on from Sensorless homing - Leviathan v1.2 - TMC5160 -
won’t work
,
which is now closed - same board family, same LDO-42STH48-2004MAH motors. That
thread ended on a disagreement about homing speed and current that I think my
measurements settle, at least for this motor and voltage. Point 5 below is the
direct answer to it.

1. On CoreXY, a passing axis exonerates the whole drivetrain

If X homes reliably, your belts, pulleys, motors and grub screws are proven
good
- because on CoreXY both motors and both belts turn for either axis.
An X move is not “the X motor”; it is both.

So the standard advice for a Y-only sensorless problem - “check your belt
tension, check your grub screws” - is not just unhelpful, it is ruled out by
evidence you already have
. What Y does not share with X is its own linear
rails and the gantry mass it alone drags.

I chased belts for a while before working this out. If one axis passes and the
other does not, the difference is mechanical only in the parts they do not
share.

2. A false trigger corrupts the coordinate frame, and the next move crashes

This one is worth knowing regardless of whether you tune anything.

Klipper sets the axis to position_max after any successful home,
including a false trigger. So when StallGuard trips early - say 3.5mm into a
150mm approach - Klipper believes the head is at the far rail while it is
physically still near where it started.

The next absolute move is then computed from that fiction. In my case:

  • Y tripped falsely at 3.5mm, head physically at ~154mm
  • Klipper now believed Y = 300 (then 290 after the backoff)
  • A move to Y150 was executed as 140mm in the wrong direction
  • The gantry drove into the front rail

The homing move was fine. The return move was the crash. It looked exactly
like a direction problem, and it was not.

If you script anything around sensorless homing, do not trust the coordinate
frame after a home you have not verified reached the rail. Step counters do not
lie about displacement even when the frame does.

3. SET_KINEMATIC_POSITION needs enable_force_move

Minor but easy to lose an hour to. In current Klipper it is registered inside
the guard:

self._enable_force_move = config.getboolean("enable_force_move", False)
if self._enable_force_move:
    gcode.register_command('SET_KINEMATIC_POSITION', ...)

So without [force_move] enable_force_move: True you get “Unknown command”,
not a helpful message about the missing section.

4. More homing current is not better

I assumed a heavier axis wanted more current. It does not scale like that. On
my Y, at 78mm/s:

Homing current Result
0.8A no working threshold at all
1.0A window found
1.2A no working threshold at all

Too low and the motor skips instead of stalling - a skid is soft and
gradual, and StallGuard cannot separate it from acceleration load. Too high and
the stall is blunt. There is a band, and it is narrower than I expected.

Corollary: a threshold is only valid at the current it was tuned at. Change the
current, re-tune.

5. homing_speed widened the window where current and accel could not

My Y had exactly one working threshold value - it worked, but one integer
either side failed. That is fragile: it passes cold and fails warm, because
StallGuard drifts with motor temperature. I watched my X drift from 0.00mm to
0.60mm repeatability across a single warm day and recover once cool.

Sweeping accel and current found nothing better. Raising homing_speed did:

78mm/s 100mm/s
Working window 1 value 2 values
Repeatability spread 0.93mm 0.60mm
Rail measurement scatter 2.70mm 0.40mm

StallGuard infers load from back-EMF, which scales with speed - so a faster
approach gives it a stronger signal to discriminate against. If your window is
one value wide, try speed before anything else.

Keep coolstep_threshold paired at roughly 0.83x the homing speed. Too far
below and StallGuard is active during the acceleration ramp and trips on
acceleration load rather than on the rail.

The thing nobody should skip: measure repeatability

A single successful home proves almost nothing, because a false trigger also
reports success
. The number that matters is whether the origin lands in the
same place every time.

My Y reached the rail 10 times out of 10 and still wandered 3.54mm. That
does not look like a failure - it prints as layer shifts.

The tool

MIT. It sweeps the threshold across the driver’s whole range, measures real
travel from MCU step counters, then verifies repeatability, homes from a range
of distances, and measures true rail-to-rail travel against position_max. On
failure it names what to change and by how much.

Driver-agnostic in design - StallGuard2 and StallGuard4 use different field
names and opposite sensitivity directions, and it detects which is fitted.

Caveats, stated plainly: tested on exactly one machine - Voron 2.4, CoreXY,
TMC5160, 48V, panels off and chamber unheated. The StallGuard4 paths
(2209/2240) are written from datasheets and have never been run on
hardware
. Nothing is verified hot. It deliberately drives the toolhead into
the rail, so it is not something to run unattended.

Happy to be told I have any of this wrong - particularly point 2, where I would
like to know whether that is considered expected Klipper behaviour or something
worth raising upstream.

While I applaud your diligence and deep dive into stallguard I prepose a different title for your thread that might save a less technical user a lot of flustration.

CoreXY sensorless homing. Forget it and install limit switches

Optionally has anyone considered a method to perform homing in the A,B plane then transform the results to the X,Y plane? Needing 2 stepper drivers to agree on when “stall” occurs makes repeatability hard to obtain.

Yeah sensorless homing can be tricky.
My experience also showed that higher speeds result in a nice bump than higher or lower homing currents.
My approach for the false trigger and wrong reference coordinates afterwards: after the first home, drive a bit towards the center of the bed and home X and Y again. Often when the print finishes the head parks im the rear right. The spot where it homes to… So a single homing attempt will more or less fail.

On my never finishing printer I will install mechanical switches again before upgrading it with a toolchanger.

@cardoc the two-drivers-must-agree point is the real structural difficulty and I think it is underrated - StallGuard reads each driver separately while the load is shared across both belts, so you are inferring one event from two noisy sources.

On the A/B idea: driving a single motor on CoreXY moves the head at 45 degrees, so you would be homing into a corner rather than along an axis. You would get a stall, just not one that maps cleanly onto X or Y.

First, a correction to my own post, because it invalidates part of it.

My tool had a bug. It inferred the axis position from where it believed the head was before each home, and when that belief was inherited wrong the correction was wrong too - it judged a good home to be false and rewrote the position ~135mm short. The next absolute move then started from that fiction, so the following measurement was worse, and it compounded.

It presented as a clean +2mm per run. Smooth, monotonic, entirely plausible as a physical measurement. It survived my blaming the machine, autotune, lost steps, and a stale step-counter read before I found it.

What it cost me: I had concluded that Y could not home reliably from close to the rail - failing by 3-5mm from 5mm and 15mm out - and that a second homing move fixed it. Both were artifacts. With the bug fixed, single homing passes from 5, 15, 40, 120 and 250mm, and the rail measures 300.00mm with 0.00mm spread over five runs. There was nothing to fix.

So @LifeOfBrian, I was about to post a table confirming your double-home approach, and I have to withdraw it - I cannot reproduce the failure it was solving. That is not a claim your experience is wrong, since a head parking rear-right genuinely does start a home close to the rail, and there may well be machines where that matters. Mine just is not evidence of it.

What does still hold, from the threshold sweep, which is self-referencing and so was unaffected:

  • Speed matters more than current, as you said. Raising homing_speed from 78 to 100 took Y’s working threshold window from one value to two.
  • Current is not monotonic. 1.0A worked; 0.8A and 1.2A both found no working threshold at all.
  • Repeatability is the number worth measuring. One successful home proves very little, since a false trigger also reports success.

One thing I would still argue, unrelated to the measurements: a blind G1 backoff before homing is worth avoiding. Klipper watches the StallGuard pin only during a homing move, so an ordinary G1 runs to completion whatever it hits, and there is no stop-on-endstop for a kinematic axis. If the frame is wrong after a false trigger, that backoff is computed from a fiction and can drive into the opposite rail - and a larger backoff makes it worse. A homing move stops on contact. I removed mine.

A question for anyone using klipper_tmc_autotune. Enabling it rewrites COOLCONF on my TMC5160s:

autotune off:  COOLCONF 0x00010000  sgt=1                                   TCOOLTHRS=56
autotune on:   COOLCONF 0x0001c462  semin=2 seup=3 semax=4 sedn=2 seimin=1  TCOOLTHRS=156

That enables CoolStep, and replaces my per-axis coolstep_threshold with a single shared value. CoolStep modulates current by load and StallGuard reads that same load, so is running both during a homing move actually sound, or should semin be 0 while homing? Homing still passes here with it on, so this is a question rather than a bug report.

The broader lesson, which is the useful part of the whole exercise: a measurement that drifts smoothly is the hardest kind to distrust. I would not have caught it if someone had not asked me to run it twice.