Skip to main content

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}