umsh_bsp_t1000e/
temperature.rs

1//! T1000-E NTC on P0.31/AIN7. The power task owns its ADC and shared enables.
2//!
3//! Divider: NTC above the sense node, 8.25 kΩ below it. Resistance data and
4//! the min(VBAT, 3.3 V) supply model come from MeshCore's T1000-E driver:
5//! https://github.com/meshcore-dev/MeshCore/blob/main/variants/t1000-e/t1000e_sensors.cpp
6//! The supply model is an estimate, particularly near regulator dropout.
7
8// Resistance in ohms at each whole degree Celsius from -30 through 105.
9const OHMS: [u32; 136] = [
10    113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677, 68237, 64991, 61919,
11    59011, 56258, 53650, 51178, 48835, 46613, 44506, 42506, 40600, 38791, 37073, 35442, 33892,
12    32420, 31020, 29689, 28423, 27219, 26076, 24988, 23951, 22963, 22021, 21123, 20267, 19450,
13    18670, 17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554, 12081, 11628,
14    11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624, 8315, 8018, 7734, 7461, 7199, 6948, 6707,
15    6475, 6253, 6039, 5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301, 4161, 4026, 3896,
16    3771, 3651, 3535, 3423, 3315, 3211, 3111, 3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364,
17    2294, 2228, 2163, 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716, 1669, 1622, 1578, 1535, 1493,
18    1452, 1413, 1375, 1338, 1303, 1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999, 974,
19    949, 925, 902, 880, 858,
20];
21
22/// Convert a fresh 14-bit SAADC sample (3.6 V full scale) and VBAT in mV
23/// to tenths of a kelvin. Open/short circuits and values beyond the table
24/// are unavailable, never clamped into plausible endpoint temperatures.
25pub fn from_adc(raw: i16, battery_mv: u16) -> Option<u16> {
26    if raw <= 0 || raw >= 16384 || !(2000..=5000).contains(&battery_mv) {
27        return None;
28    }
29    let node = u64::from(raw as u16) * 3600;
30    let supply = u64::from(battery_mv.min(3300)) * 16384;
31    if node >= supply {
32        return None;
33    }
34    from_resistance(8250 * (supply - node), node)
35}
36
37// Keep the resistance as a fraction to avoid rounding it before interpolation.
38fn from_resistance(numerator: u64, denominator: u64) -> Option<u16> {
39    if numerator > u64::from(OHMS[0]) * denominator
40        || numerator < u64::from(OHMS[135]) * denominator
41    {
42        return None;
43    }
44    for (index, pair) in OHMS.windows(2).enumerate() {
45        let cold = u64::from(pair[0]) * denominator;
46        let hot = u64::from(pair[1]) * denominator;
47        if numerator >= hot {
48            // 243.15 K at index zero. Round only the final 0.1 K value.
49            let span = cold - hot;
50            let twice_tenths = (4863 + index as u64 * 20) * span + 20 * (cold - numerator);
51            return Some(((twice_tenths + span) / (2 * span)) as u16);
52        }
53    }
54    None
55}
56
57#[cfg(target_os = "none")]
58mod sampling {
59    use core::sync::atomic::{AtomicU32, Ordering};
60    use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, signal::Signal};
61
62    pub(crate) static REQUEST: Signal<ThreadModeRawMutex, u32> = Signal::new();
63    pub(crate) static REPLY: Signal<ThreadModeRawMutex, (u32, Option<u16>)> = Signal::new();
64    static NEXT: AtomicU32 = AtomicU32::new(0);
65
66    /// One caller at a time. Canceling the wait does not cancel ADC work;
67    /// sequence numbers prevent its late reply satisfying the next request.
68    pub async fn sample() -> Option<u16> {
69        let id = NEXT.fetch_add(1, Ordering::Relaxed);
70        REPLY.reset();
71        REQUEST.signal(id);
72        embassy_time::with_timeout(embassy_time::Duration::from_secs(2), async {
73            loop {
74                let (reply_id, value) = REPLY.wait().await;
75                if reply_id == id {
76                    return value;
77                }
78            }
79        })
80        .await
81        .ok()
82        .flatten()
83    }
84}
85#[cfg(target_os = "none")]
86pub use sampling::sample;
87#[cfg(target_os = "none")]
88pub(crate) use sampling::{REPLY, REQUEST};
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn temperature_table_endpoints_and_every_knot() {
96        for (i, ohms) in OHMS.iter().enumerate() {
97            assert_eq!(
98                from_resistance(u64::from(*ohms), 1),
99                Some(2432 + i as u16 * 10)
100            );
101        }
102        assert_eq!(from_resistance(113348, 1), None);
103        assert_eq!(from_resistance(857, 1), None);
104        // Halfway between 25 and 26 C, including the Kelvin offset.
105        assert_eq!(from_resistance(19634, 2), Some(2987));
106    }
107
108    #[test]
109    fn temperature_adc_supply_and_faults() {
110        assert_eq!(from_adc(6790, 4000), Some(2982));
111        assert_eq!(from_adc(6173, 3000), Some(2982));
112        for raw in [i16::MIN, -1, 0, 1, 15018, 16383, 16384, i16::MAX] {
113            assert_eq!(from_adc(raw, 4000), None, "raw {raw}");
114        }
115        assert_eq!(from_adc(6789, 0), None);
116        assert_eq!(from_adc(6789, 6000), None);
117    }
118}