lilkaoxide/
adcbattery.rs

1use esp_hal::Blocking;
2use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
3use esp_hal::peripherals::{ADC1, GPIO3};
4
5/// ADC channel configuration for the battery-monitoring pin (GPIO3 / ADC1).
6///
7/// Passed as part of [`crate::LilkaConfiguration`] to [`crate::Lilka::init`].
8///
9/// # Attenuation guide
10///
11/// | Variant | Full-scale voltage | Typical use |
12/// |---|---|---|
13/// | `_0dB` | ~1.1 V | Low-voltage signals |
14/// | `_2p5dB` | ~1.5 V | — |
15/// | `_6dB` | ~2.2 V | — |
16/// | `_11dB` | ~3.1 V | **Li-Po battery** (default) |
17///
18/// The Lilka battery circuit uses a 100 kΩ / 33 kΩ voltage divider, so the
19/// full battery voltage (~4.2 V) is scaled to ~1.03 V before reaching GPIO3.
20/// With 11 dB attenuation the ADC can safely measure the full charge range.
21pub struct AdcCfg {
22    /// Attenuation applied to the ADC input channel.  Use `Attenuation::_11dB`
23    /// (the default) to cover the complete Li-Po discharge range.
24    pub attenuation: Attenuation,
25}
26
27type AdcMode = Blocking;
28
29/// Battery voltage monitor using the ESP32-S3 ADC1 peripheral.
30///
31/// Created automatically by [`crate::Lilka::init`] and available as
32/// [`crate::Lilka::adc_battery`].
33///
34/// The hardware path is:
35/// ```text
36/// Li-Po cell → 100 kΩ → GPIO3 (ADC1 CH2) → ESP32-S3 ADC1
37///                  └── 33 kΩ ──┘
38/// ```
39/// The voltage divider ratio is `33 / (100 + 33) ≈ 0.248`, so the ADC sees
40/// roughly 0.25× the actual battery voltage.
41///
42/// # Example — polling battery level
43///
44/// ```rust,no_run
45/// use lilkaoxide::prelude::*;
46///
47/// # fn game_loop(mut lilka: Lilka) {
48/// loop {
49///     let mv = lilka.adc_battery.read_voltage_mv();
50///     if mv < 3_300 {
51///         defmt::warn!("Low battery: {} mV", mv);
52///     }
53///     // ... game logic
54/// }
55/// # }
56/// ```
57pub struct Adcbattery<'d> {
58    adc: Adc<'d, ADC1<'d>, AdcMode>,
59    pin: AdcPin<GPIO3<'d>, ADC1<'d>>,
60}
61
62pub fn init_adcbattery<'d>(
63    adcpin: GPIO3<'d>,
64    adc_cfg: AdcCfg,
65    adcp: esp_hal::peripherals::ADC1<'d>,
66) -> Adcbattery<'d> {
67    let mut adc1_config = AdcConfig::new();
68    let pin = adc1_config.enable_pin(adcpin, adc_cfg.attenuation);
69    let adc = Adc::new(adcp, adc1_config);
70
71    Adcbattery { adc, pin }
72}
73
74impl<'d> Adcbattery<'d> {
75    /// Reads the raw ADC sample from the battery pin.
76    ///
77    /// Returns a 12-bit value in the range **0 – 4095** where:
78    /// * `0` → 0 V at the ADC input (battery deeply discharged / disconnected).
79    /// * `4095` → full-scale voltage for the configured attenuation (~3.1 V at 11 dB).
80    ///
81    /// Prefer [`Adcbattery::read_voltage_mv`] for human-readable values.
82    ///
83    /// # Example
84    ///
85    /// ```rust,no_run
86    /// # fn example(mut adc: lilkaoxide::Adcbattery<'static>) {
87    /// let raw: u16 = adc.read();
88    /// defmt::info!("Raw ADC: {}", raw);
89    /// # }
90    /// ```
91    pub fn read(&mut self) -> u16 {
92        self.adc.read_blocking(&mut self.pin)
93    }
94
95    /// Reads the battery voltage in **millivolts** (mV).
96    ///
97    /// Applies the voltage-divider formula to convert the raw ADC reading into
98    /// the actual battery terminal voltage:
99    ///
100    /// ```text
101    /// V_bat = (raw / 4095) × 3.1 V / (33 / (100 + 33))
102    ///       = (raw / 4095) × 11.73 V          [in volts]
103    /// ```
104    ///
105    /// Then multiplied by 1 000 to yield millivolts.
106    ///
107    /// # Accuracy notes
108    ///
109    /// * The formula assumes `Attenuation::_11dB` (full-scale ≈ 3.1 V) and the
110    ///   fixed 100 kΩ / 33 kΩ divider on the Lilka PCB.
111    /// * Typical Li-Po range: **3 000 mV** (empty) – **4 200 mV** (full charge).
112    /// * ADC non-linearity may introduce ±50–100 mV error near the rails.
113    ///
114    /// # Example
115    ///
116    /// ```rust,no_run
117    /// # fn example(mut adc: lilkaoxide::Adcbattery<'static>) {
118    /// let mv = adc.read_voltage_mv();
119    /// // Rough battery percentage (linear approximation)
120    /// let pct = ((mv.saturating_sub(3_000)) as u32 * 100 / 1_200) as u8;
121    /// defmt::info!("Battery: {} mV (~{}%)", mv, pct);
122    /// # }
123    /// ```
124    pub fn read_voltage_mv(&mut self) -> u16 {
125        let raw = self.read() as f32;
126        ((raw / 4095.0) * 3.1 / (100.0 / (33.0 + 100.0)) * 1000.0) as u16
127    }
128}