Skip to content

Porting

Run the driver on a new MCU by writing one class: the communication interface for SPI or UART.

The driver never calls your SDK directly. It calls a few methods on a class you derive from tmc51x0::SpiCommInterface<Self> or tmc51x0::UartCommInterface<Self> (in inc/tmc51x0_comm_interface.hpp). The base classes use CRTP: the calls are resolved at compile time, so there are no virtual functions and no vtable.

Layer diagram: the application calls the hardware-agnostic TMC51x0 driver and its subsystems, which use the SPI or UART communication interface, which calls the platform port you write, which drives the SPI bus or single-wire UART and the DRV_ENN pin of the TMC5160 or TMC5130.Layer diagram: the application calls the hardware-agnostic TMC51x0 driver and its subsystems, which use the SPI or UART communication interface, which calls the platform port you write, which drives the SPI bus or single-wire UART and the DRV_ENN pin of the TMC5160 or TMC5130.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one TMC51x0 object per chip, whose public members are the chip's subsystems (listed from inc/tmc51x0.hpp). Each subsystem turns a call in physical units into register reads and writes. The communication interface (SpiCommInterface or UartCommInterface in inc/tmc51x0_comm_interface.hpp) builds the 40-bit SPI datagrams or CRC-checked UART frames, handles daisy-chain padding and node addresses. The platform port is the only code you write: the methods the interface calls on it, read from the header. Below it is the TMC5160 or TMC5130.

Drawn from: inc/tmc51x0.hpp TMC51x0 members; inc/tmc51x0_comm_interface.hpp SpiCommInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method SPI UART Contract
GetMode() yes yes Return CommMode::SPI or CommMode::UART
SpiTransfer(tx, rx, length) yes One full-duplex transfer of length bytes with CSN low throughout. length is a multiple of 5
UartSend(data, length) yes Send one frame (8 bytes for a write, 4 for a read request) and remove the echo
UartReceive(data, length) yes Return success only when all length bytes (an 8-byte reply) arrived
SetNaiPin(active), GetNaoPin() yes Drive NAI / read NAO for sequential addressing; return UNSUPPORTED when not wired
GpioSet(pin, signal) yes yes Drive a control pin (at least EN, DRV_ENN) to ACTIVE / INACTIVE
GpioRead(pin) yes yes Read a pin as ACTIVE / INACTIVE; return UNSUPPORTED / INVALID_VALUE for pins you do not have
DelayMs(ms), DelayUs(us) yes yes Block for at least that long
DebugLog(level, tag, format, va_list) yes yes Print one line (0 = error ... 4 = verbose), or do nothing
SetClkFreq(hz) optional optional Drive the CLK pin: 0 = tie low for the internal clock. Default: UNSUPPORTED
SetPowerEnabled(on) optional optional Switch the chip's supply, for HardReset() by power cycle. Default: UNSUPPORTED

All methods are noexcept and return tmc51x0::Result<...> where the table shows a result.

SPI port

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
};

Rules the transfer must follow:

  • SPI mode 3, MSB first, up to 4 MHz on the chip's internal clock (8 MHz with a 16 MHz external clock).
  • One transaction per call. CSN goes low before the first byte and high after the last. In a daisy chain the buffer covers every chip; never split it.
  • Full duplex, same length. Fill rx with exactly length bytes.
  • Return an error only for a bus failure. Status bits and register contents are the driver's job.
  • Serialise access. The interface uses per-object scratch buffers: one transfer at a time per port object.

UART port

my_uart.hpp
class MyUart : public tmc51x0::UartCommInterface<MyUart> {
public:
  tmc51x0::CommMode GetMode() const noexcept { return tmc51x0::CommMode::UART; }

  // Send one datagram (8 bytes for a write, 4 for a read request). On a single-wire bus the
  // transmitted bytes echo back: discard them here, or read them away in UartReceive().
  tmc51x0::Result<void> UartSend(const uint8_t* data, size_t length) noexcept {
    return platform_uart_write(data, length) ? tmc51x0::Result<void>()
                                             : tmc51x0::Result<void>(tmc51x0::ErrorCode::COMM_ERROR);
  }

  // Block until exactly `length` bytes (an 8-byte reply) arrived, or fail.
  tmc51x0::Result<void> UartReceive(uint8_t* data, size_t length) noexcept {
    return platform_uart_read(data, length, 10) ? tmc51x0::Result<void>()
                                                : tmc51x0::Result<void>(tmc51x0::ErrorCode::TIMEOUT);
  }

  // NAI (SDI_CFG1) selects which chip answers at address 0 while addresses are programmed.
  tmc51x0::Result<void> SetNaiPin(bool active) noexcept {
    return platform_gpio_write(kNaiGpio, active) ? tmc51x0::Result<void>()
                                                 : tmc51x0::Result<void>(tmc51x0::ErrorCode::COMM_ERROR);
  }

  tmc51x0::Result<bool> GetNaoPin() noexcept {
    return tmc51x0::Result<bool>(tmc51x0::ErrorCode::UNSUPPORTED);  // NAO not wired back on this board
  }

  tmc51x0::Result<void> GpioSet(tmc51x0::TMC51x0CtrlPin pin, tmc51x0::GpioSignal signal) noexcept {
    if (pin != tmc51x0::TMC51x0CtrlPin::EN) {
      return tmc51x0::Result<void>(tmc51x0::ErrorCode::UNSUPPORTED);
    }
    const bool enabled = signal == tmc51x0::GpioSignal::ACTIVE;
    return platform_gpio_write(kDrvEnnGpio, !enabled) ? tmc51x0::Result<void>()  // DRV_ENN: low = enabled
                                                      : tmc51x0::Result<void>(tmc51x0::ErrorCode::COMM_ERROR);
  }

  tmc51x0::Result<tmc51x0::GpioSignal> GpioRead(tmc51x0::TMC51x0CtrlPin /*pin*/) noexcept {
    return tmc51x0::Result<tmc51x0::GpioSignal>(tmc51x0::ErrorCode::UNSUPPORTED);
  }

  void DelayMs(uint32_t ms) noexcept { platform_delay_us(ms * 1000U); }
  void DelayUs(uint32_t us) noexcept { platform_delay_us(us); }
  void DebugLog(int /*level*/, const char* /*tag*/, const char* /*format*/, va_list /*args*/) noexcept {}

private:
  static constexpr int kNaiGpio = 9;
  static constexpr int kDrvEnnGpio = 6;
};

On a single wire the MCU's TX and RX are joined, so every frame you send comes back on RX. The driver expects UartReceive() to return the chip's reply only: flush the receive buffer after sending (as the ESP32 port does), or read and discard length echo bytes. Choose a baud rate between 9000 and fCLK/16; the chip detects it from each frame's sync nibble. Allow for the reply delay (SENDDELAY, 8 bit times by default) in the receive timeout.

Control pins

GpioSet() and GpioRead() receive a pin name (TMC51x0CtrlPin) and a state (GpioSignal). Your port turns a state into a level. tmc51x0::PinActiveLevels holds the datasheet levels; GetActiveLevel(pin) returns the level of the active state:

Pin Datasheet active level Meaning of ACTIVE
EN (DRV_ENN) low Power stage enabled
REFL_STEP, REFR_DIR as switches low (configurable in SW_MODE) Switch pressed
STEP, DIR, DCEN, DCIN high
DIAG0, DIAG1 high (push-pull default) Event signalled
SPI_MODE, SD_MODE high SPI interface, external STEP/DIR

The driver drives EN in Enable() / Disable(), and SPI_MODE / SD_MODE only in io.SetOperatingMode() / GetOperatingMode(), for boards that wire the straps to GPIOs. The other pins exist for applications that want them; a port may return UNSUPPORTED for pins it does not wire. Change a level with PinActiveLevels::SetActiveLevel() when your board inverts a signal.

Reference implementation

The ESP32 examples use esp32_tmc51x0_bus.hpp: Esp32SPI on the ESP-IDF spi_master driver with an optional mutex, and Esp32UART on the UART driver, with FreeRTOS delays, ESP_LOG output, pin mapping through TMC51x0PinConfig and CLK control. Start from it for any ESP-IDF target.

Check your port

  1. Build it: both ports on this page are compiled by the host CI from tests/docs/, with the driver instantiated over them.
  2. Read IOIN (io.ReadIcVersion()): 0x30 for a TMC5160, 0x11 for a TMC5130. 0x00 or 0xFF means the bus does not work.
  3. Run Initialize() and status.VerifySetup(): it reads back the configuration and logs mismatches.
  4. Call motorControl.Enable() and check io.ReadInputStatus().Value().drv_enn is false (pin low).

Threads

The driver is not thread-safe. Give each chip one owner task, or guard every call on a driver object (and its port) with one mutex. Chips on a shared bus share the port, so they share the mutex.

Next: Examples.