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.
Figure description and sources
From top to bottom. Your application talks to one TMC9660
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¶
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
falseonly for a bus failure. Protocol errors (bad checksum, NOT_READY) are the driver's job.
UART port¶
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¶
- Build it: both ports on this page are compiled by the host CI from
tests/docs/, with the driver instantiated over them. - Run
bootloaderInit()withretrieve_bootloader_info = true: it reads and logs the bootloader version over your port before any motor configuration. - Read
telemetry.getSupplyVoltage(): a plausible VS proves TMCL parameter access end to end. - Watch
comm.tmclStats():failuresandstaleRepliesshould 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.