xpanse_api/interfaces/adc.rs
1//! Global ADC service.
2//!
3//! Provides a global singleton backed by the RP235x ADC block. Initialised once
4//! at startup via `init_adc`, then any driver or app can:
5//!
6//! * read the chip temperature via `read_temperature` / `read_temperature_raw`
7//! * sample an ADC-capable GPIO pin via `read_adc_pin` / `read_adc_voltage`
8//!
9//! The ADC is shared through an async [`embassy_sync::mutex::Mutex`], so
10//! concurrent reads from multiple tasks are safe — they simply queue.
11//!
12//! `read_adc_pin` borrows the pin via `Peri::reborrow()`, so the driver
13//! retains ownership of its `Peri<'static, …>` — the pin's pad is temporarily
14//! reconfigured for ADC during the read and restored when the temporary
15//! [`embassy_rp::adc::Channel`] is dropped.
16
17use core::ptr::null_mut;
18use core::sync::atomic::{AtomicPtr, Ordering};
19
20use embassy_rp::Peri;
21use embassy_rp::adc::{self, Adc, AdcPin, Async, Channel};
22use embassy_rp::gpio::Pull;
23use embassy_rp::peripherals::{ADC, ADC_TEMP_SENSOR};
24use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
25use embassy_sync::mutex::Mutex;
26use static_cell::StaticCell;
27
28use crate::metadata::AVDD;
29
30embassy_rp::bind_interrupts!(struct AdcIrqs {
31 ADC_IRQ_FIFO => embassy_rp::adc::InterruptHandler;
32});
33
34/// Error returned by ADC read functions.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
36pub enum AdcError {
37 /// [`init_adc`] has not been called yet.
38 NotInitialized,
39 /// ADC conversion failed.
40 ConversionFailed,
41}
42
43/// Number of quantization levels in the 12-bit ADC.
44const ADC_LEVELS: f64 = 4096.0;
45
46/// Temperature sensor calibration constants (RP235x datasheet §12.10.4).
47const T_REF: f64 = 27.0;
48const V_REF: f64 = 0.706;
49const SLOPE: f64 = 0.001721;
50
51struct AdcService {
52 adc: Adc<'static, Async>,
53 /// Permanent channel for the internal temperature sensor.
54 temp_channel: Channel<'static>,
55}
56
57impl AdcService {
58 fn new(adc_peri: Peri<'static, ADC>, temp_peri: Peri<'static, ADC_TEMP_SENSOR>) -> Self {
59 let adc = Adc::new(adc_peri, AdcIrqs, adc::Config::default());
60 let temp_channel = Channel::new_temp_sensor(temp_peri);
61 Self { adc, temp_channel }
62 }
63
64 async fn read_temp_raw(&mut self) -> Result<u16, AdcError> {
65 self.adc
66 .read(&mut self.temp_channel)
67 .await
68 .map_err(|_| AdcError::ConversionFailed)
69 }
70
71 async fn read_temp_celsius(&mut self) -> Result<f64, AdcError> {
72 let raw = self.read_temp_raw().await?;
73 let voltage = raw as f64 * AVDD / ADC_LEVELS;
74 let temp = T_REF - (voltage - V_REF) / SLOPE;
75 Ok(temp)
76 }
77
78 async fn read_pin_raw(
79 &mut self,
80 pin: Peri<'_, impl AdcPin>,
81 pull: Pull,
82 ) -> Result<u16, AdcError> {
83 let mut channel = Channel::new_pin(pin, pull);
84 self.adc
85 .read(&mut channel)
86 .await
87 .map_err(|_| AdcError::ConversionFailed)
88 }
89}
90
91type AdcMutex = Mutex<CriticalSectionRawMutex, AdcService>;
92
93static ADC_CELL: StaticCell<AdcMutex> = StaticCell::new();
94static ADC_PTR: AtomicPtr<AdcMutex> = AtomicPtr::new(null_mut());
95
96/// Initialise the global ADC service.
97///
98/// Must be called exactly once before any `read_temperature`,
99/// `read_temperature_raw`, `read_adc_pin` or `read_adc_voltage` call.
100///
101/// # Example
102///
103/// ```ignore
104/// use embassy_rp::peripherals::{ADC, ADC_TEMP_SENSOR};
105/// use embassy_rp::Peri;
106/// use xpanse_api::interfaces::adc::init_adc;
107///
108/// # async fn example(adc: Peri<'static, ADC>, temp: Peri<'static, ADC_TEMP_SENSOR>) {
109/// init_adc(adc, temp);
110/// # }
111/// ```
112pub fn init_adc(adc: Peri<'static, ADC>, temp_sensor: Peri<'static, ADC_TEMP_SENSOR>) {
113 let mutex = ADC_CELL.init(Mutex::new(AdcService::new(adc, temp_sensor)));
114 ADC_PTR.store(mutex as *mut AdcMutex, Ordering::Release);
115}
116
117/// Read the chip temperature in degrees Celsius.
118///
119/// Returns [`AdcError::NotInitialized`] if [`init_adc`] was not called.
120///
121/// # Example
122///
123/// ```ignore
124/// use xpanse_api::interfaces::adc::{init_adc, read_temperature, AdcError};
125///
126/// # async fn example() -> Result<f64, AdcError> {
127/// let temp = read_temperature().await?;
128/// # Ok(temp)
129/// # }
130/// ```
131pub async fn read_temperature() -> Result<f64, AdcError> {
132 let mutex = adc_mutex().ok_or(AdcError::NotInitialized)?;
133 let mut guard = mutex.lock().await;
134 guard.read_temp_celsius().await
135}
136
137/// Read the raw 12-bit ADC value from the internal temperature sensor.
138///
139/// Returns [`AdcError::NotInitialized`] if [`init_adc`] was not called.
140pub async fn read_temperature_raw() -> Result<u16, AdcError> {
141 let mutex = adc_mutex().ok_or(AdcError::NotInitialized)?;
142 let mut guard = mutex.lock().await;
143 guard.read_temp_raw().await
144}
145
146/// Read the raw 12-bit ADC value from a GPIO pin.
147///
148/// The pin is borrowed (via `reborrow`), so the caller retains ownership of its
149/// `Peri<'static, …>`. The `pull` argument configures the pin's pull resistor
150/// during the read; the pad is restored to its GPIO defaults when the internal
151/// channel is dropped.
152///
153/// Returns [`AdcError::NotInitialized`] if [`init_adc`] was not called.
154pub async fn read_adc_pin<P: AdcPin>(
155 pin: &mut Peri<'static, P>,
156 pull: Pull,
157) -> Result<u16, AdcError> {
158 let mutex = adc_mutex().ok_or(AdcError::NotInitialized)?;
159 let mut guard = mutex.lock().await;
160 guard.read_pin_raw(pin.reborrow(), pull).await
161}
162
163/// Read an ADC GPIO pin and convert the result to a voltage (0..=3.3 volts).
164///
165/// Convenience wrapper around `read_adc_pin` that scales the raw reading
166/// using AVDD and the 12-bit ADC resolution.
167pub async fn read_adc_voltage<P: AdcPin>(
168 pin: &mut Peri<'static, P>,
169 pull: Pull,
170) -> Result<f64, AdcError> {
171 let raw = read_adc_pin(pin, pull).await?;
172 Ok(raw as f64 * AVDD / ADC_LEVELS)
173}
174
175fn adc_mutex() -> Option<&'static AdcMutex> {
176 let ptr = ADC_PTR.load(Ordering::Acquire);
177 if ptr.is_null() {
178 return None;
179 }
180 // SAFETY: `ptr` was stored by `init_adc` from a `&'static AdcMutex`
181 // returned by `StaticCell::init`. The `StaticCell` lives for `'static` and
182 // is never moved. The `Acquire` load above synchronises with the `Release`
183 // store in `init_adc`, so the pointed-to `AdcMutex` is fully initialised
184 // and visible.
185 Some(unsafe { &*ptr })
186}