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 six methods on a class you derive from tmc9660::SpiCommInterface<Self> or tmc9660::UartCommInterface<Self> (in inc/tmc9660_comm_interface.hpp). The base classes use CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic TMC9660 driver and its subsystems, which use the SPI or UART communication interface, which calls the platform port you write, which drives the SPI bus and four control pins of the TMC9660.Layer diagram: the application calls the hardware-agnostic TMC9660 driver and its subsystems, which use the SPI or UART communication interface, which calls the platform port you write, which drives the SPI bus and four control pins of the TMC9660.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one TMC9660 object, whose public members are the chip's subsystems (listed from inc/tmc9660.hpp). Each subsystem turns a call into TMCL parameter reads and writes. The communication interface (SpiCommInterface or UartCommInterface in inc/tmc9660_comm_interface.hpp) frames them, polls NOT_READY, pipelines batches and drives the control pins by name. The platform port is the only code you write: a class that moves bytes over SPI or UART, sets and reads four GPIOs and waits. Below it is the hardware.

Drawn from: inc/tmc9660.hpp TMC9660 members; inc/tmc9660_comm_interface.hpp SpiCommInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method SPI UART Contract
spiTransferTMCL(tx[8], rx[8]) yes One full-duplex 64-bit transfer, CS low for all 8 bytes. Return false on a bus error
spiTransferBootloader(tx[5], rx[5]) yes The same for 40-bit bootloader datagrams
uartSendTMCL(data[9]) yes Send one 9-byte TMCL frame
uartReceiveTMCL(data[9]) yes Return true only when all 9 reply bytes are in data
uartTransferBootloader(tx[8], rx[8]) yes Send 8 bytes, receive the 8-byte reply
gpioSet(pin, signal) yes yes Drive RST, DRV_EN or WAKE to ACTIVE / INACTIVE
gpioRead(pin, signal) yes yes Read FAULTN (and any other pin) as ACTIVE / INACTIVE
delayMs(ms), delayUs(us) yes yes Block for at least that long
debugLog(level, tag, fmt, va_list) optional optional Print one line; 0 = error ... 4 = verbose

SPI port

my_spi.hpp
class MySpi : public tmc9660::SpiCommInterface<MySpi> {
public:
  // Physical level of each control pin when ACTIVE: RST, DRV_EN, WAKE, FAULTN.
  MySpi() noexcept : SpiCommInterface<MySpi>(true, true, false, false) {}

  // One 64-bit TMCL datagram, full duplex, SPI mode 3, CS held low for all 8 bytes.
  bool spiTransferTMCL(std::array<uint8_t, 8>& tx, std::array<uint8_t, 8>& rx) noexcept {
    return platform_spi_transfer(tx.data(), rx.data(), tx.size());
  }

  // One 40-bit bootloader datagram (used by bootloaderInit() before parameter mode).
  bool spiTransferBootloader(std::array<uint8_t, 5>& tx, std::array<uint8_t, 5>& rx) noexcept {
    return platform_spi_transfer(tx.data(), rx.data(), tx.size());
  }

  // ACTIVE / INACTIVE to a GPIO level, using the active levels given to the constructor.
  bool gpioSet(tmc9660::TMC9660CtrlPin pin, tmc9660::GpioSignal signal) noexcept {
    return platform_gpio_write(gpioOf(pin), signalToGpioLevel(pin, signal));
  }

  bool gpioRead(tmc9660::TMC9660CtrlPin pin, tmc9660::GpioSignal& signal) noexcept {
    bool level = false;
    if (!platform_gpio_read(gpioOf(pin), level)) {
      return false;
    }
    signal = gpioLevelToSignal(pin, level);
    return true;
  }

  void delayMs(uint32_t ms) noexcept { platform_delay_us(ms * 1000U); }
  void delayUs(uint32_t us) noexcept { platform_delay_us(us); }

  // Optional: route driver logs to your logger (level 0 = error ... 4 = verbose).
  void debugLog(int /*level*/, const char* /*tag*/, const char* /*format*/, va_list /*args*/) noexcept {}

private:
  static int gpioOf(tmc9660::TMC9660CtrlPin pin) noexcept {
    switch (pin) {
    case tmc9660::TMC9660CtrlPin::RST: return 22;
    case tmc9660::TMC9660CtrlPin::DRV_EN: return 20;
    case tmc9660::TMC9660CtrlPin::FAULTN: return 19;
    case tmc9660::TMC9660CtrlPin::WAKE: return 21;
    }
    return -1;
  }
};

Rules the transfer must follow:

  • SPI mode 3, MSB first, up to 10 MHz.
  • One transaction per call. CS goes low before the first byte and high after the last; never split a datagram, never merge two.
  • No delays of your own. The driver polls the chip's NOT_READY status and backs off itself. A fixed delay in spiTransferTMCL() only makes every access slower. Timing and throughput shows the numbers.
  • Return false only for a bus failure. Protocol errors (bad checksum, NOT_READY) are the driver's job.

UART port

my_uart.hpp
class MyUart : public tmc9660::UartCommInterface<MyUart> {
public:
  MyUart() noexcept : UartCommInterface<MyUart>(true, true, false, false) {}

  bool uartSendTMCL(const std::array<uint8_t, 9>& data) noexcept {
    return platform_uart_write(data.data(), data.size());
  }

  // Return true only once all 9 reply bytes are in `data`.
  bool uartReceiveTMCL(std::array<uint8_t, 9>& data) noexcept {
    return platform_uart_read_exact(data.data(), data.size(), 20);
  }

  bool uartTransferBootloader(const std::array<uint8_t, 8>& tx, std::array<uint8_t, 8>& rx) noexcept {
    platform_uart_flush_rx();
    return platform_uart_write(tx.data(), tx.size()) && platform_uart_read_exact(rx.data(), rx.size(), 20);
  }

  bool gpioSet(tmc9660::TMC9660CtrlPin pin, tmc9660::GpioSignal signal) noexcept {
    return platform_gpio_write(static_cast<int>(pin), signalToGpioLevel(pin, signal));
  }

  bool gpioRead(tmc9660::TMC9660CtrlPin pin, tmc9660::GpioSignal& signal) noexcept {
    bool level = false;
    if (!platform_gpio_read(static_cast<int>(pin), level)) {
      return false;
    }
    signal = gpioLevelToSignal(pin, level);
    return true;
  }

  void delayMs(uint32_t ms) noexcept { platform_delay_us(ms * 1000U); }
  void delayUs(uint32_t us) noexcept { platform_delay_us(us); }
  void debugLog(int, const char*, const char*, va_list) noexcept {}
};

uartReceiveTMCL() must block until it has exactly nine bytes, or fail. Returning early hands the driver a partial frame: wrong status codes, checksum errors and faults that come and go. Flush stale bytes from the receive FIFO before a bootloader exchange.

Control-pin levels

The constructor takes the physical level of RST, DRV_EN, WAKE and FAULTN when each is active. The helpers signalToGpioLevel() and gpioLevelToSignal() apply them, so gpioSet() and gpioRead() stay one line each.

Board RST DRV_EN WAKE FAULTN Constructor
ESP32 examples, TMC9660 evaluation kits high high low low (true, true, false, false)
A board with an inverting RST buffer low high low low (false, true, false, false)

Change a level at run time with set_pin_active_level(pin, level).

Reference implementation

The ESP32 examples use esp32_tmc9660_bus.hpp: an ESP-IDF spi_master port and a UART port, with FreeRTOS delays and ESP_LOG output. 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. Run bootloaderInit() with retrieve_bootloader_info = true: it reads and logs the bootloader version over your port before any motor configuration.
  3. Read telemetry.getSupplyVoltage(): a plausible VS proves TMCL parameter access end to end.
  4. Watch comm.tmclStats(): failures and staleReplies should stay at zero on a healthy link.

Threads

The driver is not thread-safe. Give each TMC9660 one owner task, or guard every call on a driver object (and its port) with one mutex. Several chips on separate ports can run in parallel.