Skip to content

Quickstart

Spin a BLDC motor from your MCU: wire six signals, write a small port class, call five driver functions.

Before you power the motor

Use a current-limited supply for the first run and keep the motor free to turn. The example commands 1000 RPM as soon as the chip is up.

1. Wire it

MCU TMC9660 Notes
SPI SCLK, MOSI, MISO, CS SPI0_SCK, SPI0_MOSI, SPI0_MISO, SPI0_CS0 SPI mode 3, up to 10 MHz; start at 1 MHz
GPIO out RST Reset; the driver pulses it in bootloaderInit()
GPIO out DRV_EN Enables the gate driver outputs
GPIO out WAKE Wakes the chip from hibernation
GPIO in FAULTN Open drain: pull it up
GND GND Common ground between the MCU and the motor supply

The Hardware page has the full pin list, the evaluation-kit headers and the electrical limits.

2. Add the driver

Add the repository as a submodule and link the hf_tmc9660 CMake target (or include inc/ and build with C++20). Installation covers CMake and ESP-IDF.

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

3. Write the port

The port is the only platform code. It moves bytes over SPI, sets and reads the four control pins and waits. Replace the four platform_* calls with your SDK's.

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

The constructor arguments are the physical level of RST, DRV_EN, WAKE and FAULTN when each is active. Get them from your schematic: inverters and level shifters change them. Porting explains each method and shows the UART variant.

4. Spin the motor

main.cpp
#include "tmc9660.hpp"

int spin_bldc_at_1000_rpm(MySpi& spi) {
  tmc9660::TMC9660<MySpi> driver(spi);

  tmc9660::BootloaderConfig cfg{};                          // 1. boot into parameter mode
  cfg.boot.boot_mode = tmc9660::bootcfg::BootMode::Parameter;
  cfg.boot.start_motor_control = true;
  if (driver.bootloaderInit(&cfg) != tmc9660::TMC9660<MySpi>::BootloaderInitResult::Success) {
    return -1;                                              // wiring, power or SPI mode
  }

  driver.motorConfig.setType(tmc9660::tmcl::MotorType::BLDC_MOTOR, 7);  // 2. motor: 7 pole pairs
  driver.motorConfig.setPWMFrequency(25000);

  const auto ctx = driver.getMotorContext();                // 3. unit conversions from the chip
  driver.velocityControl.setTargetVelocityRpm(1000.0, ctx); // 4. spin
  return 0;
}

What each step does:

  1. bootloaderInit() resets the chip, sends the bootloader configuration and starts the motor-control firmware in parameter mode. Nothing else works until it returns Success.
  2. setType() and setPWMFrequency() tell the chip what it drives. Use the pole-pair count from the motor's datasheet.
  3. getMotorContext() reads the settings the unit conversions need (pole pairs, sensor, scaling) from the chip.
  4. setTargetVelocityRpm() converts RPM to the chip's internal unit and writes the target velocity.

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
bootloaderInit() returns Failure SPI mode 3, CS held low for the whole 8-byte frame, RST active level
Every reply is 0x00 or 0xFF MISO wiring, a common ground, the chip's supply
Motor hums but does not turn Pole pairs, phase wiring, commutation mode and feedback (API guide)
FAULTN stays active Supply under-voltage, gate-driver fault (Troubleshooting)

Next

  • API guide: configure feedback and control loops, read telemetry, handle errors.
  • Examples: complete ESP32 test applications for BLDC, stepper, DC and telemetry.
  • Bootloader: every boot option, including encoders and step/dir.