embassy_hall_analog/lib.rs
1// Copyright (C) 2026 Jorge Andre Castro
2// GPL-2.0-or-later
3//! # embassy-hall-analog
4//!
5//! Driver async `no_std` minimaliste pour le **capteur à effet Hall linéaire analogique**
6//! OPEN-SMART (compatible 49E / SS49E) sur microcontrôleur RP2040 et RP235x,
7//! basé sur le framework [Embassy](https://embassy.dev), ce n'est pas développé par l'équipe officielle attention!!.
8//!
9//! ## Description du composant
10//!
11//! Ce module capteur repose sur un capteur à effet Hall linéaire (type 49E) qui délivre
12//! une tension analogique proportionnelle à l'intensité du champ magnétique environnant.
13//! En l'absence de champ magnétique, la sortie se stabilise autour de **VCC / 2** (~1,65 V
14//! sous 3,3 V). Un pôle Sud rapproche la tension de VCC ; un pôle Nord la tire vers GND.
15//!
16//! ## Schéma de câblage
17//!
18//! ```text
19//! Module Hall OPEN-SMART RP2040 / RP235x
20//! ───────────────────────── ───────────────
21//! VCC ────────────────── 3.3V
22//! GND ────────────────── GND
23//! AO ────────────────── GP26 (ADC0)
24//! ```
25//!
26//! > **Note :** La broche AO (Analog Output) est connectée directement à la broche ADC
27//! > du microcontrôleur. Aucune résistance externe n'est nécessaire : le module embarque
28//! > déjà la polarisation interne.
29//!
30//! ## Features disponibles
31//!
32//! | Feature | Cible | Défaut |
33//! |-----------|------------------------------|--------|
34//! | `rp2040` | Raspberry Pi Pico 1 (RP2040) | ✓ |
35//! | `rp235xa` | Pico 2 A-step (RP2350A) | |
36//! | `rp235xb` | Pico 2 B-step (RP2350B) | |
37//!
38//! Ces features sont **mutuellement exclusives**. Le build échouera si zéro ou plusieurs
39//! sont activées simultanément.
40//!
41//! ## Exemple d'utilisation
42//!
43//! ```rust,no_run
44//! #![no_std]
45//! #![no_main]
46//!
47//! use embassy_executor::Spawner;
48//! use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
49//! use embassy_rp::bind_interrupts;
50//! use embassy_rp::adc::InterruptHandler;
51//! use embassy_hall_analog::HallAnalog;
52//!
53//! bind_interrupts!(struct Irqs {
54//! ADC_IRQ_FIFO => InterruptHandler;
55//! });
56//!
57//! #[embassy_executor::main]
58//! async fn main(_spawner: Spawner) {
59//! let p = embassy_rp::init(Default::default());
60//!
61//! let adc = Adc::new(p.ADC, Irqs, AdcConfig::default());
62//! let channel = Channel::new_pin(p.PIN_26, embassy_rp::gpio::Pull::None);
63//!
64//! let mut sensor = HallAnalog::new(adc, channel);
65//!
66//! loop {
67//! let raw = sensor.read_raw().await;
68//! // ~2048 (RP2040) / ~8192 (RP235x) → pas de champ
69//! // > repos → pôle Sud | < repos → pôle Nord
70//! let _ = raw;
71//! }
72//! }
73//! ```
74//!
75//! ## Calcul de tension et de champ magnétique
76//!
77//! La valeur brute ADC peut être convertie en tension :
78//!
79//! ```text
80//! V = raw × 3.3 / MAX (MAX = 4095 sur RP2040, 16383 sur RP235x)
81//! ```
82//!
83//! La déviation par rapport à la tension de repos (~VCC/2) indique l'intensité et la polarité :
84//!
85//! ```text
86//! ΔV = V - 1.65 (positif → pôle Sud, négatif → pôle Nord)
87//! ```
88//!
89//! Pour le capteur 49E, la sensibilité typique est de **1,4 mV/Gauss** à 5 V
90//! (environ **0,92 mV/Gauss** à 3,3 V) :
91//!
92//! ```text
93//! B (Gauss) = ΔV / 0.00092
94//! ```
95//!
96//! ## Caractéristiques
97//!
98//! | Paramètre | Valeur |
99//! |------------------------|-----------------------------------------|
100//! | Tension d'alimentation | 3,3 V (RP2040 / RP235x) |
101//! | Résolution ADC | 12 bits (0–4095) / 14 bits sur RP235x |
102//! | Sortie repos | ~VCC / 2 (~1,65 V sous 3,3 V) |
103//! | Sortie pôle Sud | Augmente vers VCC |
104//! | Sortie pôle Nord | Diminue vers GND |
105//! | Sensibilité typique | ~0,92 mV/Gauss @ 3,3 V (capteur 49E) |
106//! | Interface | Analogique (AO) — lecture ADC directe |
107//!
108//! ## `no_std`
109//!
110//! Cette crate ne dépend pas de la bibliothèque standard et est conçue pour
111//! tourner sur des microcontrôleurs bare-metal avec le runtime Embassy.
112
113#![no_std]
114#![forbid(unsafe_code)]
115
116// ---------------------------------------------------------------------------
117// Garde de compilation : exactement une feature cible doit être activée.
118// Le build.rs émet une erreur explicite si zéro ou plusieurs sont présentes,
119// mais ce cfg guard est une sécurité supplémentaire au niveau de la crate.
120// ---------------------------------------------------------------------------
121#[cfg(not(any(feature = "rp2040", feature = "rp235xa", feature = "rp235xb")))]
122compile_error!(
123 "embassy-hall-analog : aucune cible sélectionnée.\n\
124 Activez exactement une feature parmi : rp2040, rp235xa, rp235xb."
125);
126
127#[cfg(all(feature = "rp2040", feature = "rp235xa"))]
128compile_error!(
129 "embassy-hall-analog : features `rp2040` et `rp235xa` activées simultanément.\n\
130 Ces features sont mutuellement exclusives."
131);
132
133#[cfg(all(feature = "rp2040", feature = "rp235xb"))]
134compile_error!(
135 "embassy-hall-analog : features `rp2040` et `rp235xb` activées simultanément.\n\
136 Ces features sont mutuellement exclusives."
137);
138
139#[cfg(all(feature = "rp235xa", feature = "rp235xb"))]
140compile_error!(
141 "embassy-hall-analog : features `rp235xa` et `rp235xb` activées simultanément.\n\
142 Ces features sont mutuellement exclusives."
143);
144
145use embassy_rp::adc::{Adc, Async, Channel};
146use embassy_time::Timer;
147
148// ---------------------------------------------------------------------------
149// Constantes publiques nommées explicitement (rétrocompatibilité)
150// ---------------------------------------------------------------------------
151
152/// Valeur ADC correspondant à l'absence de champ magnétique **12 bits, RP2040**.
153///
154/// En pratique, cette valeur peut légèrement varier d'un module à l'autre.
155/// Utilisez [`HallAnalog::read_raw`] pour calibrer votre point zéro réel.
156pub const ZERO_FIELD_RAW_12BIT: u16 = 2048;
157
158/// Valeur ADC correspondant à l'absence de champ magnétique - **14 bits, RP235x**.
159pub const ZERO_FIELD_RAW_14BIT: u16 = 8192;
160
161// ---------------------------------------------------------------------------
162// Constante interne sélectionnée automatiquement selon la feature active.
163// Utilisée par `read_polarity` pour éviter toute duplication de logique.
164// ---------------------------------------------------------------------------
165
166/// Point de repos ADC sélectionné automatiquement selon la cible compilée.
167///
168/// - `rp2040` → [`ZERO_FIELD_RAW_12BIT`] (2048)
169/// - `rp235xa` / `rp235xb` → [`ZERO_FIELD_RAW_14BIT`] (8192)
170#[cfg(feature = "rp2040")]
171const ZERO_FIELD_RAW: u16 = ZERO_FIELD_RAW_12BIT;
172/// Attention : Vérifiez le multiplexage des pins ADC sur votre matériel pour la feature = "rp235xb".
173#[cfg(any(feature = "rp235xa", feature = "rp235xb"))]
174const ZERO_FIELD_RAW: u16 = ZERO_FIELD_RAW_14BIT;
175
176// ---------------------------------------------------------------------------
177// Types publics
178// ---------------------------------------------------------------------------
179
180/// Driver pour le capteur à effet Hall linéaire analogique OPEN-SMART
181/// via l'ADC du RP2040 / RP235x.
182///
183/// Ce driver encapsule un canal ADC Embassy et fournit une lecture asynchrone
184/// de la valeur brute du capteur. La valeur brute est centrée autour de
185/// [`ZERO_FIELD_RAW_12BIT`] (RP2040) ou [`ZERO_FIELD_RAW_14BIT`] (RP235x)
186/// en l'absence de champ magnétique.
187///
188/// # Exemple minimal
189///
190/// ```rust,no_run
191/// # use embassy_rp::adc::{Adc, Channel};
192/// # use embassy_hall_analog::HallAnalog;
193/// // let mut sensor = HallAnalog::new(adc, channel);
194/// // let raw: u16 = sensor.read_raw().await;
195/// ```
196pub struct HallAnalog<'d> {
197 adc: Adc<'d, Async>,
198 channel: Channel<'d>,
199}
200
201/// Polarité du champ magnétique détecté.
202///
203/// La détection est basée sur la déviation de la valeur brute ADC
204/// par rapport au point de repos (mi-échelle), avec une zone morte configurable.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum MagneticPolarity {
207 /// Pôle Sud détecté la tension monte au-dessus de VCC/2.
208 SouthPole,
209 /// Pôle Nord détecté la tension descend en dessous de VCC/2.
210 NorthPole,
211 /// Aucun champ magnétique significatif détecté (dans la zone morte).
212 NoField,
213}
214
215// ---------------------------------------------------------------------------
216// Implémentation
217// ---------------------------------------------------------------------------
218
219impl<'d> HallAnalog<'d> {
220 /// Crée une nouvelle instance du driver `HallAnalog`.
221 ///
222 /// # Arguments
223 ///
224 /// * `adc` Périphérique ADC Embassy en mode asynchrone.
225 /// * `channel` Canal ADC connecté à la broche AO du module Hall.
226 ///
227 /// # Exemple
228 ///
229 /// ```rust,no_run
230 /// # use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
231 /// # use embassy_hall_analog::HallAnalog;
232 /// // let adc = Adc::new(p.ADC, Irqs, AdcConfig::default());
233 /// // let channel = Channel::new_pin(p.PIN_26, embassy_rp::gpio::Pull::None);
234 /// // let sensor = HallAnalog::new(adc, channel);
235 /// ```
236 #[inline]
237 pub fn new(adc: Adc<'d, Async>, channel: Channel<'d>) -> Self {
238 Self { adc, channel }
239 }
240
241 /// Lit la valeur brute du convertisseur ADC.
242 ///
243 /// | Cible | Résolution | Plage | Repos |
244 /// |---------|-----------|--------------|--------|
245 /// | RP2040 | 12 bits | `0..=4095` | ~2048 |
246 /// | RP235x | 14 bits | `0..=16383` | ~8192 |
247 ///
248 /// En cas d'erreur ADC, la valeur `0` est retournée.
249 ///
250 /// # ⚠️ Note importante : Calibration matérielle
251 ///
252 /// Sur RP235x, bien que le matériel soit 14 bits, la valeur retournée
253 /// peut être sur 12 bits (0..4095) selon la configuration du HAL Embassy.
254 /// Votre point de repos réel peut donc différer des valeurs théoriques (~2048 ou ~8192).
255 ///
256 /// **Exemple mesuré sur le matériel (RP2350A testé avec calibration)** :
257 /// - En l'absence de champ magnétique, branche sur 3.3V : **~2060** (et non 2048)
258 /// - Cette valeur dépend de la résistance interne du capteur, des variations thermiques,
259 /// et de la stabilité de l'alimentation
260 ///
261 /// **Recommandation** : Utilisez [`HallAnalog::calibrate()`] lors du démarrage pour
262 /// déterminer automatiquement le point de repos réel de votre installation.
263 ///
264 /// # Exemple
265 ///
266 /// ```rust,no_run
267 /// # use embassy_hall_analog::HallAnalog;
268 /// # async fn example(mut sensor: HallAnalog<'_>) {
269 /// let raw: u16 = sensor.read_raw().await;
270 ///
271 /// // Conversion en tension (V) RP2040 12 bits
272 /// let voltage = raw as f32 * 3.3 / 4095.0;
273 ///
274 /// // Déviation par rapport au repos (ΔV)
275 /// let delta_v = voltage - 1.65;
276 ///
277 /// // Estimation du champ en Gauss (sensibilité 49E @ 3.3V ≈ 0.92 mV/Gauss)
278 /// let field_gauss = delta_v / 0.00092;
279 /// # }
280 /// ```
281 #[inline]
282 pub async fn read_raw(&mut self) -> u16 {
283 self.adc.read(&mut self.channel).await.unwrap_or(0)
284 }
285
286 /// Lit la polarité du champ magnétique détecté.
287 ///
288 /// Le point de repos (`ZERO_FIELD_RAW`) est sélectionné automatiquement
289 /// selon la feature compilée (`rp2040` → 2048, `rp235xa`/`rp235xb` → 8192).
290 ///
291 /// # Arguments
292 ///
293 /// * `deadband` Demi-largeur de la zone morte en LSB.
294 /// Valeur typique : `50` sur 12 bits, `200` sur 14 bits.
295 ///
296 /// | Condition | Résultat |
297 /// |-----------------------------------|-----------------------------------|
298 /// | `raw > ZERO + deadband` | [`MagneticPolarity::SouthPole`] |
299 /// | `raw < ZERO - deadband` | [`MagneticPolarity::NorthPole`] |
300 /// | sinon | [`MagneticPolarity::NoField`] |
301 ///
302 /// # Exemple
303 ///
304 /// ```rust,no_run
305 /// # use embassy_hall_analog::{HallAnalog, MagneticPolarity};
306 /// # async fn example(mut sensor: HallAnalog<'_>) {
307 /// match sensor.read_polarity(50).await {
308 /// MagneticPolarity::SouthPole => { /* pôle Sud */ }
309 /// MagneticPolarity::NorthPole => { /* pôle Nord */ }
310 /// MagneticPolarity::NoField => { /* pas de champ */ }
311 /// }
312 /// # }
313 /// ```
314 pub async fn read_polarity(&mut self, deadband: u16) -> MagneticPolarity {
315 let raw = self.read_raw().await;
316
317 if raw > ZERO_FIELD_RAW.saturating_add(deadband) {
318 MagneticPolarity::SouthPole
319 } else if raw < ZERO_FIELD_RAW.saturating_sub(deadband) {
320 MagneticPolarity::NorthPole
321 } else {
322 MagneticPolarity::NoField
323 }
324 }
325
326 /// Lit la valeur brute et retourne la déviation signée par rapport au point de repos.
327 ///
328 /// Utile pour évaluer l'intensité relative du champ magnétique indépendamment
329 /// de sa polarité, ou pour implémenter une logique de seuil personnalisée.
330 ///
331 /// # Arguments
332 ///
333 /// * `zero` Point de repos calibré en LSB.
334 /// Utilisez [`ZERO_FIELD_RAW_12BIT`] (RP2040) ou [`ZERO_FIELD_RAW_14BIT`] (RP235x),
335 /// ou une valeur mesurée sur votre module pour une meilleure précision.
336 ///
337 /// # Retour
338 ///
339 /// * `i32` — Déviation signée en LSB.
340 /// - Positif → pôle Sud
341 /// - Négatif → pôle Nord
342 /// - Proche de zéro → aucun champ
343 ///
344 /// # Exemple
345 ///
346 /// ```rust,no_run
347 /// # use embassy_hall_analog::{HallAnalog, ZERO_FIELD_RAW_12BIT};
348 /// # async fn example(mut sensor: HallAnalog<'_>) {
349 /// let deviation = sensor.read_deviation(ZERO_FIELD_RAW_12BIT).await;
350 /// // deviation > 0 : pôle Sud, deviation < 0 : pôle Nord
351 /// # }
352 /// ```
353 pub async fn read_deviation(&mut self, zero: u16) -> i32 {
354 let raw = self.read_raw().await;
355 raw as i32 - zero as i32
356 }
357
358
359 /// Calibre le point zéro automatiquement.
360 ///
361 /// Exécute une moyenne sur N échantillons pour déterminer le décalage (offset)
362 /// réel du capteur dans son environnement actuel.
363 ///
364 /// # Arguments
365 /// * `samples` - Nombre d'échantillons à moyenner (ex: 64 pour une grande précision).
366 pub async fn calibrate(&mut self, samples: u8) -> u16 {
367 let mut total: u32 = 0;
368 for _ in 0..samples {
369 total += self.read_raw().await as u32;
370 // On attend un tout petit peu entre chaque lecture pour laisser
371 // l'ADC se stabiliser et éviter de lire le même pic de bruit.
372 Timer::after_micros(100).await;
373 }
374 // La division se fait à la fin, et c'est cette valeur qui est retournée.
375 (total / samples as u32) as u16
376}
377
378}