Skip to content

Quickstart

Regulate a solenoid current from your MCU: wire the SPI bus and three control pins, write a small port class, call five driver functions.

Before you energise a coil

Use a current-limited VBAT supply for the first run and start with a low setpoint. Every inductive load needs a freewheeling diode from FWDx to its supply (see Hardware). The example drives 500 mA into CH0.

1. Wire it

MCU TLE92466ED (pin) Notes
SPI SCLK, MOSI, MISO SCK (6), SI (7), SO (9) Mode 1, 1 MHz to start
SPI CS CSN (5) One pulse per 32-bit frame
GPIO out RESN (26) Active low reset
GPIO out EN (27) Active high enable
GPIO in, pull-up FAULTN (11) Open drain, active low
3.3 V or 5 V VIO (8) The MCU's I/O rail
5 V VDD (29) 4.5 V to 5.5 V
GND GND, GNDPx Common ground

VBAT (pin 22) is the load supply, 6 V to 18 V. The Hardware page has every pin, the load connection and the timing.

2. Add the driver

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

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

3. Write the port

The port is the only platform code. It moves 32-bit frames over SPI, sets RESN and EN, reads FAULTN and waits. Replace the platform_* calls with your SDK's.

my_spi.hpp
class MySpi : public tle92466ed::SpiInterface<MySpi> {
public:
  template <typename T>
  using Result = tle92466ed::CommResult<T>;
  using Error = tle92466ed::CommError;
  using tle92466ed::SpiInterface<MySpi>::Log;  // keep the driver's printf-style Log() visible

  // SPI peripheral and the three GPIOs. The driver calls this first, from Init().
  Result<void> Init() noexcept {
    ready_ = platform_spi_init();
    return ready_ ? Result<void>{} : tle::unexpected(Error::HardwareNotReady);
  }
  Result<void> Deinit() noexcept {
    ready_ = false;
    return {};
  }

  // One 32-bit frame, MSB first, CSN low for exactly these four bytes.
  Result<uint32_t> Transfer32(uint32_t tx) noexcept {
    const uint8_t out[4] = {uint8_t(tx >> 24), uint8_t(tx >> 16), uint8_t(tx >> 8), uint8_t(tx)};
    uint8_t in[4] = {};
    if (!platform_spi_transfer(out, in, sizeof out)) {
      return tle::unexpected(Error::TransferError);
    }
    return (uint32_t(in[0]) << 24) | (uint32_t(in[1]) << 16) | (uint32_t(in[2]) << 8) | in[3];
  }

  // Several frames back to back: a CSN pulse per frame, the bus held for the whole sequence.
  Result<void> TransferMulti(std::span<const uint32_t> tx, std::span<uint32_t> rx) noexcept {
    if (tx.size() != rx.size()) {
      return tle::unexpected(Error::InvalidParameter);
    }
    platform_spi_lock();
    for (std::size_t i = 0; i < tx.size(); ++i) {
      auto r = Transfer32(tx[i]);
      if (!r) {
        platform_spi_unlock();
        return tle::unexpected(r.error());
      }
      rx[i] = *r;
    }
    platform_spi_unlock();
    return {};
  }

  // RESN and FAULTN are active low, EN is active high (datasheet pin table).
  Result<void> GpioSet(tle92466ed::CtrlPin pin, tle92466ed::GpioSignal s) noexcept {
    const bool active = (s == tle92466ed::GpioSignal::ACTIVE);
    const bool ok = (pin == tle92466ed::CtrlPin::RESN) ? platform_gpio_write(kResn, !active)
                    : (pin == tle92466ed::CtrlPin::EN) ? platform_gpio_write(kEn, active)
                                                       : false;  // FAULTN is an input
    return ok ? Result<void>{} : tle::unexpected(Error::InvalidParameter);
  }
  Result<tle92466ed::GpioSignal> GpioRead(tle92466ed::CtrlPin pin) noexcept {
    if (pin != tle92466ed::CtrlPin::FAULTN) {
      return tle::unexpected(Error::InvalidParameter);
    }
    bool level = true;
    if (!platform_gpio_read(kFaultn, level)) {
      return tle::unexpected(Error::BusError);
    }
    return level ? tle92466ed::GpioSignal::INACTIVE : tle92466ed::GpioSignal::ACTIVE;
  }

  Result<void> Delay(uint32_t us) noexcept {
    platform_delay_us(us);
    return {};
  }
  Result<void> Configure(const tle92466ed::SPIConfig& /*config*/) noexcept {
    return {};  // fixed bus settings in this port
  }
  bool IsReady() const noexcept {
    return ready_;
  }
  Error GetLastError() const noexcept {
    return Error::None;
  }
  Result<void> ClearErrors() noexcept {
    return {};
  }
  void Log(tle92466ed::LogLevel /*lvl*/, const char* tag, const char* fmt, va_list args) noexcept {
    std::printf("[%s] ", tag);
    std::vprintf(fmt, args);
    std::printf("\n");
  }

private:
  static constexpr int kResn = 6;    // your board's GPIO numbers
  static constexpr int kEn = 5;
  static constexpr int kFaultn = 16;
  bool ready_{false};
};

Porting explains each method, the frame rules and how to check a port.

4. Drive a channel

main.cpp
#include "tle92466ed.hpp"

int drive_ch0_at_500_ma(MySpi& spi) {
  using tle92466ed::Channel;
  tle92466ed::Driver<MySpi> driver(spi);

  if (!driver.Init()) {                                       // 1. reset, ICVID, Config Mode
    return -1;                                                //    VDD, wiring, SPI mode 1
  }
  if (!driver.ConfigurePwmPeriod(Channel::CH0, 1000.0F) ||    // 2. 1 kHz PWM,
      !driver.ConfigureDither(Channel::CH0, 50.0F, 100.0F)) { //    50 mA dither at 100 Hz
    return -2;
  }
  if (!driver.EnterMissionMode() ||                           // 3. outputs can switch
      !driver.SetCurrentSetpoint(Channel::CH0, 500) ||        // 4. regulate 500 mA
      !driver.EnableChannel(Channel::CH0, true)) {
    return -3;
  }
  return 0;
}

What each step does:

  1. Init() pulses RESN, asserts EN, reads ICVID (expects 0xC1xx), writes a default configuration and leaves the chip in Config Mode. Nothing else works until it succeeds.
  2. ConfigurePwmPeriod() sets the channel's PWM target (110 Hz to 4 kHz). ConfigureDither() sets the dither and starts the channel's measurement clock, without which current feedback never updates.
  3. EnterMissionMode() sets OP_MODE: channels can be switched on only in Mission Mode.
  4. SetCurrentSetpoint() writes the 15-bit target for 500 mA; EnableChannel() sets EN_CH0 and the current controller starts regulating.

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
Init() returns DeviceNotResponding or HardwareError MISO wiring, SPI mode 1, CSN per 32-bit frame, VIO present
Init() returns FaultDetected Critical fault frames: VDD (5 V) missing or out of range, clock fault
Init() returns WrongDeviceID Another device on the bus, or bits shifted by a wrong SPI mode
EnableChannel() returns WrongMode Call EnterMissionMode() first
Current reads FeedbackNotReady Dither clock not configured (ConfigureDither() or ConfigureDitherClock())

Troubleshooting has the full list.

Next

  • API guide: configure channels, read feedback, handle faults.
  • Current control: setpoint scaling, PWM, dither and the feedback window.
  • Examples: ESP32-S3 applications, from a no-hardware API test to a proportional-valve cycle.