* [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600
@ 2026-10-16 15:48 Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 1/4] rust: io: add FallibleIoCapable Muchamad Coirul Anwar
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Muchamad Coirul Anwar @ 2026-10-16 15:48 UTC (permalink / raw)
To: Jonathan Cameron, Miguel Ojeda, Igor Korotin, Danilo Krummrich
Cc: Alice Ryhl, Daniel Almeida, Lars-Peter Clausen, Mark Brown,
Brandon StJohn, linux-iio, linux-i2c, rust-for-linux,
driver-core, linux-kernel
Add a Rust driver for the ams AS5600 12-bit magnetic rotary position
sensor.
The series adds generic fallible I/O support, SMBus access, a minimal
read-only Rust IIO abstraction, and the AS5600 driver. The driver exposes
raw angle and scale through IIO sysfs.
Reviews of the lifetime, concurrency, and teardown contracts in the Rust
I2C and IIO abstractions would be especially helpful. Danilo and Igor, if
you have time, I would appreciate your help reviewing those parts.
The Region-based view models byte-addressed registers and is not a
general model for command-indexed 16-bit registers. The standalone
word-read helpers accept any SMBus command byte.
The current interface is intentionally limited while a Rust regmap
abstraction is unavailable. Feedback on whether this is an appropriate
interim approach, or whether the work should wait for regmap, would be
appreciated.
This series is based on commit 3fadfb9362cf
("rust: firmware: avoid UB in example by passing parameter"), which
contains Gary Guo's io_projection-v6 infrastructure. It also depends on
rust-next commit b93fb6e76ec1
("rust: error: add remaining error codes") for ENODATA; that commit is
not included in this series.
Previous version:
https://lore.kernel.org/linux-iio/20260822062725.60519-1-muchamadcoirulanwar@gmail.com/
Changes since RFC v5:
- Split FallibleIoCapable and the generic fallible try_* routing into a
prerequisite patch.
- Require CONFIG_I2C=y because kernel::i2c is currently exported only
when I2C is built in.
- Keep a bound parent borrow in the unregistered IIO builder and transfer
registration ownership to parent devres.
- Derive the bound I2C client from the parent during read_raw instead of
storing an ARef<I2cClient<Bound>> in the driver.
- Make IioDriver unsafe and document the validity requirements for raw
channel specifications and the unsupported channel features.
- Reject operating modes other than INDIO_DIRECT_MODE.
- Use the external error-code dependency for ENODATA instead of adding
the error code in this series.
- Clarify the writable-storage contract for read_raw output pointers.
- Replace the stale datasheet URL with the stable document identifier.
---
Testing:
Built for ARM with Rust enabled, I2C built in, and AS5600 built as a
module. Tested on a BeagleBone Black with an AS5600 on i2c-2 at 0x36,
running kernel 7.2.0-rc1-as5600-debug+.
Raw angle and scale reads passed with the magnet detected. With the
magnet-detected bit clear, 10 consecutive reads returned ENODATA. I2C
transport errors were propagated during a physical bus disconnect, and
reads recovered after reconnection.
Concurrency testing used 10 parallel readers and repeated reads during
20 bind/unbind cycles. Lifecycle testing covered 50 bind/unbind cycles,
50 module load/unload cycles, module removal during active reads, module
removal with an open sysfs file descriptor, and module removal while the
I2C bus was in an error state.
KFENCE, UBSAN, lockdep, and kmemleak reported no issues. KASAN was not
enabled because 32-bit ARM currently selects HAVE_RUST only when KASAN
is disabled.
Muchamad Coirul Anwar (4):
rust: io: add FallibleIoCapable
i2c: rust: implement SMBus access via IoBackend
rust: add minimal IIO subsystem abstractions
iio: position: add Rust driver for ams AS5600
drivers/iio/position/Kconfig | 11 +
drivers/iio/position/Makefile | 1 +
drivers/iio/position/as5600.rs | 177 ++++++++++++
include/linux/iio/iio.h | 6 +-
rust/bindings/bindings_helper.h | 2 +
rust/kernel/bits.rs | 29 ++
rust/kernel/i2c.rs | 248 +++++++++++++++++
rust/kernel/iio.rs | 480 ++++++++++++++++++++++++++++++++
rust/kernel/io.rs | 66 +++--
rust/kernel/lib.rs | 2 +
10 files changed, 1000 insertions(+), 22 deletions(-)
create mode 100644 drivers/iio/position/as5600.rs
create mode 100644 rust/kernel/iio.rs
--
2.50.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH v6 1/4] rust: io: add FallibleIoCapable
2026-10-16 15:48 [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
@ 2026-10-16 15:48 ` Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 2/4] i2c: rust: implement SMBus access via IoBackend Muchamad Coirul Anwar
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Muchamad Coirul Anwar @ 2026-10-16 15:48 UTC (permalink / raw)
To: Jonathan Cameron, Miguel Ojeda, Igor Korotin, Danilo Krummrich
Cc: Alice Ryhl, Daniel Almeida, Lars-Peter Clausen, Mark Brown,
Brandon StJohn, linux-iio, linux-i2c, rust-for-linux,
driver-core, linux-kernel
Add a fallible I/O capability for backends whose accesses may fail
independently of address validation.
Route try_read, try_write, try_write_reg, and try_update through the
fallible capability. Provide a blanket implementation for infallible
IoCapable backends so existing users retain their current behavior.
This allows transport-backed implementations such as I2C to expose only
fallible access while keeping infallible operations unavailable.
Link: https://lore.kernel.org/all/DJVQ852J7SOH.26YBIJTQ9B66G@kernel.org/
Suggested-by: Danilo Krummrich <dakr@kernel.org>
Signed-off-by: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
---
rust/kernel/io.rs | 66 +++++++++++++++++++++++++++++++++--------------
1 file changed, 46 insertions(+), 20 deletions(-)
diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
index 95f46bb75f9e..516895ca2082 100644
--- a/rust/kernel/io.rs
+++ b/rust/kernel/io.rs
@@ -276,6 +276,36 @@ pub trait IoCapable<T>: IoBackend {
fn io_write<'a>(view: Self::View<'a, T>, value: T);
}
+/// Fallible counterpart of [`IoCapable`] for I/O backends where operations can fail at the
+/// transport level (e.g. I2C, SPI).
+///
+/// Infallible backends ([`IoCapable`] implementors) get this for free via blanket implementation.
+/// Fallible-only backends implement this trait directly without implementing [`IoCapable`]; the
+/// infallible [`Io::read`], [`Io::write`], and [`Io::update`] methods will then be unavailable,
+/// enforcing that callers use the `try_*` variants instead.
+pub trait FallibleIoCapable<T>: IoBackend {
+ /// Performs an I/O read of type `T` at `view` and returns the result, or an error if the
+ /// transport-level operation fails.
+ fn io_try_read<'a>(view: Self::View<'a, T>) -> Result<T>;
+
+ /// Performs an I/O write of `value` at `view`, or returns an error if the transport-level
+ /// operation fails.
+ fn io_try_write<'a>(view: Self::View<'a, T>, value: T) -> Result;
+}
+
+impl<B: IoCapable<T>, T> FallibleIoCapable<T> for B {
+ #[inline(always)]
+ fn io_try_read<'a>(view: Self::View<'a, T>) -> Result<T> {
+ Ok(Self::io_read(view))
+ }
+
+ #[inline(always)]
+ fn io_try_write<'a>(view: Self::View<'a, T>, value: T) -> Result {
+ Self::io_write(view, value);
+ Ok(())
+ }
+}
+
/// Trait indicating that an I/O backend supports memory copy operations.
pub trait IoCopyable: IoBackend {
/// Copy contents of `view` to `buffer`.
@@ -645,7 +675,7 @@ fn copy_to_slice(self, data: &mut [u8])
fn try_read8(self, offset: usize) -> Result<u8>
where
usize: IoLoc<Self::Target, u8, IoType = u8>,
- Self::Backend: IoCapable<u8>,
+ Self::Backend: FallibleIoCapable<u8>,
{
self.try_read(offset)
}
@@ -655,7 +685,7 @@ fn try_read8(self, offset: usize) -> Result<u8>
fn try_read16(self, offset: usize) -> Result<u16>
where
usize: IoLoc<Self::Target, u16, IoType = u16>,
- Self::Backend: IoCapable<u16>,
+ Self::Backend: FallibleIoCapable<u16>,
{
self.try_read(offset)
}
@@ -665,7 +695,7 @@ fn try_read16(self, offset: usize) -> Result<u16>
fn try_read32(self, offset: usize) -> Result<u32>
where
usize: IoLoc<Self::Target, u32, IoType = u32>,
- Self::Backend: IoCapable<u32>,
+ Self::Backend: FallibleIoCapable<u32>,
{
self.try_read(offset)
}
@@ -675,7 +705,7 @@ fn try_read32(self, offset: usize) -> Result<u32>
fn try_read64(self, offset: usize) -> Result<u64>
where
usize: IoLoc<Self::Target, u64, IoType = u64>,
- Self::Backend: IoCapable<u64>,
+ Self::Backend: FallibleIoCapable<u64>,
{
self.try_read(offset)
}
@@ -685,7 +715,7 @@ fn try_read64(self, offset: usize) -> Result<u64>
fn try_write8(self, value: u8, offset: usize) -> Result
where
usize: IoLoc<Self::Target, u8, IoType = u8>,
- Self::Backend: IoCapable<u8>,
+ Self::Backend: FallibleIoCapable<u8>,
{
self.try_write(offset, value)
}
@@ -695,7 +725,7 @@ fn try_write8(self, value: u8, offset: usize) -> Result
fn try_write16(self, value: u16, offset: usize) -> Result
where
usize: IoLoc<Self::Target, u16, IoType = u16>,
- Self::Backend: IoCapable<u16>,
+ Self::Backend: FallibleIoCapable<u16>,
{
self.try_write(offset, value)
}
@@ -705,7 +735,7 @@ fn try_write16(self, value: u16, offset: usize) -> Result
fn try_write32(self, value: u32, offset: usize) -> Result
where
usize: IoLoc<Self::Target, u32, IoType = u32>,
- Self::Backend: IoCapable<u32>,
+ Self::Backend: FallibleIoCapable<u32>,
{
self.try_write(offset, value)
}
@@ -715,7 +745,7 @@ fn try_write32(self, value: u32, offset: usize) -> Result
fn try_write64(self, value: u64, offset: usize) -> Result
where
usize: IoLoc<Self::Target, u64, IoType = u64>,
- Self::Backend: IoCapable<u64>,
+ Self::Backend: FallibleIoCapable<u64>,
{
self.try_write(offset, value)
}
@@ -827,10 +857,10 @@ fn write64(self, value: u64, offset: usize)
fn try_read<T, L>(self, location: L) -> Result<T>
where
L: IoLoc<Self::Target, T>,
- Self::Backend: IoCapable<L::IoType>,
+ Self::Backend: FallibleIoCapable<L::IoType>,
{
let view = io_view::<Self, L::IoType>(self, location.offset())?;
- Ok(Self::Backend::io_read(view).into())
+ Ok(Self::Backend::io_try_read(view)?.into())
}
/// Generic fallible write with runtime bounds check.
@@ -860,12 +890,11 @@ fn try_read<T, L>(self, location: L) -> Result<T>
fn try_write<T, L>(self, location: L, value: T) -> Result
where
L: IoLoc<Self::Target, T>,
- Self::Backend: IoCapable<L::IoType>,
+ Self::Backend: FallibleIoCapable<L::IoType>,
{
let view = io_view::<Self, L::IoType>(self, location.offset())?;
let io_value = value.into();
- Self::Backend::io_write(view, io_value);
- Ok(())
+ Self::Backend::io_try_write(view, io_value)
}
/// Generic fallible write of a fully-located register value.
@@ -905,7 +934,7 @@ fn try_write_reg<T, L, V>(self, value: V) -> Result
where
L: IoLoc<Self::Target, T>,
V: LocatedRegister<Self::Target, Location = L, Value = T>,
- Self::Backend: IoCapable<L::IoType>,
+ Self::Backend: FallibleIoCapable<L::IoType>,
{
let (location, value) = value.into_io_op();
@@ -938,16 +967,13 @@ fn try_write_reg<T, L, V>(self, value: V) -> Result
fn try_update<T, L, F>(self, location: L, f: F) -> Result
where
L: IoLoc<Self::Target, T>,
- Self::Backend: IoCapable<L::IoType>,
+ Self::Backend: FallibleIoCapable<L::IoType>,
F: FnOnce(T) -> T,
{
let view = io_view::<Self, L::IoType>(self, location.offset())?;
-
- let value: T = Self::Backend::io_read(view).into();
+ let value: T = Self::Backend::io_try_read(view)?.into();
let io_value = f(value).into();
- Self::Backend::io_write(view, io_value);
-
- Ok(())
+ Self::Backend::io_try_write(view, io_value)
}
/// Generic infallible read with compile-time bounds check.
--
2.50.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH v6 2/4] i2c: rust: implement SMBus access via IoBackend
2026-10-16 15:48 [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 1/4] rust: io: add FallibleIoCapable Muchamad Coirul Anwar
@ 2026-10-16 15:48 ` Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 3/4] rust: add minimal IIO subsystem abstractions Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 4/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
3 siblings, 0 replies; 5+ messages in thread
From: Muchamad Coirul Anwar @ 2026-10-16 15:48 UTC (permalink / raw)
To: Jonathan Cameron, Miguel Ojeda, Igor Korotin, Danilo Krummrich
Cc: Alice Ryhl, Daniel Almeida, Lars-Peter Clausen, Mark Brown,
Brandon StJohn, linux-iio, linux-i2c, rust-for-linux,
driver-core, linux-kernel
Implement fallible SMBus byte and word access for bound I2C clients
using the generic I/O backend infrastructure.
I2C transactions may fail due to NACK, arbitration loss, or timeout, so
use FallibleIoCapable rather than the infallible IoCapable interface.
Propagate errors returned by the I2C core to callers.
Provide smbus_io() for byte-addressed register access and standalone
word helpers for devices whose word register starts at an odd offset.
Also provide smbus_read_word_swapped() for devices storing the high byte
at the first register address.
Restrict these operations to I2cClient<Bound>, ensuring that bus I/O is
only available while the client remains bound.
Signed-off-by: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
---
rust/kernel/i2c.rs | 248 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 248 insertions(+)
diff --git a/rust/kernel/i2c.rs b/rust/kernel/i2c.rs
index 624b971ca8b0..a53f7ee42b13 100644
--- a/rust/kernel/i2c.rs
+++ b/rust/kernel/i2c.rs
@@ -14,8 +14,15 @@
devres::Devres,
driver,
error::*,
+ io::{
+ FallibleIoCapable,
+ IoBackend,
+ IoBase,
+ Region, //
+ },
of,
prelude::*,
+ ptr::KnownSize,
sync::aref::{
ARef,
AlwaysRefCounted, //
@@ -601,3 +608,244 @@ unsafe impl Send for Registration {}
// SAFETY: `Registration` offers no interior mutability (no mutation through &self
// and no mutable access is exposed)
unsafe impl Sync for Registration {}
+
+/// I/O backend for SMBus register access via I2C.
+///
+/// This backend implements only [`FallibleIoCapable`] and not
+/// [`crate::io::IoCapable`], because I2C/SMBus transactions can fail
+/// regardless of address validity. The infallible [`crate::io::Io::read`],
+/// [`crate::io::Io::write`], and [`crate::io::Io::update`] methods are
+/// therefore unavailable for this backend.
+pub struct I2cBackend;
+
+/// An SMBus view associated with a bound I2C client.
+///
+/// The pointer encodes a command offset and is never dereferenced as
+/// memory. Its type and, for dynamically sized targets, metadata describe
+/// the extent of the view.
+///
+/// # Invariants
+///
+/// The view is derived from the client's SMBus command region.
+/// Projections preserve the bounds of the source view.
+/// `client` remains valid and bound for the lifetime of the view.
+pub struct I2cView<'a, T: ?Sized> {
+ client: &'a I2cClient<device::Bound>,
+ ptr: *mut T,
+}
+
+impl<T: ?Sized> Copy for I2cView<'_, T> {}
+
+impl<T: ?Sized> Clone for I2cView<'_, T> {
+ #[inline]
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl IoBackend for I2cBackend {
+ type View<'a, T: ?Sized + KnownSize> = I2cView<'a, T>;
+
+ #[inline]
+ fn as_ptr<'a, T: ?Sized + KnownSize>(view: Self::View<'a, T>) -> *mut T {
+ view.ptr
+ }
+
+ #[inline]
+ unsafe fn project_view<'a, T: ?Sized + KnownSize, U: ?Sized + KnownSize>(
+ view: Self::View<'a, T>,
+ ptr: *mut U,
+ ) -> Self::View<'a, U> {
+ // INVARIANT: Per safety requirement.
+ I2cView {
+ client: view.client,
+ ptr,
+ }
+ }
+}
+
+impl FallibleIoCapable<u8> for I2cBackend {
+ #[inline]
+ fn io_try_read<'a>(view: I2cView<'a, u8>) -> Result<u8> {
+ // `io_view()` ensures `offset + 1 <= 256`, so `addr()` is at most 255;
+ // the `as u8` cast below is therefore lossless.
+ let reg = Self::as_ptr(view).addr() as u8;
+
+ // SAFETY: `view.client.as_raw()` returns a valid `*mut struct i2c_client`
+ // pointer as guaranteed by the type invariant of `I2cClient`.
+ // `i2c_smbus_read_byte_data` is safe to call with any valid client pointer
+ // and any u8 command byte.
+ let ret = unsafe { bindings::i2c_smbus_read_byte_data(view.client.as_raw(), reg) };
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(ret as u8)
+ }
+ }
+
+ #[inline]
+ fn io_try_write<'a>(view: I2cView<'a, u8>, value: u8) -> Result {
+ // `io_view()` ensures `offset + 1 <= 256`, so `addr()` is at most 255;
+ // the `as u8` cast below is therefore lossless.
+ let reg = Self::as_ptr(view).addr() as u8;
+
+ // SAFETY: `view.client.as_raw()` returns a valid `*mut struct i2c_client`
+ // pointer as guaranteed by the type invariant of `I2cClient`.
+ // `i2c_smbus_write_byte_data` is safe to call with any valid client pointer
+ // and any u8 command byte and value.
+ let ret = unsafe { bindings::i2c_smbus_write_byte_data(view.client.as_raw(), reg, value) };
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+/// The `Region<256>` view is byte-addressed. Consequently, its generic
+/// 16-bit operations reject odd command bytes and command byte `0xFF`.
+/// Devices whose command bytes each select an independent 16-bit register
+/// should use [`I2cClient::smbus_read_word`] or
+/// [`I2cClient::smbus_read_word_swapped`] directly.
+impl FallibleIoCapable<u16> for I2cBackend {
+ #[inline]
+ fn io_try_read<'a>(view: I2cView<'a, u16>) -> Result<u16> {
+ // `io_view()` ensures `offset + 2 <= 256`, so `addr()` is at most 254;
+ // the `as u8` cast below is therefore lossless.
+ let reg = Self::as_ptr(view).addr() as u8;
+
+ // SAFETY: `view.client.as_raw()` returns a valid `*mut struct i2c_client`
+ // pointer as guaranteed by the type invariant of `I2cClient`.
+ // `i2c_smbus_read_word_data` is safe to call with any valid client pointer
+ // and any u8 command byte.
+ let ret = unsafe { bindings::i2c_smbus_read_word_data(view.client.as_raw(), reg) };
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(ret as u16)
+ }
+ }
+
+ #[inline]
+ fn io_try_write<'a>(view: I2cView<'a, u16>, value: u16) -> Result {
+ // `io_view()` ensures `offset + 2 <= 256`, so `addr()` is at most 254;
+ // the `as u8` cast below is therefore lossless.
+ let reg = Self::as_ptr(view).addr() as u8;
+
+ // SAFETY: `view.client.as_raw()` returns a valid `*mut struct i2c_client`
+ // pointer as guaranteed by the type invariant of `I2cClient`.
+ // `i2c_smbus_write_word_data` is safe to call with any valid client pointer
+ // and any u8 command byte and u16 value.
+ let ret = unsafe { bindings::i2c_smbus_write_word_data(view.client.as_raw(), reg, value) };
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl<'a, T: ?Sized + KnownSize> IoBase<'a> for I2cView<'a, T> {
+ type Backend = I2cBackend;
+ type Target = T;
+
+ #[inline]
+ fn as_view(self) -> I2cView<'a, T> {
+ self
+ }
+}
+
+// SAFETY: The bound client borrow remains valid when the view is moved
+// between threads. The view exposes only SMBus transactions, which use
+// the I2C core's bus locking, and pointer projections that never
+// dereference the encoded address as memory. `T: Sync` preserves the
+// shared-view restriction.
+unsafe impl<T: ?Sized + Sync> Send for I2cView<'_, T> {}
+
+// SAFETY: Sharing a view permits concurrent SMBus transactions, which
+// use the I2C core's bus locking. The bound client remains valid for
+// the view's lifetime, and the encoded pointer is never dereferenced
+// as memory. `T: Sync` preserves the shared-view restriction.
+unsafe impl<T: ?Sized + Sync> Sync for I2cView<'_, T> {}
+
+impl I2cClient<device::Bound> {
+ /// Returns an I/O handle for SMBus register access on this I2C client.
+ ///
+ /// The returned handle provides fallible read and write methods for the
+ /// SMBus command byte range (0x00–0xFF). A command byte is a data byte which
+ /// often selects a register on the device. The 7-bit device address is handled
+ /// separately by the I2C core.
+ ///
+ /// [`crate::io::Io::try_read16`] and
+ /// [`crate::io::Io::try_write16`] on the returned handle reject
+ /// odd offsets because the [`Region`]-based bounds check requires
+ /// `offset % 2 == 0`. This model does not cover every SMBus register layout,
+ /// including layouts where auto-increment or auto-decrement results in command
+ /// bytes that appear unaligned. Use [`Self::smbus_read_word`] or
+ /// [`Self::smbus_read_word_swapped`] for word access at an odd command byte.
+ #[inline]
+ pub fn smbus_io(&self) -> I2cView<'_, Region<256>> {
+ // INVARIANT: The view borrows this bound client and covers the 256-byte
+ // SMBus command region.
+ //
+ // The provenance-free pointer encodes a command offset rather than a
+ // memory address. Its initial address is zero, and the region metadata
+ // supplies the length used for bounds checks. Projections select command
+ // offsets; SMBus accessors extract those offsets without dereferencing
+ // the pointer.
+ I2cView {
+ client: self,
+ ptr: Region::<256>::ptr_from_raw_parts_mut(core::ptr::without_provenance_mut(0), 256),
+ }
+ }
+
+ /// Reads the 16-bit SMBus word value returned for a command.
+ ///
+ /// Wraps `i2c_smbus_read_word_data`. The `reg` parameter is the SMBus
+ /// command byte (0x00–0xFF), a data byte which often selects a register on
+ /// the device. SMBus transmits the low byte first.
+ ///
+ /// Use [`Self::smbus_read_word_swapped`] for devices that transmit the
+ /// high byte of a multi-byte value first.
+ ///
+ /// Returns `Err` if the bus transaction fails, for example because of a
+ /// NACK, arbitration loss, or timeout.
+ #[inline]
+ pub fn smbus_read_word(&self, reg: u8) -> Result<u16> {
+ // SAFETY: `self.as_raw()` returns a valid `*mut struct i2c_client`
+ // pointer as guaranteed by the type invariant of `I2cClient`.
+ // `i2c_smbus_read_word_data` is safe to call with any valid client
+ // pointer and any u8 command byte.
+ let ret = unsafe { bindings::i2c_smbus_read_word_data(self.as_raw(), reg) };
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(ret as u16)
+ }
+ }
+
+ /// Reads a 16-bit word from an SMBus command and swaps its two bytes.
+ ///
+ /// Wraps `i2c_smbus_read_word_data` and applies [`u16::swap_bytes`] to the
+ /// result. The `reg` parameter is the SMBus command byte (0x00–0xFF), a data
+ /// byte which often selects a register on the device.
+ ///
+ /// SMBus transmits the low byte first. This method swaps the returned bytes
+ /// for devices that transmit the high byte of a multi-byte value first.
+ ///
+ /// Returns `Err` if the bus transaction fails, for example because of a
+ /// NACK, arbitration loss, or timeout.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// // AS5600 stores the 12-bit raw angle at command byte 0x0C with its
+ /// // high byte first.
+ /// let raw = client.smbus_read_word_swapped(0x0C)?;
+ /// let angle = raw & 0x0fff;
+ /// ```
+ #[inline]
+ pub fn smbus_read_word_swapped(&self, reg: u8) -> Result<u16> {
+ self.smbus_read_word(reg).map(u16::swap_bytes)
+ }
+}
--
2.50.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH v6 3/4] rust: add minimal IIO subsystem abstractions
2026-10-16 15:48 [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 1/4] rust: io: add FallibleIoCapable Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 2/4] i2c: rust: implement SMBus access via IoBackend Muchamad Coirul Anwar
@ 2026-10-16 15:48 ` Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 4/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
3 siblings, 0 replies; 5+ messages in thread
From: Muchamad Coirul Anwar @ 2026-10-16 15:48 UTC (permalink / raw)
To: Jonathan Cameron, Miguel Ojeda, Igor Korotin, Danilo Krummrich
Cc: Alice Ryhl, Daniel Almeida, Lars-Peter Clausen, Mark Brown,
Brandon StJohn, linux-iio, linux-i2c, rust-for-linux,
driver-core, linux-kernel
Add minimal Rust abstractions needed to register direct-mode IIO
devices exposing read_raw channel information.
Provide a type-safe IioChanInfo selector, IioVal return types, and an
IioDriver trait. Add a C callback trampoline that validates the channel
information selector, invokes the Rust driver, and converts IioVal into
the representation expected by the IIO core.
Use typestate to distinguish unregistered and registered devices.
The unregistered builder borrows the bound parent. After successful
registration, transfer ownership to parent devres so that the IIO device
is unregistered during parent resource teardown, before destroying the
Rust private data and releasing the allocation reference.
Keep allocation and private-data destruction under Rust ownership;
parent devres owns the registration wrapper rather than managing the
allocation through devm_iio_device_alloc().
Accept only INDIO_DIRECT_MODE. Triggered modes require additional C-side
state that this abstraction does not initialize. In particular,
INDIO_EVENT_TRIGGERED exposes trigger selection that requires a valid
pollfunc_event.
The initial abstraction intentionally supports read_raw only. It does
not expose write_raw, buffers, triggers, events, extended attributes, or
available-value attributes.
Signed-off-by: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
---
include/linux/iio/iio.h | 6 +-
rust/bindings/bindings_helper.h | 2 +
rust/kernel/iio.rs | 480 ++++++++++++++++++++++++++++++++
rust/kernel/lib.rs | 2 +
4 files changed, 488 insertions(+), 2 deletions(-)
create mode 100644 rust/kernel/iio.rs
diff --git a/include/linux/iio/iio.h b/include/linux/iio/iio.h
index 711c00f67371..512b1829f661 100644
--- a/include/linux/iio/iio.h
+++ b/include/linux/iio/iio.h
@@ -445,8 +445,10 @@ struct iio_trigger; /* forward declaration */
* @read_raw: function to request a value from the device.
* mask specifies which value. Note 0 means a reading of
* the channel in question. Return value will specify the
- * type of value returned by the device. val and val2 will
- * contain the elements making up the returned value.
+ * type of value returned by the device. val and val2
+ * point to writable integer storage valid for the
+ * duration of the callback. The return value determines
+ * which outputs contain meaningful data.
* @read_raw_multi: function to return values from the device.
* mask specifies which value. Note 0 means a reading of
* the channel in question. Return value will specify the
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index 1124785e210b..f311959bab18 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -62,6 +62,8 @@
#include <linux/firmware.h>
#include <linux/fs.h>
#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/types.h>
#include <linux/interrupt.h>
#include <linux/io-pgtable.h>
#include <linux/ioport.h>
diff --git a/rust/kernel/iio.rs b/rust/kernel/iio.rs
new file mode 100644
index 000000000000..9ad2f50948de
--- /dev/null
+++ b/rust/kernel/iio.rs
@@ -0,0 +1,480 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
+//! IIO subsystem abstractions.
+//!
+//! Provides [`Device`] for allocating and registering an IIO device, and
+//! [`IioDriver`] for implementing `read_raw` callbacks. Implementing the
+//! trait requires validating the raw channel specifications.
+
+use crate::{
+ bindings::{
+ __iio_device_register,
+ iio_chan_spec,
+ iio_dev,
+ iio_device_alloc,
+ iio_device_free,
+ iio_device_unregister,
+ iio_info,
+ INDIO_DIRECT_MODE, //
+ },
+ device::{
+ self,
+ Bound, //
+ },
+ devres,
+ error::{
+ code::*,
+ to_result,
+ Result, //
+ },
+ prelude::*,
+ ThisModule, //
+};
+
+use core::{
+ ffi::c_int,
+ marker::PhantomData,
+ mem::{
+ forget,
+ size_of,
+ zeroed, //
+ },
+ num::NonZeroI32,
+ pin::Pin,
+ ptr::drop_in_place, //
+};
+
+use pin_init::{
+ pin_data,
+ pinned_drop, //
+};
+
+/// IIO value type: single integer (`IIO_VAL_INT`).
+pub const IIO_VAL_INT: c_int = crate::bindings::IIO_VAL_INT as c_int;
+
+/// IIO value type: integer plus micro part (`IIO_VAL_INT_PLUS_MICRO`).
+pub const IIO_VAL_INT_PLUS_MICRO: c_int = crate::bindings::IIO_VAL_INT_PLUS_MICRO as c_int;
+
+/// IIO value type: integer plus nano part (`IIO_VAL_INT_PLUS_NANO`).
+pub const IIO_VAL_INT_PLUS_NANO: c_int = crate::bindings::IIO_VAL_INT_PLUS_NANO as c_int;
+
+/// IIO value type: fractional (`IIO_VAL_FRACTIONAL`).
+pub const IIO_VAL_FRACTIONAL: c_int = crate::bindings::IIO_VAL_FRACTIONAL as c_int;
+
+/// Generates a Rust enum wrapper for C `enum iio_chan_info_enum`.
+///
+/// This macro creates a type-safe enum with automatic `TryFrom<u32>`
+/// conversion. Drivers match directly on `IioChanInfo` variants in
+/// `read_raw`, and the compiler enforces match exhaustiveness.
+/// Additional variants can be added as drivers require them.
+macro_rules! build_iio_chan_info_enum {
+ (
+ $(
+ $(#[$meta:meta])*
+ $rust_name:ident = $c_const:ident
+ ),* $(,)?
+ ) => {
+ /// Channel information selector for [`IioDriver::read_raw`].
+ ///
+ /// Wraps supported C `enum iio_chan_info_enum` values identifying
+ /// the requested channel information.
+ ///
+ /// Conversion from an unsupported value returns `EINVAL`.
+ #[repr(u32)]
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ pub enum IioChanInfo {
+ $(
+ $(#[$meta])*
+ $rust_name = bindings::$c_const,
+ )*
+ }
+ impl TryFrom<u32> for IioChanInfo {
+ type Error = Error;
+ fn try_from(value: u32) -> Result<Self, Self::Error> {
+ match value {
+ $( bindings::$c_const => Ok(IioChanInfo::$rust_name), )*
+ _ => Err(EINVAL),
+ }
+ }
+ }
+ };
+}
+
+build_iio_chan_info_enum! {
+ /// Raw unprocessed value from the channel (`IIO_CHAN_INFO_RAW`).
+ ///
+ /// For sensors, this is typically the ADC reading or register value
+ /// before any scaling or offset correction.
+ Raw = iio_chan_info_enum_IIO_CHAN_INFO_RAW,
+
+ /// Scale factor for converting raw values to channel units
+ /// (`IIO_CHAN_INFO_SCALE`).
+ ///
+ /// Apply any offset before scaling: `(raw + offset) * scale`.
+ /// The unit is defined by the IIO ABI for the channel type.
+ Scale = iio_chan_info_enum_IIO_CHAN_INFO_SCALE,
+}
+
+/// Represents the return value of a `read_raw` operation.
+///
+/// Each variant corresponds to an `IIO_VAL_*` constant and tells the
+/// IIO core how to format `val` and `val2` for userspace.
+pub enum IioVal {
+ /// A single integer value.
+ Int(i32),
+
+ /// A fractional value represented as `val / val2`.
+ /// The denominator is `NonZeroI32` to prevent division-by-zero in
+ /// `iio_format_value()`.
+ Fractional(i32, NonZeroI32),
+
+ /// An integer part and a fractional part in millionths
+ /// (`IIO_VAL_INT_PLUS_MICRO`).
+ ///
+ /// For negative values, put the sign on the integer part, or on the
+ /// fractional part when the integer part is zero. For example,
+ /// `IntPlusMicro(-1, 500_000)` represents -1.5, and
+ /// `IntPlusMicro(0, -500_000)` represents -0.5.
+ IntPlusMicro(i32, i32),
+
+ /// An integer part and a fractional part in billionths
+ /// (`IIO_VAL_INT_PLUS_NANO`).
+ ///
+ /// Uses the same sign convention as [`IioVal::IntPlusMicro`].
+ IntPlusNano(i32, i32),
+}
+
+/// Trait implemented by IIO driver private data.
+///
+/// # Safety
+///
+/// The channel specifications returned by `channels()` must satisfy the
+/// IIO core's requirements. Channel types, modifiers, strings, and
+/// pointer/count pairs must be valid. Referenced storage must remain
+/// valid for every access by the IIO core.
+///
+/// Channels must not expose events, buffers, extended attributes, or
+/// available-value attributes. This abstraction currently supports only
+/// channel information handled by its `read_raw` callback.
+pub unsafe trait IioDriver: Send + Sync {
+ /// Called by the IIO core to read channel information.
+ ///
+ /// `parent` is the parent device supplied to [`Device::build_device`].
+ /// `chan` identifies the channel, and `info` selects the requested
+ /// information, such as its raw value or scale.
+ fn read_raw(
+ &self,
+ parent: &device::Device<Bound>,
+ chan: *const iio_chan_spec,
+ info: IioChanInfo,
+ ) -> Result<IioVal>;
+
+ /// Returns the channel specifications for this driver.
+ ///
+ /// The default implementation returns an empty slice.
+ fn channels(&self) -> &'static [iio_chan_spec] {
+ &[]
+ }
+}
+
+/// C-compatible trampoline for the `iio_info.read_raw` callback.
+///
+/// # Safety
+///
+/// Callers must ensure that:
+/// - `indio_dev` points to a valid `iio_dev` allocated with space for `T`.
+/// Its private data contains an initialized `T` that remains valid and
+/// may be shared for the duration of this callback.
+/// - `chan` points to a valid channel specification for `indio_dev`.
+/// - `val` and `val2` are non-null, properly aligned pointers to writable
+/// `c_int` storage valid for the duration of this callback.
+/// - The parent of `indio_dev` remains bound for the entire callback.
+/// Registration ownership and callback teardown must enforce this.
+unsafe extern "C" fn read_raw_callback<T: IioDriver>(
+ indio_dev: *mut iio_dev,
+ chan: *const iio_chan_spec,
+ val: *mut c_int,
+ val2: *mut c_int,
+ info: isize,
+) -> c_int {
+ // SAFETY: `indio_dev` is valid and was allocated with space for `T` in its
+ // private data area. The `priv_` field was initialized in `Device::build_device()`.
+ let priv_ptr = unsafe { (*indio_dev).priv_ as *mut T };
+ // SAFETY: The callback contract guarantees that `priv_ptr` points to an
+ // initialized `T` that remains valid for the duration of this callback.
+ let driver = unsafe { &*priv_ptr };
+
+ // SAFETY: `indio_dev` is valid by the callback contract. Its parent
+ // was supplied as a bound device to `build_device`.
+ //
+ // During registration, the builder holds that bound parent borrow.
+ // After ownership is transferred, parent devres runs the registered
+ // device's destructor, which unregisters the IIO device and drains
+ // the supported read_raw paths before destroying the private data.
+ // The parent therefore remains bound throughout this callback.
+ let parent = unsafe { device::Device::<Bound>::from_raw((*indio_dev).dev.parent) };
+
+ // `info` is `long` from C. Negative values and values > u32::MAX
+ // are unsupported iio_chan_info variants; both map to EINVAL.
+ let info_enum = match u32::try_from(info)
+ .ok()
+ .and_then(|value| IioChanInfo::try_from(value).ok())
+ {
+ Some(info) => info,
+ None => return EINVAL.to_errno(),
+ };
+
+ match driver.read_raw(parent, chan, info_enum) {
+ Ok(IioVal::Int(v)) => {
+ // SAFETY: `val` is valid per the function's Safety contract above.
+ // `val2` is not written; `IIO_VAL_INT` signals to the IIO core
+ // that only `val` carries meaningful data.
+ unsafe {
+ *val = v;
+ }
+ IIO_VAL_INT
+ }
+ Ok(IioVal::Fractional(v, v2)) => {
+ // SAFETY: both `val` and `val2` are valid per the Safety contract.
+ unsafe {
+ *val = v;
+ *val2 = v2.get();
+ }
+ IIO_VAL_FRACTIONAL
+ }
+ Ok(IioVal::IntPlusMicro(v, v2)) => {
+ // SAFETY: both `val` and `val2` are valid per the Safety contract.
+ unsafe {
+ *val = v;
+ *val2 = v2;
+ }
+ IIO_VAL_INT_PLUS_MICRO
+ }
+ Ok(IioVal::IntPlusNano(v, v2)) => {
+ // SAFETY: both `val` and `val2` are valid per the Safety contract.
+ unsafe {
+ *val = v;
+ *val2 = v2;
+ }
+ IIO_VAL_INT_PLUS_NANO
+ }
+ Err(e) => e.to_errno(),
+ }
+}
+
+// Device<T, State>: IIO device wrapper with typestate.
+
+/// Marker type for an unregistered IIO device.
+pub struct Unregistered;
+
+/// Marker type for a registered IIO device.
+pub struct Registered;
+
+/// An initialized IIO device awaiting registration.
+///
+/// The parent remains bound until this builder is consumed or dropped.
+pub struct UnregisteredDevice<'a, T: IioDriver> {
+ device: Device<T>,
+ parent: &'a device::Device<Bound>,
+}
+
+/// An owned IIO device with in-place initialized driver data.
+///
+/// Dropping a registered device unregisters it before destroying the driver
+/// data. Dropping either state then releases the allocation reference with
+/// `iio_device_free`.
+///
+/// # Invariants
+///
+/// - `indio_dev` points to an `iio_dev` allocated by `iio_device_alloc`,
+/// and this wrapper owns its allocation reference.
+/// - Its private data contains an initialized `T` that remains at a fixed
+/// address until it is destroyed.
+/// - `registered` is true exactly when this wrapper owns a successful
+/// registration that has not yet been undone.
+#[pin_data(PinnedDrop)]
+pub struct Device<T: IioDriver, State = Unregistered> {
+ indio_dev: *mut iio_dev,
+ registered: bool,
+ _p: PhantomData<(T, State)>,
+}
+
+// SAFETY: Moving the wrapper transfers ownership without moving the separately
+// allocated `iio_dev` or its private data. `T: Send` permits transferring the
+// private data to another thread.
+unsafe impl<T: IioDriver, State> Send for Device<T, State> {}
+
+// SAFETY: All `&self` access to the `iio_dev` is read-only or goes through the
+// IIO core which provides its own synchronization. `T` is required to be `Sync`.
+unsafe impl<T: IioDriver, State> Sync for Device<T, State> {}
+
+#[pinned_drop]
+impl<T: IioDriver, State> PinnedDrop for Device<T, State> {
+ fn drop(self: Pin<&mut Self>) {
+ if self.registered {
+ // SAFETY: `__iio_device_register` succeeded.
+ //
+ // iio_device_unregister() deletes the character device, disables
+ // buffers, clears `info` while holding `info_exist_lock`, and removes
+ // the remaining buffer sysfs state. Device removal also removes and
+ // drains its sysfs files. In-kernel read helpers that use
+ // `info_exist_lock` cannot race this `info` teardown.
+ //
+ // Any callback path not covered by those mechanisms requires a
+ // separate teardown proof before this abstraction can expose it.
+ unsafe { iio_device_unregister(self.indio_dev) };
+ }
+
+ // SAFETY: `priv_` was fully initialized in `build_device` via
+ // `init.__pinned_init(priv_ptr)`. `drop_in_place` runs `T`'s destructor
+ // (including any pinned fields like Mutex). After that, `iio_device_free`
+ // calls `put_device` which decrements the kref. The underlying `iio_dev`
+ // memory is only freed when kref reaches 0.
+ unsafe {
+ let priv_ptr = (*self.indio_dev).priv_ as *mut T;
+ drop_in_place(priv_ptr);
+ iio_device_free(self.indio_dev);
+ }
+ }
+}
+
+impl<T: IioDriver> Device<T> {
+ // SAFETY:
+ // - `read_raw_callback::<T>` is a valid function pointer whose signature
+ // matches the IIO core's `read_raw` contract.
+ // - All remaining fields are pointers or function pointers; zeroed values
+ // are NULL, and the IIO core checks for NULL before invoking any optional
+ // callback or dereferencing any optional attribute group.
+ const VTABLE: iio_info = iio_info {
+ read_raw: Some(read_raw_callback::<T>),
+ ..unsafe { zeroed() }
+ };
+
+ /// Allocates an unregistered IIO device and initializes its driver data
+ /// in place.
+ ///
+ /// `parent` becomes the IIO device's parent. `name` supplies its name.
+ /// Only `INDIO_DIRECT_MODE` is supported.
+ ///
+ /// Returns `EINVAL` if `modes` is not `INDIO_DIRECT_MODE`, if the
+ /// private-data size or channel count cannot be represented by the
+ /// corresponding C type, if `T` is zero-sized, or if the private
+ /// storage is not suitably aligned.
+ /// Allocation and initialization errors are propagated.
+ ///
+ /// Call [`UnregisteredDevice::register`] to register the device.
+ pub fn build_device<'a, E>(
+ parent: &'a device::Device<Bound>,
+ name: &'static CStr,
+ modes: u32,
+ init: impl PinInit<T, E>,
+ ) -> Result<UnregisteredDevice<'a, T>>
+ where
+ Error: From<E>,
+ {
+ if modes != INDIO_DIRECT_MODE {
+ return Err(EINVAL);
+ }
+
+ let modes = c_int::try_from(modes).map_err(|_| EINVAL)?;
+
+ let priv_size = i32::try_from(size_of::<T>()).map_err(|_| EINVAL)?;
+ if priv_size == 0 {
+ return Err(EINVAL);
+ }
+
+ // SAFETY: `parent.as_raw()` returns a valid `struct device` pointer.
+ // `iio_device_alloc` allocates an `iio_dev` with `sizeof(T)` bytes of
+ // private data. Returns NULL on failure.
+ let indio_dev = unsafe { iio_device_alloc(parent.as_raw(), priv_size) };
+ if indio_dev.is_null() {
+ return Err(ENOMEM);
+ }
+
+ // SAFETY: `indio_dev` is non-null and was just successfully allocated by
+ // `iio_device_alloc`. Accessing `priv_` yields a pointer to zeroed
+ // memory (kzalloc'd) sized for `T`.
+ let priv_ptr = unsafe { (*indio_dev).priv_ as *mut T };
+ if !priv_ptr.is_aligned() {
+ // SAFETY: `indio_dev` was allocated by `iio_device_alloc` and `priv_`
+ // has not been initialized, so no destructor needs to run.
+ unsafe { iio_device_free(indio_dev) };
+ return Err(EINVAL);
+ }
+
+ // SAFETY: `priv_ptr` is aligned and points to exclusively owned storage
+ // large enough for `T`. The private data remains at a fixed address in
+ // the separate IIO allocation until it is dropped. On initialization
+ // failure, the error path only frees the allocation.
+ let init_result = unsafe { init.__pinned_init(priv_ptr) };
+ if let Err(e) = init_result {
+ // SAFETY: `pin_init` guarantees partial-init rollback internally.
+ // `priv_` memory was not fully initialized, so we only free the
+ // container without running `T`'s destructor.
+ unsafe { iio_device_free(indio_dev) };
+ return Err(Error::from(e));
+ }
+
+ let device = Self {
+ indio_dev,
+ registered: false,
+ _p: PhantomData,
+ };
+
+ // SAFETY: `priv_ptr` points to the initialized `T` owned by `device`.
+ // It remains valid for this shared borrow.
+ let channels = unsafe { (&*priv_ptr).channels() };
+ let num_channels = c_int::try_from(channels.len()).map_err(|_| EINVAL)?;
+
+ // SAFETY: `priv_ptr` is now fully initialized. We set up the IIO
+ // device fields:
+ // - `name` is a `'static` C string that outlives the device.
+ // - The reference to `VTABLE` is promoted to static storage.
+ // - `channels()` returns a static slice, so its backing storage
+ // outlives the `iio_dev` allocation.
+ // - `modes` was checked to be `INDIO_DIRECT_MODE`, so registration
+ // does not expose unsupported buffer or trigger paths.
+ // - `T`'s `IioDriver` safety contract guarantees that the channel
+ // specifications and their referenced storage satisfy the IIO
+ // core's requirements.
+
+ unsafe {
+ (*indio_dev).name = name.as_char_ptr();
+ (*indio_dev).info = &Self::VTABLE;
+ (*indio_dev).channels = channels.as_ptr();
+ (*indio_dev).num_channels = num_channels;
+ (*indio_dev).modes = modes;
+ }
+
+ Ok(UnregisteredDevice { device, parent })
+ }
+}
+
+impl<T: IioDriver + 'static> UnregisteredDevice<'_, T> {
+ /// Registers the IIO device and transfers ownership to parent devres.
+ ///
+ /// Parent resource teardown unregisters the device and completes its
+ /// supported `read_raw` callbacks before destroying the private data.
+ ///
+ /// On failure, the device is released and any successful registration is
+ /// undone.
+ #[inline]
+ pub fn register(self, module: &'static ThisModule) -> Result {
+ let Self { device, parent } = self;
+
+ // SAFETY: `device.indio_dev` is a valid, fully-initialized `iio_dev`
+ // allocated by `iio_device_alloc` with its name, info vtable, channels,
+ // modes, and private data all set. `module.as_ptr()` provides the owning
+ // module for proper reference counting.
+ to_result(unsafe { __iio_device_register(device.indio_dev, module.as_ptr()) })?;
+
+ let registered: Device<T, Registered> = Device {
+ indio_dev: device.indio_dev,
+ registered: true,
+ _p: PhantomData,
+ };
+ forget(device);
+
+ devres::register(parent, registered, GFP_KERNEL)
+ }
+}
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 68f4d9a3425d..726a23e2d579 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -81,6 +81,8 @@
#[cfg(CONFIG_I2C = "y")]
pub mod i2c;
pub mod id_pool;
+#[cfg(CONFIG_IIO)]
+pub mod iio;
#[doc(hidden)]
pub mod impl_flags;
pub mod init;
--
2.50.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH v6 4/4] iio: position: add Rust driver for ams AS5600
2026-10-16 15:48 [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
` (2 preceding siblings ...)
2026-10-16 15:48 ` [RFC PATCH v6 3/4] rust: add minimal IIO subsystem abstractions Muchamad Coirul Anwar
@ 2026-10-16 15:48 ` Muchamad Coirul Anwar
3 siblings, 0 replies; 5+ messages in thread
From: Muchamad Coirul Anwar @ 2026-10-16 15:48 UTC (permalink / raw)
To: Jonathan Cameron, Miguel Ojeda, Igor Korotin, Danilo Krummrich
Cc: Alice Ryhl, Daniel Almeida, Lars-Peter Clausen, Mark Brown,
Brandon StJohn, linux-iio, linux-i2c, rust-for-linux,
driver-core, linux-kernel
Add a Rust driver for the ams AS5600 12-bit magnetic rotary position
sensor.
Expose the raw angle and scale through the IIO sysfs interface in direct
mode. Read the status register before each angle sample and return
ENODATA when the magnet-detected bit is clear. Serialize the status and
angle transaction sequence with a mutex and propagate I2C transport
errors directly to userspace.
The AS5600 stores the raw angle with the high byte at register 0x0c.
Use the byte-swapped SMBus word helper and mask the result to 12 bits.
Signed-off-by: Muchamad Coirul Anwar <muchamadcoirulanwar@gmail.com>
---
drivers/iio/position/Kconfig | 11 ++
drivers/iio/position/Makefile | 1 +
drivers/iio/position/as5600.rs | 177 +++++++++++++++++++++++++++++++++
rust/kernel/bits.rs | 29 ++++++
4 files changed, 218 insertions(+)
create mode 100644 drivers/iio/position/as5600.rs
diff --git a/drivers/iio/position/Kconfig b/drivers/iio/position/Kconfig
index 1576a6380b53..14ed4bef82e1 100644
--- a/drivers/iio/position/Kconfig
+++ b/drivers/iio/position/Kconfig
@@ -6,6 +6,17 @@
menu "Linear and angular position sensors"
+config AS5600
+ tristate "ams AS5600 magnetic rotary position sensor"
+ depends on I2C=y && RUST
+ help
+ Support for the ams OSRAM AS5600 12-bit magnetic rotary
+ position sensor. Provides in_angl_raw (0-4095) and
+ in_angl_scale (radians per LSB) via sysfs.
+
+ To compile this driver as a module, choose M here: the
+ module will be called as5600.
+
config IQS624_POS
tristate "Azoteq IQS624/625 angular position sensors"
depends on MFD_IQS62X || COMPILE_TEST
diff --git a/drivers/iio/position/Makefile b/drivers/iio/position/Makefile
index d70902f2979d..2d26f6d6ace3 100644
--- a/drivers/iio/position/Makefile
+++ b/drivers/iio/position/Makefile
@@ -4,5 +4,6 @@
# When adding new entries keep the list in alphabetical order
+obj-$(CONFIG_AS5600) += as5600.o
obj-$(CONFIG_HID_SENSOR_CUSTOM_INTEL_HINGE) += hid-sensor-custom-intel-hinge.o
obj-$(CONFIG_IQS624_POS) += iqs624-pos.o
diff --git a/drivers/iio/position/as5600.rs b/drivers/iio/position/as5600.rs
new file mode 100644
index 000000000000..b72775241e81
--- /dev/null
+++ b/drivers/iio/position/as5600.rs
@@ -0,0 +1,177 @@
+// 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.
+//!
+//! Exposes `in_angl_raw` and `in_angl_scale` through the IIO sysfs interface
+//! in `INDIO_DIRECT_MODE`.
+//!
+//! Datasheet: AS5600, document DS000365.
+
+use kernel::{
+ bindings::{
+ iio_chan_info_enum_IIO_CHAN_INFO_RAW,
+ iio_chan_info_enum_IIO_CHAN_INFO_SCALE,
+ iio_chan_spec,
+ iio_chan_type_IIO_ANGL,
+ INDIO_DIRECT_MODE, //
+ },
+ bits::{
+ bit_u8,
+ bit_usize,
+ genmask_u16, //
+ },
+ device::{
+ Bound,
+ Core, //
+ },
+ error::code::ENODATA,
+ i2c::{
+ DeviceId,
+ Driver,
+ I2cClient,
+ IdTable, //
+ },
+ i2c_device_table,
+ iio::{
+ Device,
+ IioChanInfo,
+ IioDriver,
+ IioVal, //
+ },
+ io::Io,
+ 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_STATUS_MD: u8 = bit_u8(5);
+const AS5600_RAW_ANGLE_MASK: u16 = genmask_u16(0..=11);
+
+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"), ())]
+);
+
+struct As5600Channels([iio_chan_spec; 1]);
+
+// SAFETY: `iio_chan_spec` is a plain C struct with no interior mutability.
+// All pointer fields (`event_spec`, `ext_info`, `extend_name`, etc.) are
+// NULL - set via `zeroed()` and never reassigned - so no shared mutable
+// state exists behind them. The static is a compile-time constant with no
+// `&mut` access path, making concurrent shared access safe.
+unsafe impl Sync for As5600Channels {}
+
+static AS5600_CHANNELS: As5600Channels = As5600Channels({
+ // SAFETY: `iio_chan_spec` is a repr(C) struct where all-zeroes is valid
+ // (integers default to 0, pointers to NULL).
+ let mut chan: iio_chan_spec = unsafe { core::mem::zeroed() };
+ chan.type_ = iio_chan_type_IIO_ANGL;
+ chan.info_mask_separate = bit_usize(iio_chan_info_enum_IIO_CHAN_INFO_RAW)
+ | bit_usize(iio_chan_info_enum_IIO_CHAN_INFO_SCALE);
+ [chan]
+});
+
+#[pin_data]
+struct As5600Priv {
+ #[pin]
+ io_lock: Mutex<()>,
+}
+
+// SAFETY: AS5600_CHANNELS contains one valid, immutable IIO_ANGL channel
+// exposing only RAW and SCALE. It has no event or extension callbacks,
+// and all optional pointer fields are null.
+unsafe impl IioDriver for As5600Priv {
+ fn read_raw(
+ &self,
+ parent: &kernel::device::Device<Bound>,
+ _chan: *const iio_chan_spec,
+ info: IioChanInfo,
+ ) -> Result<IioVal> {
+ match info {
+ IioChanInfo::Raw => {
+ let _guard = self.io_lock.lock();
+ let client: &I2cClient<Bound> = parent.try_into()?;
+
+ let io = client.smbus_io();
+ // Read status register to verify magnet presence before
+ // reading the angle.
+ let status = io.try_read8(AS5600_REG_STATUS as usize)?;
+
+ if (status & AS5600_STATUS_MD) == 0 {
+ return Err(ENODATA);
+ }
+
+ // Word read at register 0x0C returns big-endian data.
+ // smbus_read_word_swapped() handles the byte swap.
+ let raw = client.smbus_read_word_swapped(AS5600_REG_RAW_ANGLE_H)?;
+ let angle = raw & AS5600_RAW_ANGLE_MASK;
+ Ok(IioVal::Int(angle as i32))
+ }
+ // Scale factor: radians per LSB = 2*pi / 4096 ~= 0.001533981
+ IioChanInfo::Scale => Ok(IioVal::IntPlusNano(0, 1533981)),
+ }
+ }
+
+ fn channels(&self) -> &'static [iio_chan_spec] {
+ &AS5600_CHANNELS.0
+ }
+}
+
+#[pin_data]
+struct As5600 {
+ _registration: (),
+}
+
+impl Driver for As5600 {
+ type IdInfo = ();
+ type Data<'bound> = As5600;
+
+ const I2C_ID_TABLE: Option<IdTable<Self::IdInfo>> = Some(&I2C_TABLE);
+ const OF_ID_TABLE: Option<of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
+
+ // `try_pin_init!` returns a concrete anonymous type that may expose more
+ // bounds than the trait signature declares (e.g. auto-traits like `Send`).
+ // This refinement of the RPITIT return type is intentional.
+ #[allow(refining_impl_trait)]
+ fn probe<'bound>(
+ dev: &'bound I2cClient<Core<'_>>,
+ _id_info: Option<&'bound Self::IdInfo>,
+ ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
+ try_pin_init!(As5600 {
+ _registration: {
+ let priv_init = pin_init!(As5600Priv {
+ io_lock <- new_mutex!(()),
+ });
+
+ Device::build_device(dev.as_ref(), c"as5600", INDIO_DIRECT_MODE, priv_init)?
+ .register(&crate::THIS_MODULE)?;
+ dev_dbg!(dev.as_ref(), "AS5600 magnetic position sensor ready\n");
+ }
+ })
+ }
+}
diff --git a/rust/kernel/bits.rs b/rust/kernel/bits.rs
index 2daead125626..a6537a668dd6 100644
--- a/rust/kernel/bits.rs
+++ b/rust/kernel/bits.rs
@@ -41,6 +41,7 @@ pub const fn [<bit_ $ty>](n: u32) -> $ty {
impl_bit_fn!(u32);
impl_bit_fn!(u16);
impl_bit_fn!(u8);
+impl_bit_fn!(usize);
macro_rules! impl_genmask_fn {
(
@@ -203,3 +204,31 @@ pub const fn [<genmask_ $ty>](range: RangeInclusive<u32>) -> $ty {
/// assert_eq!(genmask_u8(0..=7), u8::MAX);
/// ```
);
+
+impl_genmask_fn!(
+ usize,
+ /// # Examples
+ ///
+ /// ```
+ /// # #![expect(clippy::reversed_empty_ranges)]
+ /// # use kernel::bits::genmask_checked_usize;
+ /// assert_eq!(genmask_checked_usize(0..=0), Some(0b1));
+ /// assert_eq!(genmask_checked_usize(0..=3), Some(0b1111));
+ /// assert_eq!(genmask_checked_usize(1..=3), Some(0b1110));
+ ///
+ /// // `200` is out of the supported bit range on all platforms.
+ /// assert_eq!(genmask_checked_usize(0..=200), None);
+ ///
+ /// // Invalid range where the start is bigger than the end.
+ /// assert_eq!(genmask_checked_usize(5..=2), None);
+ /// ```
+ ,
+ /// # Examples
+ ///
+ /// ```
+ /// # use kernel::bits::genmask_usize;
+ /// assert_eq!(genmask_usize(0..=0), 0b1);
+ /// assert_eq!(genmask_usize(0..=3), 0b1111);
+ /// assert_eq!(genmask_usize(1..=3), 0b1110);
+ /// ```
+);
--
2.50.0
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-16 15:48 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-16 15:48 [RFC PATCH v6 0/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 1/4] rust: io: add FallibleIoCapable Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 2/4] i2c: rust: implement SMBus access via IoBackend Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 3/4] rust: add minimal IIO subsystem abstractions Muchamad Coirul Anwar
2026-10-16 15:48 ` [RFC PATCH v6 4/4] iio: position: add Rust driver for ams AS5600 Muchamad Coirul Anwar
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®