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.