From 29a6d4e68061f69629b3e4c4edbf92eedc0feeb3 Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Fri, 19 Jul 2024 20:32:17 +0200 Subject: [PATCH 1/6] feat: first implementation for AT24CM01 --- Cargo.toml | 1 + src/lib.rs | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d2b39c0..7caeebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/atovproject/at24cx" [dependencies] embedded-hal-async = "1.0" +embedded-storage-async = "0.4" [dev-dependencies] embedded-hal-mock = { version="0.11", features = ["eh1", "embedded-hal-async"] } diff --git a/src/lib.rs b/src/lib.rs index b2ef6bd..ae41d36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,272 @@ #![cfg_attr(not(test), no_std)] +use core::cmp::min; +use core::fmt::Debug; +use embedded_hal_async::{ + delay::DelayNs, + i2c::{ErrorType as I2cErrorType, I2c}, +}; +use embedded_storage_async::nor_flash::{ + ErrorType as StorageErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, +}; + +// TODO: These are only valid for AT24CM01. Implement the others +const PAGE_SIZE: usize = 256; +const ADDRESS_BYTES: usize = 2; + +// Adds up to 5ms after which the at24x should definitely be ready +const POLL_MAX_RETRIES: usize = 50; +const POLL_DELAY_US: u32 = 100; + +/// Custom error type for the various errors that can be thrown by AT24Cx +/// Can be converted into a NorFlashError. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + I2cError(E), + NotAligned, + OutOfBounds, + WriteEnableFail, + ReadbackFail, +} + +impl NorFlashError for Error { + fn kind(&self) -> NorFlashErrorKind { + match self { + Error::NotAligned => NorFlashErrorKind::NotAligned, + Error::OutOfBounds => NorFlashErrorKind::OutOfBounds, + _ => NorFlashErrorKind::Other, + } + } +} + +pub struct Address(pub u8, pub u8); + +impl From
for u8 { + fn from(a: Address) -> Self { + 0xa0 | (a.1 << 2) | (a.0 << 1) + } +} + +pub struct At24Cx { + address_bits: usize, + base_address: u8, + delay: D, + i2c: I2C, +} + +impl At24Cx +where + I2C: I2c, +{ + pub fn new(i2c: I2C, address: Address, address_bits: usize, delay: D) -> Self { + Self { + address_bits, + base_address: address.into(), + delay, + i2c, + } + } + + fn get_device_address(&self, memory_address: u32) -> Result> { + if memory_address >= (1 << self.address_bits) { + return Err(Error::OutOfBounds); + } + let p0 = if memory_address & 1 << 16 == 0 { 0 } else { 1 }; + Ok(self.base_address & p0 << 1) + } + + async fn poll_ack(&mut self, offset: u32) -> Result<(), Error> { + let device_addr = self.get_device_address(offset)?; + for _ in 0..POLL_MAX_RETRIES { + match self.i2c.write(device_addr, &[]).await { + Ok(_) => return Ok(()), // ACK received, write cycle complete + Err(_) => { + // NACK received, wait a bit and try again + self.delay.delay_us(POLL_DELAY_US).await; + } + } + } + + Err(Error::WriteEnableFail) // Timeout waiting for ACK + } + + async fn page_write(&mut self, address: u32, data: &[u8]) -> Result<(), Error> { + if data.is_empty() { + return Ok(()); + } + + // check this before to ensure that data.len() fits into u32 + // ($page_size always fits as its maximum value is 256). + if data.len() > PAGE_SIZE { + // This would actually be supported by the EEPROM but + // the data in the page would be overwritten + return Err(Error::OutOfBounds); + } + + let page_boundary = address | (PAGE_SIZE as u32 - 1); + if address + data.len() as u32 > page_boundary + 1 { + // This would actually be supported by the EEPROM but + // the data in the page would be overwritten + return Err(Error::OutOfBounds); + } + // + let device_addr = self.get_device_address(address)?; + let mut payload: [u8; ADDRESS_BYTES + PAGE_SIZE] = [0; ADDRESS_BYTES + PAGE_SIZE]; + payload[0] = (address >> 8) as u8; + payload[1] = address as u8; + payload[ADDRESS_BYTES..ADDRESS_BYTES + data.len()].copy_from_slice(data); + self.i2c + .write(device_addr, &payload[..ADDRESS_BYTES + data.len()]) + .await + .map_err(Error::I2cError) + } +} + +impl StorageErrorType for At24Cx +where + I2C: I2cErrorType, +{ + type Error = Error; +} + +impl ReadNorFlash for At24Cx +where + I2C: I2c, +{ + const READ_SIZE: usize = 1; + + async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + match check_read(self, offset, bytes.len()) { + Err(NorFlashErrorKind::NotAligned) => return Err(Error::NotAligned), + Err(_) => return Err(Error::OutOfBounds), + Ok(_) => {} + } + let device_address = self.get_device_address(offset)?; + let mut memaddr = [0; 2]; + memaddr[0] = (offset >> 8) as u8; + memaddr[1] = offset as u8; + self.i2c + .write_read(device_address, &memaddr[..2], bytes) + .await + .map_err(Error::I2cError) + } + + fn capacity(&self) -> usize { + 1 << self.address_bits + } +} + +impl NorFlash for At24Cx +where + I2C: I2c, + E: Into>, +{ + const WRITE_SIZE: usize = 1; + + const ERASE_SIZE: usize = 1; + + async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + // Check if the erase operation is valid + match check_erase(self, from, to) { + Err(NorFlashErrorKind::NotAligned) => return Err(Error::NotAligned), + Err(_) => return Err(Error::OutOfBounds), + Ok(_) => {} + } + + let mut current_address = from; + while current_address < to { + let page_offset = current_address as usize % PAGE_SIZE; + let bytes_to_erase = min(PAGE_SIZE - page_offset, (to - current_address) as usize); + + // Create a buffer of 0xFF bytes for erasing + let erase_buffer = [0xFF; PAGE_SIZE]; + + // Erase (write 0xFF) to the current page + self.page_write(current_address, &erase_buffer[..bytes_to_erase]) + .await?; + + current_address += bytes_to_erase as u32; + + self.poll_ack(current_address).await?; + } + + Ok(()) + } + + async fn write(&mut self, mut offset: u32, mut bytes: &[u8]) -> Result<(), Self::Error> { + match check_write(self, offset, bytes.len()) { + Err(NorFlashErrorKind::NotAligned) => return Err(Error::NotAligned), + Err(_) => return Err(Error::OutOfBounds), + Ok(_) => {} + } + while !bytes.is_empty() { + let this_page_offset = offset as usize % PAGE_SIZE; + let this_page_remaining = PAGE_SIZE - this_page_offset; + let chunk_size = min(bytes.len(), this_page_remaining); + self.page_write(offset, &bytes[..chunk_size]).await?; + offset += chunk_size as u32; + bytes = &bytes[chunk_size..]; + self.poll_ack(offset).await?; + } + Ok(()) + } +} + +// Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs +// TODO: It's not in the async version yet +fn check_slice( + flash: &T, + align: usize, + offset: u32, + length: usize, +) -> Result<(), NorFlashErrorKind> { + let offset = offset as usize; + if length > flash.capacity() || offset > flash.capacity() - length { + return Err(NorFlashErrorKind::OutOfBounds); + } + if offset % align != 0 || length % align != 0 { + return Err(NorFlashErrorKind::NotAligned); + } + Ok(()) +} + +// Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs +// TODO: It's not in the async version yet +/// Return whether a read operation is within bounds. +fn check_read( + flash: &T, + offset: u32, + length: usize, +) -> Result<(), NorFlashErrorKind> { + check_slice(flash, T::READ_SIZE, offset, length) +} + +// Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs +// TODO: It's not in the async version yet +/// Return whether an erase operation is aligned and within bounds. +fn check_erase(flash: &T, from: u32, to: u32) -> Result<(), NorFlashErrorKind> { + let (from, to) = (from as usize, to as usize); + if from > to || to > flash.capacity() { + return Err(NorFlashErrorKind::OutOfBounds); + } + if from % T::ERASE_SIZE != 0 || to % T::ERASE_SIZE != 0 { + return Err(NorFlashErrorKind::NotAligned); + } + Ok(()) +} + +// Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs +// TODO: It's not in the async version yet +/// Return whether a write operation is aligned and within bounds. +fn check_write( + flash: &T, + offset: u32, + length: usize, +) -> Result<(), NorFlashErrorKind> { + check_slice(flash, T::WRITE_SIZE, offset, length) +} + #[cfg(test)] mod tests { use super::*; From d5e3f3abca763d0236588681eb9753c0a9edab2c Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Sat, 10 Aug 2024 17:54:29 +0200 Subject: [PATCH 2/6] fix: slightly increase the max poll ack time to 6ms --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ae41d36..ec7eeef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,8 @@ use embedded_storage_async::nor_flash::{ const PAGE_SIZE: usize = 256; const ADDRESS_BYTES: usize = 2; -// Adds up to 5ms after which the at24x should definitely be ready -const POLL_MAX_RETRIES: usize = 50; +// Adds up to 6ms after which the at24x should definitely be ready +const POLL_MAX_RETRIES: usize = 60; const POLL_DELAY_US: u32 = 100; /// Custom error type for the various errors that can be thrown by AT24Cx From e22e1543e82fa41a881cc63437efcfc5cb121cd5 Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Sun, 11 Aug 2024 17:48:47 +0200 Subject: [PATCH 3/6] fix: implement from I2cError for Error --- src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ec7eeef..f1b9bbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use core::cmp::min; use core::fmt::Debug; use embedded_hal_async::{ delay::DelayNs, - i2c::{ErrorType as I2cErrorType, I2c}, + i2c::{Error as I2cError, ErrorType as I2cErrorType, I2c}, }; use embedded_storage_async::nor_flash::{ ErrorType as StorageErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, @@ -40,6 +40,12 @@ impl NorFlashError for Error { } } +impl From for Error { + fn from(error: E) -> Self { + Error::I2cError(error) + } +} + pub struct Address(pub u8, pub u8); impl From
for u8 { From 2ec7e6dc8d70a8c9daf089b68e007de744b986ae Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Sun, 11 Aug 2024 17:49:44 +0200 Subject: [PATCH 4/6] fix: address bitwise fixes --- src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f1b9bbb..85ec17a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub struct Address(pub u8, pub u8); impl From
for u8 { fn from(a: Address) -> Self { - 0xa0 | (a.1 << 2) | (a.0 << 1) + 0x50 | (a.1 << 2) | (a.0 << 1) } } @@ -79,7 +79,7 @@ where return Err(Error::OutOfBounds); } let p0 = if memory_address & 1 << 16 == 0 { 0 } else { 1 }; - Ok(self.base_address & p0 << 1) + Ok(self.base_address | p0) } async fn poll_ack(&mut self, offset: u32) -> Result<(), Error> { @@ -149,9 +149,7 @@ where Ok(_) => {} } let device_address = self.get_device_address(offset)?; - let mut memaddr = [0; 2]; - memaddr[0] = (offset >> 8) as u8; - memaddr[1] = offset as u8; + let memaddr = [(offset >> 8) as u8, offset as u8]; self.i2c .write_read(device_address, &memaddr[..2], bytes) .await From 0b0957ae0cdc2246f67f44289d00e593517204d4 Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Sun, 11 Aug 2024 17:50:07 +0200 Subject: [PATCH 5/6] fix: fix ack polling --- src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 85ec17a..6670b43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ pub enum Error { OutOfBounds, WriteEnableFail, ReadbackFail, + WriteAckTimeout, } impl NorFlashError for Error { @@ -84,8 +85,9 @@ where async fn poll_ack(&mut self, offset: u32) -> Result<(), Error> { let device_addr = self.get_device_address(offset)?; + let mut empty = [0]; for _ in 0..POLL_MAX_RETRIES { - match self.i2c.write(device_addr, &[]).await { + match self.i2c.read(device_addr, &mut empty).await { Ok(_) => return Ok(()), // ACK received, write cycle complete Err(_) => { // NACK received, wait a bit and try again @@ -93,8 +95,8 @@ where } } } - - Err(Error::WriteEnableFail) // Timeout waiting for ACK + // Timeout waiting for ACK + Err(Error::WriteAckTimeout) } async fn page_write(&mut self, address: u32, data: &[u8]) -> Result<(), Error> { From e7c86b71644dc51be5f821f68b6e3b2489638dc9 Mon Sep 17 00:00:00 2001 From: Chris Maniewski Date: Sun, 11 Aug 2024 17:50:24 +0200 Subject: [PATCH 6/6] fix: remove erase function, not needed for at24x --- src/lib.rs | 43 +++---------------------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6670b43..54b2a80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,33 +170,10 @@ where { const WRITE_SIZE: usize = 1; - const ERASE_SIZE: usize = 1; - - async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { - // Check if the erase operation is valid - match check_erase(self, from, to) { - Err(NorFlashErrorKind::NotAligned) => return Err(Error::NotAligned), - Err(_) => return Err(Error::OutOfBounds), - Ok(_) => {} - } - - let mut current_address = from; - while current_address < to { - let page_offset = current_address as usize % PAGE_SIZE; - let bytes_to_erase = min(PAGE_SIZE - page_offset, (to - current_address) as usize); - - // Create a buffer of 0xFF bytes for erasing - let erase_buffer = [0xFF; PAGE_SIZE]; - - // Erase (write 0xFF) to the current page - self.page_write(current_address, &erase_buffer[..bytes_to_erase]) - .await?; - - current_address += bytes_to_erase as u32; - - self.poll_ack(current_address).await?; - } + const ERASE_SIZE: usize = PAGE_SIZE; + async fn erase(&mut self, _from: u32, _to: u32) -> Result<(), Self::Error> { + // No explicit erase needed Ok(()) } @@ -248,20 +225,6 @@ fn check_read( check_slice(flash, T::READ_SIZE, offset, length) } -// Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs -// TODO: It's not in the async version yet -/// Return whether an erase operation is aligned and within bounds. -fn check_erase(flash: &T, from: u32, to: u32) -> Result<(), NorFlashErrorKind> { - let (from, to) = (from as usize, to as usize); - if from > to || to > flash.capacity() { - return Err(NorFlashErrorKind::OutOfBounds); - } - if from % T::ERASE_SIZE != 0 || to % T::ERASE_SIZE != 0 { - return Err(NorFlashErrorKind::NotAligned); - } - Ok(()) -} - // Copied from https://github.com/rust-embedded-community/embedded-storage/blob/master/src/nor_flash.rs // TODO: It's not in the async version yet /// Return whether a write operation is aligned and within bounds.