[tmc2209] Add TMC2209 stepper driver (#20437)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Remco van Essen
2026-10-12 13:13:11 -10:00
committed by GitHub
co-authored by J. Nick Koston
parent 0c43d689d7
commit 132e9dac8b
12 changed files with 888 additions and 0 deletions
+2
View File
@@ -617,6 +617,8 @@ esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @mrtoy-me
esphome/components/tmc2209/* @remcom
esphome/components/tmc22xx/* @remcom
esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath
+10
View File
@@ -0,0 +1,10 @@
import esphome.codegen as cg
from esphome.components import tmc22xx
CODEOWNERS = ["@remcom"]
DOMAIN = "tmc2209"
tmc2209_ns = cg.esphome_ns.namespace("tmc2209")
TMC2209Stepper = tmc2209_ns.class_("TMC2209Stepper", tmc22xx.TMC22XXStepper)
tmc22xx.register_actions("tmc2209", TMC2209Stepper)
+14
View File
@@ -0,0 +1,14 @@
from esphome.components import tmc22xx
from esphome.types import ConfigType
from . import TMC2209Stepper
AUTO_LOAD = ["tmc22xx"]
DEPENDENCIES = ["uart"]
CONFIG_SCHEMA = tmc22xx.tmc22xx_schema(TMC2209Stepper, max_address=3)
FINAL_VALIDATE_SCHEMA = tmc22xx.final_validate
async def to_code(config: ConfigType) -> None:
await tmc22xx.new_tmc22xx(config)
+13
View File
@@ -0,0 +1,13 @@
#include "tmc2209.h"
#include "esphome/core/log.h"
namespace esphome::tmc2209 {
ESPHOME_LOG_TAG(TAG, "tmc2209");
void TMC2209Stepper::dump_config() {
ESP_LOGCONFIG(TAG, "TMC2209:");
TMC22XXStepper::dump_config();
}
} // namespace esphome::tmc2209
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/tmc22xx/tmc22xx.h"
namespace esphome::tmc2209 {
class TMC2209Stepper : public tmc22xx::TMC22XXStepper {
public:
void dump_config() override;
protected:
uint8_t expected_version() const override { return 0x21; }
};
} // namespace esphome::tmc2209
+211
View File
@@ -0,0 +1,211 @@
"""Shared code for Trinamic stepper drivers configured over the single wire UART interface."""
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import stepper, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_DIR_PIN,
CONF_ID,
CONF_STEP_PIN,
CONF_UART_ID,
)
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@remcom"]
DOMAIN = "tmc22xx"
DEPENDENCIES = ["uart"]
CONF_ANALOG_CURRENT_SCALE = "analog_current_scale"
CONF_CLOCK_FREQUENCY = "clock_frequency"
CONF_ENABLE_SPREADCYCLE = "enable_spreadcycle"
CONF_ENN_PIN = "enn_pin"
CONF_HOLD_CURRENT = "hold_current"
CONF_IHOLD = "ihold"
CONF_IHOLDDELAY = "iholddelay"
CONF_INDEX_PIN = "index_pin"
CONF_INTERPOLATION = "interpolation"
CONF_INVERSE_DIRECTION = "inverse_direction"
CONF_IRUN = "irun"
CONF_MICROSTEPS = "microsteps"
CONF_OTTRIM = "ottrim"
CONF_RSENSE = "rsense"
CONF_RUN_CURRENT = "run_current"
CONF_STANDSTILL_MODE = "standstill_mode"
CONF_TPOWERDOWN = "tpowerdown"
CONF_TPWM_THRESHOLD = "tpwm_threshold"
CONF_VSENSE = "vsense"
tmc22xx_ns = cg.esphome_ns.namespace("tmc22xx")
TMC22XXStepper = tmc22xx_ns.class_(
"TMC22XXStepper", stepper.Stepper, cg.Component, uart.UARTDevice
)
StandstillMode = tmc22xx_ns.enum("StandstillMode")
STANDSTILL_MODES = {
"normal": StandstillMode.STANDSTILL_MODE_NORMAL,
"freewheeling": StandstillMode.STANDSTILL_MODE_FREEWHEELING,
"coil_short_ls": StandstillMode.STANDSTILL_MODE_COIL_SHORT_LS,
"coil_short_hs": StandstillMode.STANDSTILL_MODE_COIL_SHORT_HS,
}
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:
"""Return the stepper schema for a family member; `max_address` is 0 for chips without address pins."""
return cv.All(
stepper.STEPPER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(cls),
cv.Optional(CONF_ADDRESS, default=0): cv.int_range(0, max_address),
cv.Optional(CONF_ENN_PIN): pins.gpio_output_pin_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_CLOCK_FREQUENCY, default="12MHz"): cv.All(
cv.frequency, cv.int_range(min=1)
),
cv.Optional(CONF_RSENSE): cv.All(
cv.resistance, cv.float_range(min=0.0, min_included=False)
),
cv.Optional(CONF_VSENSE): cv.boolean,
cv.Optional(CONF_OTTRIM): cv.int_range(0, 3),
cv.Optional(CONF_ANALOG_CURRENT_SCALE, default=False): cv.boolean,
cv.Optional(CONF_RUN_CURRENT): validate_current,
cv.Optional(CONF_HOLD_CURRENT): validate_current,
cv.Optional(CONF_MICROSTEPS, default=16): validate_microsteps,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA),
cv.has_exactly_one_key(CONF_INDEX_PIN, CONF_STEP_PIN),
cv.has_none_or_all_keys(CONF_STEP_PIN, CONF_DIR_PIN),
_validate_analog_current_scale,
)
def _validate_analog_current_scale(config: ConfigType) -> ConfigType:
if config[CONF_ANALOG_CURRENT_SCALE]:
for key in (CONF_RUN_CURRENT, CONF_HOLD_CURRENT):
if key in config:
raise cv.Invalid(
f"'{key}' can't be used with '{CONF_ANALOG_CURRENT_SCALE}', the "
f"current then depends on VREF; set '{CONF_IRUN}'/'{CONF_IHOLD}' "
"with the currents action instead",
path=[key],
)
return config
def final_validate(config: ConfigType) -> ConfigType:
"""Reject two drivers of the family with the same address on one UART bus."""
for other in fv.full_config.get().get("stepper", []):
if other is config:
break
if (
other.get(CONF_UART_ID) == config[CONF_UART_ID]
and other.get(CONF_ADDRESS) == config[CONF_ADDRESS]
):
raise cv.Invalid(
f"Address {config[CONF_ADDRESS]} is already used by '{other[CONF_ID]}' "
"on the same UART bus"
)
return config
async def new_tmc22xx(config: ConfigType) -> cg.Pvariable:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
await stepper.register_stepper(var, config)
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_clock_frequency(config[CONF_CLOCK_FREQUENCY]))
cg.add(var.set_analog_current_scale(config[CONF_ANALOG_CURRENT_SCALE]))
cg.add(var.set_initial_microsteps(config[CONF_MICROSTEPS]))
for key, setter in (
(CONF_ENN_PIN, var.set_enn_pin),
(CONF_STEP_PIN, var.set_step_pin),
(CONF_DIR_PIN, var.set_dir_pin),
(CONF_INDEX_PIN, var.set_index_pin),
):
if (pin := config.get(key)) is not None:
cg.add(setter(await cg.gpio_pin_expression(pin)))
for key, setter in (
(CONF_RSENSE, var.set_rsense),
(CONF_VSENSE, var.set_vsense),
(CONF_OTTRIM, var.set_ottrim),
(CONF_RUN_CURRENT, var.set_initial_run_current),
(CONF_HOLD_CURRENT, var.set_initial_hold_current),
):
if (value := config.get(key)) is not None:
cg.add(setter(value))
return var
def register_actions(domain: str, cls: MockObjClass) -> None:
"""Register the actions shared by the family under `domain`, e.g. `tmc2209.enable`."""
id_schema = {cv.GenerateID(): cv.use_id(cls)}
for name, enabled in (("enable", "true"), ("disable", "false")):
automation.register_apply_action(
f"{domain}.{name}",
automation.maybe_simple_id(id_schema),
automation.ApplyCall(f"set_enabled({enabled})"),
)
automation.register_apply_action(
f"{domain}.configure",
cv.Schema(
{
**id_schema,
cv.Optional(CONF_INVERSE_DIRECTION): cv.templatable(cv.boolean),
cv.Optional(CONF_MICROSTEPS): cv.templatable(validate_microsteps),
cv.Optional(CONF_INTERPOLATION): cv.templatable(cv.boolean),
cv.Optional(CONF_ENABLE_SPREADCYCLE): cv.templatable(cv.boolean),
cv.Optional(CONF_TPWM_THRESHOLD): cv.templatable(
cv.int_range(0, 2**20 - 1)
),
}
),
automation.ApplyField(
CONF_INVERSE_DIRECTION, "set_inverse_direction", cg.bool_
),
automation.ApplyField(CONF_MICROSTEPS, "set_microsteps", cg.uint16),
automation.ApplyField(CONF_INTERPOLATION, "set_interpolation", cg.bool_),
automation.ApplyField(CONF_ENABLE_SPREADCYCLE, "set_spreadcycle", cg.bool_),
automation.ApplyField(CONF_TPWM_THRESHOLD, "set_tpwm_threshold", cg.uint32),
)
automation.register_apply_action(
f"{domain}.currents",
cv.Schema(
{
**id_schema,
cv.Exclusive(CONF_RUN_CURRENT, "run"): cv.templatable(validate_current),
cv.Exclusive(CONF_IRUN, "run"): cv.templatable(cv.int_range(0, 31)),
cv.Exclusive(CONF_HOLD_CURRENT, "hold"): cv.templatable(
validate_current
),
cv.Exclusive(CONF_IHOLD, "hold"): cv.templatable(cv.int_range(0, 31)),
cv.Optional(CONF_IHOLDDELAY): cv.templatable(cv.int_range(0, 15)),
cv.Optional(CONF_TPOWERDOWN): cv.templatable(cv.int_range(0, 255)),
cv.Optional(CONF_STANDSTILL_MODE): cv.templatable(
cv.enum(STANDSTILL_MODES, lower=True)
),
}
),
automation.ApplyField(CONF_RUN_CURRENT, "set_run_current", cg.float_),
automation.ApplyField(CONF_IRUN, "set_irun", cg.uint8),
automation.ApplyField(CONF_HOLD_CURRENT, "set_hold_current", cg.float_),
automation.ApplyField(CONF_IHOLD, "set_ihold", cg.uint8),
automation.ApplyField(CONF_IHOLDDELAY, "set_iholddelay", cg.uint8),
automation.ApplyField(CONF_TPOWERDOWN, "set_tpowerdown", cg.uint8),
automation.ApplyField(
CONF_STANDSTILL_MODE, "set_standstill_mode", StandstillMode
),
)
+372
View File
@@ -0,0 +1,372 @@
#include "tmc22xx.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <cmath>
#include <cstring>
#include <numbers>
namespace esphome::tmc22xx {
ESPHOME_LOG_TAG(TAG, "tmc22xx");
static constexpr uint8_t SYNC_BYTE = 0x05;
static constexpr uint8_t MASTER_ADDRESS = 0xFF;
static constexpr uint8_t WRITE_BIT = 0x80;
static constexpr size_t READ_REQUEST_SIZE = 4;
static constexpr size_t DATAGRAM_SIZE = 8;
static constexpr uint8_t DEFAULT_TOFF = 3;
static constexpr float SENSE_RESISTOR_OFFSET = 0.02f; // Ohm, added to RSENSE in the datasheet current formula
static constexpr float INTERNAL_RSENSE_OHM = 0.17f; // Equivalent sense resistance when using internal sensing
// INDEX pulses are counted once per loop, so near the target the speed is limited to cover the remaining
// distance in no less than this time. A slow loop pass then cannot run far past the target.
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;
// 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) {
uint8_t crc = 0;
for (size_t i = 0; i < len; i++) {
const uint8_t reflected = reverse_bits(data[i]);
crc = crc8(&reflected, 1, crc, 0x07, true);
}
return crc;
}
void IRAM_ATTR IndexPulseStore::gpio_intr(IndexPulseStore *arg) { arg->pulses = arg->pulses + arg->direction; }
void TMC22XXStepper::setup() {
if (this->enn_pin_ != nullptr) {
this->enn_pin_->setup();
this->enn_pin_->digital_write(true);
}
auto version = this->read_field(VERSION);
if (!version.has_value()) {
ESP_LOGE(TAG, "No response from driver at address %u", this->address_);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->version_ = *version;
if (this->version_ != this->expected_version()) {
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->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);
this->write_field(PDN_DISABLE, true);
this->write_field(MSTEP_REG_SELECT, true);
this->write_field(INTERNAL_RSENSE, std::isnan(this->rsense_));
this->write_field(I_SCALE_ANALOG, this->analog_current_scale_);
if (this->vsense_.has_value())
this->write_field(VSENSE, *this->vsense_);
if (this->ottrim_.has_value())
this->write_field(OTTRIM, *this->ottrim_);
this->vsense_active_ = this->read_field(VSENSE).value_or(0);
// TOFF=0 disables the outputs, keep the current value so it can be restored on enable
auto toff = this->read_field(TOFF);
if (toff.has_value() && *toff != 0)
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);
}
void TMC22XXStepper::dump_config() {
ESP_LOGCONFIG(TAG,
" Address: %u\n"
" IC version: 0x%02X",
this->address_, this->version_);
// Register reads on a failed driver would only wait for the UART timeout
if (!this->is_failed()) {
uint8_t irun = this->read_field(IRUN).value_or(0);
uint8_t ihold = this->read_field(IHOLD).value_or(0);
ESP_LOGCONFIG(TAG, " Microsteps: %u", this->get_microsteps());
if (this->analog_current_scale_) {
ESP_LOGCONFIG(TAG,
" Run current scale: %u\n"
" Hold current scale: %u\n"
" Current scaled by VREF",
irun, ihold);
} else {
ESP_LOGCONFIG(TAG,
" Run current: %.2f A\n"
" Hold current: %.2f A\n"
" Max current: %.2f A",
this->scale_to_current(irun), this->scale_to_current(ihold), this->scale_to_current(31));
}
}
LOG_PIN(" ENN Pin: ", this->enn_pin_);
LOG_PIN(" STEP Pin: ", this->step_pin_);
LOG_PIN(" DIR Pin: ", this->dir_pin_);
LOG_PIN(" INDEX Pin: ", this->index_pin_);
LOG_STEPPER(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
void TMC22XXStepper::loop() {
if (this->index_pin_ != nullptr)
this->add_index_pulses_();
if (this->has_reached_target()) {
this->high_freq_.stop();
} else {
this->high_freq_.start();
if (!this->enabled_)
this->set_enabled(true);
}
if (this->index_pin_ != nullptr) {
this->loop_serial_();
} else {
this->loop_step_dir_();
}
}
void TMC22XXStepper::add_index_pulses_() {
InterruptLock lock;
this->current_position += this->index_store_.pulses;
this->index_store_.pulses = 0;
}
void TMC22XXStepper::loop_serial_() {
int32_t remaining = this->target_position - this->current_position;
int8_t direction = (remaining > 0) - (remaining < 0);
// Ran past the target, restart the ramp from standstill in the other direction
if (direction != 0 && direction == -this->direction_)
this->current_speed_ = 0.0f;
this->calculate_speed_(micros());
this->current_speed_ = std::min(this->current_speed_, static_cast<float>(std::abs(remaining)) / APPROACH_TIME_S);
int32_t vactual = direction * this->speed_to_vactual_(this->current_speed_);
int32_t last = this->get_vactual_();
if (vactual == last)
return;
if (vactual != 0 && (vactual > 0) == (last > 0) && std::abs(vactual - last) < std::abs(last) / VACTUAL_CHANGE_DIVISOR)
return;
// Pulses that arrive while VACTUAL changes direction are counted in the new direction
this->index_store_.direction = direction;
this->direction_ = direction;
this->write_field(VACTUAL, static_cast<uint32_t>(vactual));
}
void TMC22XXStepper::loop_step_dir_() {
int32_t direction = this->should_step_();
if (direction == 0)
return;
if (direction != this->direction_) {
this->dir_pin_->digital_write(direction > 0);
this->direction_ = direction;
delayMicroseconds(1);
}
this->step_state_ = !this->step_state_;
this->step_pin_->digital_write(this->step_state_);
}
void TMC22XXStepper::on_shutdown() {
if (this->get_vactual_() != 0)
this->write_field(VACTUAL, 0);
}
void TMC22XXStepper::set_enabled(bool enabled) {
if (!enabled) {
if (this->get_vactual_() != 0)
this->write_field(VACTUAL, 0);
if (this->index_pin_ != nullptr) {
// Count the pulses up to the stop, so the position is current before the target is set
this->add_index_pulses_();
this->index_store_.direction = 0;
this->direction_ = 0;
}
// Stop at the current position, so the motor does not run off again when re-enabled
this->target_position = this->current_position;
}
if (this->enn_pin_ != nullptr) {
this->enn_pin_->digital_write(!enabled);
} else {
this->write_field(TOFF, enabled ? this->toff_ : 0);
}
this->enabled_ = enabled;
}
void TMC22XXStepper::set_microsteps(uint16_t microsteps) {
for (uint8_t mres = 0; mres <= 8; mres++) {
if ((256u >> mres) == microsteps) {
this->write_field(MRES, mres);
return;
}
}
ESP_LOGW(TAG, "Invalid microsteps %u", microsteps);
}
uint16_t TMC22XXStepper::get_microsteps() {
auto mres = this->read_field(MRES);
if (!mres.has_value() || *mres > 8)
return 0;
return 256u >> *mres;
}
void TMC22XXStepper::set_run_current(float current) {
if (auto scale = this->current_to_scale_(current))
this->write_field(IRUN, *scale);
}
void TMC22XXStepper::set_hold_current(float current) {
if (auto scale = this->current_to_scale_(current))
this->write_field(IHOLD, *scale);
}
float TMC22XXStepper::full_scale_current_() {
return (this->vsense_active_ ? 0.180f : 0.325f) /
((std::isnan(this->rsense_) ? INTERNAL_RSENSE_OHM : this->rsense_) + SENSE_RESISTOR_OFFSET);
}
float TMC22XXStepper::scale_to_current(uint8_t scale) {
return (std::min<uint8_t>(scale, 31) + 1) / 32.0f * this->full_scale_current_() / std::numbers::sqrt2_v<float>;
}
optional<uint8_t> TMC22XXStepper::current_to_scale_(float current) {
if (this->analog_current_scale_) {
ESP_LOGW(TAG, "Current in A is not supported with analog current scale, use irun/ihold");
return {};
}
float scale = 32.0f * std::numbers::sqrt2_v<float> * current / this->full_scale_current_() - 1.0f;
if (scale > 31.0f) {
ESP_LOGW(TAG, "%.2f A is above the %.2f A limit, using the limit", current, this->scale_to_current(31));
return 31;
}
return static_cast<uint8_t>(std::max(0.0f, std::round(scale)));
}
int32_t TMC22XXStepper::speed_to_vactual_(float speed) const {
// VACTUAL is in microsteps per 2^24 clock cycles
return static_cast<int32_t>(std::lround(speed * 16777216.0f / this->clock_frequency_));
}
uint32_t *TMC22XXStepper::shadow_register(uint8_t reg) {
switch (reg) {
case REG_IHOLD_IRUN:
return &this->ihold_irun_;
case REG_TPOWERDOWN:
return &this->tpowerdown_;
case REG_TPWMTHRS:
return &this->tpwmthrs_;
case REG_VACTUAL:
return &this->vactual_reg_;
default:
return nullptr;
}
}
bool TMC22XXStepper::write_register(uint8_t reg, uint32_t value) {
uint8_t datagram[DATAGRAM_SIZE] = {SYNC_BYTE,
this->address_,
static_cast<uint8_t>(reg | WRITE_BIT),
static_cast<uint8_t>(value >> 24),
static_cast<uint8_t>(value >> 16),
static_cast<uint8_t>(value >> 8),
static_cast<uint8_t>(value),
0};
datagram[7] = datagram_crc(datagram, 7);
while (this->available())
this->read();
this->write_array(datagram, DATAGRAM_SIZE);
this->flush();
// On the single wire bus every byte sent is also received, drop that echo
uint8_t echo[DATAGRAM_SIZE];
if (!this->read_array(echo, DATAGRAM_SIZE) || memcmp(echo, datagram, DATAGRAM_SIZE) != 0) {
ESP_LOGV(TAG, "No echo writing register 0x%02X", reg);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->status_clear_warning();
if (uint32_t *shadow = this->shadow_register(reg))
*shadow = value;
return true;
}
optional<uint32_t> TMC22XXStepper::read_register(uint8_t reg) {
if (uint32_t *shadow = this->shadow_register(reg))
return *shadow;
uint8_t request[READ_REQUEST_SIZE] = {SYNC_BYTE, this->address_, reg, 0};
request[3] = datagram_crc(request, 3);
while (this->available())
this->read();
this->write_array(request, READ_REQUEST_SIZE);
this->flush();
uint8_t reply[DATAGRAM_SIZE];
if (!this->read_array(reply, READ_REQUEST_SIZE) || !this->read_array(reply, DATAGRAM_SIZE)) {
ESP_LOGV(TAG, "No reply reading register 0x%02X", reg);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return {};
}
if (reply[0] != SYNC_BYTE || reply[1] != MASTER_ADDRESS || reply[2] != reg || reply[7] != datagram_crc(reply, 7)) {
ESP_LOGV(TAG, "Invalid reply reading register 0x%02X", reg);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return {};
}
this->status_clear_warning();
return encode_uint32(reply[3], reply[4], reply[5], reply[6]);
}
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);
if (!data.has_value())
return false;
uint32_t mask = field.mask();
return this->write_register(field.reg, (*data & ~mask) | ((value << field.shift) & mask));
}
optional<uint32_t> TMC22XXStepper::read_field(const RegisterField &field) {
auto data = this->read_register(field.reg);
if (!data.has_value())
return {};
return extract_field(*data, field);
}
uint32_t TMC22XXStepper::extract_field(uint32_t data, const RegisterField &field) {
uint32_t value = (data & field.mask()) >> field.shift;
if (field.is_signed && field.width < 32) {
uint32_t sign_bit = 1u << (field.width - 1);
value = (value ^ sign_bit) - sign_bit;
}
return value;
}
} // namespace esphome::tmc22xx
+167
View File
@@ -0,0 +1,167 @@
#pragma once
#include <cmath>
#include "esphome/components/stepper/stepper.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::tmc22xx {
/// A bit field inside one register.
struct RegisterField {
uint8_t reg;
uint8_t shift;
uint8_t width;
bool is_signed;
constexpr uint32_t mask() const { return ((width >= 32) ? 0xFFFFFFFFu : ((1u << width) - 1u)) << shift; }
};
// Registers shared by the TMC22xx family (TMC2202, TMC2208, TMC2209, TMC2224, TMC2225, TMC2226)
static constexpr uint8_t REG_GCONF = 0x00;
static constexpr uint8_t REG_GSTAT = 0x01;
static constexpr uint8_t REG_IOIN = 0x06;
static constexpr uint8_t REG_FACTORY_CONF = 0x07;
static constexpr uint8_t REG_IHOLD_IRUN = 0x10;
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_PWMCONF = 0x70;
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};
static constexpr RegisterField SHAFT{REG_GCONF, 3, 1, false};
static constexpr RegisterField INDEX_OTPW{REG_GCONF, 4, 1, false};
static constexpr RegisterField INDEX_STEP{REG_GCONF, 5, 1, false};
static constexpr RegisterField PDN_DISABLE{REG_GCONF, 6, 1, false};
static constexpr RegisterField MSTEP_REG_SELECT{REG_GCONF, 7, 1, false};
static constexpr RegisterField MULTISTEP_FILT{REG_GCONF, 8, 1, false};
static constexpr RegisterField VERSION{REG_IOIN, 24, 8, false};
static constexpr RegisterField OTTRIM{REG_FACTORY_CONF, 8, 2, false};
static constexpr RegisterField IHOLD{REG_IHOLD_IRUN, 0, 5, false};
static constexpr RegisterField IRUN{REG_IHOLD_IRUN, 8, 5, false};
static constexpr RegisterField IHOLDDELAY{REG_IHOLD_IRUN, 16, 4, false};
static constexpr RegisterField TPOWERDOWN{REG_TPOWERDOWN, 0, 8, false};
static constexpr RegisterField TPWMTHRS{REG_TPWMTHRS, 0, 20, false};
static constexpr RegisterField VACTUAL{REG_VACTUAL, 0, 24, true};
static constexpr RegisterField TOFF{REG_CHOPCONF, 0, 4, false};
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 FREEWHEEL{REG_PWMCONF, 20, 2, false};
enum StandstillMode : uint8_t {
STANDSTILL_MODE_NORMAL = 0,
STANDSTILL_MODE_FREEWHEELING = 1,
STANDSTILL_MODE_COIL_SHORT_LS = 2,
STANDSTILL_MODE_COIL_SHORT_HS = 3,
};
/// Counts the step pulses the driver reports on INDEX while it runs from VACTUAL.
struct IndexPulseStore {
volatile int32_t pulses{0};
volatile int8_t direction{0};
static void gpio_intr(IndexPulseStore *arg);
};
/// Base class for Trinamic stepper drivers that are configured over the single wire UART interface.
class TMC22XXStepper : public stepper::Stepper, public Component, public uart::UARTDevice {
public:
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void set_address(uint8_t address) { this->address_ = address; }
void set_enn_pin(GPIOPin *pin) { this->enn_pin_ = pin; }
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_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; }
void set_ottrim(uint8_t ottrim) { this->ottrim_ = ottrim; }
void set_analog_current_scale(bool enable) { this->analog_current_scale_ = enable; }
void set_initial_run_current(float current) { this->initial_run_current_ = current; }
void set_initial_hold_current(float current) { this->initial_hold_current_ = current; }
void set_initial_microsteps(uint16_t microsteps) { this->initial_microsteps_ = microsteps; }
/// 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_; }
void set_microsteps(uint16_t microsteps);
uint16_t get_microsteps();
void set_interpolation(bool enable) { this->write_field(INTPOL, enable); }
void set_spreadcycle(bool enable) { this->write_field(EN_SPREADCYCLE, enable); }
void set_inverse_direction(bool inverse) { this->write_field(SHAFT, inverse); }
void set_tpwm_threshold(uint32_t threshold) { this->write_field(TPWMTHRS, threshold); }
/// Set the motor run or hold current in A RMS.
/// Not supported with analog current scale, use set_irun() and set_ihold() instead.
void set_run_current(float current);
void set_hold_current(float current);
void set_irun(uint8_t irun) { this->write_field(IRUN, irun); }
void set_ihold(uint8_t ihold) { this->write_field(IHOLD, ihold); }
void set_iholddelay(uint8_t delay) { this->write_field(IHOLDDELAY, delay); }
void set_tpowerdown(uint8_t tpowerdown) { this->write_field(TPOWERDOWN, tpowerdown); }
void set_standstill_mode(StandstillMode mode) { this->write_field(FREEWHEEL, mode); }
/// Convert a current scale (0-31) to A RMS, without analog current scale.
float scale_to_current(uint8_t scale);
bool write_register(uint8_t reg, uint32_t value);
optional<uint32_t> read_register(uint8_t reg);
bool write_field(const RegisterField &field, uint32_t value);
optional<uint32_t> read_field(const RegisterField &field);
static uint32_t extract_field(uint32_t data, const RegisterField &field);
protected:
/// IC version reported in IOIN by the supported chip.
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);
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)); }
int32_t speed_to_vactual_(float speed) const;
void add_index_pulses_();
void loop_serial_();
void loop_step_dir_();
GPIOPin *enn_pin_{nullptr};
GPIOPin *step_pin_{nullptr};
GPIOPin *dir_pin_{nullptr};
InternalGPIOPin *index_pin_{nullptr};
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};
// 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};
optional<bool> vsense_{};
optional<uint8_t> ottrim_{};
uint8_t address_{0};
uint8_t toff_{3};
uint8_t version_{0};
int8_t direction_{0};
bool analog_current_scale_{false};
bool enabled_{false};
bool vsense_active_{false};
bool step_state_{false};
HighFrequencyLoopRequester high_freq_;
};
} // namespace esphome::tmc22xx
+57
View File
@@ -0,0 +1,57 @@
stepper:
- platform: tmc2209
id: tmc2209_step_dir
uart_id: uart_bus
address: 0
step_pin: ${step_pin}
dir_pin: ${dir_pin}
enn_pin: ${enn_pin}
rsense: 110 mOhm
vsense: false
ottrim: 1
run_current: 800mA
hold_current: 400mA
microsteps: 16
max_speed: 2000 steps/s
acceleration: 1000 steps/s^2
deceleration: 1000 steps/s^2
- platform: tmc2209
id: tmc2209_serial
uart_id: uart_bus
address: 1
index_pin: ${index_pin}
clock_frequency: 12MHz
analog_current_scale: true
max_speed: 1000 steps/s
esphome:
on_boot:
then:
- tmc2209.configure:
id: tmc2209_step_dir
inverse_direction: true
microsteps: 8
interpolation: true
enable_spreadcycle: false
tpwm_threshold: 0
- tmc2209.configure:
id: tmc2209_serial
microsteps: !lambda "return 32;"
- tmc2209.currents:
id: tmc2209_step_dir
run_current: 1A
hold_current: 0.5A
iholddelay: 4
tpowerdown: 20
standstill_mode: freewheeling
- tmc2209.currents:
id: tmc2209_serial
irun: 20
ihold: !lambda "return 5;"
- tmc2209.enable: tmc2209_serial
- stepper.set_target:
id: tmc2209_serial
target: 1000
- tmc2209.disable:
id: tmc2209_step_dir
@@ -0,0 +1,9 @@
substitutions:
step_pin: GPIO12
dir_pin: GPIO13
enn_pin: GPIO14
index_pin: GPIO15
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
tmc2209: !include common.yaml
@@ -0,0 +1,9 @@
substitutions:
step_pin: GPIO12
dir_pin: GPIO13
enn_pin: GPIO14
index_pin: GPIO15
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
tmc2209: !include common.yaml
@@ -0,0 +1,9 @@
substitutions:
step_pin: GPIO2
dir_pin: GPIO3
enn_pin: GPIO6
index_pin: GPIO7
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
tmc2209: !include common.yaml