umsh_ulcp/
battery_gauge_config.rs

1//! On-demand BQ27220 configuration snapshot. Integer values are unsigned PUI;
2//! calibration F4 bit patterns use four LE octets and remain uninterpreted.
3use crate::{Status, pui};
4
5pub const FORMAT: u32 = 1;
6pub const VERSION: u32 = 1;
7
8pub struct Field {
9    pub name: &'static str,
10    pub address: u16,
11    pub width: usize,
12    pub unit: &'static str,
13}
14
15macro_rules! fields {
16    ($(($name:literal, $address:literal, $width:literal, $unit:literal)),* $(,)?) => {
17        pub const FIELDS: &[Field] = &[$(Field { name: $name, address: $address, width: $width, unit: $unit }),*];
18    };
19}
20
21// TI SLUUBD4A table 3-2: RAM locations only. Ordering is part of version 1.
22fields![
23    ("battery_id", 0x929a, 1, "bits"),
24    ("operation_config_a", 0x9206, 2, "bits"),
25    ("operation_config_b", 0x9208, 2, "bits"),
26    ("cedv_config", 0x929b, 2, "bits"),
27    ("full_capacity", 0x929d, 2, "mAh"),
28    ("design_capacity", 0x929f, 2, "mAh"),
29    ("design_voltage", 0x92a3, 2, "mV"),
30    ("termination_voltage_margin", 0x92a5, 2, "mV"),
31    ("charging_voltage", 0x91fd, 2, "mV"),
32    ("taper_current", 0x9201, 2, "mA"),
33    ("discharge_detection", 0x9228, 2, "mA"),
34    ("charge_detection", 0x922a, 2, "mA"),
35    ("quit_current", 0x922c, 2, "mA"),
36    ("battery_low", 0x9251, 2, "0.01%"),
37    ("learning_low_temp", 0x925b, 1, "raw"),
38    ("overload_current", 0x9264, 2, "mA"),
39    ("near_full", 0x926b, 2, "mAh"),
40    ("reserve_capacity", 0x926d, 2, "mAh"),
41    ("charge_efficiency", 0x926f, 1, "%"),
42    ("discharge_efficiency", 0x9270, 1, "%"),
43    ("fixed_edv0", 0x92b4, 2, "mV"),
44    ("edv0_hold", 0x92b6, 1, "s"),
45    ("fixed_edv1", 0x92b7, 2, "mV"),
46    ("edv1_hold", 0x92b9, 1, "s"),
47    ("fixed_edv2", 0x92ba, 2, "mV"),
48    ("edv2_hold", 0x92bc, 1, "s"),
49    ("emf", 0x92a7, 2, "raw"),
50    ("c0", 0x92a9, 2, "raw"),
51    ("r0", 0x92ab, 2, "raw"),
52    ("t0", 0x92ad, 2, "raw"),
53    ("r1", 0x92af, 2, "raw"),
54    ("tc", 0x92b1, 1, "raw"),
55    ("c1", 0x92b2, 1, "raw"),
56    ("age_factor", 0x92b3, 1, "raw"),
57    ("flag_config_a", 0x927f, 2, "bits"),
58    ("flag_config_b", 0x9281, 1, "bits"),
59    ("full_set_voltage", 0x9288, 2, "mV"),
60    ("full_clear_voltage", 0x928a, 2, "mV"),
61    ("full_set_soc", 0x928c, 1, "%"),
62    ("full_clear_soc", 0x928d, 1, "%"),
63    ("smoothing_config", 0x9271, 1, "bits"),
64    ("cc_gain", 0x9184, 4, "F4 bits"),
65    ("cc_delta", 0x9188, 4, "F4 bits"),
66    ("current_deadband", 0x91de, 1, "mA"),
67    ("cc_deadband", 0x91df, 1, "294 nV"),
68    ("voltage_0_dod", 0x92bd, 2, "mV"),
69    ("voltage_50_dod", 0x92c7, 2, "mV"),
70    ("voltage_100_dod", 0x92d1, 2, "mV"),
71];
72
73pub const MAX_ENCODED: usize = 2 + FIELDS.len() * 3 + 2;
74
75const fn offset(index: usize) -> usize {
76    let mut result = 0;
77    let mut i = 0;
78    while i < index {
79        result += FIELDS[i].width;
80        i += 1;
81    }
82    result
83}
84pub const RAW_LEN: usize = offset(FIELDS.len());
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct Config {
88    // Retain hardware-width bytes rather than widening every field in all the
89    // embedded session/future copies of a battery sample.
90    data: [u8; RAW_LEN],
91}
92
93impl Default for Config {
94    fn default() -> Self {
95        Self { data: [0; RAW_LEN] }
96    }
97}
98
99impl Config {
100    pub fn taper_current_ma(&self) -> u16 {
101        self.value(9) as u16
102    }
103
104    pub fn value(&self, index: usize) -> u32 {
105        let start = offset(index);
106        self.data[start..start + FIELDS[index].width]
107            .iter()
108            .fold(0u32, |v, b| (v << 8) | u32::from(*b))
109    }
110
111    pub fn values(&self) -> impl Iterator<Item = u32> + '_ {
112        (0..FIELDS.len()).map(|i| self.value(i))
113    }
114
115    pub fn set(&mut self, index: usize, value: u32) -> Result<(), Status> {
116        let field = FIELDS.get(index).ok_or(Status::INVALID_ARGUMENT)?;
117        if field.width < 4 && value >= 1 << (8 * field.width) {
118            return Err(Status::INVALID_ARGUMENT);
119        }
120        let start = offset(index);
121        self.data[start..start + field.width]
122            .copy_from_slice(&value.to_be_bytes()[4 - field.width..]);
123        Ok(())
124    }
125
126    pub fn encode(&self, out: &mut [u8]) -> Result<usize, Status> {
127        let mut used = pui::encode(FORMAT, out).map_err(|_| Status::NOMEM)?;
128        used += pui::encode(VERSION, &mut out[used..]).map_err(|_| Status::NOMEM)?;
129        for (field, value) in FIELDS.iter().zip(self.values()) {
130            if field.width == 4 {
131                out.get_mut(used..used + 4)
132                    .ok_or(Status::NOMEM)?
133                    .copy_from_slice(&value.to_le_bytes());
134                used += 4;
135            } else {
136                if value >= 1 << (8 * field.width) {
137                    return Err(Status::INVALID_ARGUMENT);
138                }
139                used += pui::encode(value, &mut out[used..]).map_err(|_| Status::NOMEM)?;
140            }
141        }
142        Ok(used)
143    }
144
145    pub fn decode(mut bytes: &[u8]) -> Result<Self, Status> {
146        fn next(bytes: &mut &[u8]) -> Result<u32, Status> {
147            let (value, used) = pui::decode(bytes).map_err(|_| Status::PARSE_ERROR)?;
148            *bytes = &bytes[used..];
149            Ok(value)
150        }
151        if next(&mut bytes)? != FORMAT || next(&mut bytes)? != VERSION {
152            return Err(Status::UNIMPLEMENTED);
153        }
154        let mut result = Self::default();
155        for (index, field) in FIELDS.iter().enumerate() {
156            let value = if field.width == 4 {
157                let value = u32::from_le_bytes(
158                    bytes
159                        .get(..4)
160                        .ok_or(Status::PARSE_ERROR)?
161                        .try_into()
162                        .unwrap(),
163                );
164                bytes = &bytes[4..];
165                value
166            } else {
167                next(&mut bytes)?
168            };
169            result.set(index, value).map_err(|_| Status::PARSE_ERROR)?;
170        }
171        if !bytes.is_empty() {
172            return Err(Status::PARSE_ERROR);
173        }
174        Ok(result)
175    }
176
177    pub fn cedv_config(&self) -> u32 {
178        self.value(3)
179    }
180}
181
182impl core::fmt::Display for Config {
183    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
184        writeln!(f, "BQ27220 configuration v1")?;
185        for (field, value) in FIELDS.iter().zip(self.values()) {
186            if field.unit.contains("bits") {
187                writeln!(f, "    {}: {value:#x} ({})", field.name, field.unit)?;
188            } else {
189                writeln!(f, "    {}: {value} {}", field.name, field.unit)?;
190            }
191        }
192        write!(
193            f,
194            "    fcc_limit: {}; independent_charger: {}; edv_compensation: {}",
195            self.cedv_config() & 0x100 != 0,
196            self.cedv_config() & 0x10 != 0,
197            self.cedv_config() & 8 != 0
198        )
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    #[test]
206    fn compact_snapshot_rejects_truncation_unknown_versions_and_trailing_data() {
207        let mut value = Config::default();
208        value.set(3, 0x102a).unwrap();
209        value.set(4, 1372).unwrap();
210        value.set(5, 1500).unwrap();
211        value.set(41, 0x3f2c3d70).unwrap();
212        let mut bytes = [0; MAX_ENCODED];
213        let len = value.encode(&mut bytes).unwrap();
214        assert_eq!(&bytes[..8], &[1, 1, 0, 0, 0, 0xaa, 0x20, 0xdc]);
215        assert_eq!(Config::decode(&bytes[..len]), Ok(value));
216        for end in 0..len {
217            assert!(Config::decode(&bytes[..end]).is_err());
218        }
219        assert!(Config::decode(&bytes[..len + 1]).is_err());
220        bytes[1] = 2;
221        assert_eq!(Config::decode(&bytes[..len]), Err(Status::UNIMPLEMENTED));
222    }
223}