mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[tmc2209] Add TMC2209 stepper driver (#20437)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
0c43d689d7
commit
132e9dac8b
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user