Porting¶
Run the driver on a new MCU by writing one class: the SPI interface.
The driver never calls your SDK directly. It calls twelve methods on a class you derive from
tle92466ed::SpiInterface<Self> (in inc/tle92466ed_spi_interface.hpp).
The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and
nothing to allocate. Frame building, the CRC, the pipeline and reply validation are in the base class; your port
only moves words and toggles pins.
Figure description and sources
From top to bottom. Your application talks to one Driver
Drawn from: inc/tle92466ed_registers.hpp CentralReg; inc/tle92466ed_registers.hpp ChannelReg; inc/tle92466ed_spi_interface.hpp SpiInterface; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Contract |
|---|---|
Init() |
Bring up the SPI peripheral and the GPIOs. Called first, from Driver::Init() |
Deinit() |
Release them |
Transfer32(tx) |
One full-duplex 32-bit frame, MSB first, CSN low for exactly 32 clocks. Return the received word |
TransferMulti(tx, rx) |
tx.size() frames in order, one CSN pulse each, without another device's traffic in between |
GpioSet(pin, signal) |
Drive RESN or EN to ACTIVE / INACTIVE |
GpioRead(pin) |
Read FAULTN as ACTIVE (low) / INACTIVE |
Delay(us) |
Block for at least that many microseconds (the driver waits up to 50 ms at a time) |
Configure(config) |
Apply an SPIConfig (frequency, mode) if your port supports changing it; otherwise succeed |
IsReady() |
true after a successful Init() |
GetLastError(), ClearErrors() |
Your port's sticky error, if it keeps one |
Log(level, tag, fmt, va_list) |
Print one line; Error ... Verbose. Add using SpiInterface<Self>::Log; |
Each returns a tle92466ed::CommResult<T>; return tle::unexpected(CommError::...) on failure. The C++20 concept
SpiInterfaceLike checks the core signatures at compile time.
Keep the base Log() visible
The driver calls the base class's printf-style Log(level, tag, fmt, ...). Declaring your own
Log(..., va_list) hides it unless you add using tle92466ed::SpiInterface<MySpi>::Log;, and the driver then
fails to compile.
The port¶
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};
};
Transfer rules¶
- SPI mode 1, MSB first, at most 8 MHz. Start at 1 MHz.
- Exactly 32 bits per CSN low period. The chip ignores a frame of any other length. A 4-byte transfer is fine; a 3- or 5-byte one is not.
- A CSN pulse per frame in
TransferMulti(), with CSN high for at least 600 ns between frames (tCSN_TD). Most SPI drivers that toggle CS per transaction meet this at 1 MHz; check it at 8 MHz. - No other device between the frames of one call. The reply to the command frame comes out during the next
frame; if another device's transaction runs in between, the reply is lost. On a shared bus take the bus lock
for the whole
TransferMulti()call (spi_device_acquire_bus()on ESP-IDF, a mutex elsewhere). - Bit 31 first.
Transfer32()takes and returns the frame as auint32_t; put bit 31 on the wire first. On a little-endian MCU with a byte-buffer SPI API that means swapping the bytes, as the port above does. - Return an error only for a bus failure. CRC, status and reply-width problems are the base class's job.
Control-pin levels¶
| Pin | ACTIVE level on the chip |
Port mapping on a direct connection |
|---|---|---|
| RESN | Low (reset) | ACTIVE writes 0 |
| EN | High (outputs enabled) | ACTIVE writes 1 |
| FAULTN | Low (fault) | Pin reads 0: return ACTIVE |
Invert the mapping in your port when a buffer or level shifter inverts a signal; the driver code stays the same. FAULTN is open drain: enable a pull-up on the MCU or fit one to VIO.
Reference implementation¶
The ESP32 examples use
esp32_tle92466ed_bus.hpp: an ESP-IDF spi_master port with
one spi_device_transmit() per frame, GPIO control for RESN and EN, and ESP_LOG output. TransferMulti() holds
the bus with spi_device_acquire_bus() for the whole chain, so it is safe on a bus shared with other devices. Start
from it for any ESP-IDF target.
Check your port¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Call
Init().DeviceNotRespondingmeans MISO stayed idle.WrongDeviceIDwith anICVIDsuch as0x82xxor0x60xx(0xC1shifted by one bit) means the sampling edge is wrong: check the SPI mode. - Read
ReadAllSupplyVoltages(): a plausible VBAT and VDD prove 22-bit feedback reads end to end. - Enable the SPI watchdog with a short
WD_TIMEand stop reloading it:SPI_WD_ERRmust appear inGetAllFaults(), which proves writes land.
Threads¶
The driver is not thread-safe. Give each TLE92466ED 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.
Next: Examples.