mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[tmc2209] Add driver diagnostics and stall detection (#20618)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1d8423edd8
commit
55261ae7e5
@@ -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_),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user