cat25040 0.1.1

A `no_std`, async Rust driver for the **ON Semiconductor CAT25040** (4-Kbit SPI EEPROM), built on [`embedded-hal-async`](https://crates.io/crates/embedded-hal-async) traits.
Documentation

cat25040

A no_std, async Rust driver for the ON Semiconductor CAT25040 (4-Kbit SPI EEPROM), built on embedded-hal-async traits.

Works with any platform that implements the driver's Spi and HardwareInterface traits — no framework lock-in.

Features

  • Byte read/write with automatic write-only-if-changed optimization
  • Page write (16 bytes per page, single write cycle)
  • Sequential read of arbitrary length
  • 9-bit addressing — handles the A8 bit encoding in opcodes automatically
  • Busy polling after writes with configurable delay
  • Software block protection — protect memory regions (quarter, half, or full array)
  • Power-up initialization — proper timing delays per datasheet (tPUW = 1ms)
  • Type-safe register access — uses device-driver crate for compile-time safe register operations
  • YAML-defined device model — register and command definitions in declarative YAML
  • Optional defmt logging (enable the defmt feature)

Usage

use cat25040::{Cat25040, BlockProtection};

// Provide your SPI and delay implementations via custom traits
let mut eeprom = Cat25040::new(spi, hardware_interface);

// Initialize after power-on (waits required tPUW = 1ms)
eeprom.init().await.unwrap();

// Read 16 bytes starting at address 0x00
let mut buf = [0u8; 16];
eeprom.read(0x00, &mut buf).await.unwrap();

// Write a single byte (skips if value already matches)
eeprom.write_byte(0xAB, 0x10).await.unwrap();

// Write a full 16-byte page (address must be page-aligned)
let page = *b"EEPROM data here";
eeprom.write_page(&page, 0x00).await.unwrap();

// Type-safe status register access
let status = eeprom.read_status().await.unwrap();
if status.busy() {
    // Write in progress
}
if status.wel() {
    // Write enable latch is set
}

// Protect upper half of memory (addresses 0x100-0x1FF)
eeprom.set_block_protection(BlockProtection::UpperHalf).await.unwrap();

// Check current protection level
let level = eeprom.get_block_protection().await.unwrap();

// Clear all protection
eeprom.clear_block_protection().await.unwrap();

// Send write enable/disable commands
eeprom.write_enable().await.unwrap();
eeprom.write_disable().await.unwrap();

API

Method Description
new(spi, hardware_interface) Create a new driver instance
init() Initialize device after power-on (waits tPUW = 1ms)
read(address, buf) Read buf.len() bytes starting at address
write_byte(data, address) Write a single byte (skips if unchanged)
write_page(data, address) Write exactly 16 bytes to a page-aligned address
read_status() Read the EEPROM status register (returns type-safe Status)
is_busy() Check if a write cycle is in progress (WIP bit)
write_enable() Send the WREN command (uses device-driver)
write_disable() Send the WRDI command (uses device-driver)
get_block_protection() Get current block protection level
set_block_protection(level) Set block protection (None, UpperQuarter, UpperHalf, Full)
clear_block_protection() Clear all block protection (convenience method)
device() Access underlying device-driver device for advanced usage

Architecture

Device-Driver Integration

This driver uses the device-driver crate for type-safe register and command operations. Key components:

YAML Device Model (src/cat25040.yaml):

  • Defines registers (STATUS_REG) with named fields (BUSY, WEL, BP0, BP1)
  • Defines commands (WREN, WRDI, WRSR) with opcodes
  • Auto-generates type-safe Rust code at build time

SpiRegisterInterface (src/register_interface.rs):

  • Bridges the driver's Spi trait with device-driver's RegisterInterface and CommandInterface traits
  • Handles opcode-based register access specific to CAT25040
  • Enables compile-time verification of register operations

Generated Device API:

  • Cat25040Device provides the low-level device API
  • Status (alias for StatusReg) provides type-safe field access
  • Commands like wren() and wrdi() are generated from YAML

Custom Traits

The driver defines its own traits to avoid framework lock-in:

Spi trait - Wraps embedded_hal_async::spi::SpiDevice:

pub trait Spi {
    async fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Cat25040Error>;
}

HardwareInterface trait - Provides delay functionality:

pub trait HardwareInterface {
    async fn wait_ms(&mut self, timeout_ms: u32);
}

These traits can be easily implemented for any platform. See the spi_device module for an example implementation using embedded-hal-async directly.

Block Protection

The CAT25040 supports software-controlled write protection for different memory regions using the BP0 and BP1 bits in the status register:

Level Protected Addresses Protected Size Description
None None 0 bytes No protection (BP1=0, BP0=0)
UpperQuarter 0x180-0x1FF 128 bytes Upper 1/4 protected (BP1=0, BP0=1)
UpperHalf 0x100-0x1FF 256 bytes Upper 1/2 protected (BP1=1, BP0=0)
Full 0x000-0x1FF 512 bytes Entire array protected (BP1=1, BP0=1)

Hardware Override: The WP (Write Protect) pin can override software protection. When WP is low, all writes are inhibited regardless of BP0/BP1 settings. When not using the WP pin, tie it to VCC.

Use Cases:

  • Protect calibration data, configuration, or factory settings from accidental overwrites
  • Reserve upper memory for read-only parameters while allowing writes to lower addresses
  • Implement multi-level security by combining hardware (WP pin) and software protection

Cargo Features

  • std (default) — enables standard library support for testing
  • defmt — enables debug logging via defmt

Hardware Pins

The CAT25040 has several hardware control pins that affect driver behavior:

  • CS (Chip Select): Managed by your SPI implementation. CS must go high to start internal write cycles.
  • WP (Write Protect): Hardware write protection. When low, all writes are inhibited regardless of software BP0/BP1 settings. Tie to VCC when not used.
  • HOLD: Pauses SPI communication when taken low (while SCK is low). Allows the master to service higher-priority interrupts. Tie to VCC when not used.
  • SCK, SI, SO: Standard SPI signals (clock, data in, data out).

Important: This driver does not directly control WP or HOLD pins. These are hardware features that should be managed by your platform's GPIO implementation if needed.

Power-On Timing

After power-on, the device requires up to 1ms before it's ready for operations (tPUW per datasheet). Always call init() after creating the driver instance:

let mut eeprom = Cat25040::new(spi, hardware_interface);
eeprom.init().await.unwrap(); // Waits 1ms and verifies device is ready

Implementing for Your Platform

To use this driver, implement the Spi and HardwareInterface traits for your platform:

use cat25040::{Cat25040, Spi, HardwareInterface, Operation, Cat25040Error};
use embedded_hal_async::spi::SpiDevice;
use embedded_hal_async::delay::DelayNs;

// Wrapper for SpiDevice
struct MySpi<S: SpiDevice> {
    spi: S,
}

impl<S: SpiDevice> Spi for MySpi<S> {
    async fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Cat25040Error> {
        self.spi.transaction(operations).await.map_err(|_| Cat25040Error::Spi)
    }
}

// Wrapper for DelayNs
struct MyHardwareInterface<D: DelayNs> {
    delay: D,
}

impl<D: DelayNs> HardwareInterface for MyHardwareInterface<D> {
    async fn wait_ms(&mut self, timeout_ms: u32) {
        self.delay.delay_ms(timeout_ms).await;
    }
}

// Use with your platform (e.g., embassy)
let spi = MySpi { spi: spi_device };
let hw = MyHardwareInterface { delay: embassy_time::Delay };
let mut eeprom = Cat25040::new(spi, hw);

The spi_device module in this crate provides a ready-to-use implementation for platforms using embedded-hal-async.

Device Compatibility

Designed for the CAT25040 but should work with other CAT250xx family EEPROMs that use the same SPI command set and 9-bit addressing (e.g., CAT25020). The driver handles the A8 address bit encoding automatically by setting bit 3 of the opcode when addressing the upper 256 bytes.

Project Structure

src/
├── lib.rs                    # Main driver implementation
├── cat25040.yaml             # Device model (registers, commands)
├── register_interface.rs     # SpiRegisterInterface implementation
└── spi_device.rs             # Example SPI trait implementations
build.rs                      # Code generation from YAML

The build script uses device-driver-generation to generate Rust code from the YAML manifest. Generated code is placed in target/<profile>/build/cat25040-*/out/cat25040_device.rs.

Testing

cargo test

The test suite includes 26 tests covering:

  • 9-bit address encoding (A8 bit in opcode)
  • Read/write operations at various addresses
  • Page writes with alignment validation
  • Status register access with type-safe fields
  • Block protection configuration
  • Power-up initialization

Tests use mock Spi and HardwareInterface implementations — no hardware required.

License

See the repository root for license information.