Quickstart¶
Turn a stepper motor half a revolution from your MCU: wire the SPI bus and DRV_ENN, write a small port class, call six driver functions.
Before you power the motor
Set the current-limit of your bench supply to about the motor's rated current and keep the shaft free to turn. Never connect or disconnect the motor while VS is on: the inductive kick can destroy the MOSFETs.
1. Wire it¶
| MCU | TMC5160 | Notes |
|---|---|---|
| SPI SCK, MOSI, MISO, CS | SCK_CFG2, SDI_CFG1, SDO_CFG0, CSN_CFG3 | SPI mode 3; up to 4 MHz on the internal clock, start at 1 MHz |
| GPIO out | DRV_ENN | High switches the power stage off; the driver drives it low in Enable() |
| VCC_IO supply | SPI_MODE | Strap high: SPI interface |
| GND | SD_MODE | Strap low: internal ramp generator |
| GND | CLK | Internal 12 MHz clock (or feed an external clock) |
| GND | GND | One ground for MCU, logic and motor supply |
VCC_IO is the MCU's I/O voltage (3.3 V or 5 V). Hardware has the full pin list, the UART wiring and the power ranges.
2. Add the driver¶
Add the repository as a submodule and link the hf_tmc51x0 CMake target (or include inc/ and build with
C++20). Installation covers CMake and ESP-IDF.
3. Write the port¶
The port is the only platform code. It moves bytes over SPI, sets and reads control pins and waits. Replace the
platform_* calls with your SDK's.
class MySpi : public tmc51x0::SpiCommInterface<MySpi> {
public:
tmc51x0::CommMode GetMode() const noexcept { return tmc51x0::CommMode::SPI; }
// One full-duplex transfer of `length` bytes (5 per chip in the chain), SPI mode 3, CSN low throughout.
tmc51x0::Result<void> SpiTransfer(const uint8_t* tx, uint8_t* rx, size_t length) noexcept {
if (!platform_spi_transfer(tx, rx, length)) {
return tmc51x0::Result<void>(tmc51x0::ErrorCode::COMM_ERROR);
}
return tmc51x0::Result<void>();
}
// ACTIVE / INACTIVE to a GPIO level. DRV_ENN is active low: ACTIVE (enabled) drives it low.
tmc51x0::Result<void> GpioSet(tmc51x0::TMC51x0CtrlPin pin, tmc51x0::GpioSignal signal) noexcept {
const int gpio = GpioOf(pin);
const bool level = (signal == tmc51x0::GpioSignal::ACTIVE) == levels_.GetActiveLevel(pin);
if (gpio < 0 || !platform_gpio_write(gpio, level)) {
return tmc51x0::Result<void>(tmc51x0::ErrorCode::INVALID_VALUE);
}
return tmc51x0::Result<void>();
}
tmc51x0::Result<tmc51x0::GpioSignal> GpioRead(tmc51x0::TMC51x0CtrlPin pin) noexcept {
bool level = false;
const int gpio = GpioOf(pin);
if (gpio < 0 || !platform_gpio_read(gpio, level)) {
return tmc51x0::Result<tmc51x0::GpioSignal>(tmc51x0::ErrorCode::INVALID_VALUE);
}
return tmc51x0::Result<tmc51x0::GpioSignal>(level == levels_.GetActiveLevel(pin)
? tmc51x0::GpioSignal::ACTIVE
: tmc51x0::GpioSignal::INACTIVE);
}
void DelayMs(uint32_t ms) noexcept { platform_delay_us(ms * 1000U); }
void DelayUs(uint32_t us) noexcept { platform_delay_us(us); }
// Route driver logs to your logger (level 0 = error ... 4 = verbose), or leave it empty.
void DebugLog(int /*level*/, const char* /*tag*/, const char* /*format*/, va_list /*args*/) noexcept {}
private:
static int GpioOf(tmc51x0::TMC51x0CtrlPin pin) noexcept {
switch (pin) {
case tmc51x0::TMC51x0CtrlPin::EN: return 6; // DRV_ENN
case tmc51x0::TMC51x0CtrlPin::DIAG0: return 7; // optional: interrupt / stall output
case tmc51x0::TMC51x0CtrlPin::DIAG1: return 8; // optional: position compare output
default: return -1; // not wired on this board
}
}
tmc51x0::PinActiveLevels levels_{}; // defaults: DRV_ENN active low, the other pins active high
};
GpioSet() receives a state (ACTIVE / INACTIVE), not a level. PinActiveLevels maps it to a level:
DRV_ENN is active low, so ACTIVE (power stage on) drives the pin low. Porting explains each
method and shows the UART variant.
4. Move the motor¶
#include "tmc51x0.hpp"
int move_half_a_turn(MySpi& spi) {
tmc51x0::TMC51x0<MySpi> driver(spi);
tmc51x0::DriverConfig cfg{}; // 1. motor and board
cfg.motor_spec.rated_current_ma = 1500; // motor datasheet, RMS
cfg.motor_spec.sense_resistor_mohm = 50; // R_SENSE on your board
cfg.motor_spec.supply_voltage_mv = 24000; // VS
if (!driver.Initialize(cfg)) {
return -1; // wiring, power or SPI mode
}
driver.rampControl.SetMaxSpeed(60.0f, tmc51x0::Unit::RPM); // 2. ramp
driver.rampControl.SetAcceleration(2.0f, tmc51x0::Unit::RevPerSec); // rev/s²
driver.motorControl.Enable(); // 3. DRV_ENN low
driver.rampControl.SetTargetPosition(180.0f, tmc51x0::Unit::Deg); // 4. move
for (;;) { // 5. wait for position_reached
auto reached = driver.rampControl.IsTargetReached();
if (!reached) return -2;
if (reached.Value()) return 0;
}
}
What each step does:
Initialize()clears the reset flags, computesGLOBAL_SCALER,IRUNandIHOLDfrom the motor and board values, writes the chopper and StealthChop defaults, reads the chip version and selects positioning mode. Nothing else works until it returns success.SetMaxSpeed()andSetAcceleration()convert RPM and rev/s² to the chip'sVMAXandAMAX/DMAX.Enable()sets DRV_ENN active through your port: the bridges are now driven.SetTargetPosition()converts 180° to 25 600 microsteps (200 full steps × 256 / 2) and writesXTARGET; the ramp generator starts at once.IsTargetReached()readsposition_reachedfromRAMP_STAT. In real code, sleep between polls.
This example is built by the host CI, so it compiles against the current headers.
If it does not work¶
| Symptom | First thing to check |
|---|---|
Initialize() returns COMM_ERROR |
SPI mode 3, CS low for the whole 5-byte datagram, SPI_MODE strapped high |
Reads return 0x00000000 or 0xFFFFFFFF |
MISO wiring, a common ground, VCC_IO and VS present |
Motor does not move after Enable() |
DRV_ENN wiring and level, SD_MODE low, VS in range (Troubleshooting) |
| Motor vibrates or loses steps | rated_current_ma and sense_resistor_mohm against your hardware (Current and chopper) |
Next¶
- API guide: velocity mode, readings, StallGuard, homing, encoder, faults.
- Examples: complete ESP32 applications.
- Motion and units: the ramp generator and the unit conversions.