Re: [RFC PATCH v5 1/3] i2c: rust: implement SMBus access via IoBackend and FallibleIoCapable
Muchamad Coirul Anwar <[email protected]>
| Newsgroups | org.kernel.vger.linux-iio,org.kernel.vger.linux-i2c,org.kernel.vger.linux-kernel,org.kernel.vger.rust-for-linux |
|---|---|
| Message-ID | <CAO26r3QwUZ1QeO3PYNPoYQrbKpzfhttYAi0kNNB+QumAN=vZkQ@mail.gmail.com> |
Hi Jonathan, Thanks for the detailed review and for CC-ing Mark. On Mon, 24 Aug 2026 at 06:41, Jonathan Cameron <[email protected]> wrote: > > I2cClient<Bound>::smbus_io() returns an I2cView handle for use with the > > generic try_read8/try_read16 methods. Two standalone methods are also > > provided for odd-offset word access that bypasses the alignment check > > in the Io trait: > > Given some devices implement auto address increment and others decrement > even in aligned byte pairs it seems you will see things that 'smell' like > they are unaligned. > > I'd forgotten this fun corner of smbus like i2c devices! The standalone smbus_read_word() and smbus_read_word_swapped() methods exist for odd-offset access, but I didn't document that try_read16 and try_write16 reject odd offsets and that auto-increment devices may need to use them. I'll add that to smbus_io()'s doc comment in v6. > Just to repeat myself (and I appreciate the challenges that exist for > rust support in general and that it may be easier to look at the > i2c layer) I think that if it we are looking at bindings that are > register like then regmap is the way to go. The space of what you can > build that is register based and uses these i2c_smbus commands is a lot > richer than you might think. Either you end up reinventing all the > infrastructure regmap has to handle these, or you just use regmap. Agreed. regmap-rs is the correct long-term path. This series uses the IoBackend/SMBus route because regmap-rs does not yet exist, and the scope is intentionally minimal: byte and word reads only. I should have stated this in the cover letter from the start. I'll fix that in v6. Mark, if you have a view on whether this stepping-stone approach is acceptable or the series should wait, I'd appreciate your view. > There are mixed devices where register stuff is used alongside other accesses, > however for those I'm not sure it is worth doing anything other than > wrapping the raw bus access functions. > > A few more references to the real variations we have to cope with inline. Agreed. smbus_read_word() and smbus_read_word_swapped() on I2cClient<Bound> are exactly that: thin wrappers over the raw bus functions. For mixed devices that don't fit the register model, those standalone methods are the intended path, not the IoBackend machinery. > p.s. One day the rust driver in IIO won't be the bottom of my 'to review' > list :( > > > +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; > > There are smbus devices that have fully 2 byte registers. For those you'd > need to divide this by 2 and the range would go up to 510 > E.g. drivers/light/cm32181.c (though that doesn't have that many registers). Understood. The current maxsize=256 works for AS5600 but doesn't cover devices where all 256 command bytes address 16-bit registers; for those the range would need to go up to 510. I'll note this as a limitation in v6. > > +impl I2cClient<device::Bound> { > > + /// Returns an I/O handle for SMBus register access on this I2C client. > > + /// > > + /// The returned handle provides fallible read/write methods for the > > + /// 256-byte SMBus command address space (0x00–0xFF). This is the SMBus > > + /// command byte range, NOT the 7-bit device address, which is handled > > + /// by the I2C core at the adapter level. > > That rather feels like you are correcting my confusion in the earlier version! > I'd assume people are more awake than me and skip the NOT part ;) Sorry for the tone. I didn't mean to correct you. I'll drop that line. > > + /// Reads a 16-bit word from an SMBus register in CPU-native byte order. > > + /// > > + /// Wraps `i2c_smbus_read_word_data`. The `reg` parameter is the SMBus > > + /// command byte (0x00–0xFF) — an instruction sent to the device over the > > + /// serial bus, not a memory address. There is no alignment requirement: > > + /// any command byte value is valid regardless of whether it is odd or even. > > It might be a memory address, could be almost anything. Maybe 'not necessarily' > a memory address. > The kernel docs have it as: > "Command byte, a data byte which often selects a register on the device" I'll use the kernel-standard phrasing > > + /// Reads a 16-bit word from an SMBus register with bytes unconditionally > > + /// swapped. > > + /// > > + /// Wraps `i2c_smbus_read_word_data` and applies [`u16::swap_bytes`] to the > > + /// result. Use this for devices that store multi-byte registers in > > + /// big-endian (MSB-first) format, which is common among I2C sensors whose > > + /// datasheets do not reference the SMBus specification. > > This last bit feels like cover letter, patch description material. > I wouldn't normally expect function documentation to justify how useful > a function is! Fair point. I'll remove it. > > + /// > > + /// The swap is **unconditional** — it is not equivalent to `be16_to_cpu`. > > + /// On a big-endian CPU, `be16_to_cpu` would be a no-op, but this method > > + /// still swaps. The reason: SMBus always transmits the low byte first, so > > + /// the driver always receives data in little-endian wire order regardless > > + /// of CPU endianness. The swap corrects for the device's wire-level byte > > + /// order, not the CPU's native order. > > This feels like we are justifying why it isn't a different implementation. > Can we rewrite to not need that reference to what else it isn't. Agreed. I was over-explaining in the doc. I'll rewrite to describe only what the function does. > > + /// > > + /// The `reg` parameter is the SMBus command byte (0x00–0xFF). There is no > > + /// alignment requirement; any command byte value is valid. > > What would an alignment requirement mean here? Right, it doesn't apply here. I'll drop that sentence. > > + /// > > + /// Returns `Err` if the bus transaction fails (e.g. NACK, arbitration loss, > > + /// or timeout). > > + /// > > + /// # Example > > + /// > > + /// ```ignore > > + /// // AS5600 stores the 12-bit raw angle big-endian at register 0x0C. > > + /// let raw = client.smbus_read_word_swapped(0x0C)?; > > + /// let angle = raw & 0x0FFF; > > + /// ``` > > + #[inline] > > + pub fn smbus_read_word_swapped(&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`. > > + 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).swap_bytes()) > > + } > > + } > > +} > > 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). > > +/// > > Why is this part in the patch adding the i2c specific use case? > I'd expect it to be a precursor patch. I'll move FallibleIoCapable to a separate prerequisite patch in v6. > > +/// 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. >