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}