diff --git a/Cargo.lock b/Cargo.lock index 388af15..5850b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,15 @@ dependencies = [ "vcell", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -536,6 +545,7 @@ dependencies = [ name = "lib-bms-protocol" version = "0.1.0" dependencies = [ + "chrono", "embedded-hal 1.0.0", "thiserror", ] diff --git a/lib-bms-protocol/Cargo.toml b/lib-bms-protocol/Cargo.toml index b374162..3b2f071 100644 --- a/lib-bms-protocol/Cargo.toml +++ b/lib-bms-protocol/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = { version = "0.4.44", default-features = false } embedded-hal = "1.0.0" thiserror = { version = "2.0.17", default-features = false } diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 95f8d90..b9c77a5 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -1,8 +1,18 @@ #![no_std] +#[cfg(test)] +extern crate std; + +extern crate chrono; + +mod types; +mod timestamp; + use embedded_hal::i2c::{I2c, Operation, SevenBitAddress}; use thiserror::Error; +use crate::types::Transit_U26; + pub const BMS_I2C_ADDRESS: u8 = 0x55; pub trait BmsReadable { @@ -22,6 +32,8 @@ pub trait BmsWriteable { pub enum BmsProtocolError { #[error("i2c communication failed")] I2cCommunicationError, + #[error("checksum error in received data")] + ChecksumError, } impl From for BmsProtocolError @@ -33,6 +45,15 @@ where } } +impl From for BmsProtocolError { + fn from(error: crate::types::U26Error) -> Self { + match error { + crate::types::U26Error::ChecksumError => Self::ChecksumError, + _ => Self::I2cCommunicationError, + } + } +} + #[derive(Debug)] pub struct BmsSoftwareReset; @@ -68,7 +89,8 @@ impl BmsReadable for ProtocolVersion { Operation::Read(&mut version), ], )?; - Ok(ProtocolVersion(u32::from_be_bytes(version))) + let transit = Transit_U26::from_be_bytes(version)?; + Ok(ProtocolVersion(transit.into())) } } @@ -91,7 +113,8 @@ impl BmsReadable for FirmwareVersion { Operation::Read(&mut version), ], )?; - Ok(FirmwareVersion(u32::from_be_bytes(version))) + let transit = Transit_U26::from_be_bytes(version)?; + Ok(FirmwareVersion(transit.into())) } } @@ -122,9 +145,9 @@ impl BmsReadable for Config { ], )?; Ok(Config { - capacity_mah: u32::from_be_bytes(capacity_mah), - v_full_mv: u32::from_be_bytes(v_full_mv), - v_empty_mv: u32::from_be_bytes(v_empty_mv), + capacity_mah: Transit_U26::from_be_bytes(capacity_mah)?.into(), + v_full_mv: Transit_U26::from_be_bytes(v_full_mv)?.into(), + v_empty_mv: Transit_U26::from_be_bytes(v_empty_mv)?.into(), }) } } @@ -162,11 +185,12 @@ impl BmsReadable for BatteryState { ], )?; Ok(BatteryState { - lifetime_capacity_mah: u32::from_be_bytes(lifetime_capacity_mah), - remaining_capacity_mah: u32::from_be_bytes(remaining_capacity_mah), - current_mv: u32::from_be_bytes(current_mv), - temperature_celcius: i32::from_be_bytes(temperature_celcius), - health_percent: u32::from_be_bytes(health_percent), + lifetime_capacity_mah: Transit_U26::from_be_bytes(lifetime_capacity_mah)?.into(), + remaining_capacity_mah: Transit_U26::from_be_bytes(remaining_capacity_mah)?.into(), + current_mv: Transit_U26::from_be_bytes(current_mv)?.into(), + temperature_celcius: crate::types::Transit_I26::from_be_bytes(temperature_celcius)? + .into(), + health_percent: Transit_U26::from_be_bytes(health_percent)?.into(), }) } } @@ -189,9 +213,9 @@ impl BmsReadable for ChargeInfoWindowSec { Operation::Read(&mut charge_info_window_sec), ], )?; - Ok(ChargeInfoWindowSec(u32::from_be_bytes( - charge_info_window_sec, - ))) + Ok(ChargeInfoWindowSec( + Transit_U26::from_be_bytes(charge_info_window_sec)?.into(), + )) } } @@ -201,11 +225,12 @@ impl BmsWriteable for ChargeInfoWindowSec { I: I2c, { let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs; + let transit = Transit_U26::try_from(self.0)?; i2c_dev.transaction( BMS_I2C_ADDRESS, &mut [ Operation::Write(&(cmd as u32).to_be_bytes()), - Operation::Write(&self.0.to_be_bytes()), + Operation::Write(&transit.to_be_bytes()), ], )?; Ok(()) @@ -245,11 +270,11 @@ impl BmsReadable for ChargeInfo { ], )?; Ok(ChargeInfo { - total_charge_ma: u32::from_be_bytes(total_charge_ma), - total_discharge_ma: u32::from_be_bytes(total_discharge_ma), - max_charge_mw: u32::from_be_bytes(max_charge_mw), - max_discharge_mw: u32::from_be_bytes(max_discharge_mw), - avg_voltage_mv: u32::from_be_bytes(avg_voltage_mv), + total_charge_ma: Transit_U26::from_be_bytes(total_charge_ma)?.into(), + total_discharge_ma: Transit_U26::from_be_bytes(total_discharge_ma)?.into(), + max_charge_mw: Transit_U26::from_be_bytes(max_charge_mw)?.into(), + max_discharge_mw: Transit_U26::from_be_bytes(max_discharge_mw)?.into(), + avg_voltage_mv: Transit_U26::from_be_bytes(avg_voltage_mv)?.into(), }) } } diff --git a/lib-bms-protocol/src/timestamp.rs b/lib-bms-protocol/src/timestamp.rs new file mode 100644 index 0000000..34d01ed --- /dev/null +++ b/lib-bms-protocol/src/timestamp.rs @@ -0,0 +1,373 @@ +use chrono::{DateTime, Utc}; + +use crate::types::{Stored_U26, Transit_U26, U26Error}; + +#[derive(Debug, PartialEq)] +/// Error type for DateTimeUtc wrapper types +pub enum DateTimeUtcError { + Overflow, + InvalidTimestamp, + ConversionError, +} + +impl From for DateTimeUtcError { + fn from(error: U26Error) -> Self { + match error { + U26Error::Overflow => DateTimeUtcError::Overflow, + U26Error::InvalidCodeWord => DateTimeUtcError::InvalidTimestamp, + U26Error::IncorrectibleError => DateTimeUtcError::InvalidTimestamp, + U26Error::ChecksumError => DateTimeUtcError::InvalidTimestamp, + } + } +} + + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +/// Wrapper type for storing DateTime with error correction +/// Uses two Stored_U26 values internally to store 52 bits of milliseconds since Unix epoch +pub struct Store_DateTimeUtc(Stored_U26, Stored_U26); + +impl Store_DateTimeUtc { + /// Maximum timestamp value that can be stored (in milliseconds) + const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; + + /// Create a new Store_DateTimeUtc from a DateTime + pub fn new(dt: DateTime) -> Result { + let timestamp_ms = dt.timestamp_millis(); + if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { + return Err(DateTimeUtcError::Overflow); + } + let timestamp_u64 = timestamp_ms as u64; + let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; + let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; + let stored_low = Stored_U26::try_from(low_26)?; + let stored_high = Stored_U26::try_from(high_26)?; + Ok(Self(stored_low, stored_high)) + } + + /// Get the DateTime value + pub fn into_datetime(self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.into(); + let high_26: u32 = self.1.into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Get the DateTime value (borrowed) + pub fn to_datetime(&self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.clone().into(); + let high_26: u32 = self.1.clone().into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Serialize to big-endian bytes with CRC values + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] + pub fn to_be_bytes(&self) -> [u8; 10] { + let mut bytes = [0u8; 10]; + let high_bytes = self.1.to_be_bytes(); + let high_crc = self.1.calc_crc6(); + let low_bytes = self.0.to_be_bytes(); + let low_crc = self.0.calc_crc6(); + + bytes[0] = high_bytes[0]; + bytes[1] = high_bytes[1]; + bytes[2] = high_bytes[2]; + bytes[3] = high_bytes[3]; + bytes[4] = high_crc; + bytes[5] = low_bytes[0]; + bytes[6] = low_bytes[1]; + bytes[7] = low_bytes[2]; + bytes[8] = low_bytes[3]; + bytes[9] = low_crc; + + bytes + } + + /// Deserialize from big-endian bytes with CRC check + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] + pub fn from_be_bytes(bytes: [u8; 10]) -> Result { + let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let high_crc = bytes[4]; + let low_bytes = [bytes[5], bytes[6], bytes[7], bytes[8]]; + let low_crc = bytes[9]; + + let stored_high = Stored_U26::from_be_bytes(high_bytes, high_crc)?; + let stored_low = Stored_U26::from_be_bytes(low_bytes, low_crc)?; + Ok(Self(stored_low, stored_high)) + } +} + +impl From for DateTime { + fn from(value: Store_DateTimeUtc) -> Self { + value.into_datetime().unwrap() + } +} + +impl TryFrom> for Store_DateTimeUtc { + type Error = DateTimeUtcError; + + fn try_from(value: DateTime) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(value: Store_DateTimeUtc) -> Self { + let low_26: u32 = value.0.into(); + let high_26: u32 = value.1.into(); + ((high_26 as u64) << 26) | (low_26 as u64) + } +} + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +/// Wrapper type for transmitting DateTime with CRC error detection +/// Uses two Transit_U26 values internally to store 52 bits of milliseconds since Unix epoch +pub struct Transit_DateTimeUtc(Transit_U26, Transit_U26); + +impl Transit_DateTimeUtc { + /// Maximum timestamp value that can be transmitted (in milliseconds) + const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; + + /// Create a new Transit_DateTimeUtc from a DateTime + pub fn new(dt: DateTime) -> Result { + let timestamp_ms = dt.timestamp_millis(); + if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { + return Err(DateTimeUtcError::Overflow); + } + let timestamp_u64 = timestamp_ms as u64; + let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; + let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; + let transit_low = Transit_U26::try_from(low_26)?; + let transit_high = Transit_U26::try_from(high_26)?; + Ok(Self(transit_low, transit_high)) + } + + /// Get the DateTime value + pub fn into_datetime(self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.into(); + let high_26: u32 = self.1.into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Get the DateTime value (borrowed) + pub fn to_datetime(&self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.clone().into(); + let high_26: u32 = self.1.clone().into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Serialize to big-endian bytes + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] + pub fn to_be_bytes(&self) -> [u8; 8] { + let mut bytes = [0u8; 8]; + let high_bytes = self.1.to_be_bytes(); + let low_bytes = self.0.to_be_bytes(); + + bytes[0] = high_bytes[0]; + bytes[1] = high_bytes[1]; + bytes[2] = high_bytes[2]; + bytes[3] = high_bytes[3]; + bytes[4] = low_bytes[0]; + bytes[5] = low_bytes[1]; + bytes[6] = low_bytes[2]; + bytes[7] = low_bytes[3]; + + bytes + } + + /// Deserialize from big-endian bytes with CRC check + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] + pub fn from_be_bytes(bytes: [u8; 8]) -> Result { + let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let low_bytes = [bytes[4], bytes[5], bytes[6], bytes[7]]; + + let transit_high = Transit_U26::from_be_bytes(high_bytes)?; + let transit_low = Transit_U26::from_be_bytes(low_bytes)?; + Ok(Self(transit_low, transit_high)) + } +} + +impl From for DateTime { + fn from(value: Transit_DateTimeUtc) -> Self { + value.into_datetime().unwrap() + } +} + +impl TryFrom> for Transit_DateTimeUtc { + type Error = DateTimeUtcError; + + fn try_from(value: DateTime) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(value: Transit_DateTimeUtc) -> Self { + let low_26: u32 = value.0.into(); + let high_26: u32 = value.1.into(); + ((high_26 as u64) << 26) | (low_26 as u64) + } +} + + +#[cfg(test)] +mod timestamp_test { + use super::*; + use chrono::{DateTime, TimeZone, Utc}; + + #[test] + fn store_datetimeutc_basic_operations() { + // Test with Unix epoch + let epoch = Utc.timestamp_millis_opt(0).unwrap(); + let stored = Store_DateTimeUtc::new(epoch).unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, epoch); + + // Test with a specific date (within 52-bit range) + let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_serialization() { + let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let bytes = stored.to_be_bytes(); + + // Deserialize and verify + let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_error_correction() { + let dt = Utc.with_ymd_and_hms(2020, 3, 10, 14, 25, 10).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let mut bytes = stored.to_be_bytes(); + + // Introduce a single bit error in low bytes that can be corrected + bytes[7] ^= 0x01; + let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_overflow() { + // Test with timestamp beyond max value + let max_timestamp = Store_DateTimeUtc::MAX_TIMESTAMP_MS; + let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); + let result = Store_DateTimeUtc::new(dt); + assert!(matches!(result, Err(DateTimeUtcError::Overflow))); + } + + #[test] + fn store_datetimeutc_borrowed_method() { + let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + + // Test the borrowed to_datetime method + let retrieved = stored.to_datetime().unwrap(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_try_from() { + let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); + let stored: Store_DateTimeUtc = dt.try_into().unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_crc_error_detection() { + let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let mut bytes = stored.to_be_bytes(); + + // Introduce multiple bit errors in low bytes that can't be corrected + bytes[5] ^= 0x01; + bytes[6] ^= 0x01; + let result = Store_DateTimeUtc::from_be_bytes(bytes); + assert!(result.is_err()); + } + + #[test] + fn transit_datetimeutc_basic_operations() { + // Test with Unix epoch + let epoch = Utc.timestamp_millis_opt(0).unwrap(); + let transit = Transit_DateTimeUtc::new(epoch).unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, epoch); + + // Test with a specific date (within 52-bit range) + let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_serialization() { + let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let bytes = transit.to_be_bytes(); + + // Deserialize and verify + let deserialized = Transit_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_crc_error_detection() { + let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let mut bytes = transit.to_be_bytes(); + + // Introduce bit errors in low bytes that will cause CRC mismatch + bytes[4] ^= 0x01; + bytes[5] ^= 0x01; + let result = Transit_DateTimeUtc::from_be_bytes(bytes); + assert!(result.is_err()); + } + + #[test] + fn transit_datetimeutc_overflow() { + // Test with timestamp beyond max value + let max_timestamp = Transit_DateTimeUtc::MAX_TIMESTAMP_MS; + let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); + let result = Transit_DateTimeUtc::new(dt); + assert!(matches!(result, Err(DateTimeUtcError::Overflow))); + } + + #[test] + fn transit_datetimeutc_borrowed_method() { + let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + + // Test the borrowed to_datetime method + let retrieved = transit.to_datetime().unwrap(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_try_from() { + let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); + let transit: Transit_DateTimeUtc = dt.try_into().unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, dt); + } +} diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs new file mode 100644 index 0000000..8f6ab68 --- /dev/null +++ b/lib-bms-protocol/src/types.rs @@ -0,0 +1,625 @@ +//! # Base Value Types +//! +//! This module implements multiples types used for storing and transmitting/receiving data with +//! build in error detection/correction. +//! +//! For the purposes in the context of this project the following assumptions where made to determine the required data size. +//! +//! The largest value that could be stored or transmitted would be the running counter for the total amount of charge +//! or discharge that has occured since the beginning of recording. The intended use here is the monitoring of single +//! 12V LiFePO4 batteries which come in sizes up to 300 Ah. Many of the larger scale ones already come with a more advanced +//! BMS, so this is targetting lower tier ones with sizes up to 15 Ah. +//! +//! Modern LiFePO4 battery can easily withstand past 3000 charge cycles with about 365 charge cycles per year correspoding to +//! a charge cycle per day with solar energy. +//! +//! Targeting a 10 year lifespan would result in an absolute maxium of +//! 10,000 mAh * 10 years * 365 days = 53,400,000 mAh of total charge or discharge. +//! +//! Using a u26 would give us a counter for up to 2**26 = 67,108,863 mAh which is sufficient and allows using up to 6 bits for +//! redundancy within the 32bit word width. +//! +//! ## Use cases: +//! +//! ### Storage +//! +//! Storing data on flash for long term history retention might yield errors when flash cells are faulty leading most probably to on +//! bit errors. Ussing a hemming code for storing data (giving us 1bit error correction) requires 5 bits which is easily fits in the 6 bit redundancy data. For extra +//! redundancy a crc sum could be stored additionally to non recoverable errors. +//! +//! ### Transmission +//! +//! During transmission of data via serial interfaces, on bit errors ar less of a concern, here burst errors are more common du to +//! possible interfence on the transmission line. Using hemming codes would be waistul here. Instead a crc code is used for error +//! detection. If an error is detected the client device is expected to rerequest the data. +//! +//! ## CRC Selection +//! +//! Selecting a good crc polynomial requires careful consideration on the use data length and possible room within the code desired +//! code word lenght. For a good summary on CRC tradeoffs refer to this [paper](https://users.ece.cmu.edu/~koopman/roses/dsn04/koopman04_crc_poly_embedded.pdf). +//! For the purposes of this project the following selection was made. +//! +//! CRC-6 using the generator polynomial 0x21 is the best choice for a data length of 26 bits and giving us a hemming distance of +//! 3 for every valid code word. Meaning it is guaranteed that any error up to 3 bits will always be deteced. Beyond that more +//! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code. +//! Meaning the chance of errors going undetected is <2%. + +const U26_VALUE_MASK: u32 = 0xFFFFFFc0; +const U26_MAX_VALUE: u32 = 0x03FFFFFF; +const U26_HEMMING_MASK: u32 = 0x0000003e; + +#[derive(Debug, PartialEq)] +pub enum U26Error { + InvalidCodeWord, + IncorrectibleError, + Overflow, + ChecksumError, +} + +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] +/// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption +/// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in +/// hemming code for one mit error correction. +/// +/// additionally it is advised to store an extra checks sum to protect against multi bit errors +/// +/// this type is stored in u32 in the following form: +/// | 26 bits u26 value | 5 hemming bits | 1 unused extrabit | +pub struct Stored_U26(u32); + +impl Stored_U26 { + pub fn new(value: u32) -> Self { + assert!(value <= U26_MAX_VALUE); + let hemming_code = calc_hemming(value); + Self((value << 6) | ((hemming_code & 0x3F) << 1) as u32) + } + + pub fn calc_crc6(&self) -> u8 { + calc_crc6(self.0) + } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } + + pub fn from_be_bytes(bytes: [u8; 4], crc: u8) -> Result { + let raw_u32 = u32::from_be_bytes(bytes); + // the last bit is expetced to be 0 + if raw_u32 & 0x1 != 0x0 { + return Err(U26Error::InvalidCodeWord); + } + let value = (raw_u32 & U26_VALUE_MASK) >> 6; + let hemming = ((raw_u32 & U26_HEMMING_MASK) >> 1) as u8; + match check_hemming(value, hemming) { + Ok(()) => Ok(Stored_U26(raw_u32)), + Err(correction_data) => { + let (dc, hc) = correction_data.calc_correction_data(); + if check_hemming(value ^ dc, hemming ^ hc).is_err() { + // this case will not occur in our case because + // or codeword length corresponds to 31 and thus a perfect hemming + // code, flipping the correct bit will always produce a valid + // codeword even though the data was not the orginal stored data, + // to secure against this a crc is required + Err(U26Error::IncorrectibleError) + } else { + let value = Self::new(value ^ dc); + if value.calc_crc6() == crc { + Ok(value) + } else { + Err(U26Error::IncorrectibleError) + } + } + } + } + } +} + +impl From for u32 { + fn from(value: Stored_U26) -> Self { + (value.0 & U26_VALUE_MASK) >> 6 + } +} + +impl TryFrom for Stored_U26 { + type Error = U26Error; + + fn try_from(value: u32) -> Result { + if value > U26_MAX_VALUE { + Err(U26Error::Overflow) + } else { + Ok(Self::new(value)) + } + } +} + +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] +/// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur +/// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used +/// +/// this type is stored in u32 in the following form: +/// | 26 bits u26 value | 6 bits crc value | +pub struct Transit_U26(u32); + +impl Transit_U26 { + pub fn new(value: u32) -> Self { + assert!(value <= U26_MAX_VALUE); + let crc6 = calc_crc6(value); + Self((value << 6) | (crc6 & 0x3F) as u32) + } + + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + let raw_val = u32::from_be_bytes(bytes); + let crc = (raw_val & 0x3F) as u8; + let val = raw_val >> 6; + if check_crc6(val, crc) { + Ok(Self(raw_val)) + } else { + Err(U26Error::ChecksumError) + } + } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } +} + +impl From for u32 { + fn from(value: Transit_U26) -> Self { + (value.0 & U26_VALUE_MASK) >> 6 + } +} + +impl TryFrom for Transit_U26 { + type Error = U26Error; + + fn try_from(value: u32) -> Result { + if value > U26_MAX_VALUE { + Err(U26Error::Overflow) + } else { + Ok(Transit_U26::new(value)) + } + } +} + +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] +/// this type is used for transmitting signed 26-bit integer data +/// It wraps the Transit_U26 type to provide i32 support +/// The i32 value is converted to u32 by casting, preserving the bit pattern +/// This ensures proper sign extension when converting between signed and unsigned +/// Valid range: -33554432 to 33554431 (26-bit signed) +pub struct Transit_I26(Transit_U26); + +impl Transit_I26 { + pub fn new(value: i32) -> Result { + // Check if value is within 26-bit signed range + if value < -0x02000000 || value > 0x01FFFFFF { + return Err(U26Error::Overflow); + } + // Extract only the lower 26 bits + let u26_val = (value as u32) & 0x03FFFFFF; + let transit_u26 = Transit_U26::try_from(u26_val)?; + Ok(Self(transit_u26)) + } + + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + let transit_u26 = Transit_U26::from_be_bytes(bytes)?; + Ok(Self(transit_u26)) + } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } +} + +impl From for i32 { + fn from(value: Transit_I26) -> Self { + // Convert u32 back to i32 by casting, preserving the exact bit pattern + let u32_val: u32 = value.0.into(); + // Interpret as signed 26-bit integer + if u32_val & 0x02000000 != 0 { + // Sign-extend to 32 bits + (u32_val as i32) | 0xFC000000u32 as i32 + } else { + u32_val as i32 + } + } +} + +impl TryFrom for Transit_I26 { + type Error = U26Error; + + fn try_from(value: i32) -> Result { + Self::new(value) + } +} + +// only calc hemming code for the first 24 data bits including the 5 hemming bits +// pos | | p1 | p2 | p3 | p4 | p5 | +// 00001 | p1 | o | | | | | +// 00010 | p2 | | o | | | | +// 00011 | d1 | x | x | | | | +// 00100 | p3 | | | o | | | +// 00101 | d2 | x | | x | | | +// 00110 | d3 | | x | x | | | +// 00111 | d4 | x | x | x | | | +// 01000 | p4 | | | | o | | +// 01001 | d5 | x | | | x | | +// 01010 | d6 | | x | | x | | +// 01011 | d7 | x | x | | x | | +// 01100 | d8 | | | x | x | | +// 01101 | d9 | x | | x | x | | +// 01110 | d10 | | x | x | x | | +// 01111 | d11 | x | x | x | x | | +// 10000 | p5 | | | | | o | +// 10001 | d12 | x | | | | x | +// 10010 | d13 | | x | | | x | +// 10011 | d14 | x | x | | | x | +// 10100 | d15 | | | x | | x | +// 10101 | d16 | x | | x | | x | +// 10110 | d17 | | x | x | | x | +// 10111 | d18 | x | x | x | | x | +// 11000 | d19 | | | | x | x | +// 11001 | d20 | x | | | x | x | +// 11010 | d21 | | x | | x | x | +// 11011 | d22 | x | x | | x | x | +// 11100 | d23 | | | x | x | x | +// 11101 | d24 | x | | x | x | x | +// 11110 | d25 | | x | x | x | x | +// 11111 | d26 | x | x | x | x | x | +fn calc_hemming(data_bits: u32) -> u8 { + let p1 = ((data_bits) + ^ (data_bits >> 1) + ^ (data_bits >> 3) + ^ (data_bits >> 4) + ^ (data_bits >> 6) + ^ (data_bits >> 8) + ^ (data_bits >> 10) + ^ (data_bits >> 11) + ^ (data_bits >> 13) + ^ (data_bits >> 15) + ^ (data_bits >> 17) + ^ (data_bits >> 19) + ^ (data_bits >> 21) + ^ (data_bits >> 23) + ^ (data_bits >> 25)) + & 0x1; + let p2 = ((data_bits) + ^ (data_bits >> 2) + ^ (data_bits >> 3) + ^ (data_bits >> 5) + ^ (data_bits >> 6) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 12) + ^ (data_bits >> 13) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x1; + let p3 = ((data_bits >> 1) + ^ (data_bits >> 2) + ^ (data_bits >> 3) + ^ (data_bits >> 7) + ^ (data_bits >> 8) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 14) + ^ (data_bits >> 15) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x1; + let p4 = ((data_bits >> 4) + ^ (data_bits >> 5) + ^ (data_bits >> 6) + ^ (data_bits >> 7) + ^ (data_bits >> 8) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 18) + ^ (data_bits >> 19) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x1; + let p5 = ((data_bits >> 11) + ^ (data_bits >> 12) + ^ (data_bits >> 13) + ^ (data_bits >> 14) + ^ (data_bits >> 15) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 18) + ^ (data_bits >> 19) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x1; + + 0x00 | (p1 as u8) << 0 | (p2 as u8) << 1 | (p3 as u8) << 2 | (p4 as u8) << 3 | (p5 as u8) << 4 +} + +/// if the hemming code does not equal the expected stored hemming value it calculated +/// hemming code will indicate the position of the invalid bit, this type is used +/// to hold the logic to use this information for correcting the read data in case of +/// an error +pub struct HemmingCorrectionValue(u8); + +impl HemmingCorrectionValue { + pub fn new(invalid_hemming: u8) -> Self { + HemmingCorrectionValue(invalid_hemming) + } + + /// calculate xor bit patterns to apply to the data or the stored hemming code + /// returns tuple -> (xor with data, xor with hemming code) + fn calc_correction_data(&self) -> (u32, u8) { + if [1, 2, 4, 8, 16].contains(&self.0) { + (0x00000000, 0x1 << self.0.ilog2()) + } else { + if self.0 == 3 { + (0x1, 0x00) + } else if self.0 < 8 { + (0x1 << self.0 - 3 - 1, 0x00) + } else if self.0 < 16 { + (0x1 << self.0 - 4 - 1, 0x00) + } else { + (0x1 << self.0 - 5 - 1, 0x00) + } + } + } +} + +/// check if hemming code matches the expected value for the provided data +pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValue> { + let h = calc_hemming(data); + if h == hemming { + Ok(()) + } else { + Err(HemmingCorrectionValue::new(h ^ hemming)) + } +} + +/// CRC-6 calculation using polynomial 0x21 (MSB first) +/// +/// This function calculates a 6-bit CRC for a u32 value. +/// The polynomial used is 0x21 in the crc world polynoms +/// are named after the bit pattern with implicit assumption +/// of the trailing 1, thus 0x21 become 0x43 referring to +/// the 0b1000011 pattern. (Don't ask why just accept it …) +/// +/// # Algorithm +/// +/// The CRC is calculated using the standard CRC algorithm with MSB first bit ordering, +/// the inital value is equal to 0x00 and no additional transformat at the end is performed. +/// +/// # Error Detection +/// This CRC provides a Hamming distance of 3, meaning it can detect: +/// - All single-bit errors +/// - All double-bit errors +/// - Random errors with a probability of 2^-6 = 0.015625 of going undetected +pub fn calc_crc6(data: u32) -> u8 { + let mut crc: u8 = 0x00; + for pos in 0..32 { + if ((data >> (31 - pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 { + crc = ((crc << 1) ^ 0x03) & 0x3F; + } else { + crc = (crc << 1) & 0x3F; + } + } + crc +} + +/// CRC-6 checking +/// +/// This function is the mirror piece to the crc6 calculation, it checks a crc value against +/// the data to determine if the crc is valid. +pub fn check_crc6(data: u32, crc: u8) -> bool { + let mut check_crc: u8 = 0x00; + for pos in 0..32 { + if ((data >> (31 - pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 { + check_crc = ((check_crc << 1) ^ 0x03) & 0x3F; + } else { + check_crc = (check_crc << 1) & 0x3F; + } + } + check_crc == crc +} + +#[cfg(test)] +mod base_type_tests { + use super::{ + calc_crc6, calc_hemming, check_crc6, check_hemming, Stored_U26, Transit_I26, Transit_U26, + U26_MAX_VALUE, + }; + use crate::types::U26Error; + + #[test] + fn hemming_code_generation() { + assert_eq!(calc_hemming(0x000), 0b00000); + assert_eq!(calc_hemming(0x001), 0b00011); + assert_eq!(calc_hemming(0x002), 0b00101); + assert_eq!(calc_hemming(0x003), 0b00110); + assert_eq!(calc_hemming(0x011), 0b01010); + } + + #[test] + fn hemming_failure() { + assert!(check_hemming(0x011, 0b00011).is_err()); + } + + #[test] + fn hemming_correct_invalid_value() { + let invalid_data = 0x011; + let hemming = 0b00011; + let correction = check_hemming(invalid_data, hemming).unwrap_err(); + let (data_correction, hemming_correction) = correction.calc_correction_data(); + assert_eq!(hemming_correction, 0); + assert_eq!(data_correction, 0x10); + let valid_data = invalid_data ^ data_correction; + assert!(check_hemming(valid_data, hemming).is_ok()); + } + + #[test] + fn hemming_correct_invalid_hemming_code() { + let data = 0x001; + let invalid_hemming = 0b00001; + let correction = check_hemming(data, invalid_hemming).unwrap_err(); + let (data_correction, hemming_correction) = correction.calc_correction_data(); + assert_eq!(hemming_correction, 0b00010); + assert_eq!(data_correction, 0x00); + let valid_hemming = invalid_hemming ^ hemming_correction; + assert!(check_hemming(data, valid_hemming).is_ok()); + } + + #[test] + fn full_onebit_value_correction_sweep() { + for val in 1..U26_MAX_VALUE { + let hemming = calc_hemming(val); + for i in 0..26 { + let bit_error = 0x1 << i; + let invalid_value = val ^ bit_error; + //println!("0x{val:x}, 0x{bit_error:x}, 0x{invalid_value:x}, 0b{hemming:b}, 0b{:b}", calc_hemming(invalid_value)); + let correction = check_hemming(invalid_value, hemming).unwrap_err(); + let (dc, _) = correction.calc_correction_data(); + assert_eq!(invalid_value ^ dc, val); + } + } + } + + #[test] + fn full_hemming_code_error_sweep() { + let data = 0x123456; + let hemming = calc_hemming(data); + for i in 0..5 { + let bit_error = 0x1 << i; + let invalid_hemming = hemming ^ bit_error; + let correction = check_hemming(data, invalid_hemming).unwrap_err(); + let (_, hc) = correction.calc_correction_data(); + assert_eq!(invalid_hemming ^ hc, hemming); + } + } + + #[test] + fn stored_u26_to_from_bytes() { + let data: Stored_U26 = 0x123456.try_into().unwrap(); + let crc = data.calc_crc6(); + let mut bytes = data.to_be_bytes(); + assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); + // introduce 1 bit error + bytes[2] = bytes[2] ^ 0x01; + assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); + // introduce second bit error + bytes[0] = bytes[0] ^ 0x01; + assert_eq!( + Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), + U26Error::IncorrectibleError + ); + } + + #[test] + fn crc6_simple_data() { + assert_eq!(calc_crc6(0x0000000), 0x00); + assert_eq!(calc_crc6(0x0000001), 0x03); + assert_eq!(calc_crc6(0x0000043), 0x00); + assert_eq!(calc_crc6(0x3ffffff), 0x06); + } + + #[test] + fn crc6_consistency() { + // Test that calc crc6 and check crc6 agree on validity + let data = 0x123456; + let crc = calc_crc6(data); + assert!(check_crc6(data, crc)) + } + + #[test] + fn transit_u26_to_from_bytes() { + let data1 = Transit_U26::new(0x123456); + // check that to from bytes conversion produces the same value + let mut bytes = data1.to_be_bytes(); + assert_eq!(data1, Transit_U26::from_be_bytes(bytes).unwrap()); + // test that changed data leads to checksum error + bytes[1] = 0x00; + assert!(Transit_U26::from_be_bytes(bytes).unwrap_err() == U26Error::ChecksumError) + } + + #[test] + fn crc6_bit_error_detection() { + // Test that single bit errors are detected + let original_data = 0x123456; + let original_crc = calc_crc6(original_data); + + // Flip each bit and verify CRC changes + for bit_pos in 0..26 { + let mut corrupted_data = original_data; + let mask = 1 << (25 - bit_pos); // Flip specific bit + corrupted_data ^= mask; + + // With high probability, CRC should change + assert!(!check_crc6(corrupted_data, original_crc)); + } + } + + #[test] + fn transit_i26_positive_values() { + let data = Transit_I26::new(12345).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 12345); + } + + #[test] + fn transit_i26_negative_values() { + let data = Transit_I26::new(-12345).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), -12345); + } + + #[test] + fn transit_i26_zero() { + let data = Transit_I26::new(0).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 0); + } + + #[test] + fn transit_i26_max_positive() { + let data = Transit_I26::new(0x01FFFFFF).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 0x01FFFFFF as i32); + } + + #[test] + fn transit_i26_max_negative() { + let data = Transit_I26::new(-0x01FFFFFF).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), -0x01FFFFFF as i32); + } + + #[test] + fn transit_i26_checksum_error() { + let data = Transit_I26::new(12345).unwrap(); + let mut bytes = data.to_be_bytes(); + bytes[1] ^= 0x01; // Corrupt one byte + assert!(Transit_I26::from_be_bytes(bytes).is_err()); + } +}