Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ See the [Kalico Additions document](https://docs.kalico.gg/Kalico_Additions.html

- [indx: support for the Bondtech INDX toolboard](https://github.com/KalicoCrew/kalico/pull/904)

- [tmc: sensorless homing via SPI](https://github.com/KalicoCrew/kalico/pull/992)

If you're feeling adventurous, take a peek at the extra features in the bleeding-edge-v2 branch [feature documentation](docs/Bleeding_Edge.md)
and [feature configuration reference](docs/Config_Reference_Bleeding_Edge.md):

Expand Down
9 changes: 9 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -4539,6 +4539,9 @@ sense_resistor:
# "sensorless homing". (Be sure to also set driver_SGT to an
# appropriate sensitivity value.) The default is to not enable
# sensorless homing.
# If no diag pin is specified, the virtual_endstop is still available
# and the micro-controller polls the stallguard flag over SPI instead.
# See the TMC_Drivers.md document for details.
```

### [tmc2208]
Expand Down Expand Up @@ -4921,6 +4924,9 @@ run_current:
# "sensorless homing". (Be sure to also set driver_SGT OR driver_SG4_THRS
# to an appropriate sensitivity value.) The default is to not enable
# sensorless homing.
# If no diag pin is specified, the virtual_endstop is still available
# and the micro-controller polls the stallguard flag over SPI instead.
# See the TMC_Drivers.md document for details.
```

### [tmc5160]
Expand Down Expand Up @@ -5075,6 +5081,9 @@ sense_resistor:
# "sensorless homing". (Be sure to also set driver_SGT to an
# appropriate sensitivity value.) The default is to not enable
# sensorless homing.
# If no diag pin is specified, the virtual_endstop is still available
# and the micro-controller polls the stallguard flag over SPI instead.
# See the TMC_Drivers.md document for details.
```

## Run-time stepper motor current configuration
Expand Down
1 change: 1 addition & 0 deletions docs/Kalico_Additions.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
## TMC Drivers

- [`[tmc2240] driver_CS and current_range`](./Config_Reference.md#tmc2240) let you tune the current scaler and current range of your tmc2240 drivers.
- [Sensorless homing via SPI](./TMC_Drivers.md#sensorless-homing-via-spi) polls the stallguard flag of SPI drivers, so no DIAG pin is needed.

## Macros

Expand Down
24 changes: 23 additions & 1 deletion docs/TMC_Drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ A few prerequisites are needed to use sensorless homing:
2. SPI / UART interface of the TMC driver wired to micro-controller
(stand-alone mode does not work).
3. The appropriate "DIAG" or "SG_TST" pin of TMC driver connected to
the micro-controller.
the micro-controller. (Not needed on SPI drivers, see
[Sensorless homing via SPI](#sensorless-homing-via-spi).)
4. The steps in the [config checks](Config_checks.md) document must be
run to confirm the stepper motors are configured and working
properly.
Expand Down Expand Up @@ -258,6 +259,27 @@ homing. See the
[config reference](Config_Reference.md#tmc-stepper-driver-configuration)
for all the available options.

##### Sensorless homing via SPI

On tmc2130, tmc2240, tmc5160 and tmc2160 drivers wired over SPI, the
DIAG pin is not required. If no `diag0_pin` or `diag1_pin` is set, the
micro-controller polls the driver's stallguard flag over the SPI bus
during homing instead of watching a pin:
```
[tmc5160 stepper_x]
driver_SGT: 1
...

[stepper_x]
endstop_pin: tmc5160_stepper_x:virtual_endstop
...
```
The flag is polled every 1ms and is ignored until the axis has reached
the homing speed (plus 50ms), so the homing move must start far enough
from the end of the rail to finish accelerating. Setting `min_home_dist`
in the stepper section (e.g. 15mm) makes Kalico back off and home again
when it starts too close. This is not available on UART drivers.

#### Find highest sensitivity that successfully homes

Place the carriage near the center of the rail. Use the SET_TMC_FIELD
Expand Down
113 changes: 110 additions & 3 deletions klippy/extras/tmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import collections
import logging

from klippy import stepper
from klippy import mcu, stepper

from . import bulk_sensor

Expand Down Expand Up @@ -735,7 +735,7 @@ def setup_pin(self, pin_type, pin_params):
raise ppins.error("tmc virtual endstop only useful as endstop")
if pin_params["invert"] or pin_params["pullup"]:
raise ppins.error("Can not pullup/invert tmc virtual pin")
if self.diag_pin is None:
if self.diag_pin is None and not hasattr(self.mcu_tmc, "tmc_spi"):
raise ppins.error("tmc virtual endstop requires diag pin config")
# Setup for sensorless homing
self.printer.register_event_handler(
Expand All @@ -744,7 +744,10 @@ def setup_pin(self, pin_type, pin_params):
self.printer.register_event_handler(
"homing:homing_move_end", self.handle_homing_move_end
)
self.mcu_endstop = ppins.setup_pin("endstop", self.diag_pin)
if self.diag_pin is None:
self.mcu_endstop = TMCSpiEndstop(self.printer, self.mcu_tmc)
else:
self.mcu_endstop = ppins.setup_pin("endstop", self.diag_pin)
return self.mcu_endstop

def _set_field(self, field_name, value):
Expand Down Expand Up @@ -797,6 +800,110 @@ def handle_homing_move_end(self, hmove):
self._prev_state.clear()


# Sensorless homing by polling the stallguard flag over spi
class TMCSpiEndstop:
POLL_TIME = 0.001
# Stallguard needs some time at speed before its flag is valid
SETTLE_TIME = 0.150

def __init__(self, printer, mcu_tmc):
tmc_spi = mcu_tmc.tmc_spi
self._printer = printer
self._mutex = mcu_tmc.mutex
self._mcu_tmc = mcu_tmc
self._mcu = tmc_spi.get_mcu()
self._oid = self._mcu.create_oid()
self._dispatch = mcu.TriggerDispatch(self._mcu)
self._home_cmd = self._query_cmd = None
self._mcu.add_config_cmd(
"config_tmc_spi_endstop oid=%d spi_oid=%d data_len=%d"
" status_pos=%d"
% (
self._oid,
tmc_spi.spi.get_oid(),
tmc_spi.chain_len * 5,
(tmc_spi.chain_len - mcu_tmc.chain_pos) * 5,
)
)
self._mcu.add_config_cmd(
"tmc_spi_endstop_home oid=%d clock=0 rest_ticks=0"
" trsync_oid=0 trigger_reason=0" % (self._oid,),
on_restart=True,
)
self._mcu.register_config_callback(self._build_config)

def _build_config(self):
cmd_queue = self._dispatch.get_command_queue()
self._home_cmd = self._mcu.lookup_command(
"tmc_spi_endstop_home oid=%c clock=%u rest_ticks=%u"
" trsync_oid=%c trigger_reason=%c",
cq=cmd_queue,
)
self._query_cmd = self._mcu.lookup_query_command(
"tmc_spi_endstop_query_state oid=%c",
"tmc_spi_endstop_state oid=%c trigger_clock=%u",
oid=self._oid,
cq=cmd_queue,
)

def get_mcu(self):
return self._mcu

def add_stepper(self, stepper):
self._dispatch.add_stepper(stepper)

def get_steppers(self):
return self._dispatch.get_steppers()

def home_start(
self, print_time, sample_time, sample_count, rest_time, triggered=True
):
toolhead = self._printer.lookup_object("toolhead")
accel = toolhead.get_max_velocity()[1]
speed = self.get_steppers()[0].get_step_dist() / rest_time
guard_time = speed / accel + self.SETTLE_TIME
# Host register reads must not interleave with the mcu polling
self._mutex.lock()
clock = self._mcu.print_time_to_clock(print_time + guard_time)
trigger_completion = self._dispatch.start(print_time)
self._home_cmd.send(
[
self._oid,
clock,
self._mcu.seconds_to_clock(self.POLL_TIME),
self._dispatch.get_oid(),
mcu.MCU_trsync.REASON_ENDSTOP_HIT,
],
reqclock=clock,
)
return trigger_completion

def home_wait(self, home_end_time):
try:
self._dispatch.wait_end(home_end_time)
self._home_cmd.send([self._oid, 0, 0, 0, 0])
finally:
self._mutex.unlock()
res = self._dispatch.stop()
if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT:
cmderr = self._printer.command_error
raise cmderr("Communication timeout during homing")
if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT:
return 0.0
if self._mcu.is_fileoutput():
return home_end_time
params = self._query_cmd.send([self._oid])
clock = self._mcu.clock32_to_clock64(params["trigger_clock"])
return self._mcu.clock_to_print_time(clock)

def query_endstop(self, print_time):
if self._mcu.is_fileoutput():
return 0
status = self._mcu_tmc.get_register_raw("DRV_STATUS")["spi_status"]
# Ignore the stale stall flag at standstill
return status & 0x0C == 0x04


######################################################################
# Config reading helpers
######################################################################
Expand Down
4 changes: 4 additions & 0 deletions src/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ config WANT_TMCUART
bool
depends on HAVE_GPIO
default y
config WANT_TMC_SPI_ENDSTOP
bool
depends on WANT_SPI && !HAVE_LIMITED_CODE_SIZE
default y
config WANT_NEOPIXEL
bool
depends on HAVE_GPIO
Expand Down
1 change: 1 addition & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ src-$(CONFIG_HAVE_GPIO_SDIO) += sdiocmds.c

src-$(CONFIG_WANT_BUTTONS) += buttons.c
src-$(CONFIG_WANT_TMCUART) += tmcuart.c
src-$(CONFIG_WANT_TMC_SPI_ENDSTOP) += tmc_spi_endstop.c
src-$(CONFIG_WANT_NEOPIXEL) += neopixel.c
src-$(CONFIG_WANT_PULSE_COUNTER) += pulse_counter.c
src-$(CONFIG_WANT_ST7920) += lcd_st7920.c
Expand Down
113 changes: 113 additions & 0 deletions src/tmc_spi_endstop.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Sensorless homing by polling the tmc stallguard flag over spi
//
// Copyright (C) 2026 Rogerio Goncalves <rogerlz@gmail.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.

#include <string.h> // memset
#include "basecmd.h" // oid_alloc
#include "board/irq.h" // irq_disable
#include "board/misc.h" // timer_read_time
#include "command.h" // DECL_COMMAND
#include "sched.h" // DECL_TASK
#include "spicmds.h" // spidev_transfer
#include "trsync.h" // trsync_do_trigger

#define MAX_DATA_LEN 40
#define SPI_STATUS_SG2 0x04

struct tmc_spi_endstop {
struct timer timer;
uint32_t rest_ticks, trigger_clock;
struct spidev_s *spi;
struct trsync *ts;
uint8_t pending, data_len, status_pos, trigger_reason;
};

static struct task_wake tmc_spi_endstop_wake;

static uint_fast8_t
tmc_spi_endstop_event(struct timer *t)
{
struct tmc_spi_endstop *e = container_of(t, struct tmc_spi_endstop, timer);
e->pending = 1;
sched_wake_task(&tmc_spi_endstop_wake);
e->timer.waketime += e->rest_ticks;
return SF_RESCHEDULE;
}

void
command_config_tmc_spi_endstop(uint32_t *args)
{
uint8_t data_len = args[2], status_pos = args[3];
if (data_len > MAX_DATA_LEN || status_pos >= data_len)
shutdown("Invalid tmc_spi_endstop config");
struct tmc_spi_endstop *e = oid_alloc(
args[0], command_config_tmc_spi_endstop, sizeof(*e));
e->timer.func = tmc_spi_endstop_event;
e->spi = spidev_oid_lookup(args[1]);
e->data_len = data_len;
e->status_pos = status_pos;
}
DECL_COMMAND(command_config_tmc_spi_endstop,
"config_tmc_spi_endstop oid=%c spi_oid=%c data_len=%c"
" status_pos=%c");

void
command_tmc_spi_endstop_home(uint32_t *args)
{
struct tmc_spi_endstop *e = oid_lookup(
args[0], command_config_tmc_spi_endstop);
sched_del_timer(&e->timer);
e->pending = 0;
e->rest_ticks = args[2];
if (!e->rest_ticks) {
e->ts = NULL;
return;
}
e->timer.waketime = e->trigger_clock = args[1];
e->ts = trsync_oid_lookup(args[3]);
e->trigger_reason = args[4];
sched_add_timer(&e->timer);
}
DECL_COMMAND(command_tmc_spi_endstop_home,
"tmc_spi_endstop_home oid=%c clock=%u rest_ticks=%u"
" trsync_oid=%c trigger_reason=%c");

void
command_tmc_spi_endstop_query_state(uint32_t *args)
{
struct tmc_spi_endstop *e = oid_lookup(
args[0], command_config_tmc_spi_endstop);
sendf("tmc_spi_endstop_state oid=%c trigger_clock=%u"
, args[0], e->trigger_clock);
}
DECL_COMMAND(command_tmc_spi_endstop_query_state,
"tmc_spi_endstop_query_state oid=%c");

void
tmc_spi_endstop_task(void)
{
if (!sched_check_wake(&tmc_spi_endstop_wake))
return;
uint8_t oid;
struct tmc_spi_endstop *e;
foreach_oid(oid, e, command_config_tmc_spi_endstop) {
if (!e->pending)
continue;
irq_disable();
e->pending = 0;
irq_enable();
uint8_t data[MAX_DATA_LEN];
memset(data, 0, e->data_len);
uint32_t time = timer_read_time();
spidev_transfer(e->spi, 1, e->data_len, data);
if (!(data[e->status_pos] & SPI_STATUS_SG2))
continue;
sched_del_timer(&e->timer);
e->pending = 0;
e->trigger_clock = time;
trsync_do_trigger(e->ts, e->trigger_reason);
}
}
DECL_TASK(tmc_spi_endstop_task);
Loading
Loading