[tmc2209] Add driver diagnostics and stall detection (#20618)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Remco van Essen
2026-10-13 08:17:25 -10:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1d8423edd8
commit 55261ae7e5
11 changed files with 404 additions and 18 deletions
+44
View File
@@ -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_),
)
+26 -2
View File
@@ -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
)
+20
View File
@@ -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
+39
View File
@@ -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<typename F> void add_on_stall_callback(F &&callback) {
this->stall_callback_.add(std::forward<F>(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<void()> 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
+17 -1
View File
@@ -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
+171 -15
View File
@@ -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<int>(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<DriverEvent>(event))));
} else {
ESP_LOGD(TAG, "%s: %s", LOG_STR_ARG(driver_event_to_string(static_cast<DriverEvent>(event))),
active ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("cleared"));
}
this->status_callback_.call(static_cast<DriverEvent>(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<uint32_t> 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();
+55
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cmath>
#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<uint8_t, 4> 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<typename F> void add_on_status_callback(F &&callback) {
this->status_callback_.add(std::forward<F>(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<uint8_t> current_to_scale_(float current);
float full_scale_current_();
int32_t get_vactual_() const { return static_cast<int32_t>(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<void(DriverEvent, bool)> 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<uint32_t, RESTORED_REGISTERS.size()> 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<bool> vsense_{};
optional<uint8_t> 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_;
};
+29
View File
@@ -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
@@ -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
@@ -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
@@ -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