Skip to content

API guide

How to do the common jobs with the driver: detect the chip, wait, read, write, protect and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Eeprom standing for at25040::Device<MySpi>. The API reference lists every member.

The shape of the API

One at25040::Device<Spi> object per chip, constructed from a reference to your port. It is not copyable and holds no buffers beyond the stack frame of each call.

Call What it does
Probe() Two RDSR: decides whether a chip is fitted
ReadStatus(sr) One RDSR: write-cycle polling, protection level
Read(addr, dst, len) READ: any range inside the array, in transactions of up to 16 data bytes
WritePage(addr, src, len) WREN + WRITE: 1 to 8 bytes in one page, then waits for the write cycle
Write(addr, src, len) Any range inside the array: one WritePage() per 8-byte page
WaitReady(timeout_ms) RDSR until RDY clears: the end of a write cycle (default 10 ms budget)
WriteEnable(), WriteDisable() One WREN or WRDI: set or clear the write-enable latch
SetBlockProtection(level) WREN + WRSR: BP1:BP0, then waits for the write cycle
GetBlockProtection(level) One RDSR, decoded into at25040::BlockProtect
LastRdsr() No transfer: the last status byte read, for logging

Every call except LastRdsr() returns at25040::Status and never throws. Check it.

During a write cycle the chip ignores every instruction except RDSR. Every call that sends one (Read(), the writes, WriteEnable(), WriteDisable(), SetBlockProtection()) therefore reads the status register first and returns Status::Busy, sending nothing else, when RDY is 1. The driver's own writes wait for their write cycle, so you see Busy only when something else started one: firmware reset in the middle of a write, or an instruction you sent through your port.

Detect the chip

bool attach(Eeprom& eeprom) {
  switch (eeprom.Probe()) {
  case at25040::Status::Ok:
    return true;
  case at25040::Status::Busy:   // a write cycle is still running: try again in 5 ms
  case at25040::Status::Absent: // no chip, floating MISO, MISO echoing MOSI, or a write cycle
                                // (0xFF)
  default:
    std::printf("no AT25040 (RDSR 0x%02X)\n", eeprom.LastRdsr());
    return false;
  }
}

Call Probe() from an explicit attach or start-up step, not from a static constructor: the SPI bus may not be up yet. A write cycle in progress also reads as Absent (every status bit is 1 while busy), so probe before you write, or wait 5 ms after the last write. Protocol lists every decision.

Wait for a write cycle

After a WRITE or WRSR the chip runs an internal write cycle of up to 5 ms (tWC) and ignores everything except RDSR until it is done. WritePage() and Write() wait for it before they return, so the next call is never ignored. WaitReady() is the same wait on its own: use it after an instruction you sent through your port, or at start-up, when the firmware may have been reset in the middle of a write:

// After an MCU reset the chip may still be finishing a write the old firmware started. WaitReady()
// polls RDSR until RDY clears (default budget 10 ms, twice the 5 ms tWC).
bool start_up(Eeprom& eeprom) {
  return eeprom.WaitReady() == at25040::Status::Ok && eeprom.Probe() == at25040::Status::Ok;
}

WaitReady() polls RDSR until RDY (bit 0) reads 0 and returns Timeout when the budget runs out (default kWriteCycleTimeoutMs = 10 ms, twice tWC). If your port has the optional delay_us() hook (Porting), the driver sleeps 100 µs between polls; without it, it polls back to back for a bounded count that lasts at least the budget even at 20 MHz. A floating MISO reads 0xFF, which looks like a write cycle that never ends: it also returns Timeout.

Read

// Read() streams any range inside the array; the driver splits it into READ transactions.
at25040::Status read_settings(Eeprom& eeprom, uint8_t (&settings)[64]) {
  return eeprom.Read(0x100, settings, sizeof(settings)); // 0x100-0x13F, A8 set by the driver
}

Read() returns all len bytes. The chip increments the address after every byte, so one READ could stream the whole array; the driver sends one READ per 16 data bytes (Device::kReadChunkBytes) so that a port never sees more than 18 bytes, and sets A8 in each opcode for addresses 0x100 and up. A range that runs past 0x1FF is refused with Status::Arg before anything is sent: the chip would roll over to 0x000. If a transfer fails part way, Read() returns Timeout and dst holds the chunks read before it.

Write

// Write() splits the range at 8-byte page boundaries and waits for each page's write cycle.
at25040::Status store_serial(Eeprom& eeprom, const uint8_t (&serial)[12]) {
  return eeprom.Write(0x0FC, serial, sizeof(serial)); // pages 0x0F8, 0x100 and 0x108
}

A page is 8 bytes on an 8-byte boundary (kPageSizeBytes). WritePage() refuses a write that would cross one, because the chip would wrap inside the page and overwrite its start; Write() splits a longer range at the page boundaries. Each page costs one write cycle, so writing the whole 512-byte array takes 64 cycles, at most 320 ms. If a page fails, Write() returns its status and the pages before it are written.

Status and block protection

void print_status(Eeprom& eeprom) {
  uint8_t sr = 0;
  if (eeprom.ReadStatus(sr) != at25040::Status::Ok) {
    return;
  }
  const unsigned bp =
      (sr & (at25040::kSrBp1 | at25040::kSrBp0)) >> 2; // 0 none, 1 top 1/4, 2 top 1/2, 3 all
  std::printf("busy=%d write-enabled=%d protect-level=%u\n", (sr & at25040::kSrWip) != 0,
              (sr & at25040::kSrWel) != 0, bp);
}

SetBlockProtection() sends WREN and WRSR and waits for the write cycle; GetBlockProtection() reads the level back:

// Make the top half (0x100-0x1FF: board ID, calibration) read-only. WP must be high.
bool lock_identity(Eeprom& eeprom) {
  at25040::BlockProtect level = at25040::BlockProtect::None;
  if (eeprom.GetBlockProtection(level) == at25040::Status::Ok &&
      level == at25040::BlockProtect::UpperHalf) {
    return true; // already locked
  }
  if (eeprom.SetBlockProtection(at25040::BlockProtect::UpperHalf) != at25040::Status::Ok) {
    return false; // Busy, Timeout
  }
  // The chip ignores WRSR when WP is low, so read the level back.
  return eeprom.GetBlockProtection(level) == at25040::Status::Ok &&
         level == at25040::BlockProtect::UpperHalf;
}
at25040::BlockProtect BP1:BP0 Read-only addresses (AT25040B)
None 00 None
UpperQuarter 01 0x180-0x1FF
UpperHalf 10 0x100-0x1FF
All 11 0x000-0x1FF

The bits are non-volatile. With WP low the chip ignores WRSR and SetBlockProtection() still returns Ok, so read the level back as above. A write to a protected address is ignored by the chip and still returns Ok from WritePage(): read back what matters.

WriteDisable() sends WRDI, which clears the write-enable latch regardless of WP. The driver's writes set the latch themselves and the chip clears it at the end of every write cycle, so you need WRDI only after a WriteEnable() for an instruction you then decided not to send:

// Clear the write-enable latch after a WREN you sent for your own instruction.
void abandon_write(Eeprom& eeprom) {
  (void)eeprom.WriteDisable(); // WRDI; Busy if a write cycle is still running
}

Handle errors

const char* describe(at25040::Status st) {
  switch (st) {
  case at25040::Status::Ok:
    return "ok";
  case at25040::Status::Timeout:
    return "SPI transfer failed, or write cycle too long";
  case at25040::Status::Absent:
    return "no chip answered";
  case at25040::Status::Busy:
    return "write cycle in progress, try again";
  case at25040::Status::Arg:
    return "bad pointer, length or address";
  }
  return "unknown";
}
Status Meaning
Ok The transactions completed. It does not prove a write was stored
Timeout Your port's transfer() returned false, or a write cycle did not end within the WaitReady() budget
Absent Probe() only: no plausible status register (Protocol)
Busy RDSR showed a write cycle in progress, so the call sent nothing else; try again after WaitReady(). From Probe(): RDY set in a status byte that is not 0xFF
Arg Read(), WritePage(), Write(), SetBlockProtection(): null pointer, a level outside BlockProtect, zero length, range past the array, or a page crossing (nothing is sent)

The chip itself reports nothing about a refused write (WP low, latch clear, protected block). The only check is to read the data back after the write cycle.