embassy-ky023-joystick 0.1.0

Driver asynchrone no_std pour le joystick double-axe KY-023 (PS2), permettant la lecture des axes X/Y via ADC et du bouton SW via GPIO avec Embassy.
docs.rs failed to build embassy-ky023-joystick-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

embassy-ky023-joystick

Driver asynchrone no_std pour le module joystick double-axe KY-023 (compatible PS2), conçu pour le framework Embassy sur RP2040 (Raspberry Pi Pico 1) et RP2350 (Raspberry Pi Pico 2).

crates.io docs.rs license


Description matérielle

Le module KY-023 est un joystick analogique double-axe avec un bouton poussoir intégré. Il expose trois signaux :

Broche Type Description
VRx Analogique Axe X : tension proportionnelle à la position
VRy Analogique Axe Y : tension proportionnelle à la position
SW Numérique Bouton actif bas (GND = enfoncé, VCC = relâché)
              ┌──────────────┐
   GND ───────┤ GND          │
   VCC ───────┤ +5V          │
   VRx ───────┤ VRx   KY-023 │
   VRy ───────┤ VRy          │
   SW  ───────┤ SW           │
              └──────────────┘

Câblage recommandé

Raspberry Pi Pico 1 & 2

KY-023 Pico Rôle
GND GND Masse commune
+5V 3V3(OUT) Alimentation 3.3 V
VRx GP26 (ADC0) Axe X → canal ADC 0
VRy GP27 (ADC1) Axe Y → canal ADC 1
SW GP22 Bouton (pull-up interne)

Note : le KY-023 tolère 3.3 V malgré l'étiquette « +5V ». Relier à 3V3(OUT) évite tout risque de dommage sur le RP2040/RP2350.


Fonctionnalités

  • Asynchrone :Adc::read Embassy pour les axes, sans blocage
  • no_std :zéro allocation, compatible bare-metal
  • Gestion d'erreurs typée :JoystickError<AdcError>, jamais de unwrap
  • Normalisation symétrique :valeurs brutes ou normalisées dans [-1.0, +1.0]
  • Zone morte configurable :filtre le bruit mécanique au repos, re-normalisée
  • Builder validé :JoystickConfig::builder() avec validation à la construction
  • defmt optionnel: logging léger via feature defmt
  • Lecture partielle : axes seuls ou bouton seul si besoin

Installation

Ajoutez dans votre Cargo.toml :

[dependencies]
embassy-ky023-joystick = { version = "0.1", features = ["rp235xa"] }

Pour le Pico 1 (RP2040) :

[dependencies]
embassy-ky023-joystick = { version = "0.1", features = ["rp2040"] }

Avec le support de logging defmt :

[dependencies]
embassy-ky023-joystick = { version = "0.1", features = ["rp235xa", "defmt"] }

embedded-f32-sqrt et embedded-trig-f32 sont des dépendances directes du driver (même auteur). Cargo les résout automatiquement.


Utilisation

Exemple minimal

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_ky023_joystick::{Joystick, JoystickConfig, JoystickError};
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Input, Pull};
use embassy_time::Timer;

bind_interrupts!(struct Irqs {
    ADC_IRQ_FIFO => embassy_rp::adc::InterruptHandler;
});

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    let adc  = Adc::new(p.ADC, Irqs, AdcConfig::default());
    let ch_x = Channel::new_pin(p.PIN_26, Pull::None);
    let ch_y = Channel::new_pin(p.PIN_27, Pull::None);
    let sw   = Input::new(p.PIN_22, Pull::Up);

    let mut joystick = Joystick::new(adc, ch_x, ch_y, sw, JoystickConfig::default());

    loop {
        match joystick.read().await {
            Ok(state) => {
                let (x, y) = state.normalized(); // x, y ∈ [-1.0, +1.0]
                if state.button_pressed() {
                    // bouton enfoncé
                }
            }
            Err(JoystickError::AdcError(_e)) => { /* panne ADC */ }
            Err(_) => {}
        }
        Timer::after_millis(10).await;
    }
}

Configuration personnalisée

use embassy_ky023_joystick::{JoystickConfig, AdcResolution};

let config = JoystickConfig::builder()
    .resolution(AdcResolution::Bits12) // natif RP2040/RP2350
    .deadzone(0.08)                    // 8 % de zone morte
    .build::<embassy_rp::adc::Error>()
    .expect("configuration valide");

Lecture des axes seuls (sans bouton)

let axis     = joystick.read_axes().await?;
let (x, y)   = axis.normalized();
let magnitude = axis.magnitude();     // intensité du déplacement [0.0, √2]
let angle     = axis.angle_radians(); // direction en radians [-π, +π]

Lecture du bouton seul (synchrone, infaillible)

use embassy_ky023_joystick::ButtonState;

if joystick.read_button() == ButtonState::Pressed {
    // action
}

Exemple d'intégration — Défilement de scènes 3D sur SSD1306

Le joystick pilote l'axe X pour naviguer entre les scènes 3D (cube, pyramide, octaèdre) et l'axe Y pour ajuster la vitesse de rotation. Le bouton SW réinitialise l'angle à zéro.

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_ky023_joystick::{Joystick, JoystickConfig};
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Input, Pull};
use embassy_rp::i2c::{Config as I2cConfig, I2c, InterruptHandler as I2cIrqs};
use embassy_rp::peripherals::I2C0;
use embassy_ssd1306::Ssd1306;
use embassy_ssd1306_graphics::Graphics;
use embassy_ssd1306_3d::{cube, pyramid, octahedron, rotation_xyz, Camera};
use embassy_time::Timer;
use embedded_trig_f32::consts::TAU;
use {defmt_rtt as _, panic_probe as _};
use rp2350_linker as _;

bind_interrupts!(struct Irqs {
    I2C0_IRQ     => I2cIrqs<I2C0>;
    ADC_IRQ_FIFO => embassy_rp::adc::InterruptHandler;
});

#[unsafe(export_name = "_defmt_panic")]
fn forced_defmt_panic() -> ! { panic_probe::hard_fault() }

const FRAME_MS:       u64 = 50;   // 20 FPS
const NUM_SCENES:     u32 = 3;
const SCENE_THRESHOLD: f32 = 0.6;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    // ── OLED I2C ──────────────────────────────────────────────────────────────
    let mut i2c_cfg = I2cConfig::default();
    i2c_cfg.frequency = 400_000;
    let i2c = I2c::new_async(p.I2C0, p.PIN_5, p.PIN_4, Irqs, i2c_cfg);
    let mut oled = Ssd1306::new(i2c, 0x3C);
    oled.init().await.unwrap();

    // ── Joystick KY-023 ───────────────────────────────────────────────────────
    let adc  = Adc::new(p.ADC, Irqs, AdcConfig::default());
    let ch_x = Channel::new_pin(p.PIN_26, Pull::None);
    let ch_y = Channel::new_pin(p.PIN_27, Pull::None);
    let sw   = Input::new(p.PIN_22, Pull::Up);
    let mut joystick = Joystick::new(adc, ch_x, ch_y, sw, JoystickConfig::default());

    // ── Scènes 3D ─────────────────────────────────────────────────────────────
    let cam       = Camera::new(64.0, 32.0, 80.0);
    let cube_mesh = cube(18.0);
    let pyr_mesh  = pyramid(16.0, 30.0);
    let octa_mesh = octahedron(22.0);

    let mut angle:        f32  = 0.0;
    let mut speed:        f32  = 0.05;
    let mut scene:        u32  = 0;
    let mut scene_locked: bool = false;

    loop {
        // ── Lecture joystick ──────────────────────────────────────────────────
        if let Ok(state) = joystick.read().await {
            let (x, y) = state.normalized();

            // Axe Y : vitesse de rotation ([-1, +1] → [0.01, 0.12])
            speed = (0.065 + y * 0.055).max(0.01).min(0.12);

            // Axe X : changement de scène avec anti-rebond
            if !scene_locked && x > SCENE_THRESHOLD {
                scene = (scene + 1) % NUM_SCENES;
                scene_locked = true;
            } else if !scene_locked && x < -SCENE_THRESHOLD {
                scene = (scene + NUM_SCENES - 1) % NUM_SCENES;
                scene_locked = true;
            } else if x.abs() < 0.3 {
                scene_locked = false;
            }

            // Bouton SW : réinitialise l'angle
            if state.button_pressed() {
                angle = 0.0;
            }
        }

        // ── Rendu 3D ──────────────────────────────────────────────────────────
        oled.clear();
        oled.draw_rect(0, 0, 128, 64, true);
        {
            let mut gfx = Graphics::new(&mut oled);
            let rot = rotation_xyz(angle * 0.7, angle, angle * 0.4);
            match scene {
                0 => cube_mesh.draw_wireframe(&mut gfx, &cam, &rot),
                1 => pyr_mesh.draw_wireframe(&mut gfx, &cam, &rot),
                _ => octa_mesh.draw_wireframe(&mut gfx, &cam, &rot),
            }
        }
        oled.flush().await.unwrap();

        angle += speed;
        if angle > TAU { angle -= TAU; }

        Timer::after_millis(FRAME_MS).await;
    }
}

Comportement :

Entrée joystick Effet
X > +0.6 Scène suivante (cube → pyramide → octaèdre → …)
X < −0.6 Scène précédente
Y vers le haut (+1.0) Rotation rapide (~0.12 rad/frame)
Y vers le bas (−1.0) Rotation lente (~0.01 rad/frame)
Bouton SW enfoncé Réinitialise l'angle à 0

API

Joystick<'d>

Méthode Signature Description
new(...) → Self Crée le driver
read().await → Result<JoystickState, JoystickError<AdcError>> Lit axes + bouton
read_axes().await → Result<Axis, JoystickError<AdcError>> Lit axes uniquement
read_button() → ButtonState Lit le bouton (synchrone)
config() → &JoystickConfig Configuration active
set_config(c) → () Mise à jour à la volée
release() → (Adc, Channel, Channel, Input) Récupère les périphériques

JoystickState

Méthode Description
normalized() (x, y) dans [-1.0, +1.0]
button_pressed() true si SW enfoncé
is_idle() true si centré et bouton relâché
.axis Accès direct à [Axis]
.button Accès direct à [ButtonState]

Axis

Méthode Description
normalized() (x, y) dans [-1.0, +1.0]
is_centered() true si dans la zone morte
magnitude() Intensité du déplacement [0.0, √2]
angle_radians() Direction en radians [-π, +π]
.raw_x, .raw_y Valeurs ADC brutes [0, 4095]

JoystickConfig

Méthode Description
default() 12 bits, zone morte 5 %
builder() Démarre le builder validé
resolution() Retourne la résolution configurée
deadzone() Retourne la zone morte configurée

JoystickError<AdcErr>

Variant Cause
AdcError(e) Conversion ADC échouée
InvalidConfig { field } Paramètre de configuration invalide

Features

Feature Description Défaut
rp2040 Active le support RP2040 via embassy-rp non
rp235xa Active le support RP2350A via embassy-rp non
rp235xb Active le support RP2350B via embassy-rp non
defmt Active le logging via defmt non

Zone morte

La zone morte (deadzone) filtre le bruit électronique et mécanique au repos. Elle est exprimée en fraction de la plage totale.

Valeur brute ADC (12 bits) :
  0 ──────── 2047.5 ──────── 4095
             center (f32)

Après normalisation (deadzone = 5 %) :
 -1.0 ──── -0.05 │ 0.0 │ +0.05 ──── +1.0
                deadzone

La normalisation est symétrique : le diviseur max × 0.5 est identique des deux côtés, garantissant des pentes égales vers −1.0 et +1.0. La plage restante est re-normalisée pour que ±1.0 soit toujours atteint.


Compatibilité

Carte Feature requis
Raspberry Pi Pico (RP2040) rp2040
Raspberry Pi Pico 2 (RP2350A) rp235xa
Raspberry Pi Pico 2W (RP2350B) rp235xb

Licence

Ce projet est distribué sous licence GPL-2.0-or-later. Voir le fichier LICENSE pour les détails.

Copyright (C) 2026 Jorge Andre Castro