Skip to content

Porting

Run the driver on a new MCU by writing one class with one method, the SPI transfer, and optionally a delay.

The driver never calls your SDK directly. It calls transfer() on a class you derive from at25040::SpiInterface<Self> (in inc/at25040_spi_interface.hpp). The base class uses CRTP: the call is resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic AT25040 Device and its member functions, which use the CRTP SPI interface, which calls the one transfer method of the platform port you write, which drives the SPI bus and chip select of the AT25040B.Layer diagram: the application calls the hardware-agnostic AT25040 Device and its member functions, which use the CRTP SPI interface, which calls the one transfer method of the platform port you write, which drives the SPI bus and chip select of the AT25040B.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one at25040::Device object, whose public member functions (listed from inc/at25040.hpp) turn a call into AT25040 instructions: RDSR, WREN, READ and WRITE with A8 in the opcode. The communication interface (at25040::SpiInterface in inc/at25040_spi_interface.hpp) is a CRTP contract with one method. The platform port is the only code you write: a class whose transfer() runs one SPI transaction with chip select held low, and optionally delay_us() for the write-cycle wait. Below it is the hardware.

Drawn from: inc/at25040.hpp Device members; inc/at25040_spi_interface.hpp SpiInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

bool transfer(const uint8_t* tx, uint8_t* rx, std::size_t len, uint32_t timeout_ms);
Argument Contract
tx, len Send len bytes in one transaction, CS low for all of them
rx Receive len bytes: rx[i] is the byte clocked in while tx[i] went out
timeout_ms Give up after this long and return false
return true when the transaction completed, false on a bus error or timeout

The driver always passes rx of the same length as tx (at most 18 bytes) and timeout_ms = 20.

SPI port

my_spi.hpp
#include "at25040_spi_interface.hpp"

class MySpi : public at25040::SpiInterface<MySpi> {
public:
  // One transaction: CS low, `len` bytes full duplex (SPI mode 0 or 3, MSB first), CS high.
  // rx[i] is the byte clocked in while tx[i] went out. Return false only on a bus error or timeout.
  bool transfer(const uint8_t* tx, uint8_t* rx, std::size_t len, uint32_t timeout_ms) noexcept {
    return platform_spi_transfer(tx, rx, len, timeout_ms);
  }

  // Optional: sleep at least `us` microseconds. With it, WaitReady() (and every write) sleeps 100
  // us between status polls; without it, the driver polls back to back for a bounded count.
  void delay_us(uint32_t us) noexcept {
    platform_delay_us(us);
  }
};

Rules the transfer must follow:

  • SPI mode 0 or 3, MSB first, no faster than the chip allows at your VCC: 20 MHz at 4.5 V, 10 MHz at 2.5 V, 5 MHz at 1.8 V.
  • One transaction per call. CS goes low before the first byte and high after the last; never split a call into two CS periods, never merge two calls. The chip starts a write cycle on the CS rising edge after WRITE.
  • Full duplex. Fill rx with what came back on MISO; Probe() and Read() read their results from it.
  • Return false only for a bus failure. A missing chip is the driver's job: it shows up as 0xFF bytes.

Optional delay hook

void delay_us(uint32_t us);   // sleep at least us microseconds

The driver detects this member at compile time. With it, WaitReady() (and so every write) sleeps 100 µs between status polls; an RTOS port can yield there. Without it, the driver polls back to back for a bounded number of polls (kPollsPerMsWithoutDelay per millisecond of budget) that lasts at least the budget even at 20 MHz, so a write still waits for its write cycle, but keeps the bus busy while it does.

Chip select and shared buses

Drive CS from the SPI peripheral (ESP-IDF spics_io_num, STM32 hardware NSS) or toggle a GPIO around the transfer. On a shared bus, take the bus lock for the whole transfer() so another device cannot get between CS low and CS high. Keep tCSS, tCSH and tCS at 100 ns or more (200 ns at 1.8 V).

Reference implementation

The ESP32 example's port in at25040_esp32_example.cpp is a complete ESP-IDF spi_master port: one spi_transaction_t with length = len * 8, tx_buffer = tx and rx_buffer = rx, sent with spi_device_polling_transmit() on a device configured for mode 0 with CS on spics_io_num, and delay_us() on esp_rom_delay_us(). It does not use the ESP-IDF command or address phases: the driver puts the opcode (with A8 in bit 3) in tx[0] and the address byte in tx[1].

Check your port

  1. Build it: the port on this page is compiled by the host CI from tests/docs/, with the driver instantiated over it.
  2. Call Probe() and log LastRdsr(). A ready AT25040B with no protection reads 0x00; 0xFF means nothing answered.
  3. Write one page (WritePage() waits for the write cycle), read it back.
  4. Read across 0x0FF/0x100 in two calls to prove A8 reaches the chip: the two halves must hold different data.

Threads

The driver is not thread-safe. Give each EEPROM one owner task, or guard every call on a driver object (and its port) with one mutex. On a shared bus, the bus lock in transfer() is not enough for a write followed by a status poll; hold the mutex for the whole sequence.