Rust for Linux List
 help / color / mirror / Atom feed
From: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
To: Jonathan Cameron <jic23@kernel.org>
Cc: linux-iio@vger.kernel.org, rust-for-linux@vger.kernel.org,
	linux-kernel@vger.kernel.org, Miguel Ojeda <ojeda@kernel.org>,
	Igor Korotin <igor.korotin.linux@gmail.com>,
	Brandon Saint-John <branstj@gmail.com>,
	Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
Subject: [RFC PATCH v3 3/4] iio: position: add Rust driver for ams AS5600
Date: Sun, 24 May 2026 20:28:22 +0700	[thread overview]
Message-ID: <20260524132824.54918-4-muchamadcoirulanwar@gmail.com> (raw)
In-Reply-To: <20260524132824.54918-1-muchamadcoirulanwar@gmail.com>

Add a Rust driver for the ams AS5600 12-bit magnetic rotary position
sensor. The driver exposes in_angl_raw and in_angl_scale via the IIO
sysfs interface.

Features:
- Circuit breaker pattern for I/O error resilience
- Mutex-serialized multi-byte angle read sequence
- Automatic recovery from bus failures (Poisoned -> Normal)
- No magnet validation at probe time (deferred to read_raw)

Tested on BeagleBone Black (AM335x) with AS5600 on i2c-2 (0x36).

Signed-off-by: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
---
 drivers/iio/position/as5600.rs | 289 +++++++++++++++++++++++++++++++++
 1 file changed, 289 insertions(+)
 create mode 100644 drivers/iio/position/as5600.rs

diff --git a/drivers/iio/position/as5600.rs b/drivers/iio/position/as5600.rs
new file mode 100644
index 000000000000..f87df650a91d
--- /dev/null
+++ b/drivers/iio/position/as5600.rs
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (C) 2026 Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
+//! Driver for ams AS5600 12-bit magnetic rotary position sensor.
+//!
+//! Datasheet: https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf
+
+use kernel::{
+    alloc::KBox,
+    bindings::{
+        i2c_client,
+        iio_chan_info_enum_IIO_CHAN_INFO_RAW,
+        iio_chan_info_enum_IIO_CHAN_INFO_SCALE,
+        iio_chan_spec,
+        iio_chan_type_IIO_ANGL,
+        ENODATA, //
+    },
+    bits::bit_u8,
+    device::Core,
+    error::{code::EIO, Error},
+    i2c::{
+        DeviceId,
+        Driver,
+        I2cClient,
+        IdTable, //
+    },
+    i2c_device_table,
+    iio::{
+        Device,
+        IioDriver,
+        IioVal,
+        Registered, //
+    },
+    io::{Io, IoCapable},
+    module_i2c_driver, of, of_device_table,
+    prelude::*,
+    sync::{
+        new_mutex,
+        Mutex, //
+    },
+};
+
+const AS5600_REG_STATUS: u8 = 0x0B;
+const AS5600_REG_RAW_ANGLE_H: u8 = 0x0C;
+const AS5600_REG_RAW_ANGLE_L: u8 = 0x0D;
+
+const AS5600_STATUS_MD: u8 = bit_u8(5);
+
+/// Returns kernel error `ENODATA`.
+///
+/// Helper needed because `Error::from_errno` is not `const fn` and `ENODATA`
+/// is only available as a raw `u32` binding, not as a wrapped `kernel::error::code`.
+#[inline(always)]
+fn err_enodata() -> Error {
+    Error::from_errno(-(ENODATA as i32))
+}
+
+module_i2c_driver! {
+    type: As5600,
+    name: "as5600",
+    authors: ["Muchamad Coirul Anwar"],
+    description: "I2C Driver for ams OSRAM AS5600 Magnetic Rotary Position Sensor",
+    license: "GPL",
+}
+
+i2c_device_table!(
+    I2C_TABLE,
+    MODULE_I2C_TABLE,
+    <As5600 as Driver>::IdInfo,
+    [(DeviceId::new(c"as5600"), ())]
+);
+
+of_device_table!(
+    OF_TABLE,
+    MODULE_OF_TABLE,
+    <As5600 as Driver>::IdInfo,
+    [(of::DeviceId::new(c"ams,as5600"), ())]
+);
+
+#[derive(Clone, Copy)]
+struct As5600Io(*mut i2c_client);
+
+/// Tracks the health state of the hardware bus to prevent I/O storms.
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum DeviceState {
+    Normal,
+    Poisoned,
+}
+
+// SAFETY: `As5600Io` wraps a raw pointer to `i2c_client`. This is `Send`
+// and `Sync` because:
+// - The I2C subsystem guarantees the `i2c_client` (parent) outlives the
+//   IIO device (child) via the Linux Device Model unbind ordering.
+// - All hardware access is serialized through `Mutex<As5600HwState<As5600Io>>`
+//   (`io_lock`), and individual SMBus transactions are serialized by the I2C
+//   adapter lock.
+unsafe impl Send for As5600Io {}
+unsafe impl Sync for As5600Io {}
+
+impl IoCapable<u8> for As5600Io {}
+impl IoCapable<u16> for As5600Io {}
+
+impl Io for As5600Io {
+    #[inline]
+    fn addr(&self) -> usize {
+        0
+    }
+
+    #[inline]
+    fn maxsize(&self) -> usize {
+        256
+    }
+
+    #[inline]
+    fn try_read8(&self, offset: usize) -> Result<u8>
+    where
+        Self: IoCapable<u8>,
+    {
+        // SAFETY: `self.0` points to a valid `i2c_client` guaranteed by the
+        // Device Model lifetime hierarchy (parent outlives child). The cast is
+        // valid because `I2cClient` is `#[repr(transparent)]` over `i2c_client`.
+        let client = unsafe { &*(self.0 as *const I2cClient<Core>) };
+        client.try_read8(offset)
+    }
+
+    #[inline]
+    fn try_read16(&self, offset: usize) -> Result<u16>
+    where
+        Self: IoCapable<u16>,
+    {
+        // SAFETY: `self.0` points to a valid `i2c_client` guaranteed by the
+        // Device Model lifetime hierarchy (parent outlives child). The cast is
+        // valid because `I2cClient` is `#[repr(transparent)]` over `i2c_client`.
+        let client = unsafe { &*(self.0 as *const I2cClient<Core>) };
+        client.try_read16(offset)
+    }
+}
+
+#[pin_data]
+struct As5600Priv<T> {
+    #[pin]
+    io_lock: Mutex<As5600HwState<T>>,
+    channels: KBox<[iio_chan_spec; 1]>,
+}
+
+/// Encapsulates the I/O interface and its runtime health state.
+///
+/// This prevents operations on a known-dead bus (Circuit Breaker pattern).
+struct As5600HwState<T> {
+    io: T,
+    state: DeviceState,
+}
+
+impl<T: Io + IoCapable<u8>> As5600HwState<T> {
+    /// Performs a dummy read to probe bus health after an I/O failure.
+    ///
+    /// Returns `EIO` in all cases — the caller should always propagate the error.
+    /// The side effect determines recovery behavior:
+    /// - If the dummy read **succeeds**: state is reset to `Normal`, meaning the
+    ///   next `read_raw` call will attempt normal operation directly.
+    /// - If the dummy read **fails**: state is set to `Poisoned`, meaning the
+    ///   next `read_raw` call will attempt recovery before normal operation.
+    fn handle_io_error(&mut self) -> Error {
+        match self.io.try_read8(AS5600_REG_STATUS as usize) {
+            Ok(_) => {
+                self.state = DeviceState::Normal;
+                EIO
+            }
+            Err(_) => {
+                self.state = DeviceState::Poisoned;
+                EIO
+            }
+        }
+    }
+}
+
+// SAFETY: `As5600Priv<T>` is `Send` and `Sync` because:
+// - `T: IoCapable<u8>` is a marker trait with no interior mutability.
+//   The underlying `As5600Io` wrapper's Send/Sync is guaranteed by its
+//   manual impls (serialized via Mutex + I2C adapter lock).
+// - `channels` is a heap-allocated array (`KBox`) with no interior mutability.
+// - `io_lock: Mutex<As5600HwState<T>>` provides synchronized interior mutability.
+// - `DeviceState` is a plain enum without interior mutability (Send + Sync
+//   implicitly).
+// All concurrent access to hardware goes through the `Mutex` guard.
+// The `Unpin` bound is strictly required because `kernel::sync::lock::Guard`
+// only implements `DerefMut` for `T: Unpin`. Without it, state mutation fails.
+unsafe impl<T: IoCapable<u8> + Unpin> Send for As5600Priv<T> {}
+unsafe impl<T: IoCapable<u8> + Unpin> Sync for As5600Priv<T> {}
+
+impl<T: Io + IoCapable<u8> + Unpin> IioDriver for As5600Priv<T> {
+    fn read_raw(&self, _chan: *const iio_chan_spec, mask: isize) -> Result<IioVal> {
+        match mask {
+            // IIO_CHAN_INFO_RAW — read the 12-bit raw angle value.
+            m if m == iio_chan_info_enum_IIO_CHAN_INFO_RAW as isize => {
+                let mut hw_guard = self.io_lock.lock();
+
+                // If the bus was previously poisoned, attempt a single recovery
+                // read before proceeding with normal operation.
+                let status = if hw_guard.state == DeviceState::Poisoned {
+                    match hw_guard.io.try_read8(AS5600_REG_STATUS as usize) {
+                        Ok(s) => {
+                            hw_guard.state = DeviceState::Normal;
+                            s
+                        }
+                        Err(_) => return Err(EIO),
+                    }
+                } else {
+                    match hw_guard.io.try_read8(AS5600_REG_STATUS as usize) {
+                        Ok(s) => s,
+                        Err(_) => return Err(hw_guard.handle_io_error()),
+                    }
+                };
+
+                // Check magnet presence (MD bit). Without a magnet the angle
+                // register contains stale/invalid data.
+                if (status & AS5600_STATUS_MD) == 0 {
+                    return Err(err_enodata());
+                }
+
+                // Read the 12-bit angle as two bytes. The AS5600 hardware
+                // freezes the internal angle value on reading the high byte
+                // until the low byte is read — the Mutex ensures this
+                // sequence is not interleaved by concurrent readers.
+                let angle_h = match hw_guard.io.try_read8(AS5600_REG_RAW_ANGLE_H as usize) {
+                    Ok(v) => v as u16,
+                    Err(_) => return Err(hw_guard.handle_io_error()),
+                };
+                let angle_l = match hw_guard.io.try_read8(AS5600_REG_RAW_ANGLE_L as usize) {
+                    Ok(v) => v as u16,
+                    Err(_) => return Err(hw_guard.handle_io_error()),
+                };
+
+                let angle = (angle_h << 8 | angle_l) & 0x0FFF;
+                Ok(IioVal::Int(angle as i32))
+            }
+            // IIO_CHAN_INFO_SCALE — radians per LSB: 2π / 4096 ≈ 0.001533981.
+            m if m == iio_chan_info_enum_IIO_CHAN_INFO_SCALE as isize => {
+                Ok(IioVal::IntPlusNano(0, 1533981))
+            }
+            _ => Err(kernel::error::code::EINVAL),
+        }
+    }
+
+    fn channels(&self) -> &[iio_chan_spec] {
+        &self.channels[..]
+    }
+}
+
+struct As5600 {
+    _iio_dev: Device<As5600Priv<As5600Io>, Registered>,
+}
+
+impl Driver for As5600 {
+    type IdInfo = ();
+    const I2C_ID_TABLE: Option<IdTable<Self::IdInfo>> = Some(&I2C_TABLE);
+    const OF_ID_TABLE: Option<of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
+
+    #[allow(refining_impl_trait)]
+    fn probe(dev: &I2cClient<Core>, _id_info: Option<&Self::IdInfo>) -> Result<Self> {
+        // SAFETY: `iio_chan_spec` is a C struct whose fields are all integers
+        // and pointers. Zero is a valid initialization for all of them.
+        let mut channels_alloc = kernel::alloc::KBox::new(
+            [unsafe { core::mem::zeroed::<iio_chan_spec>() }],
+            kernel::alloc::flags::GFP_KERNEL,
+        )?;
+
+        channels_alloc[0].info_mask_separate = (1 << iio_chan_info_enum_IIO_CHAN_INFO_RAW)
+            | (1 << iio_chan_info_enum_IIO_CHAN_INFO_SCALE);
+        channels_alloc[0].type_ = iio_chan_type_IIO_ANGL;
+
+        let client_ptr = dev as *const _ as *mut i2c_client;
+
+        let priv_init = pin_init!(As5600Priv {
+            io_lock <- new_mutex!(As5600HwState {
+                io: As5600Io(client_ptr),
+                state: DeviceState::Normal,
+            }),
+            channels: channels_alloc,
+        });
+
+        let iio_dev = Device::build_device(dev.as_ref(), c"as5600", priv_init)?;
+        let iio_dev_registered = iio_dev.register(&crate::THIS_MODULE)?;
+
+        dev_info!(dev.as_ref(), "AS5600 magnetic position sensor ready\n");
+        Ok(As5600 {
+            _iio_dev: iio_dev_registered,
+        })
+    }
+}
-- 
2.50.0


  parent reply	other threads:[~2026-05-24 13:28 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-24 13:28 [RFC PATCH v3 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
2026-05-24 13:28 ` [RFC PATCH v3 1/4] i2c: rust: implement kernel::io::Io trait for I2cClient Muchamad Coirul Anwar
2026-05-28 15:25   ` Jonathan Cameron
2026-06-01  7:58     ` Muchamad Coirul Anwar
2026-06-01  9:05       ` Jonathan Cameron
2026-06-02  8:11         ` Muchamad Coirul Anwar
2026-06-02 11:59           ` Jonathan Cameron
2026-05-24 13:28 ` [RFC PATCH v3 2/4] rust: add minimal IIO subsystem abstractions Muchamad Coirul Anwar
2026-05-28 16:09   ` Jonathan Cameron
2026-06-01  8:30     ` Muchamad Coirul Anwar
2026-06-01  9:10       ` Jonathan Cameron
2026-05-24 13:28 ` Muchamad Coirul Anwar [this message]
2026-05-28 16:08   ` [RFC PATCH v3 3/4] iio: position: add Rust driver for ams AS5600 Jonathan Cameron
2026-06-01 11:11     ` Muchamad Coirul Anwar
2026-06-02 12:06       ` Jonathan Cameron
2026-06-03  8:52         ` Muchamad Coirul Anwar
2026-05-29  5:37   ` Brandon Saint-John
2026-06-01 11:33     ` Muchamad Coirul Anwar
2026-05-24 13:28 ` [RFC PATCH v3 4/4] iio: position: as5600: add Kconfig and Makefile entries Muchamad Coirul Anwar
2026-05-28 16:09   ` Jonathan Cameron
2026-06-01  8:00     ` Muchamad Coirul Anwar

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260524132824.54918-4-muchamadcoirulanwar@gmail.com \
    --to=muchamadcoirulanwar@gmail.com \
    --cc=branstj@gmail.com \
    --cc=igor.korotin.linux@gmail.com \
    --cc=jic23@kernel.org \
    --cc=linux-iio@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=ojeda@kernel.org \
    --cc=rust-for-linux@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox