From 55261ae7e5df30e846d2e84bc1b6775b3eab49ad Mon Sep 17 00:00:00 2001 From: Remco van Essen Date: Tue, 13 Oct 2026 20:17:25 +0200 Subject: [PATCH] [tmc2209] Add driver diagnostics and stall detection (#20618) Co-authored-by: Claude Opus 5.5 --- esphome/components/tmc2209/__init__.py | 44 +++++ esphome/components/tmc2209/stepper.py | 28 ++- esphome/components/tmc2209/tmc2209.cpp | 20 ++ esphome/components/tmc2209/tmc2209.h | 39 ++++ esphome/components/tmc22xx/__init__.py | 18 +- esphome/components/tmc22xx/tmc22xx.cpp | 186 ++++++++++++++++-- esphome/components/tmc22xx/tmc22xx.h | 55 ++++++ tests/components/tmc2209/common.yaml | 29 +++ tests/components/tmc2209/test.esp32-idf.yaml | 1 + .../components/tmc2209/test.esp8266-ard.yaml | 1 + tests/components/tmc2209/test.rp2040-ard.yaml | 1 + 11 files changed, 404 insertions(+), 18 deletions(-) diff --git a/esphome/components/tmc2209/__init__.py b/esphome/components/tmc2209/__init__.py index b3822f7582e..06dd8603ff5 100644 --- a/esphome/components/tmc2209/__init__.py +++ b/esphome/components/tmc2209/__init__.py @@ -1,10 +1,54 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import tmc22xx +import esphome.config_validation as cv +from esphome.const import CONF_THRESHOLD CODEOWNERS = ["@remcom"] DOMAIN = "tmc2209" +CONF_SEDN = "sedn" +CONF_SEIMIN = "seimin" +CONF_SEMAX = "semax" +CONF_SEMIN = "semin" +CONF_SEUP = "seup" +CONF_TCOOL_THRESHOLD = "tcool_threshold" + tmc2209_ns = cg.esphome_ns.namespace("tmc2209") TMC2209Stepper = tmc2209_ns.class_("TMC2209Stepper", tmc22xx.TMC22XXStepper) tmc22xx.register_actions("tmc2209", TMC2209Stepper) + +automation.register_apply_action( + "tmc2209.stallguard", + cv.Schema( + { + cv.GenerateID(): cv.use_id(TMC2209Stepper), + cv.Optional(CONF_THRESHOLD): cv.templatable(cv.uint8_t), + cv.Optional(CONF_TCOOL_THRESHOLD): cv.templatable( + cv.int_range(0, 2**20 - 1) + ), + } + ), + automation.ApplyField(CONF_THRESHOLD, "set_stallguard_threshold", cg.uint8), + automation.ApplyField(CONF_TCOOL_THRESHOLD, "set_tcool_threshold", cg.uint32), +) + +automation.register_apply_action( + "tmc2209.coolconf", + cv.Schema( + { + cv.GenerateID(): cv.use_id(TMC2209Stepper), + cv.Optional(CONF_SEMIN): cv.templatable(cv.int_range(0, 15)), + cv.Optional(CONF_SEMAX): cv.templatable(cv.int_range(0, 15)), + cv.Optional(CONF_SEUP): cv.templatable(cv.int_range(0, 3)), + cv.Optional(CONF_SEDN): cv.templatable(cv.int_range(0, 3)), + cv.Optional(CONF_SEIMIN): cv.templatable(cv.boolean), + } + ), + automation.ApplyField(CONF_SEMIN, "set_semin", cg.uint8), + automation.ApplyField(CONF_SEMAX, "set_semax", cg.uint8), + automation.ApplyField(CONF_SEUP, "set_seup", cg.uint8), + automation.ApplyField(CONF_SEDN, "set_sedn", cg.uint8), + automation.ApplyField(CONF_SEIMIN, "set_seimin", cg.bool_), +) diff --git a/esphome/components/tmc2209/stepper.py b/esphome/components/tmc2209/stepper.py index 5ef91dc591f..8867242aa25 100644 --- a/esphome/components/tmc2209/stepper.py +++ b/esphome/components/tmc2209/stepper.py @@ -1,4 +1,6 @@ +from esphome import automation from esphome.components import tmc22xx +import esphome.config_validation as cv from esphome.types import ConfigType from . import TMC2209Stepper @@ -6,9 +8,31 @@ from . import TMC2209Stepper AUTO_LOAD = ["tmc22xx"] DEPENDENCIES = ["uart"] -CONFIG_SCHEMA = tmc22xx.tmc22xx_schema(TMC2209Stepper, max_address=3) +CONF_ON_STALL = "on_stall" + + +def _validate_stall_needs_diag(config: ConfigType) -> ConfigType: + if CONF_ON_STALL in config and tmc22xx.CONF_DIAG_PIN not in config: + raise cv.Invalid( + f"'{CONF_ON_STALL}' requires '{tmc22xx.CONF_DIAG_PIN}', stalls are only reported on DIAG" + ) + return config + + +CONFIG_SCHEMA = cv.All( + tmc22xx.tmc22xx_schema( + TMC2209Stepper, + max_address=3, + extra={cv.Optional(CONF_ON_STALL): automation.validate_automation({})}, + ), + _validate_stall_needs_diag, +) FINAL_VALIDATE_SCHEMA = tmc22xx.final_validate async def to_code(config: ConfigType) -> None: - await tmc22xx.new_tmc22xx(config) + var = await tmc22xx.new_tmc22xx(config) + for conf in config.get(CONF_ON_STALL, []): + await automation.build_callback_automation( + var, "add_on_stall_callback", [], conf + ) diff --git a/esphome/components/tmc2209/tmc2209.cpp b/esphome/components/tmc2209/tmc2209.cpp index 040b362a8d5..21625a5147d 100644 --- a/esphome/components/tmc2209/tmc2209.cpp +++ b/esphome/components/tmc2209/tmc2209.cpp @@ -10,4 +10,24 @@ void TMC2209Stepper::dump_config() { TMC22XXStepper::dump_config(); } +uint32_t *TMC2209Stepper::shadow_register(uint8_t reg) { + switch (reg) { + case REG_TCOOLTHRS: + return &this->tcoolthrs_; + case REG_SGTHRS: + return &this->sgthrs_; + case REG_COOLCONF: + return &this->coolconf_; + default: + return TMC22XXStepper::shadow_register(reg); + } +} + +void TMC2209Stepper::restore_registers() { + TMC22XXStepper::restore_registers(); + this->write_register(REG_TCOOLTHRS, this->tcoolthrs_); + this->write_register(REG_SGTHRS, this->sgthrs_); + this->write_register(REG_COOLCONF, this->coolconf_); +} + } // namespace esphome::tmc2209 diff --git a/esphome/components/tmc2209/tmc2209.h b/esphome/components/tmc2209/tmc2209.h index 2f880251e56..f1c83485fa3 100644 --- a/esphome/components/tmc2209/tmc2209.h +++ b/esphome/components/tmc2209/tmc2209.h @@ -4,12 +4,51 @@ namespace esphome::tmc2209 { +using tmc22xx::RegisterField; + +// StallGuard and CoolStep registers, which the TMC2208 does not have +static constexpr uint8_t REG_TCOOLTHRS = 0x14; +static constexpr uint8_t REG_SGTHRS = 0x40; +static constexpr uint8_t REG_COOLCONF = 0x42; + +static constexpr RegisterField TCOOLTHRS{REG_TCOOLTHRS, 0, 20, false}; +static constexpr RegisterField SGTHRS{REG_SGTHRS, 0, 8, false}; +static constexpr RegisterField SEMIN{REG_COOLCONF, 0, 4, false}; +static constexpr RegisterField SEUP{REG_COOLCONF, 5, 2, false}; +static constexpr RegisterField SEMAX{REG_COOLCONF, 8, 4, false}; +static constexpr RegisterField SEDN{REG_COOLCONF, 13, 2, false}; +static constexpr RegisterField SEIMIN{REG_COOLCONF, 15, 1, false}; + class TMC2209Stepper : public tmc22xx::TMC22XXStepper { public: void dump_config() override; + /// Called when DIAG reports a stall. StallGuard only works in StealthChop, between TCOOLTHRS and TPWMTHRS. + template void add_on_stall_callback(F &&callback) { + this->stall_callback_.add(std::forward(callback)); + } + + /// A stall is reported when the StallGuard result drops below twice this value. + void set_stallguard_threshold(uint8_t threshold) { this->write_field(SGTHRS, threshold); } + /// Lower velocity limit, as TSTEP, for CoolStep and the StallGuard output on DIAG. + void set_tcool_threshold(uint32_t threshold) { this->write_field(TCOOLTHRS, threshold); } + void set_semin(uint8_t semin) { this->write_field(SEMIN, semin); } + void set_semax(uint8_t semax) { this->write_field(SEMAX, semax); } + void set_seup(uint8_t seup) { this->write_field(SEUP, seup); } + void set_sedn(uint8_t sedn) { this->write_field(SEDN, sedn); } + void set_seimin(bool seimin) { this->write_field(SEIMIN, seimin); } + protected: uint8_t expected_version() const override { return 0x21; } + uint32_t *shadow_register(uint8_t reg) override; + void restore_registers() override; + void on_diag_without_error() override { this->stall_callback_.call(); } + + LazyCallbackManager stall_callback_; + // Write-only registers with their power-on values + uint32_t tcoolthrs_{0}; + uint32_t sgthrs_{0}; + uint32_t coolconf_{0}; }; } // namespace esphome::tmc2209 diff --git a/esphome/components/tmc22xx/__init__.py b/esphome/components/tmc22xx/__init__.py index ad7221f0b21..da760dde2c5 100644 --- a/esphome/components/tmc22xx/__init__.py +++ b/esphome/components/tmc22xx/__init__.py @@ -21,6 +21,7 @@ DEPENDENCIES = ["uart"] CONF_ANALOG_CURRENT_SCALE = "analog_current_scale" CONF_CLOCK_FREQUENCY = "clock_frequency" +CONF_DIAG_PIN = "diag_pin" CONF_ENABLE_SPREADCYCLE = "enable_spreadcycle" CONF_ENN_PIN = "enn_pin" CONF_HOLD_CURRENT = "hold_current" @@ -31,6 +32,7 @@ CONF_INTERPOLATION = "interpolation" CONF_INVERSE_DIRECTION = "inverse_direction" CONF_IRUN = "irun" CONF_MICROSTEPS = "microsteps" +CONF_ON_STATUS = "on_status" CONF_OTTRIM = "ottrim" CONF_RSENSE = "rsense" CONF_RUN_CURRENT = "run_current" @@ -44,6 +46,7 @@ TMC22XXStepper = tmc22xx_ns.class_( "TMC22XXStepper", stepper.Stepper, cg.Component, uart.UARTDevice ) StandstillMode = tmc22xx_ns.enum("StandstillMode") +DriverEvent = tmc22xx_ns.enum("DriverEvent") STANDSTILL_MODES = { "normal": StandstillMode.STANDSTILL_MODE_NORMAL, @@ -56,7 +59,9 @@ validate_microsteps = cv.one_of(1, 2, 4, 8, 16, 32, 64, 128, 256, int=True) validate_current = cv.All(cv.current, cv.positive_float) -def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema: +def tmc22xx_schema( + cls: MockObjClass, max_address: int, extra: dict | None = None +) -> cv.Schema: """Return the stepper schema for a family member; `max_address` is 0 for chips without address pins.""" return cv.All( stepper.STEPPER_SCHEMA.extend( @@ -67,6 +72,7 @@ def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema: cv.Optional(CONF_STEP_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_DIR_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_INDEX_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_DIAG_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_CLOCK_FREQUENCY, default="12MHz"): cv.All( cv.frequency, cv.int_range(min=1) ), @@ -79,6 +85,8 @@ def tmc22xx_schema(cls: MockObjClass, max_address: int) -> cv.Schema: cv.Optional(CONF_RUN_CURRENT): validate_current, cv.Optional(CONF_HOLD_CURRENT): validate_current, cv.Optional(CONF_MICROSTEPS, default=16): validate_microsteps, + cv.Optional(CONF_ON_STATUS): automation.validate_automation({}), + **(extra or {}), } ) .extend(cv.COMPONENT_SCHEMA) @@ -133,6 +141,7 @@ async def new_tmc22xx(config: ConfigType) -> cg.Pvariable: (CONF_STEP_PIN, var.set_step_pin), (CONF_DIR_PIN, var.set_dir_pin), (CONF_INDEX_PIN, var.set_index_pin), + (CONF_DIAG_PIN, var.set_diag_pin), ): if (pin := config.get(key)) is not None: cg.add(setter(await cg.gpio_pin_expression(pin))) @@ -145,6 +154,13 @@ async def new_tmc22xx(config: ConfigType) -> cg.Pvariable: ): if (value := config.get(key)) is not None: cg.add(setter(value)) + for conf in config.get(CONF_ON_STATUS, []): + await automation.build_callback_automation( + var, + "add_on_status_callback", + [(DriverEvent, "event"), (cg.bool_, "active")], + conf, + ) return var diff --git a/esphome/components/tmc22xx/tmc22xx.cpp b/esphome/components/tmc22xx/tmc22xx.cpp index 1628572d406..2aed97bea10 100644 --- a/esphome/components/tmc22xx/tmc22xx.cpp +++ b/esphome/components/tmc22xx/tmc22xx.cpp @@ -23,6 +23,49 @@ static constexpr float INTERNAL_RSENSE_OHM = 0.17f; // Equivalent sense resis static constexpr float APPROACH_TIME_S = 0.02f; // Skip VACTUAL writes that change the speed by less than 1/16, each write blocks the loop for the UART transfer static constexpr int32_t VACTUAL_CHANGE_DIVISOR = 16; +static constexpr uint32_t STATUS_POLL_INTERVAL_MS = 1000; +// While the driver does not answer, for example without motor supply, only poll every 10 s; each failed read +// blocks the loop for the UART timeout +static constexpr uint8_t UNREACHABLE_POLL_SKIP = 9; +static constexpr uint32_t GSTAT_FLAGS = 0x07; // reset, drv_err, uv_cp +static constexpr uint32_t DRV_STATUS_FLAGS = 0x0FFF; + +static const LogString *driver_event_to_string(DriverEvent event) { + switch (event) { + case DRIVER_EVENT_RESET: + return LOG_STR("Driver reset"); + case DRIVER_EVENT_DRIVER_ERROR: + return LOG_STR("Driver error"); + case DRIVER_EVENT_CHARGE_PUMP_UNDERVOLTAGE: + return LOG_STR("Charge pump undervoltage"); + case DRIVER_EVENT_OVERTEMPERATURE_PREWARNING: + return LOG_STR("Overtemperature prewarning"); + case DRIVER_EVENT_OVERTEMPERATURE: + return LOG_STR("Overtemperature shutdown"); + case DRIVER_EVENT_SHORT_TO_GROUND_A: + return LOG_STR("Short to ground on phase A"); + case DRIVER_EVENT_SHORT_TO_GROUND_B: + return LOG_STR("Short to ground on phase B"); + case DRIVER_EVENT_LOW_SIDE_SHORT_A: + return LOG_STR("Low side short on phase A"); + case DRIVER_EVENT_LOW_SIDE_SHORT_B: + return LOG_STR("Low side short on phase B"); + case DRIVER_EVENT_OPEN_LOAD_A: + return LOG_STR("Open load on phase A"); + case DRIVER_EVENT_OPEN_LOAD_B: + return LOG_STR("Open load on phase B"); + case DRIVER_EVENT_TEMPERATURE_120C: + return LOG_STR("Temperature above 120C"); + case DRIVER_EVENT_TEMPERATURE_143C: + return LOG_STR("Temperature above 143C"); + case DRIVER_EVENT_TEMPERATURE_150C: + return LOG_STR("Temperature above 150C"); + case DRIVER_EVENT_TEMPERATURE_157C: + return LOG_STR("Temperature above 157C"); + default: + return LOG_STR("Unknown"); + } +} // CRC-8 with polynomial 0x07 over the datagram bits in the order they go out on the wire, LSB first static uint8_t datagram_crc(const uint8_t *data, size_t len) { @@ -34,8 +77,19 @@ static uint8_t datagram_crc(const uint8_t *data, size_t len) { return crc; } +// Index into RESTORED_REGISTERS, or -1 when the register is not restored after a driver reset +static int restored_index(uint8_t reg) { + for (size_t i = 0; i < RESTORED_REGISTERS.size(); i++) { + if (RESTORED_REGISTERS[i] == reg) + return static_cast(i); + } + return -1; +} + void IRAM_ATTR IndexPulseStore::gpio_intr(IndexPulseStore *arg) { arg->pulses = arg->pulses + arg->direction; } +void IRAM_ATTR DiagPinStore::gpio_intr(DiagPinStore *arg) { arg->triggered = true; } + void TMC22XXStepper::setup() { if (this->enn_pin_ != nullptr) { this->enn_pin_->setup(); @@ -53,13 +107,54 @@ void TMC22XXStepper::setup() { ESP_LOGW(TAG, "Unexpected IC version 0x%02X", this->version_); } - // Bring write-only registers in line with their cached values and clear the reset flag + this->restore_registers(); + this->write_register(REG_GSTAT, GSTAT_FLAGS); + this->apply_config_(); + // Currents live in a write-only register, so later changes survive a driver reset through its cached value + if (!std::isnan(this->initial_run_current_)) + this->set_run_current(this->initial_run_current_); + if (!std::isnan(this->initial_hold_current_)) + this->set_hold_current(this->initial_hold_current_); + + if (this->index_pin_ != nullptr) { + this->index_pin_->setup(); + this->index_pin_->attach_interrupt(IndexPulseStore::gpio_intr, &this->index_store_, gpio::INTERRUPT_ANY_EDGE); + } else { + this->step_pin_->setup(); + this->step_pin_->digital_write(false); + this->dir_pin_->setup(); + this->dir_pin_->digital_write(false); + } + + if (this->diag_pin_ != nullptr) { + this->diag_pin_->setup(); + this->diag_pin_->attach_interrupt(DiagPinStore::gpio_intr, &this->diag_store_, gpio::INTERRUPT_RISING_EDGE); + } + // Polling notices a driver reset, and conditions that clear without a DIAG edge + this->set_interval(STATUS_POLL_INTERVAL_MS, [this]() { + if (this->status_poll_skip_ > 0) { + this->status_poll_skip_--; + return; + } + if (!this->update_status_()) + this->status_poll_skip_ = UNREACHABLE_POLL_SKIP; + }); + + this->set_enabled(true); +} + +void TMC22XXStepper::restore_registers() { this->write_register(REG_IHOLD_IRUN, this->ihold_irun_); this->write_register(REG_TPOWERDOWN, this->tpowerdown_); this->write_register(REG_TPWMTHRS, this->tpwmthrs_); this->write_register(REG_VACTUAL, 0); - this->write_register(REG_GSTAT, 0x07); + for (size_t i = 0; i < RESTORED_REGISTERS.size(); i++) { + if ((this->restored_written_ & (1u << i)) != 0) + this->write_register(RESTORED_REGISTERS[i], this->restored_values_[i]); + } +} +void TMC22XXStepper::apply_config_() { this->write_field(PDN_DISABLE, true); this->write_field(MSTEP_REG_SELECT, true); this->write_field(INTERNAL_RSENSE, std::isnan(this->rsense_)); @@ -76,28 +171,59 @@ void TMC22XXStepper::setup() { this->toff_ = *toff; this->set_microsteps(this->initial_microsteps_); - if (!std::isnan(this->initial_run_current_)) - this->set_run_current(this->initial_run_current_); - if (!std::isnan(this->initial_hold_current_)) - this->set_hold_current(this->initial_hold_current_); if (this->index_pin_ != nullptr) { // INDEX toggles on every step of the internal pulse generator that VACTUAL drives this->write_field(INDEX_OTPW, false); this->write_field(INDEX_STEP, true); - this->index_pin_->setup(); - this->index_pin_->attach_interrupt(IndexPulseStore::gpio_intr, &this->index_store_, gpio::INTERRUPT_ANY_EDGE); } else { // Step on both edges, so every step pin toggle is one step this->write_field(DEDGE, true); this->write_field(MULTISTEP_FILT, false); - this->step_pin_->setup(); - this->step_pin_->digital_write(false); - this->dir_pin_->setup(); - this->dir_pin_->digital_write(false); } +} - this->set_enabled(true); +bool TMC22XXStepper::update_status_() { + auto gstat = this->read_register(REG_GSTAT); + if (!gstat.has_value()) + return false; + uint32_t drv_status = 0; + // DRV_STATUS only feeds on_status and the DIAG check, skip the transfer when neither is used + if (this->diag_pin_ != nullptr || !this->status_callback_.empty()) { + auto value = this->read_register(REG_DRV_STATUS); + if (!value.has_value()) + return false; + drv_status = *value; + } + // GSTAT flags stay set until written with 1; they are set again while the condition lasts + if ((*gstat & GSTAT_FLAGS) != 0) + this->write_register(REG_GSTAT, *gstat & GSTAT_FLAGS); + + uint16_t status = (*gstat & GSTAT_FLAGS) | ((drv_status & DRV_STATUS_FLAGS) << 3); + // Restore before reporting, so an on_status automation for the reset sees and can change the restored settings + if ((status & (1u << DRIVER_EVENT_RESET)) != 0) { + ESP_LOGW(TAG, "Restoring settings after driver reset"); + this->restore_registers(); + // The motor stopped with the reset, ramp up again from standstill + this->current_speed_ = 0.0f; + this->set_enabled(this->enabled_); + } + uint16_t changed = status ^ this->status_; + this->status_ = status; + for (uint8_t event = 0; event < DRIVER_EVENT_COUNT; event++) { + if ((changed & (1u << event)) == 0) + continue; + bool active = (status & (1u << event)) != 0; + // Open load and temperature levels are informational, the driver keeps running + if (active && event < DRIVER_EVENT_OPEN_LOAD_A) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(driver_event_to_string(static_cast(event)))); + } else { + ESP_LOGD(TAG, "%s: %s", LOG_STR_ARG(driver_event_to_string(static_cast(event))), + active ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("cleared")); + } + this->status_callback_.call(static_cast(event), active); + } + return true; } void TMC22XXStepper::dump_config() { @@ -128,6 +254,7 @@ void TMC22XXStepper::dump_config() { LOG_PIN(" STEP Pin: ", this->step_pin_); LOG_PIN(" DIR Pin: ", this->dir_pin_); LOG_PIN(" INDEX Pin: ", this->index_pin_); + LOG_PIN(" DIAG Pin: ", this->diag_pin_); LOG_STEPPER(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -137,12 +264,29 @@ void TMC22XXStepper::dump_config() { void TMC22XXStepper::loop() { if (this->index_pin_ != nullptr) this->add_index_pulses_(); + if (this->diag_store_.triggered) { + this->diag_store_.triggered = false; + // DIAG rises on a driver error, any other rise is chip specific + if (!this->update_status_()) { + ESP_LOGW(TAG, "DIAG rose, but the driver status could not be read"); + } else if ((this->status_ & GSTAT_FLAGS) == 0) { + this->on_diag_without_error(); + } + } + if (this->has_reached_target()) { this->high_freq_.stop(); + this->moving_ = false; } else { this->high_freq_.start(); if (!this->enabled_) this->set_enabled(true); + // An unreachable driver is polled only every 10 s. Read it before the first step, so settings lost when the + // motor supply dropped are restored before the move runs on the chip defaults. + if (!this->moving_ && this->status_poll_skip_ > 0) { + this->status_poll_skip_ = this->update_status_() ? 0 : UNREACHABLE_POLL_SKIP; + } + this->moving_ = true; } if (this->index_pin_ != nullptr) { @@ -311,8 +455,12 @@ bool TMC22XXStepper::write_register(uint8_t reg, uint32_t value) { } this->status_clear_warning(); - if (uint32_t *shadow = this->shadow_register(reg)) + if (uint32_t *shadow = this->shadow_register(reg)) { *shadow = value; + } else if (int index = restored_index(reg); index >= 0) { + this->restored_values_[index] = value; + this->restored_written_ |= 1u << index; + } return true; } @@ -346,7 +494,15 @@ bool TMC22XXStepper::write_field(const RegisterField &field, uint32_t value) { // Skip the UART timeout when the driver never answered during setup if (this->is_failed()) return false; - auto data = this->read_register(field.reg); + optional data; + int index = restored_index(field.reg); + if (index >= 0 && (this->restored_written_ & (1u << index)) != 0) { + // Modify the last written value, not the chip: after a driver reset that the poll has not seen yet, the chip + // holds its defaults, and those would replace the values to restore + data = this->restored_values_[index]; + } else { + data = this->read_register(field.reg); + } if (!data.has_value()) return false; uint32_t mask = field.mask(); diff --git a/esphome/components/tmc22xx/tmc22xx.h b/esphome/components/tmc22xx/tmc22xx.h index 8b0c92d3520..546a27e70d3 100644 --- a/esphome/components/tmc22xx/tmc22xx.h +++ b/esphome/components/tmc22xx/tmc22xx.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/components/stepper/stepper.h" @@ -30,8 +31,12 @@ static constexpr uint8_t REG_TPOWERDOWN = 0x11; static constexpr uint8_t REG_TPWMTHRS = 0x13; static constexpr uint8_t REG_VACTUAL = 0x22; static constexpr uint8_t REG_CHOPCONF = 0x6C; +static constexpr uint8_t REG_DRV_STATUS = 0x6F; static constexpr uint8_t REG_PWMCONF = 0x70; +// Readable registers that a driver reset sets back to their defaults; the last written values are restored +static constexpr std::array RESTORED_REGISTERS{REG_GCONF, REG_FACTORY_CONF, REG_CHOPCONF, REG_PWMCONF}; + static constexpr RegisterField I_SCALE_ANALOG{REG_GCONF, 0, 1, false}; static constexpr RegisterField INTERNAL_RSENSE{REG_GCONF, 1, 1, false}; static constexpr RegisterField EN_SPREADCYCLE{REG_GCONF, 2, 1, false}; @@ -63,6 +68,33 @@ enum StandstillMode : uint8_t { STANDSTILL_MODE_COIL_SHORT_HS = 3, }; +/// Driver conditions reported to on_status, in the order of their status bits: GSTAT bits 0-2, then DRV_STATUS bits +/// 0-11. +enum DriverEvent : uint8_t { + DRIVER_EVENT_RESET = 0, + DRIVER_EVENT_DRIVER_ERROR, + DRIVER_EVENT_CHARGE_PUMP_UNDERVOLTAGE, + DRIVER_EVENT_OVERTEMPERATURE_PREWARNING, + DRIVER_EVENT_OVERTEMPERATURE, + DRIVER_EVENT_SHORT_TO_GROUND_A, + DRIVER_EVENT_SHORT_TO_GROUND_B, + DRIVER_EVENT_LOW_SIDE_SHORT_A, + DRIVER_EVENT_LOW_SIDE_SHORT_B, + DRIVER_EVENT_OPEN_LOAD_A, + DRIVER_EVENT_OPEN_LOAD_B, + DRIVER_EVENT_TEMPERATURE_120C, + DRIVER_EVENT_TEMPERATURE_143C, + DRIVER_EVENT_TEMPERATURE_150C, + DRIVER_EVENT_TEMPERATURE_157C, + DRIVER_EVENT_COUNT, +}; + +/// Set by a rising edge on DIAG, which signals a driver error or, on chips that support it, a stall. +struct DiagPinStore { + volatile bool triggered{false}; + static void gpio_intr(DiagPinStore *arg); +}; + /// Counts the step pulses the driver reports on INDEX while it runs from VACTUAL. struct IndexPulseStore { volatile int32_t pulses{0}; @@ -84,6 +116,7 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U void set_step_pin(GPIOPin *pin) { this->step_pin_ = pin; } void set_dir_pin(GPIOPin *pin) { this->dir_pin_ = pin; } void set_index_pin(InternalGPIOPin *pin) { this->index_pin_ = pin; } + void set_diag_pin(InternalGPIOPin *pin) { this->diag_pin_ = pin; } void set_clock_frequency(uint32_t frequency) { this->clock_frequency_ = frequency; } void set_rsense(float rsense) { this->rsense_ = rsense; } void set_vsense(bool vsense) { this->vsense_ = vsense; } @@ -97,6 +130,11 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U void set_enabled(bool enabled); bool is_enabled() const { return this->enabled_; } + /// Called with the condition and whether it became active or cleared. + template void add_on_status_callback(F &&callback) { + this->status_callback_.add(std::forward(callback)); + } + void set_microsteps(uint16_t microsteps); uint16_t get_microsteps(); void set_interpolation(bool enable) { this->write_field(INTPOL, enable); } @@ -127,6 +165,15 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U virtual uint8_t expected_version() const = 0; /// Return the cached value of a write-only register, or nullptr when the register is readable. virtual uint32_t *shadow_register(uint8_t reg); + /// Write the cached write-only registers and the last written configuration registers to the driver, at setup + /// and after a driver reset. + virtual void restore_registers(); + /// Write the settings from the configuration to the readable registers. + void apply_config_(); + /// Called when DIAG rises without a driver error. + virtual void on_diag_without_error() {} + /// Read GSTAT and DRV_STATUS and report changes. Returns false when the status could not be read. + bool update_status_(); optional current_to_scale_(float current); float full_scale_current_(); int32_t get_vactual_() const { return static_cast(extract_field(this->vactual_reg_, VACTUAL)); } @@ -139,28 +186,36 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U GPIOPin *step_pin_{nullptr}; GPIOPin *dir_pin_{nullptr}; InternalGPIOPin *index_pin_{nullptr}; + InternalGPIOPin *diag_pin_{nullptr}; + LazyCallbackManager status_callback_; uint32_t clock_frequency_{12000000}; // Write-only registers shared by all family members, with their power-on values uint32_t ihold_irun_{0x00071703}; uint32_t tpowerdown_{0x14}; uint32_t tpwmthrs_{0}; uint32_t vactual_reg_{0}; + std::array restored_values_{}; // NaN while not configured float rsense_{NAN}; float initial_run_current_{NAN}; float initial_hold_current_{NAN}; IndexPulseStore index_store_{}; uint16_t initial_microsteps_{16}; + uint16_t status_{0}; optional vsense_{}; optional ottrim_{}; uint8_t address_{0}; uint8_t toff_{3}; uint8_t version_{0}; + uint8_t restored_written_{0}; // Bit per RESTORED_REGISTERS entry that has been written + uint8_t status_poll_skip_{0}; int8_t direction_{0}; + DiagPinStore diag_store_{}; bool analog_current_scale_{false}; bool enabled_{false}; bool vsense_active_{false}; bool step_state_{false}; + bool moving_{false}; HighFrequencyLoopRequester high_freq_; }; diff --git a/tests/components/tmc2209/common.yaml b/tests/components/tmc2209/common.yaml index ff10c79edcf..e53c3ba8233 100644 --- a/tests/components/tmc2209/common.yaml +++ b/tests/components/tmc2209/common.yaml @@ -6,6 +6,7 @@ stepper: step_pin: ${step_pin} dir_pin: ${dir_pin} enn_pin: ${enn_pin} + diag_pin: ${diag_pin} rsense: 110 mOhm vsense: false ottrim: 1 @@ -15,6 +16,17 @@ stepper: max_speed: 2000 steps/s acceleration: 1000 steps/s^2 deceleration: 1000 steps/s^2 + on_stall: + - stepper.report_position: + id: tmc2209_step_dir + position: 0 + - stepper.set_target: + id: tmc2209_step_dir + target: 0 + on_status: + - logger.log: + format: "Driver event %u active %d" + args: [event, active] - platform: tmc2209 id: tmc2209_serial @@ -24,6 +36,12 @@ stepper: clock_frequency: 12MHz analog_current_scale: true max_speed: 1000 steps/s + on_status: + - if: + condition: + lambda: "return event == tmc22xx::DRIVER_EVENT_OVERTEMPERATURE_PREWARNING && active;" + then: + - tmc2209.disable: tmc2209_serial esphome: on_boot: @@ -49,6 +67,17 @@ esphome: id: tmc2209_serial irun: 20 ihold: !lambda "return 5;" + - tmc2209.stallguard: + id: tmc2209_step_dir + threshold: 50 + tcool_threshold: !lambda "return 400;" + - tmc2209.coolconf: + id: tmc2209_step_dir + semin: 5 + semax: 2 + seup: 1 + sedn: 0 + seimin: true - tmc2209.enable: tmc2209_serial - stepper.set_target: id: tmc2209_serial diff --git a/tests/components/tmc2209/test.esp32-idf.yaml b/tests/components/tmc2209/test.esp32-idf.yaml index 5634c186860..18fcfbcaed0 100644 --- a/tests/components/tmc2209/test.esp32-idf.yaml +++ b/tests/components/tmc2209/test.esp32-idf.yaml @@ -3,6 +3,7 @@ substitutions: dir_pin: GPIO13 enn_pin: GPIO14 index_pin: GPIO15 + diag_pin: GPIO4 packages: uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml diff --git a/tests/components/tmc2209/test.esp8266-ard.yaml b/tests/components/tmc2209/test.esp8266-ard.yaml index 3e4ea33f7a5..fe1c6289af0 100644 --- a/tests/components/tmc2209/test.esp8266-ard.yaml +++ b/tests/components/tmc2209/test.esp8266-ard.yaml @@ -3,6 +3,7 @@ substitutions: dir_pin: GPIO13 enn_pin: GPIO14 index_pin: GPIO15 + diag_pin: GPIO2 packages: uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml diff --git a/tests/components/tmc2209/test.rp2040-ard.yaml b/tests/components/tmc2209/test.rp2040-ard.yaml index 722f8f201fc..d7ceff9ee1f 100644 --- a/tests/components/tmc2209/test.rp2040-ard.yaml +++ b/tests/components/tmc2209/test.rp2040-ard.yaml @@ -3,6 +3,7 @@ substitutions: dir_pin: GPIO3 enn_pin: GPIO6 index_pin: GPIO7 + diag_pin: GPIO8 packages: uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml