cat24c32_rs/
lib.rs

1//! # CAT24C32 EEPROM Driver
2//!
3//! Platform-agnostic Rust driver for the CAT24C32 EEPROM Serial 32-Kb I2C device
4//! using the [`embedded-hal`] traits.
5//!
6//! The CAT24C32 is an EEPROM Serial 32-Kb I2C device, internally organized as 4096 words of 8 bits each.
7//! It features a 32-byte page write buffer and supports the Standard (100 kHz), Fast (400 kHz)
8//! and Fast-Plus (1 MHz) I2C protocol.
9//!
10//! ## Features
11//!
12//! - Platform-agnostic using embedded-hal traits
13//! - Single byte and page write operations
14//! - Single byte and sequential read operations
15//! - Support for multiple devices on the same bus via address pins
16//! - Compatible with embedded-storage traits for ecosystem integration
17//!
18//! ## Usage
19//!
20//! ```rust,no_run
21//! use cat24c32_rs::{Cat24c32, SlaveAddr};
22//! # use linux_embedded_hal::I2cdev;
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! #     let dev = I2cdev::new("/dev/i2c-1")?;
26//! let mut eeprom = Cat24c32::new(dev, SlaveAddr::Default);
27//!
28//! // Write a byte
29//! let _ = eeprom.write_byte(0x00, 0x42);
30//!
31//! // Read a byte  
32//! let value = eeprom.read_byte(0x00);
33//! match value {
34//!     Ok(val) => println!("Read value: 0x{:02X}", val),
35//!     Err(_) => println!("Read error"),
36//! }
37//! #     Ok(())
38//! # }
39//! ```
40//!
41//! ### Using with embedded-storage
42//!
43//! The driver implements the `embedded-storage` traits for ecosystem compatibility:
44//!
45//! ```rust,no_run
46//! use cat24c32_rs::{Cat24c32, SlaveAddr};
47//! use embedded_storage::nor_flash::NorFlash;
48//! # use linux_embedded_hal::I2cdev;
49//!
50//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
51//! #     let dev = I2cdev::new("/dev/i2c-1")?;
52//! let mut eeprom = Cat24c32::new(dev, SlaveAddr::Default);
53//!
54//! // Using NorFlash trait (with automatic page write optimization)
55//! let data = b"Hello, storage!";
56//! let _ = NorFlash::write(&mut eeprom, 0x0000, data);
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! [`embedded-hal`]: https://github.com/rust-embedded/embedded-hal
62
63#![no_std]
64#![doc(html_root_url = "https://docs.rs/cat24c32-rs/0.1.0")]
65#![warn(missing_docs)]
66
67use embedded_hal::i2c::I2c;
68use embedded_storage::{ReadStorage, Storage};
69use embedded_storage::nor_flash::{ErrorType, ReadNorFlash, NorFlash, NorFlashError, NorFlashErrorKind};
70
71// Re-export for convenience
72pub use embedded_storage;
73
74/// All possible errors in this crate
75#[derive(Debug, Clone, PartialEq)]
76pub enum Error<E> {
77    /// I²C bus error
78    I2C(E),
79    /// Too much data passed for a write operation
80    ///
81    /// This occurs when trying to write more data than fits in a single page,
82    /// or when the write operation would cross a page boundary.
83    TooMuchData,
84    /// Memory address is out of range
85    ///
86    /// The provided address exceeds the device's memory capacity (0x0000-0x0FFF).
87    InvalidAddr,
88}
89
90impl<E: core::fmt::Debug> NorFlashError for Error<E> {
91    fn kind(&self) -> NorFlashErrorKind {
92        match self {
93            Error::I2C(_) => NorFlashErrorKind::Other,
94            Error::TooMuchData => NorFlashErrorKind::OutOfBounds,
95            Error::InvalidAddr => NorFlashErrorKind::OutOfBounds,
96        }
97    }
98}
99
100/// Possible slave addresses for CAT24C32 devices
101///
102/// The CAT24C32 supports up to 8 devices on the same I2C bus by using
103/// different combinations of the A2, A1, and A0 address pins.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum SlaveAddr {
106    /// Default slave address (0x50)
107    ///
108    /// This corresponds to A2=A1=A0=0 (all address pins tied to ground).
109    Default,
110    /// Alternative slave address providing bit values for A2, A1 and A0
111    ///
112    /// # Arguments
113    /// * First bool: A2 pin state (true = VCC, false = GND)
114    /// * Second bool: A1 pin state (true = VCC, false = GND)  
115    /// * Third bool: A0 pin state (true = VCC, false = GND)
116    ///
117    /// # Examples
118    /// ```
119    /// use cat24c32_rs::SlaveAddr;
120    ///
121    /// // A2=1, A1=0, A0=1 -> address 0x55
122    /// let addr = SlaveAddr::Alternative(true, false, true);
123    /// ```
124    Alternative(bool, bool, bool),
125}
126
127impl SlaveAddr {
128    /// Get the base I2C address for this slave address configuration
129    pub(crate) fn addr(self) -> u8 {
130        match self {
131            SlaveAddr::Default => 0x50,
132            SlaveAddr::Alternative(a2, a1, a0) => {
133                0x50 | ((a2 as u8) << 2) | ((a1 as u8) << 1) | (a0 as u8)
134            }
135        }
136    }
137
138    /// Get the device address for a specific memory address
139    ///
140    /// For CAT24C32, the high bits of the memory address are encoded
141    /// in the device address for addressing beyond 256 bytes.
142    pub(crate) fn devaddr(self, memory_address: u32, address_bits: u8) -> u8 {
143        let base_addr = self.addr();
144
145        if address_bits > 8 {
146            // For addresses requiring more than 8 bits, use the high bits
147            // in the device address (bits A1 and A0 of device address)
148            let high_bits = ((memory_address >> 8) & 0x07) as u8;
149            let device_bits = match self {
150                SlaveAddr::Default => high_bits,
151                SlaveAddr::Alternative(a2, _, _) => {
152                    // Preserve A2, use memory high bits for A1,A0
153                    ((a2 as u8) << 2) | (high_bits & 0x03)
154                }
155            };
156            0x50 | device_bits
157        } else {
158            base_addr
159        }
160    }
161}
162
163/// CAT24C32 EEPROM driver
164///
165/// This struct represents a CAT24C32 EEPROM device connected via I2C.
166///
167/// # Constants
168/// - Memory size: 32 Kb (4096 bytes)
169/// - Address range: 0x0000 to 0x0FFF
170/// - Page size: 32 bytes
171/// - Address size: 2 bytes
172#[derive(Debug)]
173pub struct Cat24c32<I2C> {
174    /// The I²C bus implementation
175    i2c: I2C,
176    /// The I²C device address configuration
177    address: SlaveAddr,
178}
179
180impl<I2C> Cat24c32<I2C> {
181    /// Create a new CAT24C32 EEPROM driver instance
182    ///
183    /// # Arguments
184    /// * `i2c` - I2C bus implementation
185    /// * `address` - Slave address configuration
186    ///
187    /// # Examples
188    /// ```rust,no_run
189    /// use cat24c32_rs::{Cat24c32, SlaveAddr};
190    /// # use linux_embedded_hal::I2cdev;
191    /// # let i2c = I2cdev::new("/dev/i2c-1").unwrap();
192    ///
193    /// let eeprom = Cat24c32::new(i2c, SlaveAddr::Default);
194    /// ```
195    pub fn new(i2c: I2C, address: SlaveAddr) -> Self {
196        Self { i2c, address }
197    }
198
199    /// Destroy driver instance and return the I²C bus
200    pub fn destroy(self) -> I2C {
201        self.i2c
202    }
203
204    /// Get the device I2C address for a specific memory address
205    fn get_device_address<E>(&self, memory_address: u32) -> Result<u8, Error<E>> {
206        if memory_address > 0x0FFF {
207            return Err(Error::InvalidAddr);
208        }
209        Ok(self.address.devaddr(memory_address, 12))
210    }
211
212    /// Return the device's page size in bytes (always 32 for CAT24C32)
213    pub fn page_size(&self) -> usize {
214        32
215    }
216}
217
218impl<I2C, E> Cat24c32<I2C>
219where
220    I2C: I2c<Error = E>,
221{
222    /// Write a single byte to a specific address
223    ///
224    /// After writing a byte, the EEPROM enters an internally-timed write cycle
225    /// to the nonvolatile memory. During this time all inputs are disabled and
226    /// the EEPROM will not respond until the write is complete (typically 5ms).
227    ///
228    /// # Arguments
229    /// * `address` - Memory address to write to (0x0000 to 0x0FFF)
230    /// * `data` - The byte value to write
231    ///
232    /// # Errors
233    /// Returns `Error::InvalidAddr` if the address is out of range,
234    /// or `Error::I2C` if there's an I2C communication error.
235    pub fn write_byte(&mut self, address: u32, data: u8) -> Result<(), Error<E>> {
236        let devaddr = self.get_device_address(address)?;
237        let payload = [(address >> 8) as u8, address as u8, data];
238        self.i2c.write(devaddr, &payload).map_err(Error::I2C)
239    }
240
241    /// Read a single byte from a specific address
242    ///
243    /// # Arguments
244    /// * `address` - Memory address to read from (0x0000 to 0x0FFF)
245    ///
246    /// # Returns
247    /// The byte value at the specified address.
248    ///
249    /// # Errors
250    /// Returns `Error::InvalidAddr` if the address is out of range,
251    /// or `Error::I2C` if there's an I2C communication error.
252    pub fn read_byte(&mut self, address: u32) -> Result<u8, Error<E>> {
253        let devaddr = self.get_device_address(address)?;
254        let memaddr = [(address >> 8) as u8, address as u8];
255        let mut data = [0u8; 1];
256        self.i2c
257            .write_read(devaddr, &memaddr, &mut data)
258            .map_err(Error::I2C)?;
259        Ok(data[0])
260    }
261
262    /// Read multiple bytes starting from a specific address
263    ///
264    /// This performs a sequential read operation, automatically incrementing
265    /// the address for each byte read.
266    ///
267    /// # Arguments
268    /// * `address` - Starting memory address (0x0000 to 0x0FFF)
269    /// * `data` - Buffer to fill with read data
270    ///
271    /// # Errors
272    /// Returns `Error::InvalidAddr` if the address is out of range,
273    /// or `Error::I2C` if there's an I2C communication error.
274    pub fn read_data(&mut self, address: u32, data: &mut [u8]) -> Result<(), Error<E>> {
275        let devaddr = self.get_device_address(address)?;
276        let memaddr = [(address >> 8) as u8, address as u8];
277        self.i2c
278            .write_read(devaddr, &memaddr, data)
279            .map_err(Error::I2C)
280    }
281
282    /// Read from the current address pointer
283    ///
284    /// This reads the contents of the last address accessed during the last read
285    /// or write operation, _incremented by one_. This feature may not be available
286    /// on all I2C implementations.
287    ///
288    /// # Returns
289    /// The byte value at the current address pointer.
290    ///
291    /// # Errors
292    /// Returns `Error::I2C` if there's an I2C communication error.
293    pub fn read_current_address(&mut self) -> Result<u8, Error<E>> {
294        let mut data = [0u8; 1];
295        self.i2c
296            .read(self.address.addr(), &mut data)
297            .map_err(Error::I2C)?;
298        Ok(data[0])
299    }
300
301    /// Write up to one page of data starting at a specific address
302    ///
303    /// The CAT24C32 has a 32-byte page write buffer. Data written in a single
304    /// page write operation must not cross page boundaries. If too much data is passed
305    /// or if the write would cross a page boundary, `Error::TooMuchData` will be returned.
306    ///
307    /// After writing, the EEPROM enters an internally-timed write cycle
308    /// to the nonvolatile memory. During this time all inputs are disabled and
309    /// the EEPROM will not respond until the write is complete (typically 5ms).
310    ///
311    /// # Arguments
312    /// * `address` - Starting memory address for the write
313    /// * `data` - Data to write (must not exceed 32 bytes or cross page boundaries)
314    ///
315    /// # Errors
316    /// * `Error::TooMuchData` - If data exceeds page size or crosses page boundary
317    /// * `Error::InvalidAddr` - If the address is out of range
318    /// * `Error::I2C` - If there's an I2C communication error
319    pub fn write_page(&mut self, address: u32, data: &[u8]) -> Result<(), Error<E>> {
320        if data.is_empty() {
321            return Ok(());
322        }
323
324        if data.len() > 32 {
325            return Err(Error::TooMuchData);
326        }
327
328        // Check page boundary (32-byte pages)
329        let page_boundary = address | 31; // 32-byte page boundary
330        if address + data.len() as u32 > page_boundary + 1 {
331            return Err(Error::TooMuchData);
332        }
333
334        let devaddr = self.get_device_address(address)?;
335        let mut payload = [0u8; 34]; // 2 bytes address + 32 bytes max data
336        payload[0] = (address >> 8) as u8;
337        payload[1] = address as u8;
338        payload[2..2 + data.len()].copy_from_slice(data);
339
340        self.i2c
341            .write(devaddr, &payload[..2 + data.len()])
342            .map_err(Error::I2C)
343    }
344}
345
346// Embedded Storage trait implementations
347impl<I2C, E> Storage for Cat24c32<I2C>
348where
349    I2C: I2c<Error = E>,
350    E: core::fmt::Debug,
351{
352    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), <Self as ReadStorage>::Error> {
353        NorFlash::write(self, offset, bytes)
354    }
355}
356
357impl<I2C, E> ReadStorage for Cat24c32<I2C>
358where
359    I2C: I2c<Error = E>,
360    E: core::fmt::Debug,
361{
362    type Error = Error<E>;
363
364    /// Read data from the EEPROM starting at the specified address
365    ///
366    /// # Arguments
367    /// * `offset` - Memory address to read from (0x0000 to 0x0FFF)
368    /// * `bytes` - Buffer to fill with read data
369    ///
370    /// # Errors
371    /// Returns `Error::InvalidAddr` if the address is out of range,
372    /// or `Error::I2C` if there's an I2C communication error.
373    fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
374        self.read_data(offset, bytes)
375    }
376
377    /// Get the capacity of the storage device in bytes
378    ///
379    /// Returns 4096 bytes (4KB) for the CAT24C32.
380    fn capacity(&self) -> usize {
381        4096
382    }
383}
384
385// ErrorType implementation for nor_flash traits
386impl<I2C, E> ErrorType for Cat24c32<I2C>
387where
388    I2C: I2c<Error = E>,
389    E: core::fmt::Debug,
390{
391    type Error = Error<E>;
392}
393
394// Note: CAT24C32 is EEPROM, not NOR flash, but we can implement similar traits
395// for compatibility with embedded-storage ecosystem
396impl<I2C, E> ReadNorFlash for Cat24c32<I2C>
397where
398    I2C: I2c<Error = E>,
399    E: core::fmt::Debug,
400{
401    const READ_SIZE: usize = 1; // Can read individual bytes
402
403    /// Read data from the EEPROM
404    fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
405        self.read_data(offset, bytes)
406    }
407
408    /// Get the capacity of the storage device
409    fn capacity(&self) -> usize {
410        4096
411    }
412}
413
414impl<I2C, E> NorFlash for Cat24c32<I2C>
415where
416    I2C: I2c<Error = E>,
417    E: core::fmt::Debug,
418{
419    const WRITE_SIZE: usize = 1; // Can write individual bytes
420    const ERASE_SIZE: usize = 1; // EEPROM doesn't need explicit erase, smallest unit is 1 byte
421
422    /// Write data to the EEPROM
423    ///
424    /// For optimal performance, try to write complete pages (32 bytes) when possible.
425    /// This implementation will automatically use page writes when the data fits
426    /// within a single page boundary.
427    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
428        if bytes.is_empty() {
429            return Ok(());
430        }
431
432        // For writes that fit within a single page, use page write for efficiency
433        if bytes.len() <= 32 {
434            let page_boundary = offset | 31; // 32-byte page boundary
435            if offset + bytes.len() as u32 <= page_boundary + 1 {
436                return self.write_page(offset, bytes);
437            }
438        }
439
440        // For larger writes or writes that cross page boundaries, write byte by byte
441        for (i, &byte) in bytes.iter().enumerate() {
442            self.write_byte(offset + i as u32, byte)?;
443        }
444        Ok(())
445    }
446
447    /// EEPROM doesn't require explicit erase - data can be overwritten directly
448    ///
449    /// This is a no-op since EEPROM cells can be written directly without erasing.
450    fn erase(&mut self, _from: u32, _to: u32) -> Result<(), Self::Error> {
451        // EEPROM doesn't need explicit erase operation
452        Ok(())
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_slave_address_default() {
462        let addr = SlaveAddr::Default;
463        assert_eq!(addr.addr(), 0x50);
464    }
465
466    #[test]
467    fn test_slave_address_alternative() {
468        let addr = SlaveAddr::Alternative(true, false, true);
469        assert_eq!(addr.addr(), 0x55); // 0x50 | 0x04 | 0x01
470    }
471
472    #[test]
473    fn test_page_boundary_calculation() {
474        // Test that page boundary checks work correctly
475        let boundary_32 = 0x00 | 31; // Should be 31 (0x1F)
476        assert_eq!(boundary_32, 31);
477
478        let boundary_64 = 0x40 | 31; // Should be 95 (0x5F)
479        assert_eq!(boundary_64, 95);
480    }
481}