diff --git a/esphome/components/tmc2209/sensor/__init__.py b/esphome/components/tmc2209/sensor/__init__.py new file mode 100644 index 00000000000..dd5aadcf7ca --- /dev/null +++ b/esphome/components/tmc2209/sensor/__init__.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +from esphome.components import sensor +from esphome.components.tmc22xx import CONF_ANALOG_CURRENT_SCALE +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_CURRENT, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_PERCENT, +) +import esphome.final_validate as fv +from esphome.types import ConfigType + +from .. import TMC2209Stepper, tmc2209_ns + +CONF_ACTUAL_CURRENT = "actual_current" +CONF_MOTOR_LOAD = "motor_load" +CONF_PWM_GRAD_AUTO = "pwm_grad_auto" +CONF_PWM_OFS_AUTO = "pwm_ofs_auto" +CONF_PWM_SCALE_AUTO = "pwm_scale_auto" +CONF_PWM_SCALE_SUM = "pwm_scale_sum" +CONF_STALLGUARD_RESULT = "stallguard_result" +CONF_TMC2209_ID = "tmc2209_id" + +TMC2209Sensor = tmc2209_ns.class_("TMC2209Sensor", cg.PollingComponent) + +_diagnostic_schema = sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TMC2209Sensor), + cv.GenerateID(CONF_TMC2209_ID): cv.use_id(TMC2209Stepper), + cv.Optional(CONF_ACTUAL_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MOTOR_LOAD): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_STALLGUARD_RESULT): _diagnostic_schema, + cv.Optional(CONF_PWM_SCALE_SUM): _diagnostic_schema, + cv.Optional(CONF_PWM_SCALE_AUTO): _diagnostic_schema, + cv.Optional(CONF_PWM_OFS_AUTO): _diagnostic_schema, + cv.Optional(CONF_PWM_GRAD_AUTO): _diagnostic_schema, + } +).extend(cv.polling_component_schema("60s")) + + +def _final_validate(config: ConfigType) -> ConfigType: + if CONF_ACTUAL_CURRENT not in config: + return config + full_config = fv.full_config.get() + path = full_config.get_path_for_id(config[CONF_TMC2209_ID])[:-1] + if full_config.get_config_for_path(path).get(CONF_ANALOG_CURRENT_SCALE): + raise cv.Invalid( + f"'{CONF_ACTUAL_CURRENT}' can't be used with '{CONF_ANALOG_CURRENT_SCALE}', " + "the current then depends on VREF", + path=[CONF_ACTUAL_CURRENT], + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_TMC2209_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + sensors = sensor.sub_sensors(config) + await sensors(CONF_ACTUAL_CURRENT, var.set_actual_current_sensor) + await sensors(CONF_STALLGUARD_RESULT, var.set_stallguard_result_sensor) + await sensors(CONF_MOTOR_LOAD, var.set_motor_load_sensor) + await sensors(CONF_PWM_SCALE_SUM, var.set_pwm_scale_sum_sensor) + await sensors(CONF_PWM_SCALE_AUTO, var.set_pwm_scale_auto_sensor) + await sensors(CONF_PWM_OFS_AUTO, var.set_pwm_ofs_auto_sensor) + await sensors(CONF_PWM_GRAD_AUTO, var.set_pwm_grad_auto_sensor) diff --git a/esphome/components/tmc2209/sensor/tmc2209_sensor.cpp b/esphome/components/tmc2209/sensor/tmc2209_sensor.cpp new file mode 100644 index 00000000000..a3a6237bca3 --- /dev/null +++ b/esphome/components/tmc2209/sensor/tmc2209_sensor.cpp @@ -0,0 +1,95 @@ +#include "tmc2209_sensor.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::tmc2209 { + +ESPHOME_LOG_TAG(TAG, "tmc2209.sensor"); + +using tmc22xx::TMC22XXStepper; + +// Highest StallGuard result, reached with no load +static constexpr float STALLGUARD_RESULT_MAX = 510.0f; + +static void publish_field(sensor::Sensor *sensor, const optional &data, const RegisterField &field) { + if (sensor == nullptr) + return; + if (!data.has_value()) { + sensor->publish_state(NAN); + return; + } + uint32_t value = TMC22XXStepper::extract_field(*data, field); + sensor->publish_state(field.is_signed ? static_cast(static_cast(value)) : static_cast(value)); +} + +void TMC2209Sensor::update() { + if (this->parent_->is_failed()) + return; + + // Each failed read blocks the loop for the UART timeout. Leave the bus alone while the driver is known to be + // unreachable or after the first failed read; the remaining sensors then publish NaN. + bool reachable = !this->parent_->is_unreachable(); + auto read = [this, &reachable](uint8_t reg) -> optional { + if (!reachable) + return {}; + auto data = this->parent_->read_register(reg); + reachable = data.has_value(); + return data; + }; + + if (this->actual_current_sensor_ != nullptr) { + auto drv_status = read(tmc22xx::REG_DRV_STATUS); + float current = NAN; + if (drv_status.has_value()) { + auto scale = static_cast(TMC22XXStepper::extract_field(*drv_status, tmc22xx::CS_ACTUAL)); + current = this->parent_->scale_to_current(scale); + } + this->actual_current_sensor_->publish_state(current); + } + + // StallGuard only measures the load while the motor turns + bool moving = !this->parent_->has_reached_target(); + if (this->stallguard_result_sensor_ != nullptr || (this->motor_load_sensor_ != nullptr && moving)) { + auto sg_result = read(REG_SG_RESULT); + publish_field(this->stallguard_result_sensor_, sg_result, SG_RESULT); + if (this->motor_load_sensor_ != nullptr) { + // 100% is the load at which a stall is reported, where the result drops below twice the threshold + float stall_result = 2.0f * this->parent_->get_stallguard_threshold(); + float load = NAN; + if (moving && sg_result.has_value() && stall_result < STALLGUARD_RESULT_MAX) { + float result = TMC22XXStepper::extract_field(*sg_result, SG_RESULT); + load = (STALLGUARD_RESULT_MAX - result) / (STALLGUARD_RESULT_MAX - stall_result) * 100.0f; + } + this->motor_load_sensor_->publish_state(load); + } + } else if (this->motor_load_sensor_ != nullptr) { + this->motor_load_sensor_->publish_state(NAN); + } + + if (this->pwm_scale_sum_sensor_ != nullptr || this->pwm_scale_auto_sensor_ != nullptr) { + auto pwm_scale = read(tmc22xx::REG_PWM_SCALE); + publish_field(this->pwm_scale_sum_sensor_, pwm_scale, tmc22xx::PWM_SCALE_SUM); + publish_field(this->pwm_scale_auto_sensor_, pwm_scale, tmc22xx::PWM_SCALE_AUTO); + } + + if (this->pwm_ofs_auto_sensor_ != nullptr || this->pwm_grad_auto_sensor_ != nullptr) { + auto pwm_auto = read(tmc22xx::REG_PWM_AUTO); + publish_field(this->pwm_ofs_auto_sensor_, pwm_auto, tmc22xx::PWM_OFS_AUTO); + publish_field(this->pwm_grad_auto_sensor_, pwm_auto, tmc22xx::PWM_GRAD_AUTO); + } +} + +void TMC2209Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "TMC2209 Sensor:"); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Actual Current", this->actual_current_sensor_); + LOG_SENSOR(" ", "StallGuard Result", this->stallguard_result_sensor_); + LOG_SENSOR(" ", "Motor Load", this->motor_load_sensor_); + LOG_SENSOR(" ", "PWM Scale Sum", this->pwm_scale_sum_sensor_); + LOG_SENSOR(" ", "PWM Scale Auto", this->pwm_scale_auto_sensor_); + LOG_SENSOR(" ", "PWM Offset Auto", this->pwm_ofs_auto_sensor_); + LOG_SENSOR(" ", "PWM Gradient Auto", this->pwm_grad_auto_sensor_); +} + +} // namespace esphome::tmc2209 diff --git a/esphome/components/tmc2209/sensor/tmc2209_sensor.h b/esphome/components/tmc2209/sensor/tmc2209_sensor.h new file mode 100644 index 00000000000..4ebbf82259a --- /dev/null +++ b/esphome/components/tmc2209/sensor/tmc2209_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/tmc2209/tmc2209.h" +#include "esphome/core/component.h" + +namespace esphome::tmc2209 { + +class TMC2209Sensor : public PollingComponent { + public: + explicit TMC2209Sensor(TMC2209Stepper *parent) : parent_(parent) {} + + void update() override; + void dump_config() override; + + void set_actual_current_sensor(sensor::Sensor *sensor) { this->actual_current_sensor_ = sensor; } + void set_stallguard_result_sensor(sensor::Sensor *sensor) { this->stallguard_result_sensor_ = sensor; } + void set_motor_load_sensor(sensor::Sensor *sensor) { this->motor_load_sensor_ = sensor; } + void set_pwm_scale_sum_sensor(sensor::Sensor *sensor) { this->pwm_scale_sum_sensor_ = sensor; } + void set_pwm_scale_auto_sensor(sensor::Sensor *sensor) { this->pwm_scale_auto_sensor_ = sensor; } + void set_pwm_ofs_auto_sensor(sensor::Sensor *sensor) { this->pwm_ofs_auto_sensor_ = sensor; } + void set_pwm_grad_auto_sensor(sensor::Sensor *sensor) { this->pwm_grad_auto_sensor_ = sensor; } + + protected: + TMC2209Stepper *parent_; + sensor::Sensor *actual_current_sensor_{nullptr}; + sensor::Sensor *stallguard_result_sensor_{nullptr}; + sensor::Sensor *motor_load_sensor_{nullptr}; + sensor::Sensor *pwm_scale_sum_sensor_{nullptr}; + sensor::Sensor *pwm_scale_auto_sensor_{nullptr}; + sensor::Sensor *pwm_ofs_auto_sensor_{nullptr}; + sensor::Sensor *pwm_grad_auto_sensor_{nullptr}; +}; + +} // namespace esphome::tmc2209 diff --git a/esphome/components/tmc2209/tmc2209.h b/esphome/components/tmc2209/tmc2209.h index f1c83485fa3..49923316884 100644 --- a/esphome/components/tmc2209/tmc2209.h +++ b/esphome/components/tmc2209/tmc2209.h @@ -9,10 +9,12 @@ 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_SG_RESULT = 0x41; 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 SG_RESULT{REG_SG_RESULT, 0, 10, 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}; @@ -30,6 +32,7 @@ class TMC2209Stepper : public tmc22xx::TMC22XXStepper { /// 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); } + uint8_t get_stallguard_threshold() const { return extract_field(this->sgthrs_, SGTHRS); } /// 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); } diff --git a/esphome/components/tmc22xx/tmc22xx.h b/esphome/components/tmc22xx/tmc22xx.h index 546a27e70d3..19e4813b70d 100644 --- a/esphome/components/tmc22xx/tmc22xx.h +++ b/esphome/components/tmc22xx/tmc22xx.h @@ -33,6 +33,8 @@ 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; +static constexpr uint8_t REG_PWM_SCALE = 0x71; +static constexpr uint8_t REG_PWM_AUTO = 0x72; // 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}; @@ -59,7 +61,12 @@ static constexpr RegisterField VSENSE{REG_CHOPCONF, 17, 1, false}; static constexpr RegisterField MRES{REG_CHOPCONF, 24, 4, false}; static constexpr RegisterField INTPOL{REG_CHOPCONF, 28, 1, false}; static constexpr RegisterField DEDGE{REG_CHOPCONF, 29, 1, false}; +static constexpr RegisterField CS_ACTUAL{REG_DRV_STATUS, 16, 5, false}; static constexpr RegisterField FREEWHEEL{REG_PWMCONF, 20, 2, false}; +static constexpr RegisterField PWM_SCALE_SUM{REG_PWM_SCALE, 0, 8, false}; +static constexpr RegisterField PWM_SCALE_AUTO{REG_PWM_SCALE, 16, 9, true}; +static constexpr RegisterField PWM_OFS_AUTO{REG_PWM_AUTO, 0, 8, false}; +static constexpr RegisterField PWM_GRAD_AUTO{REG_PWM_AUTO, 16, 8, false}; enum StandstillMode : uint8_t { STANDSTILL_MODE_NORMAL = 0, @@ -129,6 +136,8 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U /// Enable or disable the motor outputs, using the ENN pin when configured and TOFF otherwise. void set_enabled(bool enabled); bool is_enabled() const { return this->enabled_; } + /// True while the driver does not answer, until the status poll reads it again (every 10 s). + bool is_unreachable() const { return this->status_poll_skip_ > 0; } /// Called with the condition and whether it became active or cleared. template void add_on_status_callback(F &&callback) { diff --git a/tests/components/tmc2209/common.yaml b/tests/components/tmc2209/common.yaml index e53c3ba8233..c39d3fd8ba2 100644 --- a/tests/components/tmc2209/common.yaml +++ b/tests/components/tmc2209/common.yaml @@ -84,3 +84,26 @@ esphome: target: 1000 - tmc2209.disable: id: tmc2209_step_dir + +sensor: + - platform: tmc2209 + tmc2209_id: tmc2209_step_dir + update_interval: 10s + actual_current: + name: Actual Current + stallguard_result: + name: StallGuard Result + motor_load: + name: Motor Load + pwm_scale_sum: + name: PWM Scale Sum + pwm_scale_auto: + name: PWM Scale Auto + pwm_ofs_auto: + name: PWM Offset Auto + pwm_grad_auto: + name: PWM Gradient Auto + - platform: tmc2209 + tmc2209_id: tmc2209_serial + stallguard_result: + name: Serial StallGuard Result