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/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/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/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/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/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/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 {} 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`. 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())); + } + } +} 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 }) + } +} 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); + } +} 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())); + } + } +}