[tmc2209] Add current and diagnostic sensors (#20638)

This commit is contained in:
Remco van Essen
2026-10-13 12:35:55 -10:00
committed by GitHub
parent e265c0c0fc
commit 5c23a827b6
6 changed files with 252 additions and 0 deletions
@@ -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)
@@ -0,0 +1,95 @@
#include "tmc2209_sensor.h"
#include "esphome/core/log.h"
#include <cmath>
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<uint32_t> &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<float>(static_cast<int32_t>(value)) : static_cast<float>(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<uint32_t> {
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<uint8_t>(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
@@ -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
+3
View File
@@ -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); }
+9
View File
@@ -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<uint8_t, 4> 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<typename F> void add_on_status_callback(F &&callback) {
+23
View File
@@ -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