Skip to main content

async_ltc681x/
lib.rs

1//! **This is an `no_std` async device driver for the `LTC681x` Series of Battery AFE (Analog frontend)**
2//!
3//! For details check the `README.md` in the [`repo`](https://github.com/emkanea/ltc681x).
4//!
5//! # Basic Structure
6//!
7//! ## How to use this crate
8//! First you need to create the [`Driver Part`](crate::driver::communication::Ltc681xSpi) via its [`new function`](crate::driver::communication::Ltc681xSpi::new).
9//! This method consumes a SPI Port implementing the [`SpiBus`](embedded_hal_async::spi::SpiBus) trait,
10//! the chip select Output Pin implementing the [`OutputPin`](embedded_hal::digital::OutputPin) trait and
11//! the MISO Pin as an additional Input Pin implementing the [`InputPin`](embedded_hal::digital::InputPin) trait.
12//! For the SPI Port and the CS Pin its straight forward.
13//! Use device specific MCU HAL implementing the [`embedded-hal trait`](embedded_hal) to create the SPI Port and the CS Output Pin.
14//!
15//! The reuse of the MISO Pin as an Input Pin is more tricky as it collides with all HAL implementations that model hardware resource with Rusts ownership model.
16//! The MISO GPIO Pin is consumed by the SPI Port constructor and is not usable as a free standing Input Pin or vice versa.
17//! Before we talk about possible solutions and its safety, we need to understand why we need to reuse the MISO Pin as an Input Pin and why this hassle is worth it.
18//!
19//! ### Timing and Polling
20//! The `LTC681x` series performs ADC conversions on command from the host.
21//! The maximum time this conversion takes to finish is specified in the datasheet.
22//! So it is possible to just hard-code the necessary delay in your program before you read back the results.
23//! This has two weaknesses:
24//! 1. There are a lot of different converion modes for different channel configurations.
25//!    While this is all specified it is ofc a source of error that a wrong timing is used and we try to read back results premature.
26//!    For this approach you need a appropriate time source in the driver or let the caller deal with it.
27//! 2. Performance wise it is not optimal to always wait the maximum specified time plus some overhead for your piece of mind.
28//!    In a big High-Voltage batterie application, where many devices are chained together, acquiring all measurement results for all batteries while meeting possible
29//!    timing requirements (these requirements are also typically `FuSa` related Safety Requirements) of the application regarding data acquisition rates might become a challenge.
30//!    So reducing unnecessary waiting time might save you some trouble later!
31//!
32//! To adress this the LTC681X series allows to poll the state of the conversion via the MISO line.
33//! Details about the specifics how this polling works are out of scope, but the Input State of the MISO line needs to be availble for that.
34//! To conclude this digression: This is the reason why we have to use this Pin a second time as an Input Pin for the driver.
35//!
36//! ### Circumvent the ownership model and safety
37//!
38//! How do you circumvent the ownership model of your HAL?
39//! Most of HALs (limited research went into this statement) provide an unsafe way to clone a GPIO Pin.
40//! By cloning the MISO GPIO Pin before creating the SPI instance another Input Pin can be created.
41//!
42//! **Example from embassy-stm32**
43//! ```ignore
44//! let p = embassy_stm32::init(config);
45//! // SAFETY: Cloned Pin is used by ltc681x device driver
46//! // we use the same Pin as MISO for the SPI also used by the device driver
47//! // Author states the this is safe!
48//! let miso_input_pin = InputPin::new(unsafe{p.PB14.clone_unchecked()});
49//! let cs_pin = OutputPin::new(p.PB15); // Pin used as CS
50//! // Omitted other parameters of SPI new
51//! let spi = Spi::new(p.PB14);
52//! let ltc681x_spi = Ltc681xSpi::new(spi, cs_pin, miso_input_pin);
53//!
54//! // Now that we have finally created the driver part we can create the final device
55//! let ltc681x = Ltc681x::new(ltc681x_spi);
56//!
57//! // After some hassle to create this device driver it is very easy to use!
58//! let cells = ltc681x.convert_cells(Adcv::default()).await?;
59//! ```
60//! For details about command specifiers like [`Adcv`](crate::driver::command::Adcv) or [`Adax`](crate::driver::command::Adax) see [`Builder pattern for commands`](#builder-pattern-for-commands).
61//!
62//! **Safety**
63//!
64//! Internally the device driver only uses the MISO line as an Input during conversion waiting.
65//! No data is read back during that period until the conversion is done.
66//! So it is safe to provide another way to use the same hardware resource as it will not used at the same time.
67//!
68//! **Remark regarding Timeout detection**
69//! The driver does not implement a timeout detection.
70//! In case of serious hardware defects the async operations will never yield `Ready`.
71//! It is advised to supervise the timing behavior via software timeout detection in the application or via watchdog
72//! ## Modules
73//! This crate is split in two parts:
74//! 1. [`device Module`](crate::device) exposes the high level chip functionality like [`convert_cells`](crate::device::Device::convert_cells) or [`convert_gpios`](crate::device::Device::convert_gpios)
75//! 2. [`driver Module`](crate::driver) implements the low level SPI protocol to communicate with the chip
76//!
77//! Why?
78//! This allows for layered testing of all functionallity.
79//! The [`driver`] part is generic over the [`SpiBus`](embedded_hal_async::spi::SpiBus) and can be test by providing a SPI mock.
80//!
81//! The [`device`] part is generic over the [`Ltc681xDriver`](crate::driver::Driver) Trait.
82//! This allows to implement a `MockDriver` Part for convenient manipulation of test data.
83//! Otherwise on every testing layer we would always reach down to SPI transactions.
84//! In a safety critical application this is a quite common strategy to allow testing on every level of the software to further increase the trust.
85//! A user of this crate can directly use the concrete implementations of [`Device`](crate::device::Device) and [`Driver`](crate::driver::Driver) in the form of
86//! [`Ltc681xSpi`](crate::driver::communication::Ltc681xSpi) and [`Ltc681x`](crate::device::Ltc681x).
87//! The associated traits can be used for host based testing with more convenient mock implementations.
88//!
89//! ## Builder pattern for Commands
90//! Ltc681x uses a 11Bit Command Header as the two first bytes of every SPI sequence. Every command has a basic command bit sequence
91//! with additional optional configuration bits (see datasheet for details). Every command can be generated by [`default`](Default::default) method.
92//! Configuration is set by chaining functions like [`MDOptions`](crate::driver::command::HasMDOption::set_md_option).
93//! Possible configurations are implemented as traits for the spefic command and are listed in the Trait implementation part of the specific command.
94//!
95//!
96//! **Example**
97//! ```ignore
98//! // Adcv defaults to AdcChannelConfig::AllCells but can be reconfigured by set_channel_config
99//! // Adcv implemented the trait HasCellChannelConfig
100//! let cells =
101//!     ltc681x.convert_cells(Adcv::default().set_channel_config(AdcChannelConfig::Cell1_7_13));
102//! let gpio =
103//!     ltc681x.convert_gpios(Adax::default().set_channel_config(GpioChannelConfig::Gpio1_6));
104//! ```
105//! ## Defining the number of devices
106//! Ltc61x series can be used as a single device to monitor a single battery pack or it can be chained in daisy chain configuration to monitor several battery packs without need for a MCU in every battery pack.
107//! For typical application check the datasheet.
108//!
109//! The [`Ltc681x`](crate::device::Ltc681x) structure has a const generic parameter named `NUM_OF_DEVICES`. This defines the expected number of devices connected to the daisy chain.
110//! If there a less devices physically connected to the SPI daisy chain then specified by the paramete this will result in methods returning an CRC error.
111//! If more are connected this will be not detected as any device that exceeds the given number will not be read from/written to.
112//!
113//! Every interaction with one devices will always interact with all devices in the daisy chain.
114//! This is part of the daisy chain architecture where you "communicate through other device"
115//! Read/Write functions work with arrays generic over the `NUM_OF_DEVICES` generic parameter.
116//! The array index indicates to which physical device the payload is mapped.
117//!
118//! **Attention:**
119//!
120//! While reading the index 0 corresponds to the first device in the daisy chain (the one that is closest to the MCU) and last index to the last device.
121//! For writing it is the opposite way around and the first index is pushed to the last device
122//! As payload is "shiftet through the devices" this makes sense. Disclaimer: I havent tested this fully
123//! Might be useful to implement the logic in a way that the payload is reversed for writing so index 0 is always device 0
124//!
125//! # Feature flags
126//! `async-ltc681x` supports three features to define the used chip variant.
127//! - `ltc6811` - Deactivates all functions available only for the `LTC6812`or `LTC6813` variant
128//! - `ltc6812` - Extends the crate by functions and registers available for `LTC6812`
129//! - `ltc6813` - Extends the crate even further with the functions and registers available for `LTC6813`
130//!
131//! Default feature is set to `ltc6811` so the crate compiles without any feature flags.
132//! Default feature must be disabled if another `ltc6812` or `ltc6813` is enabled.
133//!
134//! docs.rs are build with the `ltc6813` feature flag active.
135#![cfg_attr(not(test), no_std)]
136#![allow(async_fn_in_trait)]
137#![warn(clippy::pedantic)]
138// To allow for auxa,auxb cva,cvb etc.
139#![allow(clippy::similar_names)]
140#[cfg(all(feature = "ltc6811", feature = "ltc6812"))]
141compile_error!("6811 and 6812 cant be both active");
142
143#[cfg(all(feature = "ltc6811", feature = "ltc6813"))]
144compile_error!("6811 and 6813 cant be both active");
145
146#[cfg(all(feature = "ltc6812", feature = "ltc6813"))]
147compile_error!("6812 and 6813 cant be both active");
148
149#[cfg(not(any(feature = "ltc6811", feature = "ltc6812", feature = "ltc6813")))]
150compile_error!("no chip feature active - define ltc6811 or ltc6812 or ltc6813");
151
152/// High-level chip functions
153pub mod device;
154
155/// Basic SPI Driver implementation
156pub mod driver;