Skip to content

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.

git submodule add https://github.com/hardfoc/hf-tmc5160-driver.git external/hf-tmc5160-driver

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.

my_spi.hpp
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

main.cpp
#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:

  1. Initialize() clears the reset flags, computes GLOBAL_SCALER, IRUN and IHOLD from 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.
  2. SetMaxSpeed() and SetAcceleration() convert RPM and rev/s² to the chip's VMAX and AMAX / DMAX.
  3. Enable() sets DRV_ENN active through your port: the bridges are now driven.
  4. SetTargetPosition() converts 180° to 25 600 microsteps (200 full steps × 256 / 2) and writes XTARGET; the ramp generator starts at once.
  5. IsTargetReached() reads position_reached from RAMP_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.