From 3da34814b417111e1db787850be0fa1747f9f67d Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:35:56 +0200 Subject: [PATCH 1/6] rust: i2c: add SMBus read and write helpers Add helper functions on I2cClient for common SMBus operations: smbus_read_byte_data, smbus_write_byte_data, smbus_read_word_data, and smbus_write_word_data. These helpers provide direct, safe access to standard SMBus register transactions for I2C device drivers written in Rust. Signed-off-by: Vladyslav Pobigun --- rust/kernel/i2c.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/rust/kernel/i2c.rs b/rust/kernel/i2c.rs index dd9271af5eb8ba..a17fbf76972028 100644 --- a/rust/kernel/i2c.rs +++ b/rust/kernel/i2c.rs @@ -483,9 +483,40 @@ pub struct I2cClient( ); impl I2cClient { - fn as_raw(&self) -> *mut bindings::i2c_client { + /// Returns the raw pointer to the C `struct i2c_client`. + pub fn as_raw(&self) -> *mut bindings::i2c_client { self.0.get() } + + /// Read an 8-bit byte from an SMBus register. + pub fn smbus_read_byte_data(&self, command: u8) -> Result { + let res = unsafe { bindings::i2c_smbus_read_byte_data(self.as_raw(), command) }; + if res < 0 { + Err(Error::from_errno(res)) + } else { + Ok(res as u8) + } + } + + /// Write an 8-bit byte to an SMBus register. + pub fn smbus_write_byte_data(&self, command: u8, value: u8) -> Result { + to_result(unsafe { bindings::i2c_smbus_write_byte_data(self.as_raw(), command, value) }) + } + + /// Read a 16-bit word from an SMBus register. + pub fn smbus_read_word_data(&self, command: u8) -> Result { + let res = unsafe { bindings::i2c_smbus_read_word_data(self.as_raw(), command) }; + if res < 0 { + Err(Error::from_errno(res)) + } else { + Ok(res as u16) + } + } + + /// Write a 16-bit word to an SMBus register. + pub fn smbus_write_word_data(&self, command: u8, value: u16) -> Result { + to_result(unsafe { bindings::i2c_smbus_write_word_data(self.as_raw(), command, value) }) + } } // SAFETY: `I2cClient` is a transparent wrapper of `struct i2c_client`. From 4ab689ddf31fd0c5f5ddb57304c0634aedba2d72 Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:36:01 +0200 Subject: [PATCH 2/6] rust: hwmon: add safe abstractions and LM75 temperature driver Add safe Rust abstractions for the Hardware Monitoring (HWMON) subsystem. This allows writing hardware monitoring and temperature sensor drivers in pure, safe Rust. Also implement the in-tree LM75 digital temperature sensor driver utilizing these abstractions, supporting LM75, TMP75, TMP102, and MCP980x chips with two's complement fixed-point arithmetic and KUnit tests. Signed-off-by: Vladyslav Pobigun --- drivers/hwmon/Kconfig | 17 +- drivers/hwmon/Makefile | 1 + drivers/hwmon/lm75_rust.rs | 350 +++++++++++++++++++++++++++++++++++++ rust/kernel/hwmon.rs | 275 +++++++++++++++++++++++++++++ 4 files changed, 638 insertions(+), 5 deletions(-) create mode 100644 drivers/hwmon/lm75_rust.rs create mode 100644 rust/kernel/hwmon.rs diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 2bfbcc033d599c..23c262b1cb1203 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1552,12 +1552,19 @@ config SENSORS_LM75 - Texas Instruments TMP100, TMP101, TMP105, TMP112, TMP75, TMP175, TMP275 - This driver supports driver model based binding through board - specific I2C device tables. + This driver can also be built as a module. If so, the module + will be called lm75. - It also supports the "legacy" style of driver binding. To use - that with some chips which don't replicate LM75 quirks exactly, - you may need the "force" module parameter. +config SENSORS_LM75_RUST + tristate "National Semiconductor LM75 and compatibles in Rust" + depends on RUST && I2C + help + This option enables the Rust implementation of the LM75 digital + temperature sensor driver and its compatible chips (TMP75, TMP102, + DS75, MCP980x, etc.). + + This driver can also be built as a module. If so, the module + will be called lm75_rust. This driver can also be built as a module. If so, the module will be called lm75. diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 63effc0ab8d113..15d8b78ca295cf 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -123,6 +123,7 @@ obj-$(CONFIG_SENSORS_LM63) += lm63.o obj-$(CONFIG_SENSORS_LM70) += lm70.o obj-$(CONFIG_SENSORS_LM73) += lm73.o obj-$(CONFIG_SENSORS_LM75) += lm75.o +obj-$(CONFIG_SENSORS_LM75_RUST) += lm75_rust.o obj-$(CONFIG_SENSORS_LM77) += lm77.o obj-$(CONFIG_SENSORS_LM78) += lm78.o obj-$(CONFIG_SENSORS_LM80) += lm80.o diff --git a/drivers/hwmon/lm75_rust.rs b/drivers/hwmon/lm75_rust.rs new file mode 100644 index 00000000000000..2423e4bfe4811f --- /dev/null +++ b/drivers/hwmon/lm75_rust.rs @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! LM75 and compatible digital temperature sensor driver in Rust. +//! +//! C version: `drivers/hwmon/lm75.c` +//! +//! This driver handles LM75 and compatible I2C temperature sensors, +//! including TI TMP75, TMP102, Microchip MCP980x, Dallas DS75, etc. + +use kernel::{ + bindings, + device, + error::*, + hwmon::{self, Operations, SensorType, TempAttribute}, + i2c, + of, + prelude::*, +}; + +// LM75 Register addresses. +const LM75_REG_TEMP: u8 = 0x00; +const LM75_REG_CONF: u8 = 0x01; +const LM75_REG_HYST: u8 = 0x02; +const LM75_REG_MAX: u8 = 0x03; + +// Configuration register bit flags. +const LM75_CONF_SHUTDOWN: u8 = 0x01; + +// Temperature bounds in milli-degrees Celsius (-55°C to +125°C). +const LM75_TEMP_MIN: i32 = -55_000; +const LM75_TEMP_MAX: i32 = 125_000; + +/// Chip variants supported by the driver. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ChipKind { + /// National Semiconductor LM75. + Lm75, + /// National Semiconductor / NXP LM75A with higher resolution. + Lm75a, + /// Texas Instruments TMP75. + Tmp75, + /// Texas Instruments TMP102. + Tmp102, + /// Dallas Semiconductor DS75. + Ds75, + /// Microchip MCP980x series. + Mcp980x, +} + +// I2C Device ID table. +kernel::i2c_device_table!( + I2C_TABLE, + MODULE_I2C_TABLE, + ChipKind, + [ + (i2c::DeviceId::new(c"lm75"), ChipKind::Lm75), + (i2c::DeviceId::new(c"lm75a"), ChipKind::Lm75a), + (i2c::DeviceId::new(c"tmp75"), ChipKind::Tmp75), + (i2c::DeviceId::new(c"tmp102"), ChipKind::Tmp102), + (i2c::DeviceId::new(c"ds75"), ChipKind::Ds75), + (i2c::DeviceId::new(c"mcp980x"), ChipKind::Mcp980x), + ] +); + +// Device Tree (OpenFirmware) match table. +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ChipKind, + [ + (of::DeviceId::new(c"national,lm75"), ChipKind::Lm75), + (of::DeviceId::new(c"national,lm75a"), ChipKind::Lm75a), + (of::DeviceId::new(c"ti,tmp75"), ChipKind::Tmp75), + (of::DeviceId::new(c"ti,tmp102"), ChipKind::Tmp102), + (of::DeviceId::new(c"dallas,ds75"), ChipKind::Ds75), + (of::DeviceId::new(c"microchip,mcp980x"), ChipKind::Mcp980x), + ] +); + +/// Driver private data per probed device instance. +pub struct Lm75Data { + /// The detected chip variant. + #[expect(dead_code)] + kind: ChipKind, + raw_client: *mut kernel::bindings::i2c_client, + hwmon_reg: Option, +} + +// SAFETY: Lm75Data can be safely sent and shared across threads. +unsafe impl Send for Lm75Data {} +unsafe impl Sync for Lm75Data {} + +impl Lm75Data { + /// Read 16-bit word from I2C device with byte-swapping (LM75 sends MSB first). + fn read_temp_reg(&self, reg: u8) -> Result { + let val = unsafe { kernel::bindings::i2c_smbus_read_word_data(self.raw_client, reg) }; + if val < 0 { + Err(Error::from_errno(val)) + } else { + Ok((val as u16).swap_bytes()) + } + } + + /// Write 16-bit word to I2C device with byte-swapping. + fn write_temp_reg(&self, reg: u8, val: u16) -> Result { + to_result(unsafe { + kernel::bindings::i2c_smbus_write_word_data(self.raw_client, reg, val.swap_bytes()) + }) + } + + /// Read 8-bit configuration register. + fn read_config(&self) -> Result { + let val = unsafe { kernel::bindings::i2c_smbus_read_byte_data(self.raw_client, LM75_REG_CONF) }; + if val < 0 { + Err(Error::from_errno(val)) + } else { + Ok(val as u8) + } + } + + /// Write 8-bit configuration register. + fn write_config(&self, val: u8) -> Result { + to_result(unsafe { + kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, LM75_REG_CONF, val) + }) + } + + /// Read current temperature in millicelsius. + pub fn read_temperature(&self) -> Result { + let raw = self.read_temp_reg(LM75_REG_TEMP)?; + Ok(temp_from_reg(raw)) + } + + /// Read over-temperature shutdown threshold (T_max) in millicelsius. + pub fn read_max_temperature(&self) -> Result { + let raw = self.read_temp_reg(LM75_REG_MAX)?; + Ok(temp_from_reg(raw)) + } + + /// Set over-temperature shutdown threshold (T_max) in millicelsius. + pub fn set_max_temperature(&self, temp_millicelsius: i32) -> Result { + let reg = temp_to_reg(temp_millicelsius); + self.write_temp_reg(LM75_REG_MAX, reg) + } + + /// Read hysteresis temperature threshold (T_hyst) in millicelsius. + pub fn read_hyst_temperature(&self) -> Result { + let raw = self.read_temp_reg(LM75_REG_HYST)?; + Ok(temp_from_reg(raw)) + } + + /// Set hysteresis temperature threshold (T_hyst) in millicelsius. + pub fn set_hyst_temperature(&self, temp_millicelsius: i32) -> Result { + let reg = temp_to_reg(temp_millicelsius); + self.write_temp_reg(LM75_REG_HYST, reg) + } +} + +impl Operations for Lm75Data { + fn is_visible(&self, type_: SensorType, attr: u32, _channel: i32) -> u16 { + if type_ == SensorType::Temp { + match attr { + x if x == TempAttribute::Input as u32 => 0o444, + x if x == TempAttribute::Max as u32 => 0o644, + x if x == TempAttribute::MaxHyst as u32 => 0o644, + _ => 0, + } + } else { + 0 + } + } + + fn read(&self, type_: SensorType, attr: u32, _channel: i32) -> Result { + if type_ != SensorType::Temp { + return Err(EINVAL); + } + match attr { + x if x == TempAttribute::Input as u32 => self.read_temperature().map(|v| v as i64), + x if x == TempAttribute::Max as u32 => self.read_max_temperature().map(|v| v as i64), + x if x == TempAttribute::MaxHyst as u32 => self.read_hyst_temperature().map(|v| v as i64), + _ => Err(EINVAL), + } + } + + fn write(&self, type_: SensorType, attr: u32, _channel: i32, val: i64) -> Result { + if type_ != SensorType::Temp { + return Err(EINVAL); + } + match attr { + x if x == TempAttribute::Max as u32 => self.set_max_temperature(val as i32), + x if x == TempAttribute::MaxHyst as u32 => self.set_hyst_temperature(val as i32), + _ => Err(EINVAL), + } + } +} + +// Temperature channel configuration bitmask. +const LM75_TEMP_CONFIG: [u32; 2] = [ + (1 << bindings::hwmon_temp_attributes_hwmon_temp_input) + | (1 << bindings::hwmon_temp_attributes_hwmon_temp_max) + | (1 << bindings::hwmon_temp_attributes_hwmon_temp_max_hyst), + 0, +]; + +static LM75_TEMP_CHANNEL_INFO: hwmon::ChannelInfo = + hwmon::ChannelInfo::new(bindings::hwmon_sensor_types_hwmon_temp, &LM75_TEMP_CONFIG); + +static LM75_CHANNEL_INFO_LIST: hwmon::ChannelInfoList<2> = + hwmon::ChannelInfoList::new([LM75_TEMP_CHANNEL_INFO.as_ptr(), core::ptr::null()]); + +static LM75_CHIP_INFO: hwmon::ChipInfo = + hwmon::ChipInfo::new(&hwmon::Adapter::::OPS, &LM75_CHANNEL_INFO_LIST); + +/// Convert temperature from millicelsius to 16-bit LM75 register format. +/// +/// Format: 9-bit two's complement, 0.5°C LSB, left-aligned in 16-bit word. +pub fn temp_to_reg(temp_millicelsius: i32) -> u16 { + let clamped = temp_millicelsius.clamp(LM75_TEMP_MIN, LM75_TEMP_MAX); + let rounded = if clamped < 0 { + clamped - 250 + } else { + clamped + 250 + }; + ((rounded / 500) as u16) << 7 +} + +/// Convert 16-bit LM75 register value to millicelsius. +/// +/// Arithmetic division is used to preserve sign during right shift. +pub fn temp_from_reg(reg: u16) -> i32 { + ((reg as i16) / 128) as i32 * 500 +} + +/// The LM75 I2C driver structure. +struct Lm75Driver; + +impl i2c::Driver for Lm75Driver { + type IdInfo = ChipKind; + type Data<'bound> = Lm75Data; + + const I2C_ID_TABLE: Option> = Some(&I2C_TABLE); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + dev: &'bound i2c::I2cClient>, + id_info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let kind = id_info.copied().unwrap_or(ChipKind::Lm75); + let raw_client = dev.as_raw(); + + pr_info!("LM75 Rust driver probing device at I2C adapter\n"); + + // Verify device communication by reading config register. + let status = unsafe { kernel::bindings::i2c_smbus_read_byte_data(raw_client, LM75_REG_CONF) }; + if status < 0 { + pr_err!("LM75 Rust: failed to read config register: {}\n", status); + return Err(Error::from_errno(status)); + } + + pr_info!("LM75 Rust: device successfully detected (conf=0x{:02x})\n", status); + + // Wake up chip from shutdown mode if set. + let conf = status as u8; + if conf & LM75_CONF_SHUTDOWN != 0 { + let new_conf = conf & !LM75_CONF_SHUTDOWN; + let _ = unsafe { kernel::bindings::i2c_smbus_write_byte_data(raw_client, LM75_REG_CONF, new_conf) }; + } + + // Read initial temperature reading to verify sensor data path. + let temp_raw = unsafe { kernel::bindings::i2c_smbus_read_word_data(raw_client, LM75_REG_TEMP) }; + if temp_raw >= 0 { + let temp_swapped = (temp_raw as u16).swap_bytes(); + let temp_mc = temp_from_reg(temp_swapped); + pr_info!("LM75 Rust: Initial temperature: {}.{}°C\n", temp_mc / 1000, (temp_mc.abs() % 1000) / 100); + } + + let mut data = Lm75Data { + kind, + raw_client, + hwmon_reg: None, + }; + + // Register with the kernel HWMON subsystem. + match hwmon::Registration::register(dev.as_ref(), c"lm75", &data, &LM75_CHIP_INFO) { + Ok(reg) => { + pr_info!("LM75 Rust: registered with HWMON subsystem\n"); + data.hwmon_reg = Some(reg); + } + Err(e) => { + pr_warn!("LM75 Rust: failed to register with HWMON: {:?}\n", e); + } + } + + Ok(data) + } + + fn shutdown<'bound>(_dev: &'bound i2c::I2cClient>, this: Pin<&Self::Data<'bound>>) { + pr_info!("LM75 Rust: putting device into low-power shutdown mode\n"); + if let Ok(conf) = this.read_config() { + let _ = this.write_config(conf | LM75_CONF_SHUTDOWN); + } + } +} + +kernel::module_i2c_driver! { + type: Lm75Driver, + name: "lm75_rust", + authors: ["Rust for Linux Developers"], + description: "Rust LM75 I2C Temperature Sensor Driver", + license: "GPL", +} + +#[cfg(CONFIG_KUNIT)] +mod tests { + use super::*; + + #[kunit_test] + fn test_temp_conversion_positive() { + // 25.0°C = 25000 millicelsius -> 25000 / 500 = 50 -> 50 << 7 = 0x1900 + let reg = temp_to_reg(25_000); + let back = temp_from_reg(reg); + assert_eq!(back, 25_000); + } + + #[kunit_test] + fn test_temp_conversion_zero() { + let reg = temp_to_reg(0); + let back = temp_from_reg(reg); + assert_eq!(back, 0); + } + + #[kunit_test] + fn test_temp_conversion_negative() { + // -25.0°C = -25000 millicelsius + let reg = temp_to_reg(-25_000); + let back = temp_from_reg(reg); + assert_eq!(back, -25_000); + } + + #[kunit_test] + fn test_temp_conversion_clamping() { + // Value above max (150°C) should be clamped to 125°C + let reg_max = temp_to_reg(150_000); + assert_eq!(temp_from_reg(reg_max), 125_000); + + // Value below min (-80°C) should be clamped to -55°C + let reg_min = temp_to_reg(-80_000); + assert_eq!(temp_from_reg(reg_min), -55_000); + } +} diff --git a/rust/kernel/hwmon.rs b/rust/kernel/hwmon.rs new file mode 100644 index 00000000000000..7b71b783dc4ecc --- /dev/null +++ b/rust/kernel/hwmon.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Hardware Monitoring (HWMON) subsystem abstractions. +//! +//! C header: [`include/linux/hwmon.h`](srctree/include/linux/hwmon.h) + +use crate::{ + bindings, + device, + error::*, + ffi::{c_int, c_long, c_void}, + prelude::*, +}; +use core::{marker::PhantomData, ptr::NonNull}; + +/// Sensor types recognized by the HWMON subsystem. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum SensorType { + /// Chip-level attributes. + Chip = bindings::hwmon_sensor_types_hwmon_chip, + /// Temperature sensor. + Temp = bindings::hwmon_sensor_types_hwmon_temp, + /// Voltage sensor. + In = bindings::hwmon_sensor_types_hwmon_in, + /// Current sensor. + Curr = bindings::hwmon_sensor_types_hwmon_curr, + /// Power sensor. + Power = bindings::hwmon_sensor_types_hwmon_power, + /// Fan speed sensor. + Fan = bindings::hwmon_sensor_types_hwmon_fan, + /// PWM output control. + Pwm = bindings::hwmon_sensor_types_hwmon_pwm, +} + +impl SensorType { + fn from_raw(raw: bindings::hwmon_sensor_types) -> Option { + match raw { + bindings::hwmon_sensor_types_hwmon_chip => Some(Self::Chip), + bindings::hwmon_sensor_types_hwmon_temp => Some(Self::Temp), + bindings::hwmon_sensor_types_hwmon_in => Some(Self::In), + bindings::hwmon_sensor_types_hwmon_curr => Some(Self::Curr), + bindings::hwmon_sensor_types_hwmon_power => Some(Self::Power), + bindings::hwmon_sensor_types_hwmon_fan => Some(Self::Fan), + bindings::hwmon_sensor_types_hwmon_pwm => Some(Self::Pwm), + _ => None, + } + } +} + +/// Temperature sensor attributes. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum TempAttribute { + /// Current temperature input value in millicelsius. + Input = bindings::hwmon_temp_attributes_hwmon_temp_input, + /// Maximum temperature threshold in millicelsius. + Max = bindings::hwmon_temp_attributes_hwmon_temp_max, + /// Hysteresis temperature threshold for max in millicelsius. + MaxHyst = bindings::hwmon_temp_attributes_hwmon_temp_max_hyst, + /// Minimum temperature threshold in millicelsius. + Min = bindings::hwmon_temp_attributes_hwmon_temp_min, + /// Critical temperature threshold. + Crit = bindings::hwmon_temp_attributes_hwmon_temp_crit, + /// Alarm bit indicating temperature threshold breach. + Alarm = bindings::hwmon_temp_attributes_hwmon_temp_alarm, + /// Text label for temperature sensor. + Label = bindings::hwmon_temp_attributes_hwmon_temp_label, +} + +/// Safe wrapper around `bindings::hwmon_channel_info`. +pub struct ChannelInfo(bindings::hwmon_channel_info); + +// SAFETY: ChannelInfo contains immutable descriptors safe to share across threads. +unsafe impl Send for ChannelInfo {} +unsafe impl Sync for ChannelInfo {} + +impl ChannelInfo { + /// Create a new static ChannelInfo. + pub const fn new(sensor_type: bindings::hwmon_sensor_types, config: &'static [u32]) -> Self { + Self(bindings::hwmon_channel_info { + type_: sensor_type, + config: config.as_ptr(), + }) + } + + /// Return a raw pointer to the channel info struct. + pub const fn as_ptr(&self) -> *const bindings::hwmon_channel_info { + &self.0 + } +} + +/// Safe wrapper around an array of channel info pointers. +pub struct ChannelInfoList([*const bindings::hwmon_channel_info; N]); + +// SAFETY: Pointer array is static and read-only. +unsafe impl Send for ChannelInfoList {} +unsafe impl Sync for ChannelInfoList {} + +impl ChannelInfoList { + /// Create a new channel info pointer list. + pub const fn new(list: [*const bindings::hwmon_channel_info; N]) -> Self { + Self(list) + } + + /// Return a pointer to the head of the list. + pub const fn as_ptr(&self) -> *const *const bindings::hwmon_channel_info { + self.0.as_ptr() + } +} + +/// Safe wrapper around `bindings::hwmon_chip_info`. +pub struct ChipInfo(bindings::hwmon_chip_info); + +// SAFETY: ChipInfo is static and thread-safe. +unsafe impl Send for ChipInfo {} +unsafe impl Sync for ChipInfo {} + +impl ChipInfo { + /// Create a new static ChipInfo. + pub const fn new( + ops: &'static bindings::hwmon_ops, + channel_list: &'static ChannelInfoList, + ) -> Self { + Self(bindings::hwmon_chip_info { + ops, + info: channel_list.as_ptr(), + }) + } + + /// Return the raw pointer to the chip info struct. + pub fn as_raw(&self) -> *const bindings::hwmon_chip_info { + &self.0 + } +} + +/// Operations trait for HWMON drivers. +pub trait Operations: Send + Sync + Sized { + /// Read a sensor attribute value. + fn read(&self, type_: SensorType, attr: u32, channel: i32) -> Result { + let _ = (type_, attr, channel); + Err(EINVAL) + } + + /// Write a sensor attribute value. + fn write(&self, type_: SensorType, attr: u32, channel: i32, val: i64) -> Result { + let _ = (type_, attr, channel, val); + Err(EINVAL) + } + + /// Check if an attribute is visible and return its file mode permissions (e.g. 0o444, 0o644). + fn is_visible(&self, type_: SensorType, attr: u32, channel: i32) -> u16 { + let _ = (type_, attr, channel); + 0 + } +} + +/// Adapter holding the C callbacks for a driver implementing [`Operations`]. +pub struct Adapter(PhantomData); + +impl Adapter { + unsafe extern "C" fn is_visible_callback( + drvdata: *const c_void, + type_: bindings::hwmon_sensor_types, + attr: u32, + channel: c_int, + ) -> bindings::umode_t { + if drvdata.is_null() { + return 0; + } + // SAFETY: `drvdata` was passed during registration and points to `T`. + let op = unsafe { &*drvdata.cast::() }; + match SensorType::from_raw(type_) { + Some(st) => op.is_visible(st, attr, channel), + None => 0, + } + } + + unsafe extern "C" fn read_callback( + dev: *mut bindings::device, + type_: bindings::hwmon_sensor_types, + attr: u32, + channel: c_int, + val: *mut c_long, + ) -> c_int { + // SAFETY: `dev` is valid and `dev_get_drvdata` retrieves our `T`. + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() || val.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + let st = match SensorType::from_raw(type_) { + Some(s) => s, + None => return -(bindings::EINVAL as c_int), + }; + + match op.read(st, attr, channel) { + Ok(v) => { + unsafe { *val = v as c_long }; + 0 + } + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn write_callback( + dev: *mut bindings::device, + type_: bindings::hwmon_sensor_types, + attr: u32, + channel: c_int, + val: c_long, + ) -> c_int { + // SAFETY: `dev` is valid. + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + let st = match SensorType::from_raw(type_) { + Some(s) => s, + None => return -(bindings::EINVAL as c_int), + }; + + match op.write(st, attr, channel, val as i64) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + /// Const vtable for HWMON callbacks. + pub const OPS: bindings::hwmon_ops = bindings::hwmon_ops { + visible: 0, + is_visible: Some(Self::is_visible_callback), + read: Some(Self::read_callback), + read_string: None, + write: Some(Self::write_callback), + }; +} + +/// Registration handle of a HWMON device. Unregisters device upon `Drop`. +pub struct Registration { + #[expect(dead_code)] + hwmon_dev: NonNull, +} + +impl Registration { + /// Register a new HWMON device attached to `parent_dev`. + pub fn register( + parent_dev: &device::Device, + name: &'static CStr, + drvdata: &T, + chip_info: &'static ChipInfo, + ) -> Result { + let parent_ptr = parent_dev.as_raw(); + let data_ptr = (drvdata as *const T).cast::() as *mut c_void; + + // SAFETY: We pass valid pointers to `devm_hwmon_device_register_with_info`. + let ret = unsafe { + bindings::devm_hwmon_device_register_with_info( + parent_ptr, + name.as_char_ptr(), + data_ptr, + chip_info.as_raw(), + core::ptr::null_mut(), + ) + }; + + let dev_ptr = NonNull::new(ret).ok_or(ENOMEM)?; + Ok(Self { hwmon_dev: dev_ptr }) + } +} + +// SAFETY: HWMON registration can be moved between threads. +unsafe impl Send for Registration {} +unsafe impl Sync for Registration {} From 6cff867f6ac9e21b38714b12b732103df0b67dfe Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:36:05 +0200 Subject: [PATCH 3/6] rust: rtc: add safe abstractions and PCF8563 RTC driver Add safe Rust abstractions for the Real-Time Clock (RTC) subsystem, including RtcTime representation, BCD conversion utilities, and registration within rtc_class. Implement the PCF8563/RTC8564 I2C RTC driver in Rust supporting date and time read/write and low-voltage battery status detection. Signed-off-by: Vladyslav Pobigun --- drivers/rtc/Kconfig | 12 +- drivers/rtc/Makefile | 1 + drivers/rtc/rtc_pcf8563_rust.rs | 221 ++++++++++++++++++++++++++++++++ rust/kernel/rtc.rs | 198 ++++++++++++++++++++++++++++ 4 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 drivers/rtc/rtc_pcf8563_rust.rs create mode 100644 rust/kernel/rtc.rs diff --git a/drivers/rtc/Kconfig b/drivers/rtc/Kconfig index 01def82318731c..6ef96a85607450 100644 --- a/drivers/rtc/Kconfig +++ b/drivers/rtc/Kconfig @@ -531,11 +531,19 @@ config RTC_DRV_PCF8563 help If you say yes here you get support for the Philips PCF8563 RTC chip. The Epson RTC8564 - should work as well. - This driver can also be built as a module. If so, the module will be called rtc-pcf8563. +config RTC_DRV_PCF8563_RUST + tristate "Philips PCF8563/Epson RTC8564 in Rust" + depends on RUST && I2C + help + This option enables the Rust implementation of the Philips PCF8563, + Epson RTC8564, and PCA8565 Real-Time Clock chips. + + This driver can also be built as a module. If so, the module + will be called rtc_pcf8563_rust. + config RTC_DRV_PCF8583 tristate "Philips PCF8583" help diff --git a/drivers/rtc/Makefile b/drivers/rtc/Makefile index 0347645b021f8f..e2e5b7a5b1de31 100644 --- a/drivers/rtc/Makefile +++ b/drivers/rtc/Makefile @@ -134,6 +134,7 @@ obj-$(CONFIG_RTC_DRV_PCF85063) += rtc-pcf85063.o obj-$(CONFIG_RTC_DRV_PCF8523) += rtc-pcf8523.o obj-$(CONFIG_RTC_DRV_PCF85363) += rtc-pcf85363.o obj-$(CONFIG_RTC_DRV_PCF8563) += rtc-pcf8563.o +obj-$(CONFIG_RTC_DRV_PCF8563_RUST) += rtc_pcf8563_rust.o obj-$(CONFIG_RTC_DRV_PCF8583) += rtc-pcf8583.o obj-$(CONFIG_RTC_DRV_PIC32) += rtc-pic32.o obj-$(CONFIG_RTC_DRV_PL030) += rtc-pl030.o diff --git a/drivers/rtc/rtc_pcf8563_rust.rs b/drivers/rtc/rtc_pcf8563_rust.rs new file mode 100644 index 00000000000000..83643881bb8827 --- /dev/null +++ b/drivers/rtc/rtc_pcf8563_rust.rs @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! NXP PCF8563 and compatible Real-Time Clock (RTC) driver in Rust. +//! +//! C version: `drivers/rtc/rtc-pcf8563.c` +//! +//! Handles PCF8563, Epson RTC8564, and NXP PCA8565 I2C RTC chips. + +use kernel::{ + device, + error::*, + i2c, + of, + prelude::*, + rtc::{self, bcd_to_bin, bin_to_bcd, Operations, RtcTime}, +}; + +// PCF8563 Register Map. +const PCF8563_REG_ST1: u8 = 0x00; +#[expect(dead_code)] +const PCF8563_REG_ST2: u8 = 0x01; +const PCF8563_REG_SC: u8 = 0x02; // Seconds +const PCF8563_REG_MN: u8 = 0x03; // Minutes +const PCF8563_REG_HR: u8 = 0x04; // Hours +const PCF8563_REG_DM: u8 = 0x05; // Day of month +const PCF8563_REG_DW: u8 = 0x06; // Day of week +const PCF8563_REG_MO: u8 = 0x07; // Month +const PCF8563_REG_YR: u8 = 0x08; // Year + +// Flags +const PCF8563_SC_LV: u8 = 0x80; // Low voltage / data invalid flag +#[expect(dead_code)] +const PCF8563_MO_C: u8 = 0x80; // Century bit + +/// Supported chip variants. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ChipKind { + /// NXP PCF8563. + Pcf8563, + /// Epson RTC-8564. + Rtc8564, + /// NXP PCA8565. + Pca8565, +} + +// I2C Device ID table. +kernel::i2c_device_table!( + I2C_TABLE, + MODULE_I2C_TABLE, + ChipKind, + [ + (i2c::DeviceId::new(c"pcf8563"), ChipKind::Pcf8563), + (i2c::DeviceId::new(c"rtc8564"), ChipKind::Rtc8564), + (i2c::DeviceId::new(c"pca8565"), ChipKind::Pca8565), + ] +); + +// Device Tree (OpenFirmware) match table. +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ChipKind, + [ + (of::DeviceId::new(c"nxp,pcf8563"), ChipKind::Pcf8563), + (of::DeviceId::new(c"epson,rtc8564"), ChipKind::Rtc8564), + (of::DeviceId::new(c"nxp,pca8565"), ChipKind::Pca8565), + ] +); + +/// Driver private data per probed instance. +pub struct Pcf8563Data { + #[expect(dead_code)] + kind: ChipKind, + raw_client: *mut kernel::bindings::i2c_client, + hwmon_reg: Option, +} + +// SAFETY: Data can be shared across threads. +unsafe impl Send for Pcf8563Data {} +unsafe impl Sync for Pcf8563Data {} + +impl Pcf8563Data { + fn read_reg(&self, reg: u8) -> Result { + let val = unsafe { kernel::bindings::i2c_smbus_read_byte_data(self.raw_client, reg) }; + if val < 0 { + Err(Error::from_errno(val)) + } else { + Ok(val as u8) + } + } + + fn write_reg(&self, reg: u8, val: u8) -> Result { + to_result(unsafe { + kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, reg, val) + }) + } +} + +impl Operations for Pcf8563Data { + fn read_time(&self) -> Result { + let sec_raw = self.read_reg(PCF8563_REG_SC)?; + if sec_raw & PCF8563_SC_LV != 0 { + pr_warn!("PCF8563: low voltage detected, RTC time is invalid\n"); + return Err(EINVAL); + } + + let min_raw = self.read_reg(PCF8563_REG_MN)?; + let hr_raw = self.read_reg(PCF8563_REG_HR)?; + let mday_raw = self.read_reg(PCF8563_REG_DM)?; + let wday_raw = self.read_reg(PCF8563_REG_DW)?; + let mon_raw = self.read_reg(PCF8563_REG_MO)?; + let yr_raw = self.read_reg(PCF8563_REG_YR)?; + + let tm_sec = bcd_to_bin(sec_raw & 0x7f) as u32; + let tm_min = bcd_to_bin(min_raw & 0x7f) as u32; + let tm_hour = bcd_to_bin(hr_raw & 0x3f) as u32; + let tm_mday = bcd_to_bin(mday_raw & 0x3f) as u32; + let tm_wday = (wday_raw & 0x07) as u32; + let tm_mon = bcd_to_bin(mon_raw & 0x1f).saturating_sub(1) as u32; + let tm_year = (bcd_to_bin(yr_raw) as i32) + 100; + + Ok(RtcTime { + tm_sec, + tm_min, + tm_hour, + tm_mday, + tm_mon, + tm_year, + tm_wday, + tm_yday: 0, + tm_isdst: 0, + }) + } + + fn set_time(&self, tm: &RtcTime) -> Result { + let sec_bcd = bin_to_bcd(tm.tm_sec as u8); + let min_bcd = bin_to_bcd(tm.tm_min as u8); + let hr_bcd = bin_to_bcd(tm.tm_hour as u8); + let mday_bcd = bin_to_bcd(tm.tm_mday as u8); + let mon_bcd = bin_to_bcd((tm.tm_mon + 1) as u8); + let yr_val = if tm.tm_year >= 100 { + tm.tm_year - 100 + } else { + tm.tm_year + }; + let yr_bcd = bin_to_bcd(yr_val as u8); + let wday_bcd = (tm.tm_wday & 0x07) as u8; + + self.write_reg(PCF8563_REG_SC, sec_bcd)?; + self.write_reg(PCF8563_REG_MN, min_bcd)?; + self.write_reg(PCF8563_REG_HR, hr_bcd)?; + self.write_reg(PCF8563_REG_DM, mday_bcd)?; + self.write_reg(PCF8563_REG_DW, wday_bcd)?; + self.write_reg(PCF8563_REG_MO, mon_bcd)?; + self.write_reg(PCF8563_REG_YR, yr_bcd)?; + + Ok(()) + } +} + +/// The PCF8563 I2C driver structure. +struct Pcf8563Driver; + +impl i2c::Driver for Pcf8563Driver { + type IdInfo = ChipKind; + type Data<'bound> = Pcf8563Data; + + const I2C_ID_TABLE: Option> = Some(&I2C_TABLE); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + dev: &'bound i2c::I2cClient>, + id_info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let kind = id_info.copied().unwrap_or(ChipKind::Pcf8563); + let raw_client = dev.as_raw(); + + pr_info!("PCF8563 Rust driver probing device at I2C adapter\n"); + + // Verify communication by reading status register 1. + let status = unsafe { kernel::bindings::i2c_smbus_read_byte_data(raw_client, PCF8563_REG_ST1) }; + if status < 0 { + pr_err!("PCF8563 Rust: failed to read status register: {}\n", status); + return Err(Error::from_errno(status)); + } + + pr_info!("PCF8563 Rust: device detected (st1=0x{:02x})\n", status); + + let mut data = Pcf8563Data { + kind, + raw_client, + hwmon_reg: None, + }; + + // Register RTC device with kernel RTC class. + match rtc::Registration::register( + dev.as_ref(), + c"rtc_pcf8563_rust", + &data, + &rtc::Adapter::::OPS, + ) { + Ok(reg) => { + pr_info!("PCF8563 Rust: registered as RTC device\n"); + data.hwmon_reg = Some(reg); + } + Err(e) => { + pr_warn!("PCF8563 Rust: failed to register RTC device: {:?}\n", e); + } + } + + Ok(data) + } +} + +kernel::module_i2c_driver! { + type: Pcf8563Driver, + name: "rtc_pcf8563_rust", + authors: ["Rust for Linux Developers"], + description: "Rust PCF8563 Real-Time Clock Driver", + license: "GPL", +} diff --git a/rust/kernel/rtc.rs b/rust/kernel/rtc.rs new file mode 100644 index 00000000000000..d56f1305369320 --- /dev/null +++ b/rust/kernel/rtc.rs @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Real-Time Clock (RTC) subsystem abstractions. +//! +//! C header: [`include/linux/rtc.h`](srctree/include/linux/rtc.h) + +use crate::{ + bindings, + device, + error::*, + ffi::c_int, + prelude::*, +}; +use core::{marker::PhantomData, ptr::NonNull}; + +/// Convert binary-coded decimal (BCD) byte to binary value. +#[inline(always)] +pub const fn bcd_to_bin(val: u8) -> u8 { + (val & 0x0f) + ((val >> 4) * 10) +} + +/// Convert binary value (0..99) to binary-coded decimal (BCD) byte. +#[inline(always)] +pub const fn bin_to_bcd(val: u8) -> u8 { + ((val / 10) << 4) | (val % 10) +} + +/// Representation of standard RTC date and time. +/// +/// Mirrors `struct rtc_time` in [`include/linux/rtc.h`](srctree/include/linux/rtc.h). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RtcTime { + /// Seconds in minute (0..59, up to 60 for leap seconds). + pub tm_sec: u32, + /// Minutes in hour (0..59). + pub tm_min: u32, + /// Hours in day (0..23). + pub tm_hour: u32, + /// Day of the month (1..31). + pub tm_mday: u32, + /// Month of the year (0..11, where 0 is January). + pub tm_mon: u32, + /// Years since 1900 (e.g. 2026 is 126). + pub tm_year: i32, + /// Day of the week (0..6, Sunday is 0). + pub tm_wday: u32, + /// Day in year (0..365). + pub tm_yday: u32, + /// Daylight savings flag. + pub tm_isdst: i32, +} + +impl From for RtcTime { + fn from(tm: bindings::rtc_time) -> Self { + Self { + tm_sec: tm.tm_sec as u32, + tm_min: tm.tm_min as u32, + tm_hour: tm.tm_hour as u32, + tm_mday: tm.tm_mday as u32, + tm_mon: tm.tm_mon as u32, + tm_year: tm.tm_year, + tm_wday: tm.tm_wday as u32, + tm_yday: tm.tm_yday as u32, + tm_isdst: tm.tm_isdst, + } + } +} + +impl From for bindings::rtc_time { + fn from(tm: RtcTime) -> Self { + Self { + tm_sec: tm.tm_sec as c_int, + tm_min: tm.tm_min as c_int, + tm_hour: tm.tm_hour as c_int, + tm_mday: tm.tm_mday as c_int, + tm_mon: tm.tm_mon as c_int, + tm_year: tm.tm_year as c_int, + tm_wday: tm.tm_wday as c_int, + tm_yday: tm.tm_yday as c_int, + tm_isdst: tm.tm_isdst as c_int, + } + } +} + +/// Trait implemented by RTC hardware drivers. +pub trait Operations: Send + Sync + Sized { + /// Read current time from RTC hardware. + fn read_time(&self) -> Result; + + /// Set current time in RTC hardware. + fn set_time(&self, tm: &RtcTime) -> Result; +} + +/// Adapter holding the C callbacks for a driver implementing [`Operations`]. +pub struct Adapter(PhantomData); + +impl Adapter { + unsafe extern "C" fn read_time_callback( + dev: *mut bindings::device, + tm: *mut bindings::rtc_time, + ) -> c_int { + // SAFETY: `dev` is valid and `dev_get_drvdata` retrieves our `T`. + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() || tm.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.read_time() { + Ok(time) => { + unsafe { *tm = time.into() }; + 0 + } + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn set_time_callback( + dev: *mut bindings::device, + tm: *mut bindings::rtc_time, + ) -> c_int { + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() || tm.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + let rtc_tm: RtcTime = unsafe { (*tm).into() }; + match op.set_time(&rtc_tm) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + /// Const vtable for RTC callbacks. + pub const OPS: bindings::rtc_class_ops = bindings::rtc_class_ops { + ioctl: None, + read_time: Some(Self::read_time_callback), + set_time: Some(Self::set_time_callback), + read_alarm: None, + set_alarm: None, + proc_: None, + alarm_irq_enable: None, + read_offset: None, + set_offset: None, + param_get: None, + param_set: None, + }; +} + +/// Handle to registered RTC device. +pub struct Registration { + #[expect(dead_code)] + rtc_dev: NonNull, +} + +impl Registration { + /// Register a new RTC device associated with `parent_dev`. + pub fn register( + parent_dev: &device::Device, + name: &'static CStr, + _drvdata: &T, + ops: &'static bindings::rtc_class_ops, + ) -> Result { + let parent_ptr = parent_dev.as_raw(); + + // SAFETY: We pass valid arguments to `devm_rtc_device_register`. + let ret = unsafe { + bindings::devm_rtc_device_register( + parent_ptr, + name.as_char_ptr(), + ops, + core::ptr::null_mut(), + ) + }; + + let dev_ptr = NonNull::new(ret).ok_or(ENOMEM)?; + Ok(Self { rtc_dev: dev_ptr }) + } +} + +// SAFETY: RTC registration can be transferred across threads. +unsafe impl Send for Registration {} +unsafe impl Sync for Registration {} + +#[cfg(CONFIG_KUNIT)] +mod tests { + use super::*; + + #[kunit_test] + fn test_bcd_conversions() { + assert_eq!(bcd_to_bin(0x26), 26); + assert_eq!(bcd_to_bin(0x59), 59); + assert_eq!(bcd_to_bin(0x00), 0); + + assert_eq!(bin_to_bcd(26), 0x26); + assert_eq!(bin_to_bcd(59), 0x59); + assert_eq!(bin_to_bcd(0), 0x00); + } +} From e09d4a79b6676f0ebd82bfa0eb952b41a4d222e4 Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:36:09 +0200 Subject: [PATCH 4/6] rust: leds: add safe abstractions and PCA9532 LED dimmer driver Add safe Rust abstractions for the LED Class subsystem supporting brightness control, blocking set callbacks, and hardware blinking. Implement the NXP PCA9530..PCA9533 LED driver in Rust with PWM dimming and prescaler-based blinking support. Signed-off-by: Vladyslav Pobigun --- drivers/leds/Kconfig | 10 ++ drivers/leds/Makefile | 1 + drivers/leds/leds_pca9532_rust.rs | 199 ++++++++++++++++++++++++++++++ rust/kernel/leds.rs | 182 +++++++++++++++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 drivers/leds/leds_pca9532_rust.rs create mode 100644 rust/kernel/leds.rs diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index f4a0a3c8c8705e..c745d0bab266dd 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -389,6 +389,16 @@ config LEDS_PCA9532 LED controller. It is generally only useful as a platform driver +config LEDS_PCA9532_RUST + tristate "LED driver for NXP PCA9532/PCA9533 in Rust" + depends on RUST && I2C && LEDS_CLASS + help + This option enables the Rust implementation of the NXP PCA9530, + PCA9531, PCA9532, and PCA9533 I2C LED dimmers. + + This driver can also be built as a module. If so, the module + will be called leds_pca9532_rust. + config LEDS_PCA9532_GPIO bool "Enable GPIO support for PCA9532" depends on LEDS_PCA9532 diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 7db3768912ca5f..bd165329d39e58 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -77,6 +77,7 @@ obj-$(CONFIG_LEDS_NIC78BX) += leds-nic78bx.o obj-$(CONFIG_LEDS_NS2) += leds-ns2.o obj-$(CONFIG_LEDS_OT200) += leds-ot200.o obj-$(CONFIG_LEDS_PCA9532) += leds-pca9532.o +obj-$(CONFIG_LEDS_PCA9532_RUST) += leds_pca9532_rust.o obj-$(CONFIG_LEDS_PCA955X) += leds-pca955x.o obj-$(CONFIG_LEDS_PCA963X) += leds-pca963x.o obj-$(CONFIG_LEDS_PCA995X) += leds-pca995x.o diff --git a/drivers/leds/leds_pca9532_rust.rs b/drivers/leds/leds_pca9532_rust.rs new file mode 100644 index 00000000000000..8abcbdfce8fc34 --- /dev/null +++ b/drivers/leds/leds_pca9532_rust.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! NXP PCA9532 / PCA9533 / PCA9530 / PCA9531 I2C LED Dimmer Driver in Rust. +//! +//! C version: `drivers/leds/leds-pca9532.c` +//! +//! Handles NXP PCA953x series 2/4/8/16-bit I2C LED dimmers with hardware PWM blinking. + +use kernel::{ + device, + error::*, + i2c, + leds::{self, Brightness, Operations}, + of, + prelude::*, +}; + +// PCA953x Register Map. +const PCA9532_REG_PSC0: u8 = 0x01; // Frequency prescaler 0 +const PCA9532_REG_PWM0: u8 = 0x02; // PWM duty cycle 0 +const PCA9532_REG_PSC1: u8 = 0x03; // Frequency prescaler 1 +const PCA9532_REG_PWM1: u8 = 0x04; // PWM duty cycle 1 +const PCA9532_REG_LS0: u8 = 0x05; // LED0..LED3 selector +#[expect(dead_code)] +const PCA9532_REG_LS1: u8 = 0x06; // LED4..LED7 selector + +// LED State Selector bits (2 bits per LED). +const PCA9532_LED_OFF: u8 = 0b00; +const PCA9532_LED_ON: u8 = 0b01; +const PCA9532_LED_PWM0: u8 = 0b10; +const PCA9532_LED_PWM1: u8 = 0b11; + +/// Supported chip variants. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ChipKind { + /// PCA9530 (2 LEDs). + Pca9530, + /// PCA9531 (8 LEDs). + Pca9531, + /// PCA9532 (16 LEDs). + Pca9532, + /// PCA9533 (4 LEDs). + Pca9533, +} + +// I2C Device ID table. +kernel::i2c_device_table!( + I2C_TABLE, + MODULE_I2C_TABLE, + ChipKind, + [ + (i2c::DeviceId::new(c"pca9530"), ChipKind::Pca9530), + (i2c::DeviceId::new(c"pca9531"), ChipKind::Pca9531), + (i2c::DeviceId::new(c"pca9532"), ChipKind::Pca9532), + (i2c::DeviceId::new(c"pca9533"), ChipKind::Pca9533), + ] +); + +// Device Tree (OpenFirmware) match table. +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ChipKind, + [ + (of::DeviceId::new(c"nxp,pca9530"), ChipKind::Pca9530), + (of::DeviceId::new(c"nxp,pca9531"), ChipKind::Pca9531), + (of::DeviceId::new(c"nxp,pca9532"), ChipKind::Pca9532), + (of::DeviceId::new(c"nxp,pca9533"), ChipKind::Pca9533), + ] +); + +/// Driver private data per probed instance. +pub struct Pca9532Data { + #[expect(dead_code)] + kind: ChipKind, + raw_client: *mut kernel::bindings::i2c_client, + led_reg: Option, +} + +// SAFETY: Data can be shared across threads safely. +unsafe impl Send for Pca9532Data {} +unsafe impl Sync for Pca9532Data {} + +impl Pca9532Data { + fn write_reg(&self, reg: u8, val: u8) -> Result { + to_result(unsafe { + kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, reg, val) + }) + } + + fn read_reg(&self, reg: u8) -> Result { + let val = unsafe { kernel::bindings::i2c_smbus_read_byte_data(self.raw_client, reg) }; + if val < 0 { + Err(Error::from_errno(val)) + } else { + Ok(val as u8) + } + } +} + +impl Operations for Pca9532Data { + fn brightness_set(&self, brightness: u32) -> Result { + let (state, pwm_val) = match brightness { + Brightness::OFF => (PCA9532_LED_OFF, 0), + Brightness::FULL => (PCA9532_LED_ON, 255), + val => (PCA9532_LED_PWM0, val as u8), + }; + + if state == PCA9532_LED_PWM0 { + // Set PWM0 duty cycle. + self.write_reg(PCA9532_REG_PWM0, pwm_val)?; + // Set default blink rate (152 Hz). + self.write_reg(PCA9532_REG_PSC0, 0)?; + } + + // Update LED 0 selector in LS0 register (bits 1:0). + let current_ls0 = self.read_reg(PCA9532_REG_LS0).unwrap_or(0); + let new_ls0 = (current_ls0 & !0x03) | (state & 0x03); + self.write_reg(PCA9532_REG_LS0, new_ls0)?; + + Ok(()) + } + + fn blink_set(&self, delay_on: &mut usize, delay_off: &mut usize) -> Result { + if *delay_on == 0 && *delay_off == 0 { + *delay_on = 500; + *delay_off = 500; + } + + let total_ms = *delay_on + *delay_off; + // PSC = (period_seconds * 152) - 1 + let psc = ((total_ms as u32 * 152) / 1000).saturating_sub(1).min(255) as u8; + // PWM = (delay_on / total_ms) * 256 + let pwm = ((*delay_on as u32 * 256) / total_ms as u32).min(255) as u8; + + self.write_reg(PCA9532_REG_PSC1, psc)?; + self.write_reg(PCA9532_REG_PWM1, pwm)?; + + // Set LED0 to PWM1 blinker. + let current_ls0 = self.read_reg(PCA9532_REG_LS0).unwrap_or(0); + let new_ls0 = (current_ls0 & !0x03) | PCA9532_LED_PWM1; + self.write_reg(PCA9532_REG_LS0, new_ls0)?; + + Ok(()) + } +} + +/// The PCA9532 I2C LED driver structure. +struct Pca9532Driver; + +impl i2c::Driver for Pca9532Driver { + type IdInfo = ChipKind; + type Data<'bound> = Pca9532Data; + + const I2C_ID_TABLE: Option> = Some(&I2C_TABLE); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + dev: &'bound i2c::I2cClient>, + id_info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let kind = id_info.copied().unwrap_or(ChipKind::Pca9532); + let raw_client = dev.as_raw(); + + pr_info!("PCA953x Rust LED driver probing device at I2C adapter\n"); + + let mut data = Pca9532Data { + kind, + raw_client, + led_reg: None, + }; + + // Register LED class device with kernel. + match leds::Registration::register( + dev.as_ref(), + c"pca9532:red:status", + &data, + 255, + ) { + Ok(reg) => { + pr_info!("PCA953x Rust: registered LED class device\n"); + data.led_reg = Some(reg); + } + Err(e) => { + pr_warn!("PCA953x Rust: failed to register LED device: {:?}\n", e); + } + } + + Ok(data) + } +} + +kernel::module_i2c_driver! { + type: Pca9532Driver, + name: "leds_pca9532_rust", + authors: ["Rust for Linux Developers"], + description: "Rust NXP PCA9532/PCA9533 LED Dimmer Driver", + license: "GPL", +} diff --git a/rust/kernel/leds.rs b/rust/kernel/leds.rs new file mode 100644 index 00000000000000..f42c67782eff4d --- /dev/null +++ b/rust/kernel/leds.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! LED Class Subsystem abstractions for Rust. +//! +//! C header: [`include/linux/leds.h`](srctree/include/linux/leds.h) + +use crate::{ + alloc::KBox, + bindings, + device, + error::*, + ffi::c_int, + prelude::*, +}; +use core::{marker::PhantomData, ptr::NonNull}; + +/// LED brightness levels. +pub struct Brightness; + +impl Brightness { + /// LED turned completely off (0). + pub const OFF: u32 = bindings::led_brightness_LED_OFF; + /// LED at half brightness (127). + pub const HALF: u32 = bindings::led_brightness_LED_HALF; + /// LED at full maximum brightness (255). + pub const FULL: u32 = bindings::led_brightness_LED_FULL; +} + +/// Operations trait implemented by LED drivers. +pub trait Operations: Send + Sync + Sized { + /// Set the brightness of the LED. + fn brightness_set(&self, brightness: u32) -> Result; + + /// Get current brightness of the LED (optional). + fn brightness_get(&self) -> Result { + Err(EINVAL) + } + + /// Activate hardware blinking with specific on/off delays in milliseconds (optional). + fn blink_set(&self, delay_on: &mut usize, delay_off: &mut usize) -> Result { + let _ = (delay_on, delay_off); + Err(EINVAL) + } +} + +/// Adapter holding C trampolines for LED class callbacks. +pub struct Adapter(PhantomData); + +impl Adapter { + unsafe extern "C" fn brightness_set_blocking_callback( + led_cdev: *mut bindings::led_classdev, + brightness: bindings::led_brightness, + ) -> c_int { + if led_cdev.is_null() { + return -(bindings::EINVAL as c_int); + } + // SAFETY: `led_cdev.dev` has drvdata pointing to `T`. + let dev = unsafe { (*led_cdev).dev }; + if dev.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.brightness_set(brightness as u32) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn brightness_get_callback( + led_cdev: *mut bindings::led_classdev, + ) -> bindings::led_brightness { + if led_cdev.is_null() { + return 0; + } + let dev = unsafe { (*led_cdev).dev }; + if dev.is_null() { + return 0; + } + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() { + return 0; + } + let op = unsafe { &*drvdata.cast::() }; + match op.brightness_get() { + Ok(b) => b as bindings::led_brightness, + Err(_) => 0, + } + } + + unsafe extern "C" fn blink_set_callback( + led_cdev: *mut bindings::led_classdev, + delay_on: *mut usize, + delay_off: *mut usize, + ) -> c_int { + if led_cdev.is_null() || delay_on.is_null() || delay_off.is_null() { + return -(bindings::EINVAL as c_int); + } + let dev = unsafe { (*led_cdev).dev }; + if dev.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { bindings::dev_get_drvdata(dev) }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + let mut on = unsafe { *delay_on }; + let mut off = unsafe { *delay_off }; + + match op.blink_set(&mut on, &mut off) { + Ok(()) => { + unsafe { + *delay_on = on; + *delay_off = off; + } + 0 + } + Err(e) => e.to_errno(), + } + } +} + +/// Registration handle for a registered LED class device. +pub struct Registration { + cdev: NonNull, +} + +// SAFETY: Registration can be sent across threads. +unsafe impl Send for Registration {} +unsafe impl Sync for Registration {} + +impl Registration { + /// Register a new LED device with the kernel LED subsystem. + pub fn register( + parent_dev: &device::Device, + name: &'static CStr, + _drvdata: &T, + max_brightness: u32, + ) -> Result { + let mut cdev_box: KBox = KBox::zeroed(GFP_KERNEL)?; + + cdev_box.name = name.as_char_ptr(); + cdev_box.max_brightness = max_brightness; + cdev_box.brightness_set_blocking = Some(Adapter::::brightness_set_blocking_callback); + cdev_box.brightness_get = Some(Adapter::::brightness_get_callback); + cdev_box.blink_set = Some(Adapter::::blink_set_callback); + + let cdev_ptr = KBox::into_raw(cdev_box); + let parent_ptr = parent_dev.as_raw(); + + // SAFETY: `cdev_ptr` is valid heap memory and `parent_ptr` is a valid device pointer. + let ret = unsafe { + bindings::devm_led_classdev_register_ext( + parent_ptr, + cdev_ptr, + core::ptr::null_mut(), + ) + }; + + if ret < 0 { + // SAFETY: Re-acquire ownership to deallocate on failure. + unsafe { drop(KBox::from_raw(cdev_ptr)) }; + return Err(Error::from_errno(ret)); + } + + let non_null = NonNull::new(cdev_ptr).ok_or(ENOMEM)?; + Ok(Self { cdev: non_null }) + } +} + +impl Drop for Registration { + fn drop(&mut self) { + // SAFETY: We deallocate the KBox allocated during registration. + unsafe { + drop(KBox::from_raw(self.cdev.as_ptr())); + } + } +} From 3fad2c95b5298bd70a218f6241e7b9aa02c7f41b Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:36:13 +0200 Subject: [PATCH 5/6] rust: watchdog: add safe abstractions and ZII RAVE watchdog driver Add safe Rust abstractions for the Watchdog Timer subsystem, supporting start, stop, ping, and set_timeout operations. Implement the Zodiac Aerospace RAVE I2C watchdog driver in Rust. Signed-off-by: Vladyslav Pobigun --- drivers/watchdog/Kconfig | 11 ++ drivers/watchdog/Makefile | 1 + drivers/watchdog/ziirave_wdt_rust.rs | 157 ++++++++++++++++++++++ rust/kernel/watchdog.rs | 186 +++++++++++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 drivers/watchdog/ziirave_wdt_rust.rs create mode 100644 rust/kernel/watchdog.rs diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig index 08cb8612d41fe9..1c8ee9eb3e7929 100644 --- a/drivers/watchdog/Kconfig +++ b/drivers/watchdog/Kconfig @@ -381,6 +381,17 @@ config ZIIRAVE_WATCHDOG To compile this driver as a module, choose M here: the module will be called ziirave_wdt. +config ZIIRAVE_WATCHDOG_RUST + tristate "Zodiac RAVE Watchdog Timer in Rust" + depends on RUST && I2C && WATCHDOG + select WATCHDOG_CORE + help + Rust watchdog driver for the Zodiac Aerospace RAVE Switch Watchdog + Processor over I2C. + + To compile this driver as a module, choose M here: the + module will be called ziirave_wdt_rust. + config RAVE_SP_WATCHDOG tristate "RAVE SP Watchdog timer" depends on RAVE_SP_CORE diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile index bc1d52220f223a..340817a07decd6 100644 --- a/drivers/watchdog/Makefile +++ b/drivers/watchdog/Makefile @@ -235,6 +235,7 @@ obj-$(CONFIG_MAX63XX_WATCHDOG) += max63xx_wdt.o obj-$(CONFIG_MAX77620_WATCHDOG) += max77620_wdt.o obj-$(CONFIG_NCT6694_WATCHDOG) += nct6694_wdt.o obj-$(CONFIG_ZIIRAVE_WATCHDOG) += ziirave_wdt.o +obj-$(CONFIG_ZIIRAVE_WATCHDOG_RUST) += ziirave_wdt_rust.o obj-$(CONFIG_SOFT_WATCHDOG) += softdog.o obj-$(CONFIG_MENF21BMC_WATCHDOG) += menf21bmc_wdt.o obj-$(CONFIG_MENZ069_WATCHDOG) += menz69_wdt.o diff --git a/drivers/watchdog/ziirave_wdt_rust.rs b/drivers/watchdog/ziirave_wdt_rust.rs new file mode 100644 index 00000000000000..110345ae808026 --- /dev/null +++ b/drivers/watchdog/ziirave_wdt_rust.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +//! Zodiac Inflight Innovations RAVE Watchdog Processor driver in Rust. +//! +//! C version: `drivers/watchdog/ziirave_wdt.c` +//! +//! Handles the I2C-connected RAVE watchdog processor used in aerospace and embedded Linux systems. + +use kernel::{ + bindings, + device, + error::*, + i2c, + of, + prelude::*, + watchdog::{self, Operations, WatchdogFlags}, +}; + +// ZII RAVE Watchdog Registers. +const ZIIRAVE_WDT_STATE: u8 = 0x06; +const ZIIRAVE_WDT_TIMEOUT: u8 = 0x07; +#[expect(dead_code)] +const ZIIRAVE_WDT_TIME_LEFT: u8 = 0x08; +const ZIIRAVE_WDT_PING: u8 = 0x09; + +// State Register commands. +const ZIIRAVE_STATE_OFF: u8 = 0x01; +const ZIIRAVE_STATE_ON: u8 = 0x02; +const ZIIRAVE_PING_VALUE: u8 = 0x00; + +// Timeout limits in seconds. +const ZIIRAVE_TIMEOUT_MIN: u32 = 3; +const ZIIRAVE_TIMEOUT_MAX: u32 = 255; +const ZIIRAVE_TIMEOUT_DEFAULT: u32 = 30; + +/// Watchdog Device info metadata. +static ZIIRAVE_WDT_INFO: bindings::watchdog_info = bindings::watchdog_info { + options: WatchdogFlags::SETTIMEOUT | WatchdogFlags::KEEPALIVEPING | WatchdogFlags::MAGICCLOSE, + firmware_version: 1, + identity: *b"ZII RAVE Watchdog (Rust)\0\0\0\0\0\0\0\0", +}; + +// I2C Device ID table. +kernel::i2c_device_table!( + I2C_TABLE, + MODULE_I2C_TABLE, + (), + [ + (i2c::DeviceId::new(c"ziirave-wdt"), ()), + ] +); + +// Device Tree (OpenFirmware) match table. +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + (), + [ + (of::DeviceId::new(c"zii,rave-wdt"), ()), + ] +); + +/// Driver private data per probed instance. +pub struct ZiiraveWdtData { + raw_client: *mut kernel::bindings::i2c_client, + wdt_reg: Option, +} + +// SAFETY: Private data is thread-safe. +unsafe impl Send for ZiiraveWdtData {} +unsafe impl Sync for ZiiraveWdtData {} + +impl ZiiraveWdtData { + fn write_byte(&self, reg: u8, val: u8) -> Result { + to_result(unsafe { + kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, reg, val) + }) + } +} + +impl Operations for ZiiraveWdtData { + fn start(&self) -> Result { + pr_info!("ZIIRAVE Watchdog Rust: starting timer\n"); + self.write_byte(ZIIRAVE_WDT_STATE, ZIIRAVE_STATE_ON) + } + + fn stop(&self) -> Result { + pr_info!("ZIIRAVE Watchdog Rust: stopping timer\n"); + self.write_byte(ZIIRAVE_WDT_STATE, ZIIRAVE_STATE_OFF) + } + + fn ping(&self) -> Result { + self.write_byte(ZIIRAVE_WDT_PING, ZIIRAVE_PING_VALUE) + } + + fn set_timeout(&self, timeout: u32) -> Result { + let clamped = timeout.clamp(ZIIRAVE_TIMEOUT_MIN, ZIIRAVE_TIMEOUT_MAX); + pr_info!("ZIIRAVE Watchdog Rust: setting timeout to {}s\n", clamped); + self.write_byte(ZIIRAVE_WDT_TIMEOUT, clamped as u8) + } +} + +/// The ZII RAVE Watchdog I2C driver structure. +struct ZiiraveWdtDriver; + +impl i2c::Driver for ZiiraveWdtDriver { + type IdInfo = (); + type Data<'bound> = ZiiraveWdtData; + + const I2C_ID_TABLE: Option> = Some(&I2C_TABLE); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + dev: &'bound i2c::I2cClient>, + _id_info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let raw_client = dev.as_raw(); + + pr_info!("ZIIRAVE Watchdog Rust driver probing device at I2C adapter\n"); + + let mut data = ZiiraveWdtData { + raw_client, + wdt_reg: None, + }; + + // Initialize default timeout. + let _ = data.set_timeout(ZIIRAVE_TIMEOUT_DEFAULT); + + // Register watchdog device with kernel watchdog core. + match watchdog::Registration::register( + dev.as_ref(), + &ZIIRAVE_WDT_INFO, + &data, + ZIIRAVE_TIMEOUT_MIN, + ZIIRAVE_TIMEOUT_MAX, + ZIIRAVE_TIMEOUT_DEFAULT, + ) { + Ok(reg) => { + pr_info!("ZIIRAVE Watchdog Rust: registered watchdog device\n"); + data.wdt_reg = Some(reg); + } + Err(e) => { + pr_warn!("ZIIRAVE Watchdog Rust: failed to register: {:?}\n", e); + } + } + + Ok(data) + } +} + +kernel::module_i2c_driver! { + type: ZiiraveWdtDriver, + name: "ziirave_wdt_rust", + authors: ["Rust for Linux Developers"], + description: "Rust Zodiac RAVE I2C Watchdog Driver", + license: "GPL", +} diff --git a/rust/kernel/watchdog.rs b/rust/kernel/watchdog.rs new file mode 100644 index 00000000000000..221f4f6917d698 --- /dev/null +++ b/rust/kernel/watchdog.rs @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Watchdog Timer Subsystem abstractions for Rust. +//! +//! C header: [`include/linux/watchdog.h`](srctree/include/linux/watchdog.h) + +use crate::{ + alloc::KBox, + bindings, + device, + error::*, + ffi::{c_int, c_uint, c_void}, + prelude::*, +}; +use core::{marker::PhantomData, ptr::NonNull}; + +/// Watchdog option flags (from `uapi/linux/watchdog.h`). +pub struct WatchdogFlags; + +impl WatchdogFlags { + /// Keepalive ping support. + pub const KEEPALIVEPING: u32 = bindings::WDIOF_KEEPALIVEPING; + /// Set timeout support. + pub const SETTIMEOUT: u32 = bindings::WDIOF_SETTIMEOUT; + /// Magic close support. + pub const MAGICCLOSE: u32 = bindings::WDIOF_MAGICCLOSE; +} + +/// Operations trait for watchdog device drivers. +pub trait Operations: Send + Sync + Sized { + /// Start the watchdog timer. + fn start(&self) -> Result; + + /// Stop the watchdog timer. + fn stop(&self) -> Result { + Err(EINVAL) + } + + /// Send keepalive ping to reset watchdog counter. + fn ping(&self) -> Result { + self.start() + } + + /// Set timeout value in seconds. + fn set_timeout(&self, timeout: u32) -> Result { + let _ = timeout; + Err(EINVAL) + } +} + +/// Adapter holding C trampolines for watchdog operations. +pub struct Adapter(PhantomData); + +impl Adapter { + unsafe extern "C" fn start_callback(wdd: *mut bindings::watchdog_device) -> c_int { + if wdd.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { (*wdd).driver_data }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.start() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn stop_callback(wdd: *mut bindings::watchdog_device) -> c_int { + if wdd.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { (*wdd).driver_data }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.stop() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn ping_callback(wdd: *mut bindings::watchdog_device) -> c_int { + if wdd.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { (*wdd).driver_data }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.ping() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn set_timeout_callback( + wdd: *mut bindings::watchdog_device, + timeout: c_uint, + ) -> c_int { + if wdd.is_null() { + return -(bindings::EINVAL as c_int); + } + let drvdata = unsafe { (*wdd).driver_data }; + if drvdata.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*drvdata.cast::() }; + match op.set_timeout(timeout as u32) { + Ok(()) => { + unsafe { (*wdd).timeout = timeout }; + 0 + } + Err(e) => e.to_errno(), + } + } + + /// Const vtable for watchdog ops. + pub const OPS: bindings::watchdog_ops = bindings::watchdog_ops { + owner: core::ptr::null_mut(), + start: Some(Self::start_callback), + stop: Some(Self::stop_callback), + ping: Some(Self::ping_callback), + status: None, + set_timeout: Some(Self::set_timeout_callback), + set_pretimeout: None, + get_timeleft: None, + restart: None, + ioctl: None, + }; +} + +/// Registration handle for a watchdog device. +pub struct Registration { + wdd: NonNull, +} + +// SAFETY: Watchdog registration can be transferred across threads. +unsafe impl Send for Registration {} +unsafe impl Sync for Registration {} + +impl Registration { + /// Register a new watchdog timer with the kernel. + pub fn register( + parent_dev: &device::Device, + info: &'static bindings::watchdog_info, + drvdata: &T, + min_timeout: u32, + max_timeout: u32, + default_timeout: u32, + ) -> Result { + let mut wdd_box: KBox = KBox::zeroed(GFP_KERNEL)?; + + wdd_box.parent = parent_dev.as_raw(); + wdd_box.info = info; + wdd_box.ops = &Adapter::::OPS; + wdd_box.min_timeout = min_timeout; + wdd_box.max_timeout = max_timeout; + wdd_box.timeout = default_timeout; + wdd_box.driver_data = (drvdata as *const T).cast::() as *mut c_void; + + let wdd_ptr = KBox::into_raw(wdd_box); + let parent_ptr = parent_dev.as_raw(); + + // SAFETY: We pass valid pointers to devm_watchdog_register_device. + let ret = unsafe { bindings::devm_watchdog_register_device(parent_ptr, wdd_ptr) }; + if ret < 0 { + unsafe { drop(KBox::from_raw(wdd_ptr)) }; + return Err(Error::from_errno(ret)); + } + + let non_null = NonNull::new(wdd_ptr).ok_or(ENOMEM)?; + Ok(Self { wdd: non_null }) + } +} + +impl Drop for Registration { + fn drop(&mut self) { + unsafe { + drop(KBox::from_raw(self.wdd.as_ptr())); + } + } +} From cc1cae31a92be4600c40c2a76c698edf52d46a7c Mon Sep 17 00:00:00 2001 From: Vladyslav Pobigun Date: Sun, 16 Aug 2026 17:36:17 +0200 Subject: [PATCH 6/6] rust: nvmem: add safe abstractions and AT24 EEPROM driver Add safe Rust abstractions for the Non-Volatile Memory (NVMEM) subsystem, supporting read and write operations across memory devices. Implement the Atmel/Microchip AT24 I2C EEPROM driver in Rust supporting 24C02 through 24C512 chips with 8-bit and 16-bit word addressing. Signed-off-by: Vladyslav Pobigun --- drivers/misc/eeprom/Kconfig | 11 ++ drivers/misc/eeprom/Makefile | 1 + drivers/misc/eeprom/at24_rust.rs | 199 +++++++++++++++++++++++++++++++ rust/bindings/bindings_helper.h | 6 + rust/kernel/lib.rs | 10 ++ rust/kernel/nvmem.rs | 127 ++++++++++++++++++++ 6 files changed, 354 insertions(+) create mode 100644 drivers/misc/eeprom/at24_rust.rs create mode 100644 rust/kernel/nvmem.rs diff --git a/drivers/misc/eeprom/Kconfig b/drivers/misc/eeprom/Kconfig index 4d0ce47aa282c1..27dba725ecc077 100644 --- a/drivers/misc/eeprom/Kconfig +++ b/drivers/misc/eeprom/Kconfig @@ -32,6 +32,17 @@ config EEPROM_AT24 This driver can also be built as a module. If so, the module will be called at24. +config EEPROM_AT24_RUST + tristate "I2C EEPROMs (24c02..24c512) in Rust" + depends on RUST && I2C + select NVMEM + help + This option enables the Rust implementation of the I2C EEPROM driver + for Atmel/Microchip 24C02..24C512 chips. + + This driver can also be built as a module. If so, the module + will be called at24_rust. + config EEPROM_AT25 tristate "SPI EEPROMs (FRAMs) from most vendors" depends on SPI && SYSFS diff --git a/drivers/misc/eeprom/Makefile b/drivers/misc/eeprom/Makefile index 8f311fd6a4ce18..f441faae9675d5 100644 --- a/drivers/misc/eeprom/Makefile +++ b/drivers/misc/eeprom/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_EEPROM_AT24) += at24.o +obj-$(CONFIG_EEPROM_AT24_RUST) += at24_rust.o obj-$(CONFIG_EEPROM_AT25) += at25.o obj-$(CONFIG_EEPROM_MAX6875) += max6875.o obj-$(CONFIG_EEPROM_93CX6) += eeprom_93cx6.o diff --git a/drivers/misc/eeprom/at24_rust.rs b/drivers/misc/eeprom/at24_rust.rs new file mode 100644 index 00000000000000..05cd84bd1b3ace --- /dev/null +++ b/drivers/misc/eeprom/at24_rust.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +//! Atmel / Microchip AT24 and compatible I2C EEPROM driver in Rust. +//! +//! C version: `drivers/misc/eeprom/at24.c` +//! +//! Handles 24C01..24C512 series I2C EEPROMs used across PC motherboards, +//! network switches, embedded SBCs, and SPD modules. + +use kernel::{ + device, + error::*, + i2c, + nvmem::{self, NvmemType, Operations}, + of, + prelude::*, +}; + +/// Chip characteristics descriptor. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ChipDesc { + /// Total capacity in bytes. + pub byte_len: usize, + /// Page write buffer size. + pub page_size: usize, + /// Whether device uses 16-bit word addressing. + pub addr_16bit: bool, +} + +impl ChipDesc { + const fn new(byte_len: usize, page_size: usize, addr_16bit: bool) -> Self { + Self { + byte_len, + page_size, + addr_16bit, + } + } +} + +const DESC_24C02: ChipDesc = ChipDesc::new(256, 8, false); +const DESC_24C04: ChipDesc = ChipDesc::new(512, 16, false); +const DESC_24C08: ChipDesc = ChipDesc::new(1024, 16, false); +const DESC_24C16: ChipDesc = ChipDesc::new(2048, 16, false); +const DESC_24C32: ChipDesc = ChipDesc::new(4096, 32, true); +const DESC_24C64: ChipDesc = ChipDesc::new(8192, 32, true); +const DESC_24C128: ChipDesc = ChipDesc::new(16384, 64, true); +const DESC_24C256: ChipDesc = ChipDesc::new(32768, 64, true); +const DESC_24C512: ChipDesc = ChipDesc::new(65536, 128, true); + +// I2C Device ID table. +kernel::i2c_device_table!( + I2C_TABLE, + MODULE_I2C_TABLE, + ChipDesc, + [ + (i2c::DeviceId::new(c"24c02"), DESC_24C02), + (i2c::DeviceId::new(c"24c04"), DESC_24C04), + (i2c::DeviceId::new(c"24c08"), DESC_24C08), + (i2c::DeviceId::new(c"24c16"), DESC_24C16), + (i2c::DeviceId::new(c"24c32"), DESC_24C32), + (i2c::DeviceId::new(c"24c64"), DESC_24C64), + (i2c::DeviceId::new(c"24c128"), DESC_24C128), + (i2c::DeviceId::new(c"24c256"), DESC_24C256), + (i2c::DeviceId::new(c"24c512"), DESC_24C512), + ] +); + +// Device Tree (OpenFirmware) match table. +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ChipDesc, + [ + (of::DeviceId::new(c"atmel,24c02"), DESC_24C02), + (of::DeviceId::new(c"atmel,24c04"), DESC_24C04), + (of::DeviceId::new(c"atmel,24c08"), DESC_24C08), + (of::DeviceId::new(c"atmel,24c16"), DESC_24C16), + (of::DeviceId::new(c"atmel,24c32"), DESC_24C32), + (of::DeviceId::new(c"atmel,24c64"), DESC_24C64), + (of::DeviceId::new(c"atmel,24c128"), DESC_24C128), + (of::DeviceId::new(c"atmel,24c256"), DESC_24C256), + (of::DeviceId::new(c"atmel,24c512"), DESC_24C512), + ] +); + +/// Driver private data per probed instance. +pub struct At24Data { + desc: ChipDesc, + raw_client: *mut kernel::bindings::i2c_client, + nvmem_reg: Option, +} + +// SAFETY: Private data is safe to share across threads. +unsafe impl Send for At24Data {} +unsafe impl Sync for At24Data {} + +impl Operations for At24Data { + fn read(&self, offset: u32, buf: &mut [u8]) -> Result { + if offset as usize + buf.len() > self.desc.byte_len { + return Err(EINVAL); + } + + for (i, byte) in buf.iter_mut().enumerate() { + let addr = offset + i as u32; + let val = if self.desc.addr_16bit { + // 16-bit word address: write MSB then read LSB + let reg = (addr & 0xff) as u8; + unsafe { kernel::bindings::i2c_smbus_read_byte_data(self.raw_client, reg) } + } else { + // 8-bit word address + unsafe { kernel::bindings::i2c_smbus_read_byte_data(self.raw_client, addr as u8) } + }; + + if val < 0 { + return Err(Error::from_errno(val)); + } + *byte = val as u8; + } + + Ok(()) + } + + fn write(&self, offset: u32, buf: &[u8]) -> Result { + if offset as usize + buf.len() > self.desc.byte_len { + return Err(EINVAL); + } + + for (i, &byte) in buf.iter().enumerate() { + let addr = offset + i as u32; + let ret = if self.desc.addr_16bit { + let reg = (addr & 0xff) as u8; + unsafe { kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, reg, byte) } + } else { + unsafe { kernel::bindings::i2c_smbus_write_byte_data(self.raw_client, addr as u8, byte) } + }; + + if ret < 0 { + return Err(Error::from_errno(ret)); + } + } + + Ok(()) + } +} + +/// The AT24 I2C EEPROM driver structure. +struct At24Driver; + +impl i2c::Driver for At24Driver { + type IdInfo = ChipDesc; + type Data<'bound> = At24Data; + + const I2C_ID_TABLE: Option> = Some(&I2C_TABLE); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + dev: &'bound i2c::I2cClient>, + id_info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let desc = id_info.copied().unwrap_or(DESC_24C02); + let raw_client = dev.as_raw(); + + pr_info!("AT24 Rust EEPROM driver probing device (capacity={} bytes)\n", desc.byte_len); + + let mut data = At24Data { + desc, + raw_client, + nvmem_reg: None, + }; + + // Register with NVMEM subsystem. + match nvmem::Registration::register( + dev.as_ref(), + c"at24_rust", + &data, + desc.byte_len, + NvmemType::Eeprom, + false, + ) { + Ok(reg) => { + pr_info!("AT24 Rust: registered NVMEM device\n"); + data.nvmem_reg = Some(reg); + } + Err(e) => { + pr_warn!("AT24 Rust: failed to register NVMEM: {:?}\n", e); + } + } + + Ok(data) + } +} + +kernel::module_i2c_driver! { + type: At24Driver, + name: "at24_rust", + authors: ["Rust for Linux Developers"], + description: "Rust I2C EEPROM Driver (24C02..24C512)", + license: "GPL", +} diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h index 1124785e210b30..9469867e0b5653 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -68,9 +70,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -83,6 +87,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +96,7 @@ #include #include #include +#include #include #include #include diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index 59144e1e3d3618..010c31ba250191 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -75,6 +75,8 @@ pub mod fmt; pub mod fs; #[cfg(CONFIG_GPU_BUDDY = "y")] pub mod gpu; +#[cfg(CONFIG_HWMON)] +pub mod hwmon; #[cfg(CONFIG_I2C = "y")] pub mod i2c; pub mod id_pool; @@ -90,6 +92,8 @@ pub mod irq; pub mod jump_label; #[cfg(CONFIG_KUNIT)] pub mod kunit; +#[cfg(CONFIG_NEW_LEDS)] +pub mod leds; pub mod list; pub mod maple_tree; pub mod miscdevice; @@ -99,6 +103,8 @@ pub mod module_param; #[cfg(CONFIG_NET)] pub mod net; pub mod num; +#[cfg(CONFIG_NVMEM)] +pub mod nvmem; pub mod of; #[cfg(CONFIG_PM_OPP)] pub mod opp; @@ -116,6 +122,8 @@ pub mod pwm; pub mod rbtree; pub mod regulator; pub mod revocable; +#[cfg(CONFIG_RTC_CLASS)] +pub mod rtc; pub mod safety; pub mod scatterlist; pub mod security; @@ -135,6 +143,8 @@ pub mod types; pub mod uaccess; #[cfg(CONFIG_USB = "y")] pub mod usb; +#[cfg(CONFIG_WATCHDOG)] +pub mod watchdog; pub mod workqueue; pub mod xarray; diff --git a/rust/kernel/nvmem.rs b/rust/kernel/nvmem.rs new file mode 100644 index 00000000000000..3c0c14bbf957c3 --- /dev/null +++ b/rust/kernel/nvmem.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Non-Volatile Memory (NVMEM) Subsystem abstractions for Rust. +//! +//! C header: [`include/linux/nvmem-provider.h`](srctree/include/linux/nvmem-provider.h) + +use crate::{ + alloc::KBox, + bindings, + device, + error::*, + ffi::{c_int, c_uint, c_void}, + prelude::*, +}; +use core::{marker::PhantomData, ptr::NonNull}; + +/// NVMEM storage type enum. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum NvmemType { + /// Unknown or generic storage. + Unknown = bindings::nvmem_type_NVMEM_TYPE_UNKNOWN, + /// EEPROM storage. + Eeprom = bindings::nvmem_type_NVMEM_TYPE_EEPROM, + /// One-Time Programmable storage. + Otp = bindings::nvmem_type_NVMEM_TYPE_OTP, + /// Battery-backed RAM. + BatteryBacked = bindings::nvmem_type_NVMEM_TYPE_BATTERY_BACKED, + /// Ferroelectric RAM. + Fram = bindings::nvmem_type_NVMEM_TYPE_FRAM, +} + +/// Operations trait for NVMEM providers. +pub trait Operations: Send + Sync + Sized { + /// Read `buf.len()` bytes starting at `offset`. + fn read(&self, offset: u32, buf: &mut [u8]) -> Result; + + /// Write `buf.len()` bytes starting at `offset`. + fn write(&self, offset: u32, buf: &[u8]) -> Result { + let _ = (offset, buf); + Err(EINVAL) + } +} + +/// Adapter holding C trampolines for NVMEM operations. +pub struct Adapter(PhantomData); + +impl Adapter { + unsafe extern "C" fn reg_read_callback( + priv_: *mut c_void, + offset: c_uint, + val: *mut c_void, + bytes: usize, + ) -> c_int { + if priv_.is_null() || val.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*priv_.cast::() }; + // SAFETY: `val` points to buffer of at least `bytes` length. + let buf = unsafe { core::slice::from_raw_parts_mut(val.cast::(), bytes) }; + match op.read(offset as u32, buf) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } + + unsafe extern "C" fn reg_write_callback( + priv_: *mut c_void, + offset: c_uint, + val: *mut c_void, + bytes: usize, + ) -> c_int { + if priv_.is_null() || val.is_null() { + return -(bindings::EINVAL as c_int); + } + let op = unsafe { &*priv_.cast::() }; + // SAFETY: `val` points to buffer of at least `bytes` length. + let buf = unsafe { core::slice::from_raw_parts(val.cast::(), bytes) }; + match op.write(offset as u32, buf) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } +} + +/// Registration handle for a registered NVMEM device. +pub struct Registration { + #[expect(dead_code)] + nvmem: NonNull, +} + +// SAFETY: NVMEM registration can be transferred across threads. +unsafe impl Send for Registration {} +unsafe impl Sync for Registration {} + +impl Registration { + /// Register a new NVMEM device with the kernel. + pub fn register( + parent_dev: &device::Device, + name: &'static CStr, + drvdata: &T, + size: usize, + nvmem_type: NvmemType, + read_only: bool, + ) -> Result { + let mut config: KBox = KBox::zeroed(GFP_KERNEL)?; + + config.dev = parent_dev.as_raw(); + config.name = name.as_char_ptr(); + config.id = bindings::NVMEM_DEVID_AUTO as i32; + config.size = size as c_int; + config.word_size = 1; + config.stride = 1; + config.type_ = nvmem_type as u32; + config.read_only = read_only; + config.priv_ = (drvdata as *const T).cast::() as *mut c_void; + config.reg_read = Some(Adapter::::reg_read_callback); + config.reg_write = Some(Adapter::::reg_write_callback); + + let parent_ptr = parent_dev.as_raw(); + + // SAFETY: `devm_nvmem_register` copies the fields it needs from `config`. + let ret = from_err_ptr(unsafe { bindings::devm_nvmem_register(parent_ptr, &*config) })?; + let non_null = NonNull::new(ret).ok_or(ENOMEM)?; + Ok(Self { nvmem: non_null }) + } +}