From patchwork Mon Jan 13 12:16:16 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fiona Behrens X-Patchwork-Id: 857093 Received: from gimli.kloenk.de (gimli.kloenk.de [49.12.72.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6539F21ADB4; Mon, 13 Jan 2025 12:24:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=49.12.72.200 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; cv=none; b=FbuoINWdlnNiuolUpkP/IrmGB4xw4+gTTnAU9pMLJyLrb88rayXd1iMX3yhatgez/4ljFQwLUTr3sYOLDNJ+/zIepOQGDYeb1UzfoVF48588WFSsYKn1Eox7J6I7Vh0YBB/VKqt9fvGe0z2ebj94pBpKgIh77/4ayOTb4SAIcG8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; c=relaxed/simple; bh=v1FkIg0y/Lsvezk57cy/ddGUUthGoVCIM714Z16DW0w=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=toT/tLwzoUlB6aoTqDJ8OdBzJSIfMO71+pD32dJzn6YHLSDfPn/SDDU1lYghEejh6RoTjc1u1Anp44XUESeZ4r2rrRwuUUANQf9lO26SC9rzR4R+RkYYsDhzc+YAiFlqMAjCgNsdicBasqJeFQORgpwaeYpWxUQmYh5+VyxMDtQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev; spf=pass smtp.mailfrom=kloenk.dev; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b=kShao7ND; arc=none smtp.client-ip=49.12.72.200 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b="kShao7ND" From: Fiona Behrens DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kloenk.dev; s=mail; t=1736770599; bh=zzgEIqqDKEZfXjxsOgvFdJFD0KDPqRzLi2JQNHpyhfo=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=kShao7NDNhW39ynkTbA+PdVgQwGHlbXhThDImLsFXGeJlfCrIYQbOdEwJtr+vgsmJ j7k0LccZTTKhdF3vZ9l0eiXqpf8OunwwEmFnFhiLWbmM+kdp+yqvV4qPlrHok5awFX 9YkUIVran3p5K81gUekf7fw3k61nr7gfEdgNfjjc= To: Miguel Ojeda , Alex Gaynor , Pavel Machek , Lee Jones , Jean Delvare Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Fiona Behrens , Mark Pearson , Peter Koch , rust-for-linux@vger.kernel.org, linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org, Sebastian Walz Subject: [PATCH v2 1/5] rust: dmi: Add abstractions for DMI Date: Mon, 13 Jan 2025 13:16:16 +0100 Message-ID: <20250113121620.21598-2-me@kloenk.dev> In-Reply-To: <20250113121620.21598-1-me@kloenk.dev> References: <20250113121620.21598-1-me@kloenk.dev> Precedence: bulk X-Mailing-List: linux-leds@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Introduce Rust macros and types to support DMI-based system matching, providing functionality similar to the `MODULE_DEVICE_TABLE(dmi, x)` macro in C. - Add the `dmi_system_id!` macro for defining system identifiers and matching specific DMI fields. - Add the `dmi_device_table!` macro, which allows module aliases to be available after a full build when compiled as a module. - Define the `Field` enum for DMI field IDs, ensuring compatibility with existing C bindings. These abstractions enable writing Rust kernel drivers that rely on DMI data for system-specific behavior and autoloading. Co-developed-by: Sebastian Walz Signed-off-by: Sebastian Walz Signed-off-by: Fiona Behrens --- rust/bindings/bindings_helper.h | 1 + rust/kernel/dmi.rs | 533 ++++++++++++++++++++++++++++++++ rust/kernel/lib.rs | 2 + 3 files changed, 536 insertions(+) create mode 100644 rust/kernel/dmi.rs diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h index 5c4dfe22f41a..d20b2a6af9c9 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/rust/kernel/dmi.rs b/rust/kernel/dmi.rs new file mode 100644 index 000000000000..ba2a010d0e55 --- /dev/null +++ b/rust/kernel/dmi.rs @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! DMI support. +//! +//! C header: [`inlcude/linux/dmi.h`](srctree/include/linux/dmi.h) + +use core::marker::PhantomData; +use core::num::NonZeroU32; +use core::ops::Deref; + +use crate::prelude::*; +use crate::str::CStr; + +/// Create a new [`SystemId`]. +/// +/// # Examples +/// +/// Create a [`SystemId`] that matches if the [`BiosVendor`] is `qemu`. +/// +/// ``` +/// let system_id = kernel::dmi_system_id!("qemu": [ +/// BiosVendor: "qemu" @exact, +/// ProductFamily: "rust", +/// ]); +/// assert_eq!(system_id.matches()[0].slot().unwrap().unwrap(), kernel::dmi::Field::BiosVendor); +/// assert_eq!(system_id.matches()[0].exact_match(), true); +/// assert_eq!(system_id.matches()[1].exact_match(), false); +/// ``` +/// +/// A [`SystemId`] cannot hold more than 4 matches and must not be empty, +/// otherwise it will fail to compile. +/// +// TODO: replace with `compile_fail` when supported. +/// ```ignore +/// let system_id = kernel::dmi_system_id!("qemu": [ +/// BiosVendor: "qemu", +/// BiosVersion: "8.0", +/// ProductName: "doctest", +/// ProductFamily: "rust", +/// BoardVendor: "qemu" +/// ]); +/// ``` +/// +/// [`BiosVendor`]: Field::BiosVendor +#[macro_export] +macro_rules! dmi_system_id { + (@strmatch, $slot:ident, $match:literal) => { + $crate::dmi::StrMatch::new($crate::dmi::Field::$slot, $match) + }; + (@strmatch, $slot:ident, $match:literal, @exact) => { + $crate::dmi::StrMatch::new_exact($crate::dmi::Field::$slot, $match) + }; + ($ident:literal: [$($slot:ident: $match:literal $(@$exact:ident)? $(,)?)+]) => { + const { + match $crate::dmi::SystemId::new_from_slice($crate::c_str!($ident), &[ + $($crate::dmi_system_id!(@strmatch, $slot, $match$(, @$exact)?),)+ + ]) { + Some(v) => v, + _ => $crate::build_error("Invalid length for matches"), + } + } + }; +} + +/// Create a new static [`SystemIdList`] and export it as a device table to generate alias +/// definitions to load the driver from userspace if compiled as module. +/// +/// # Examples +/// +/// Create a device table with the name `QEMU_DMI_TABLE` that loads the driver if the +/// [`BiosVendor`] is `qemu`. +/// +/// ``` +/// kernel::dmi_device_table!(1, QEMU_DMI_TABLE, ["qemu": [BiosVendor: "qemu"]]); +/// # assert_eq!(QEMU_DMI_TABLE.len(), 1); +/// ``` +/// +/// [`BiosVendor`]: Field::BiosVendor +#[macro_export] +macro_rules! dmi_device_table { + ( + $N:literal, + $name:ident, + [$($ident:literal: [$($slot:ident: $match:literal $(, @$exact:ident)?$(,)?)+]$(,)?)+] + ) => { + #[cfg_attr(MODULE, export_name = concat!("__mod_device_table__dmi__", stringify!($name)))] + #[cfg_attr(MODULE, used)] + static $name: $crate::dmi::SystemIdList<'static, $N> = + $crate::dmi::SystemIdList::new([$( + $crate::dmi_system_id!($ident: [$($slot: $match $(, @$exact)?)+]), + )+]); + }; +} + +/// DMI field id. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum Field { + // None + /// Bios Vendor + BiosVendor = bindings::dmi_field_DMI_BIOS_VENDOR as u8, + /// Bios Version + BiosVersion = bindings::dmi_field_DMI_BIOS_VERSION as u8, + /// Bios Date + BiosDate = bindings::dmi_field_DMI_BIOS_DATE as u8, + /// Bios Release + BiosRelease = bindings::dmi_field_DMI_BIOS_RELEASE as u8, + /// Embedded Controller Firmware Release + EcFirmwareRelease = bindings::dmi_field_DMI_EC_FIRMWARE_RELEASE as u8, + /// System Vendor + SysVendor = bindings::dmi_field_DMI_SYS_VENDOR as u8, + /// Product Name + ProductName = bindings::dmi_field_DMI_PRODUCT_NAME as u8, + /// Product Version + ProductVersion = bindings::dmi_field_DMI_PRODUCT_VERSION as u8, + /// Product Serial + ProductSerial = bindings::dmi_field_DMI_PRODUCT_SERIAL as u8, + /// Product UUID + ProductUuid = bindings::dmi_field_DMI_PRODUCT_UUID as u8, + /// Product SKU + ProductSku = bindings::dmi_field_DMI_PRODUCT_SKU as u8, + /// Product Family + ProductFamily = bindings::dmi_field_DMI_PRODUCT_FAMILY as u8, + /// Board Vendor + BoardVendor = bindings::dmi_field_DMI_BOARD_VENDOR as u8, + /// Board Name + BoardName = bindings::dmi_field_DMI_BOARD_NAME as u8, + /// Board Version + BoardVersion = bindings::dmi_field_DMI_BOARD_VERSION as u8, + /// Board Serial + BoardSerial = bindings::dmi_field_DMI_BOARD_SERIAL as u8, + /// Board Asset Tag + BoardAssetTag = bindings::dmi_field_DMI_BOARD_ASSET_TAG as u8, + /// Chassis Vendor + ChassisVendor = bindings::dmi_field_DMI_CHASSIS_VENDOR as u8, + /// Chassis Type + ChassisType = bindings::dmi_field_DMI_CHASSIS_TYPE as u8, + /// Chassis Version + ChassisVersion = bindings::dmi_field_DMI_CHASSIS_VERSION as u8, + /// Chassis Serial + ChassisSerial = bindings::dmi_field_DMI_CHASSIS_SERIAL as u8, + /// Chassis Asset Tag + ChassisAssetTag = bindings::dmi_field_DMI_CHASSIS_ASSET_TAG as u8, + // StringMax, + // OemString, +} + +impl Field { + /// Return DMI data value. + /// + /// Returns one DMI data value, can be used to perform complex DMI data checks. + pub fn get_system_info(self) -> Option<&'static CStr> { + // SAFETY: C call, self is a valid enum + let ptr = unsafe { bindings::dmi_get_system_info(self as u8 as _) }; + if ptr.is_null() { + None + } else { + // SAFETY: ptr checked to be none null above and to be a valid char ptr. + Some(unsafe { CStr::from_char_ptr(ptr) }) + } + } + + /// Compare a string to the dmi field (if exists). + /// + /// Returns true if the requested field equals to the check value (including None). + /// wraps the `dmi_match` C function. + pub fn compare(self, value: Option<&CStr>) -> bool { + // SAFETY: C call, self is valid enum and value is null or a string + unsafe { + bindings::dmi_match( + self as u8 as _, + value.map(CStr::as_char_ptr).unwrap_or(core::ptr::null()), + ) + } + } +} + +impl TryFrom for Field { + type Error = Error; + + /// Tries to convert a u8 id to a [`Field`]. + /// + /// # Errors + /// + /// Returns [`EINVAL`] if the id does not match a known field. + fn try_from(value: u8) -> Result { + Ok(match value as u32 { + bindings::dmi_field_DMI_BIOS_VENDOR => Self::BiosVendor, + bindings::dmi_field_DMI_BIOS_VERSION => Self::BiosVersion, + bindings::dmi_field_DMI_BIOS_DATE => Self::BiosDate, + bindings::dmi_field_DMI_BIOS_RELEASE => Self::BiosRelease, + bindings::dmi_field_DMI_EC_FIRMWARE_RELEASE => Self::EcFirmwareRelease, + bindings::dmi_field_DMI_SYS_VENDOR => Self::SysVendor, + bindings::dmi_field_DMI_PRODUCT_NAME => Self::ProductName, + bindings::dmi_field_DMI_PRODUCT_VERSION => Self::ProductVersion, + bindings::dmi_field_DMI_PRODUCT_SERIAL => Self::ProductSerial, + bindings::dmi_field_DMI_PRODUCT_UUID => Self::ProductUuid, + bindings::dmi_field_DMI_PRODUCT_SKU => Self::ProductSku, + bindings::dmi_field_DMI_PRODUCT_FAMILY => Self::ProductFamily, + bindings::dmi_field_DMI_BOARD_VENDOR => Self::BoardVendor, + bindings::dmi_field_DMI_BOARD_NAME => Self::BoardName, + bindings::dmi_field_DMI_BOARD_VERSION => Self::BoardVersion, + bindings::dmi_field_DMI_BOARD_SERIAL => Self::BoardSerial, + bindings::dmi_field_DMI_BOARD_ASSET_TAG => Self::BoardAssetTag, + bindings::dmi_field_DMI_CHASSIS_VENDOR => Self::ChassisVendor, + bindings::dmi_field_DMI_CHASSIS_TYPE => Self::ChassisType, + bindings::dmi_field_DMI_CHASSIS_VERSION => Self::ChassisVersion, + bindings::dmi_field_DMI_CHASSIS_SERIAL => Self::ChassisSerial, + bindings::dmi_field_DMI_CHASSIS_ASSET_TAG => Self::ChassisAssetTag, + _ => return Err(EINVAL), + }) + } +} + +/// String Match entry for DMI. +/// +/// Wraps the C struct `dmi_strmatch`. +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct StrMatch(bindings::dmi_strmatch); + +impl StrMatch { + /// Zeroed match entry. + pub const ZERO: Self = { + // SAFETY: all zero is a valid match with slot none. + unsafe { core::mem::zeroed() } + }; + + /// Create a new String match entry that is not an exact match. + /// + /// This function copies the string into it's own allocation. + #[inline] + pub const fn new(slot: Field, substr: &str) -> Self { + Self::new_with_exact(slot, substr, false) + } + + /// Create a new String match entry that wants an exact match. + /// + /// This function copies the string into it's own allocation. + #[inline] + pub const fn new_exact(slot: Field, substr: &str) -> Self { + Self::new_with_exact(slot, substr, true) + } + + /// Create a new String match entry. + /// + /// This function copies the string into it's own allocation. + pub const fn new_with_exact(slot: Field, substr: &str, exact: bool) -> Self { + #[cfg(target_endian = "little")] + let (exactshift, slotshift) = (7, 0); + #[cfg(target_endian = "big")] + let (exactshift, slotshift) = (0, 1); + + let bitfield = (exact as u8) << exactshift | (slot as u8) << slotshift; + + let mut this = Self(bindings::dmi_strmatch { + _bitfield_1: bindings::__BindgenBitfieldUnit::new([bitfield]), + ..Self::ZERO.0 + }); + + // copy substr into this + // core::ptr::copy_nonoverlapping is not const as mutable ref. + let input = substr.as_bytes(); + let mut index = input.len(); + let max = this.0.substr.len() - 1; + if index > max { + // TODO: find a way to generate warning from const + index = max; + } + loop { + if index == 0 { + break; + } + + index -= 1; + this.0.substr[index] = input[index] as i8; + } + + this + } + + /// Return the slot this entry matches. + /// + /// # Errors + /// + /// Returns `Ok(None)` if the slot has the value 0, or [`EINVAL`] if the field id is unknown. + /// + /// # Examples + /// ``` + /// # use kernel::dmi::{StrMatch, Field}; + /// let strmatch = StrMatch::new(Field::BiosVendor, "qemu"); + /// assert_eq!(strmatch.slot().unwrap().unwrap(), Field::BiosVendor); + /// ``` + pub fn slot(&self) -> Result> { + let slot = self.0.slot(); + if slot == 0 { + Ok(None) + } else { + Some(Field::try_from(slot)).transpose() + } + } + + /// Return if this match wants an exact match. + /// + /// # Examples + /// ``` + /// # use kernel::dmi::{StrMatch, Field}; + /// let strmatch = StrMatch::new_exact(Field::BiosVendor, "qemu"); + /// assert_eq!(strmatch.exact_match(), true); + /// ``` + #[inline] + pub fn exact_match(&self) -> bool { + self.0.exact_match() == 1 + } + + /// Return the substring to match for. + /// + /// # Examples + /// ``` + /// # use kernel::dmi::{StrMatch, Field}; + /// let strmatch = StrMatch::new(Field::BiosVendor, "qemu"); + /// assert_eq!(strmatch.substr(), "qemu"); + /// ``` + pub fn substr(&self) -> &str { + let len = self.0.substr.into_iter().take_while(|x| *x != 0).count(); + // SAFETY: substr is a valid str for len of len + unsafe { + core::str::from_utf8_unchecked(core::slice::from_raw_parts( + self.0.substr.as_ptr().cast(), + len, + )) + } + } +} + +impl core::fmt::Debug for StrMatch { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("StrMatch") + .field("slot", &self.slot().ok().flatten()) + .field("exact_match", &self.exact_match()) + .field("substr", &self.substr()) + .finish() + } +} + +impl PartialEq for StrMatch { + fn eq(&self, other: &Self) -> bool { + self.0._bitfield_1 == other.0._bitfield_1 && self.0.substr == other.0.substr + } +} + +impl Eq for StrMatch {} + +/// DMI system ID. +/// +/// To create a system ID in a const context the macro [`kernel::dmi_system_id`] can be used. +/// +/// Wraps the C struct `dmi_system_id`. +#[repr(transparent)] +pub struct SystemId<'a> { + id: bindings::dmi_system_id, + // lifetime anchor for ident, driver data and callback + _a: PhantomData<&'a ()>, +} + +impl SystemId<'static> { + /// zeroed [`SystemId`] for trailing list. + pub const ZERO: Self = { + // SAFETY: all fields zeroed is valid. + unsafe { core::mem::zeroed() } + }; +} + +impl<'a> SystemId<'a> { + /// Create a new SystemId from a matches array. + pub const fn new(ident: &'a CStr, matches: [StrMatch; 4]) -> Self { + Self { + id: bindings::dmi_system_id { + callback: None, + ident: ident.as_char_ptr(), + // SAFETY: StrMatch is transparent over bindings::dmi_strmatch + matches: unsafe { + core::mem::transmute::<[StrMatch; 4], [bindings::dmi_strmatch; 4]>(matches) + }, + driver_data: core::ptr::null_mut(), + }, + _a: PhantomData, + } + } + + /// Createa n new SystemId from a slice of matches, filling missing entries with [`StrMatch::ZERO`]. + /// + /// Copying the matches, only provided to be used in macros. + #[doc(hidden)] + pub const fn new_from_slice(ident: &'a CStr, matches: &[StrMatch]) -> Option { + if matches.is_empty() || matches.len() > 4 { + return None; + } + + let mut matches_a = [StrMatch::ZERO; 4]; + let mut index = matches.len(); + loop { + index -= 1; + matches_a[index] = matches[index]; + + if index == 0 { + break; + } + } + Some(Self::new(ident, matches_a)) + } + + /// Return the ident of the given SystemId. + /// + /// Returns a option as the C api allows to not set the ident. + pub fn ident_cstr(&self) -> Option<&'a CStr> { + let ident_ptr = self.id.ident; + if ident_ptr.is_null() { + return None; + } + + // SAFETY: ident_ptr is valid and non null. + Some(unsafe { CStr::from_char_ptr(ident_ptr) }) + } + + /// Return the ident of the given SystemId as a rust [`str`]. + /// + /// Returns a option as the C api allows to not set the ident. + /// See [`ident_cstr`]. + /// + /// [`ident_cstr`]: Self::ident_cstr + #[inline] + pub fn ident(&self) -> Option<&'a str> { + self.ident_cstr().map(CStr::to_str).and_then(Result::ok) + } + + /// Set the ident from the given optional string. + /// + /// Set to None to remove the ident. + pub fn set_ident(&mut self, ident: Option<&'a CStr>) { + if let Some(ident) = ident { + self.id.ident = ident.as_char_ptr(); + } else { + self.id.ident = core::ptr::null(); + } + } + + /// Return the [`StrMatch`] array in the SystemId. + pub fn matches(&self) -> &[StrMatch; 4] { + // SAFETY: StrMatch is transparent over bindings::dmi_strmatch + unsafe { core::mem::transmute(&self.id.matches) } + } + + /// Return a mutable reference to the StrMatch array in the SystemId. + pub fn matches_mut(&mut self) -> &mut [StrMatch; 4] { + // SAFETY: StrMatch is transparent over bindings::dmi_strmatch + unsafe { core::mem::transmute(&mut self.id.matches) } + } +} + +impl core::fmt::Debug for SystemId<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SystemId") + .field("ident", &self.ident()) + .field("matches", &self.matches()) + .field("callback", &self.id.callback) + .field("driver_data", &self.id.driver_data) + .finish() + } +} + +// SAFETY: pointer to allocation with sufficent lifetime +unsafe impl<'a> Sync for SystemId<'a> {} +// SAFETY: pointer to allocation with sufficent lifetime +unsafe impl<'a> Send for SystemId<'a> {} + +/// List of SystemIds, containing a zero sentinel to be used with dmi functions. +/// +/// Can be exported as a device table with the [`kernel::dmi_device_table`] macro. +#[repr(C)] +pub struct SystemIdList<'a, const N: usize> { + ids: [SystemId<'a>; N], + sentinel: SystemId<'static>, +} + +impl<'a, const N: usize> SystemIdList<'a, N> { + /// Create a new list from the given Id list, adding a zero sentinel. + pub const fn new(ids: [SystemId<'a>; N]) -> Self { + Self { + ids, + sentinel: SystemId::ZERO, + } + } + + /// Check system DMI data + /// + /// Walk the blacklist table running matching functions until someone + /// returns non zero or we hit the end. Callback function is called for + /// each successful match. Returns the number of matches. + pub fn check_system(&self) -> Option { + // SAFETY: C call, self has a sentinel + NonZeroU32::new(unsafe { bindings::dmi_check_system(self.as_raw_ptr()) as u32 }) + } + + /// Find the first match. + /// + /// Walk the blacklist table until the first match is found, and returns it. + pub fn first_match(&self) -> Option<&SystemId<'a>> { + // SAFETY: C call, self has a sentinel + let ptr = unsafe { bindings::dmi_first_match(self.as_raw_ptr()) }; + if ptr.is_null() { + None + } else { + // SAFETY: ptr is non null. SystemId is transparent + Some(unsafe { &*ptr.cast() }) + } + } + + /// Return the raw pointer to the dmi_system_id array, including a terminating zero sentinel. + pub fn as_raw_ptr(&self) -> *const bindings::dmi_system_id { + // ids is the first element in the struct, safe to use as base pointer. + // SystemId is transparent over bindings::dmi_system_id + self.ids.as_ptr().cast() + } +} + +impl<'a, const N: usize> Deref for SystemIdList<'a, N> { + type Target = [SystemId<'a>; N]; + + fn deref(&self) -> &Self::Target { + &self.ids + } +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index e1065a7551a3..e21f2e607963 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -35,6 +35,8 @@ mod build_assert; pub mod cred; pub mod device; +#[cfg(CONFIG_DMI)] +pub mod dmi; pub mod error; #[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)] pub mod firmware; From patchwork Mon Jan 13 12:16:17 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fiona Behrens X-Patchwork-Id: 857094 Received: from gimli.kloenk.de (gimli.kloenk.de [49.12.72.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3F36922CF20; Mon, 13 Jan 2025 12:24:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=49.12.72.200 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; cv=none; b=jTwgU843LlCvdfDI9j1SFU7u0S7F4xouUnKCTKhOvOFCMOS0izeuZptayrC7FmMtik87HrJ36qM695p4YAMV2giBh2FsoUL5McrWXtr4ZQ41aeVEbTArkETfzwal9EFyOzGdiMAirK+AUfbuUaK5sXuikA3stgJu6ad/bdhT5xI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; c=relaxed/simple; bh=InkuYB3k/SBuKJMcUmu2oiNJ9VcPHoKX+PwDidrViFw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=cR7V3n1BrIGwidXW8P/SvO15ryNPKVPfLCslrFXQ6JCs9g2QpsvywljM6Ot1YRKOgSIrgGXnxvED4jM2BS181KuYHXojwC8W1/e4IJF6oyNEVTgzInf8QVDJ8lZJcHIPNNc/VVgpx0FMurCZlscdwrVeyKxJgx0HZclbAfUEuZ8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev; spf=pass smtp.mailfrom=kloenk.dev; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b=cc5YoBTr; arc=none smtp.client-ip=49.12.72.200 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b="cc5YoBTr" From: Fiona Behrens DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kloenk.dev; s=mail; t=1736770601; bh=8Z1dr9VJxgVBQHf7eEsadNYIMRjUk4lfj7X3rMMik84=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=cc5YoBTrFP4S3g5BwxqFgc26iocyx3eZ7N3XkeH5s0Ras9et0aDn4Wpcoq2f99rgz pnx5kcqj4PQGjjNb2HyddHtAnUfqLtJqjxBuI2bw7KGa/HV4kH6BO+LprPGNQAVrQ0 3G62UJl8P9zowSZXaN4Q4bKb/snc+GKxhjwQgYMM= To: Miguel Ojeda , Alex Gaynor , Pavel Machek , Lee Jones , Jean Delvare Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Fiona Behrens , Mark Pearson , Peter Koch , rust-for-linux@vger.kernel.org, linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v2 2/5] rust: leds: Add Rust bindings for LED subsystem Date: Mon, 13 Jan 2025 13:16:17 +0100 Message-ID: <20250113121620.21598-3-me@kloenk.dev> In-Reply-To: <20250113121620.21598-1-me@kloenk.dev> References: <20250113121620.21598-1-me@kloenk.dev> Precedence: bulk X-Mailing-List: linux-leds@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 This patch introduces Rust support for the LED subsystem, adding abstractions for the `led_classdev` struct and implementing core LED operations such as `brightness_set`, `brightness_get`, and `blink_set` via a vtable trait. A `Color` enum is defined to represent various LED colors, and a `LedConfig` struct is used to configure LED properties such as name and color. The `Led` struct wraps the C `led_classdev` and ensures proper memory management by being pinned and automatically unregistered when dropped. Support for a devm register variant is not included in this patch. Signed-off-by: Fiona Behrens --- MAINTAINERS | 1 + rust/kernel/leds.rs | 341 ++++++++++++++++++++++++++++++++++++++++++++ rust/kernel/lib.rs | 2 + rust/kernel/time.rs | 7 + 4 files changed, 351 insertions(+) create mode 100644 rust/kernel/leds.rs diff --git a/MAINTAINERS b/MAINTAINERS index 30cbc3d44cd5..cef929b57159 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13019,6 +13019,7 @@ F: Documentation/leds/ F: drivers/leds/ F: include/dt-bindings/leds/ F: include/linux/leds.h +F: rust/kernel/leds.rs LEGO MINDSTORMS EV3 R: David Lechner diff --git a/rust/kernel/leds.rs b/rust/kernel/leds.rs new file mode 100644 index 000000000000..980af7c405d4 --- /dev/null +++ b/rust/kernel/leds.rs @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! LED subsystem. +//! +//! C header: [`includes/linux/leds.h`](srctree/include/linux/leds.h) + +use core::ptr; + +use crate::device::Device; +use crate::error::from_result; +use crate::ffi::c_ulong; +use crate::prelude::*; +use crate::time::Delta; +use crate::types::Opaque; + +/// Color of an LED. +#[allow(missing_docs)] +#[derive(Copy, Clone)] +#[repr(u32)] +pub enum Color { + White = bindings::LED_COLOR_ID_WHITE, + Red = bindings::LED_COLOR_ID_RED, + Green = bindings::LED_COLOR_ID_GREEN, + Blue = bindings::LED_COLOR_ID_BLUE, + Amber = bindings::LED_COLOR_ID_AMBER, + Violet = bindings::LED_COLOR_ID_VIOLET, + Yellow = bindings::LED_COLOR_ID_YELLOW, + Purple = bindings::LED_COLOR_ID_PURPLE, + Orange = bindings::LED_COLOR_ID_ORANGE, + Pink = bindings::LED_COLOR_ID_PINK, + Cyan = bindings::LED_COLOR_ID_CYAN, + Lime = bindings::LED_COLOR_ID_LIME, + + /// Infrared + IR = bindings::LED_COLOR_ID_IR, + /// Multicolor LEDs + Multi = bindings::LED_COLOR_ID_MULTI, + /// Multicolor LEDs that can do arbitrary color, + /// so this would include `RGBW` and similar + RGB = bindings::LED_COLOR_ID_RGB, +} + +impl Color { + /// Get String representation of the Color. + pub fn name_cstr(&self) -> Option<&'static CStr> { + // SAFETY: C function call, enum is valid argument. + let name = unsafe { bindings::led_get_color_name(u32::from(self) as u8) }; + if name.is_null() { + return None; + } + // SAFETY: pointer from above, valid for static lifetime. + Some(unsafe { CStr::from_char_ptr(name) }) + } + + /// Get String representation of the Color as rust type. + #[inline] + pub fn name(&self) -> Option<&'static str> { + self.name_cstr().map(|name| + // SAFETY: name from C name array which is valid UTF-8. + unsafe { name.as_str_unchecked() }) + } +} + +impl core::fmt::Display for Color { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.name().unwrap_or("Invalid color")) + } +} + +impl From for u32 { + fn from(color: Color) -> Self { + color as u32 + } +} + +impl From<&Color> for u32 { + fn from(color: &Color) -> Self { + (*color).into() + } +} + +impl TryFrom for Color { + type Error = Error; + + /// Tris to convert a u32 into a [`Color`]. + /// + /// # Errors + /// + /// Returns [`EINVAL`] if the color is not known. + fn try_from(value: u32) -> Result { + Ok(match value { + bindings::LED_COLOR_ID_WHITE => Color::White, + bindings::LED_COLOR_ID_RED => Color::Red, + bindings::LED_COLOR_ID_GREEN => Color::Green, + bindings::LED_COLOR_ID_BLUE => Color::Blue, + bindings::LED_COLOR_ID_AMBER => Color::Amber, + bindings::LED_COLOR_ID_VIOLET => Color::Violet, + bindings::LED_COLOR_ID_YELLOW => Color::Yellow, + bindings::LED_COLOR_ID_PURPLE => Color::Purple, + bindings::LED_COLOR_ID_ORANGE => Color::Orange, + bindings::LED_COLOR_ID_PINK => Color::Pink, + bindings::LED_COLOR_ID_CYAN => Color::Cyan, + bindings::LED_COLOR_ID_LIME => Color::Lime, + bindings::LED_COLOR_ID_IR => Color::IR, + bindings::LED_COLOR_ID_MULTI => Color::Multi, + bindings::LED_COLOR_ID_RGB => Color::RGB, + _ => return Err(EINVAL), + }) + } +} + +/// Data used for led registration. +#[derive(Clone)] +pub struct LedConfig<'name> { + /// Name to give the led. + pub name: Option<&'name CStr>, + /// Color of the LED. + pub color: Color, +} + +/// A Led backed by a C `struct led_classdev`, additionally offering +/// driver data storage. +/// +/// The LED is unregistered once this LED struct is dropped. +// TODO: add devm register variant +#[pin_data(PinnedDrop)] +pub struct Led { + #[pin] + led: Opaque, + /// Driver private data + pub data: T, +} + +impl Led +where + T: Operations>, +{ + /// Register a new LED. + /// + /// Copies the name if provided so that the lifetime of the name can end after the init function. + #[cfg(CONFIG_LEDS_CLASS)] + pub fn register<'a>( + device: Option<&'a Device>, + config: &'a LedConfig<'a>, + data: T, + ) -> impl PinInit + 'a + where + T: 'a, + { + try_pin_init!(Self { + led <- Opaque::try_ffi_init(move |place: *mut bindings::led_classdev| { + // SAFETY: `place` is a pointer to a live allocation, so erasing is valid. + unsafe { place.write_bytes(0, 1) }; + + if let Some(name) = &config.name { + // SAFETY: `place` is a pointer to a live allocation. + let name_ptr = unsafe { ptr::addr_of_mut!((*place).name) }; + // SAFETY: `name_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(name_ptr, name.as_char_ptr()) }; + } + + // SAFETY: `place` is a pointer to a live allocation. + let color_ptr = unsafe { ptr::addr_of_mut!((*place).color) }; + // SAFETY: `color_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(color_ptr, config.color.into()) }; + + // SAFETY: `place` is a pointer to a live allocation. + let max_brightness_ptr = unsafe { ptr::addr_of_mut!((*place).max_brightness) }; + // SAFETY: `max_brightness_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(max_brightness_ptr, T::MAX_BRIGHTNESS as _) }; + + // SAFETY: `place` is a pointer to a live allocation. + let set_fn_ptr: *mut Option<_> = unsafe { ptr::addr_of_mut!((*place).brightness_set) }; + // SAFETY: `set_fn_ptr` points to valid allocation and we have exclusive access. + unsafe { ptr::write(set_fn_ptr, Some(brightness_set::)) }; + + if T::HAS_BRIGHTNESS_GET { + // SAFETY: `place` is pointing to a live allocation. + let get_fn_ptr: *mut Option<_> = unsafe { ptr::addr_of_mut!((*place).brightness_get) }; + // SAFETY: `get_fn_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(get_fn_ptr, Some(brightness_get::)) }; + } + + if T::HAS_BLINK_SET { + // SAFETY: `place` is pointing to a live allocation. + let set_fn_ptr: *mut Option<_> = unsafe { ptr::addr_of_mut!((*place).blink_set) }; + // SAFETY: `get_fn_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(set_fn_ptr, Some(blink_set::)) }; + } + + + let dev = device.map(|dev| dev.as_raw()).unwrap_or(ptr::null_mut()); + // SAFETY: `place` is a pointer to a live allocation of `bindings::led_classdev`. + crate::error::to_result(unsafe { + bindings::led_classdev_register_ext(dev, place, ptr::null_mut()) + }) + }), + data: data, + }) + } +} + +impl private::Sealed for Led {} + +impl FromLedClassdev for Led { + #[inline] + unsafe fn led_container_of(ptr: *mut bindings::led_classdev) -> *mut Self { + let ptr = ptr.cast::>(); + + // SAFETY: By the safety requirement of this function ptr is embedded in self. + unsafe { crate::container_of!(ptr, Self, led).cast_mut() } + } +} + +#[pinned_drop] +impl PinnedDrop for Led { + fn drop(self: Pin<&mut Self>) { + // SAFETY: led is pointing to a live allocation + #[cfg(CONFIG_LEDS_CLASS)] + unsafe { + bindings::led_classdev_unregister(self.led.get()) + } + } +} + +// SAFETY: `struct led_classdev` holds a mutex. +unsafe impl Send for Led {} +// SAFETY: `struct led_classdev` holds a mutex. +unsafe impl Sync for Led {} + +/// Led operations vtable. +// TODO: add blocking variant (either via const generic or second trait) +#[macros::vtable] +pub trait Operations: Sized { + /// The maximum brightnes this led supports. + const MAX_BRIGHTNESS: u8; + + /// Type of the container to use as self in the callbacks. + /// + /// This is most often [`Led`]. + type This: FromLedClassdev; + + /// Set LED brightness level. + /// This functions must not sleep. + fn brightness_set(this: &mut Self::This, brightness: u8); + + /// Get the LED brightness level. + fn brightness_get(_this: &mut Self::This) -> u8 { + crate::build_error(crate::error::VTABLE_DEFAULT_ERROR) + } + + /// Activae hardware accelerated blink, delays are in milliseconds + fn blink_set( + _this: &mut Self::This, + _delay_on: Delta, + _delay_off: Delta, + ) -> Result<(Delta, Delta)> { + crate::build_error(crate::error::VTABLE_DEFAULT_ERROR) + } +} + +/// `brightness_set` function pointer +/// +/// # Safety +/// +/// `led_cdev` must be passed by the corresponding callback in `led_classdev` +unsafe extern "C" fn brightness_set( + led_cdev: *mut bindings::led_classdev, + brightness: bindings::led_brightness, +) where + T: Operations, +{ + // SAFETY: By the safety requirement of this function `led_cdev` is embedded inside a `T::This`. + let led = unsafe { &mut *(T::This::led_container_of(led_cdev.cast())) }; + T::brightness_set(led, brightness as _); +} + +/// `brightness_get` function pointer +/// +/// # Safety +/// +/// `led_cdev` must be passed by the corresponding callback in `led_classdev` +unsafe extern "C" fn brightness_get( + led_cdev: *mut bindings::led_classdev, +) -> bindings::led_brightness +where + T: Operations, +{ + // SAFETY: By the safety requirement of this function `led_cdev` is embedded inside a `T::This`. + let led = unsafe { &mut *(T::This::led_container_of(led_cdev.cast())) }; + T::brightness_get(led) as _ +} + +/// `blink_set` function pointer +/// +/// # Safety +/// +/// `led_cdev` must be passed by the corresponding callback in `led_classdev` +unsafe extern "C" fn blink_set( + led_cdev: *mut bindings::led_classdev, + delay_on: *mut c_ulong, + delay_off: *mut c_ulong, +) -> i32 +where + T: Operations, +{ + from_result(|| { + // SAFETY: By the safety requirement of this function `led_cdev` is embedded inside a `T::This`. + let led = unsafe { &mut *(T::This::led_container_of(led_cdev.cast())) }; + + // SAFETY: By the safety requirement of this function `delay_on` is pointing to a valid `c_ulong`. + let on = Delta::from_millis(unsafe { *delay_on } as i64); + // SAFETY: By the safety requirement of this function `delay_off` is pointing to a valid `c_ulong`. + let off = Delta::from_millis(unsafe { *delay_off } as i64); + + let (on, off) = T::blink_set(led, on, off)?; + + // writing back return values + // SAFETY: By the safety requirement of this function `delay_on` is pointing to a valid `c_ulong`. + unsafe { ptr::write(delay_on, on.as_millis_ceil() as c_ulong) }; + // SAFETY: By the safety requirement of this function `delay_off` is pointing to a valid `c_ulong`. + unsafe { ptr::write(delay_off, off.as_millis_ceil() as c_ulong) }; + + Ok(0) + }) +} + +/// Trait to get the type from a `led_classdev` pointer. +pub trait FromLedClassdev: private::Sealed { + /// Get pointer to the outer type from a `led_classdev` pointer. + /// + /// # Safety + /// + /// `ptr` must point to a [`bindings::led_classdev`] field in a struct of type `Self`. + unsafe fn led_container_of(ptr: *mut bindings::led_classdev) -> *mut Self; +} + +mod private { + /// Marker that a trait cannot be implemented outside of this crate + pub trait Sealed {} +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index e21f2e607963..8895a1683f82 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -46,6 +46,8 @@ pub mod jump_label; #[cfg(CONFIG_KUNIT)] pub mod kunit; +#[cfg(CONFIG_NEW_LEDS)] +pub mod leds; pub mod list; pub mod miscdevice; #[cfg(CONFIG_NET)] diff --git a/rust/kernel/time.rs b/rust/kernel/time.rs index 38da79925586..3c348ce4a7c2 100644 --- a/rust/kernel/time.rs +++ b/rust/kernel/time.rs @@ -143,4 +143,11 @@ pub fn as_nanos(self) -> i64 { pub fn as_micros_ceil(self) -> i64 { self.as_nanos().saturating_add(NSEC_PER_USEC - 1) / NSEC_PER_USEC } + + /// Return the smallest number of milliseconds greater than or equal + /// to the value in the `Delta`. + #[inline] + pub fn as_millis_ceil(self) -> i64 { + self.as_nanos().saturating_add(NSEC_PER_MSEC - 1) / NSEC_PER_MSEC + } } From patchwork Mon Jan 13 12:16:18 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fiona Behrens X-Patchwork-Id: 857377 Received: from gimli.kloenk.de (gimli.kloenk.de [49.12.72.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3F3E22045B6; Mon, 13 Jan 2025 12:24:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=49.12.72.200 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; cv=none; b=ChWuWIdfo6YwNJaEOlrYRPsLmfBFboifpKgqWUDAT49E2M0US5Z4TDuHH7D7Jlcm7cKfVwk1Qk8bMN4k16O7JigXKOiiDOWCPjV9hKhKALgP+YrI19nHvbbl3tJ9Wcm1bgmkLH/PI/ToVvJxStsxVQORCbdGBfXPVIsInNtjrX4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736771051; c=relaxed/simple; bh=+PyEeSLPl5fE9v99iZI8bcIBW0o1ZJP1R07PF4JjW4s=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=N1sGx+VNLaZS1LiiRGAW+jpGNs7gf3se8spC8lHevkIdx0OeAgdJkIrcebTVn1x6BE9K6SXtmHA+CZyh8F6i1depojYYpXT7Tn+Nvk5X8tJTRMFNAAPF+EYd7hn0JNscr9SrV7Nn7sIL+S/JQ9n9s2vUCaJ9zc+w9wMCYdJuHfo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev; spf=pass smtp.mailfrom=kloenk.dev; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b=gr/FaBBZ; arc=none smtp.client-ip=49.12.72.200 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b="gr/FaBBZ" From: Fiona Behrens DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kloenk.dev; s=mail; t=1736770603; bh=x0QnlD4Vmg0Kp9RPBCFlDIqtOrxbMyJ6d+E77AZiYnY=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=gr/FaBBZ/rq7Tayhhek7CHotdp9asjD45HwjqHXnvbczirWaRJ2i253tt1uJk8hfw 2CG5oMjWMIZ5/WtU8AHSdogPFTBJhg1Os1Ax3CukaOwZ7/7TpD1pbsFdVCKAutVXxM cRNoVmfbWXF7VeaRDKoTIH1UA/Mx1N0sb3QWGD2E= To: Miguel Ojeda , Alex Gaynor , Pavel Machek , Lee Jones , Jean Delvare Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Fiona Behrens , Mark Pearson , Peter Koch , rust-for-linux@vger.kernel.org, linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v2 3/5] rust: leds: Add hardware trigger support for hardware-controlled LEDs Date: Mon, 13 Jan 2025 13:16:18 +0100 Message-ID: <20250113121620.21598-4-me@kloenk.dev> In-Reply-To: <20250113121620.21598-1-me@kloenk.dev> References: <20250113121620.21598-1-me@kloenk.dev> Precedence: bulk X-Mailing-List: linux-leds@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Adds abstraction for hardware trigger support in LEDs, enabling LEDs to be controlled by external hardware events. An `Arc` is embedded within the `led_classdev` to manage the lifecycle of the hardware trigger, ensuring proper reference counting and cleanup when the LED is dropped. Signed-off-by: Fiona Behrens --- MAINTAINERS | 1 + rust/kernel/leds.rs | 95 +++++++++++++++++++++++--- rust/kernel/leds/triggers.rs | 128 +++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 rust/kernel/leds/triggers.rs diff --git a/MAINTAINERS b/MAINTAINERS index cef929b57159..954dbd311a55 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13020,6 +13020,7 @@ F: drivers/leds/ F: include/dt-bindings/leds/ F: include/linux/leds.h F: rust/kernel/leds.rs +F: rust/kernel/leds/ LEGO MINDSTORMS EV3 R: David Lechner diff --git a/rust/kernel/leds.rs b/rust/kernel/leds.rs index 980af7c405d4..f10a10b56e23 100644 --- a/rust/kernel/leds.rs +++ b/rust/kernel/leds.rs @@ -10,9 +10,14 @@ use crate::error::from_result; use crate::ffi::c_ulong; use crate::prelude::*; +#[cfg(CONFIG_LEDS_TRIGGERS)] +use crate::sync::Arc; use crate::time::Delta; use crate::types::Opaque; +#[cfg(CONFIG_LEDS_TRIGGERS)] +pub mod triggers; + /// Color of an LED. #[allow(missing_docs)] #[derive(Copy, Clone)] @@ -110,12 +115,34 @@ fn try_from(value: u32) -> Result { } /// Data used for led registration. -#[derive(Clone)] -pub struct LedConfig<'name> { +pub struct LedConfig<'name, T> { /// Name to give the led. pub name: Option<&'name CStr>, /// Color of the LED. pub color: Color, + /// Private data of the LED. + pub data: T, + + /// Default trigger name. + pub default_trigger: Option<&'static CStr>, + /// Hardware trigger. + /// + /// Setting this to some also defaults the default trigger to this hardware trigger. + /// Use `default_trigger: Some("none")` to overwrite this. + #[cfg(CONFIG_LEDS_TRIGGERS)] + pub hardware_trigger: Option>>, +} + +impl<'name, T> LedConfig<'name, T> { + /// Create a new LedConfig + pub fn new(color: Color, data: T) -> Self { + Self { + color, + data, + // SAFETY: all other fields are valid with zeroes. + ..unsafe { core::mem::zeroed() } + } + } } /// A Led backed by a C `struct led_classdev`, additionally offering @@ -141,8 +168,7 @@ impl Led #[cfg(CONFIG_LEDS_CLASS)] pub fn register<'a>( device: Option<&'a Device>, - config: &'a LedConfig<'a>, - data: T, + config: LedConfig<'a, T>, ) -> impl PinInit + 'a where T: 'a, @@ -188,14 +214,46 @@ pub fn register<'a>( unsafe { ptr::write(set_fn_ptr, Some(blink_set::)) }; } + #[cfg(CONFIG_LEDS_TRIGGERS)] + if let Some(trigger) = config.hardware_trigger { + let trigger = trigger.into_raw(); + // SAFETY: `place` is pointing to a live allocation. + let trigger_type_ptr = unsafe { ptr::addr_of_mut!((*place).trigger_type) }; + // SAFETY: `trigger` is a valid pointer + let hw_trigger = unsafe { ptr::addr_of!((*trigger).hw_type) }; + // SAFETY: `trigger_type_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(trigger_type_ptr, hw_trigger.cast_mut().cast()) }; + + // SAFETY: trigger points to a valid hardware trigger struct. + let trigger_name_ptr = unsafe { Opaque::raw_get(ptr::addr_of!( (*trigger).trigger)) }; + // SAFETY: trigger points to a valid hardware trigger struct. + let trigger_name_ptr = unsafe { (*trigger_name_ptr).name }; + // SAFETY: `place` is pointing to a live allocation. + let default_trigger_ptr = unsafe { ptr::addr_of_mut!((*place).default_trigger) }; + // SAFETY: `default_trigger_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(default_trigger_ptr, trigger_name_ptr) }; + + // SAFETY: `place` is pointing to a live allocation. + let hw_ctrl_trigger_ptr = unsafe { ptr::addr_of_mut!((*place).hw_control_trigger) }; + // SAFETY: `hw_ctrl_trigger_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(hw_ctrl_trigger_ptr, trigger_name_ptr) }; + } + + // After hw trigger impl, to overwrite default trigger + if let Some(default_trigger) = config.default_trigger { + // SAFETY: `place` is pointing to a live allocation. + let default_trigger_ptr = unsafe { ptr::addr_of_mut!((*place).default_trigger) }; + // SAFETY: `default_trigger_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(default_trigger_ptr, default_trigger.as_char_ptr()) }; + } - let dev = device.map(|dev| dev.as_raw()).unwrap_or(ptr::null_mut()); - // SAFETY: `place` is a pointer to a live allocation of `bindings::led_classdev`. - crate::error::to_result(unsafe { - bindings::led_classdev_register_ext(dev, place, ptr::null_mut()) - }) + let dev = device.map(|dev| dev.as_raw()).unwrap_or(ptr::null_mut()); + // SAFETY: `place` is a pointer to a live allocation of `bindings::led_classdev`. + crate::error::to_result(unsafe { + bindings::led_classdev_register_ext(dev, place, ptr::null_mut()) + }) }), - data: data, + data: config.data, }) } } @@ -220,6 +278,23 @@ fn drop(self: Pin<&mut Self>) { unsafe { bindings::led_classdev_unregister(self.led.get()) } + + // drop trigger if there is a hw trigger defined. + #[cfg(CONFIG_LEDS_TRIGGERS)] + { + // SAFETY: `self.led` is a valid led. + let hw_trigger_type = + unsafe { ptr::read(ptr::addr_of!((*self.led.get()).trigger_type)) }; + if !hw_trigger_type.is_null() { + // SAFETY: hw_trigger_type is a valid and non null pointer into a Hardware trigger. + let hw_trigger_type = unsafe { + crate::container_of!(hw_trigger_type, triggers::Hardware, hw_type) + }; + // SAFETY: `hw_trigger_type` is a valid pointer that came from an arc. + let hw_trigger_type = unsafe { Arc::from_raw(hw_trigger_type) }; + drop(hw_trigger_type); + } + } } } diff --git a/rust/kernel/leds/triggers.rs b/rust/kernel/leds/triggers.rs new file mode 100644 index 000000000000..d5f2b8252645 --- /dev/null +++ b/rust/kernel/leds/triggers.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! LED trigger abstractions. + +use core::marker::PhantomData; +use core::ptr; + +use crate::error::{from_result, to_result}; +use crate::prelude::*; +use crate::types::Opaque; + +use super::FromLedClassdev; + +/// LED Hardware trigger. +/// +/// Used to impement a hardware operation mode for an LED. +#[pin_data(PinnedDrop)] +pub struct Hardware { + #[pin] + pub(crate) hw_type: Opaque, + #[pin] + pub(crate) trigger: Opaque, + _t: PhantomData, +} + +impl Hardware +where + T: HardwareOperations, +{ + /// Register a new hardware Trigger with a given name. + pub fn register(name: &'static CStr) -> impl PinInit { + try_pin_init!( Self { + // SAFETY: `led_hw_trigger_type` is valid with all zeroes. + hw_type: Opaque::new(unsafe { core::mem::zeroed() }), + trigger <- Opaque::try_ffi_init(move |place: *mut bindings::led_trigger| { + // SAFETY: `place` is a pointer to a live allocation, so erasing is valid. + unsafe { place.write_bytes(0, 1) }; + + // Add name + // SAFETY: `place` is pointing to a live allocation, so the deref is safe. + let name_ptr = unsafe { ptr::addr_of_mut!((*place).name) }; + // SAFETY: `name_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(name_ptr, name.as_char_ptr()) }; + + // Add fn pointers + // SAFETY: `place` is pointing to a live allocation, so the deref is safe. + let activate_fn_ptr: *mut Option<_> = unsafe { ptr::addr_of_mut!((*place).activate) }; + // SAFETY: `activate_fn_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(activate_fn_ptr, Some(trigger_activate::)) }; + + if T::HAS_DEACTIVATE { + // SAFETY: `place` is pointing to a live allocation, so the deref is safe. + let deactivate_fn_ptr: *mut Option<_> = unsafe { ptr::addr_of_mut!((*place).deactivate) }; + // SAFETY: `deactivate_fn_ptr` points to a valid allocation and we have exclusive access. + unsafe { ptr::write(deactivate_fn_ptr, Some(trigger_deactivate::)) }; + } + + // Add hardware trigger + // SAFETY: `place` is pointing to a live allocation, so the deref is safe. + let trigger_type_ptr = unsafe { ptr::addr_of_mut!((*place).trigger_type) }; + // SAFETY: `place` is pointing to a live allocation, so the deref is safe. + let trigger_type = unsafe { crate::container_of!(place, Self, trigger).cast_mut() }; + // SAFETY: `trigger_type` is pointing to a live allocation of Self. + let trigger_type = unsafe { ptr::addr_of!((*trigger_type).hw_type) }; + // SAFETY: `trigger_type_ptr` points to a valid allocation and we have exclusive access. + unsafe{ ptr::write(trigger_type_ptr, Opaque::raw_get(trigger_type)) }; + + // SAFETY: ffi call, `place` is sufficently filled with data at this point + to_result(unsafe { + bindings::led_trigger_register(place) + }) + }), + _t: PhantomData, + }) + } +} + +#[pinned_drop] +impl PinnedDrop for Hardware { + fn drop(self: Pin<&mut Self>) { + // SAFETY: trigger is pointing to a live and registered allocation + unsafe { + bindings::led_trigger_unregister(self.trigger.get()); + } + } +} + +/// Operations for the Hardware trigger +#[macros::vtable] +pub trait HardwareOperations: super::Operations { + /// Activate the hardware trigger. + fn activate(this: &mut Self::This) -> Result; + /// Deactivate the hardware trigger. + fn deactivate(_this: &mut Self::This) { + crate::build_error(crate::error::VTABLE_DEFAULT_ERROR) + } +} + +/// `trigger_activate` function pointer +/// +/// # Safety +/// +/// `led_cdev` must be passed by the corresponding callback in `led_trigger`. +unsafe extern "C" fn trigger_activate(led_cdev: *mut bindings::led_classdev) -> i32 +where + T: HardwareOperations, +{ + from_result(|| { + // SAFETY: By the safety requirement of this function `led_cdev` is embedded inside a `T::This`. + let led = unsafe { &mut *(T::This::led_container_of(led_cdev.cast())) }; + T::activate(led)?; + Ok(0) + }) +} + +/// `trigger_deactivate` function pointer +/// +/// # Safety +/// +/// `led_cdev` must be passed by the corresponding callback in `led_trigger`. +unsafe extern "C" fn trigger_deactivate(led_cdev: *mut bindings::led_classdev) +where + T: HardwareOperations, +{ + // SAFETY: By the safety requirement of this function `led_cdev` is embedded inside a `T::This`. + let led = unsafe { &mut *(T::This::led_container_of(led_cdev.cast())) }; + T::deactivate(led) +} From patchwork Mon Jan 13 12:16:19 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fiona Behrens X-Patchwork-Id: 857095 Received: from gimli.kloenk.de (gimli.kloenk.de [49.12.72.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4345F46B5; Mon, 13 Jan 2025 12:16:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=49.12.72.200 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736770609; cv=none; b=Goj0UN0DOZt9E7yopDvzxEa+wJZblni/SOnm8O13bXGRlHPVrTmkxNVGY8C0w37pGX2dq64VzYeSpbFhtteUryYHVGC/73IO9x5bsqsVXRjDE5HqD8w9KMr1oJfQhU68XdAphOZy5E8ilcPEAquWZeCkGhiXs9l2CQ5Ohq6qEpA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736770609; c=relaxed/simple; bh=X55MW9Dg0Zi/RNAdNvnOWc+2TMmIMf4DTNcr3zscVk8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=nqMEkVFD7aHPhOk65dWCxVT7w92dYRiNMUGdNVrAovxvnyMhTk9vnrYfrItewuY1HwM5vnL3zGuDHZAXzR7c8EWwYVc/MfQURx0Mlp3c7jxnCz/+4mpxRHg6DCDHiqHHlrVDN1pn4IvpXUlASSjgRMhgQHJ7JaQNQpC33iPKKDw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev; spf=pass smtp.mailfrom=kloenk.dev; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b=W96Kf6Le; arc=none smtp.client-ip=49.12.72.200 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b="W96Kf6Le" From: Fiona Behrens DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kloenk.dev; s=mail; t=1736770604; bh=Azv4UbYvX3F38g2x+W0M5pOtjjIlGZCAIrMpGofvm+U=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=W96Kf6Leb2YWuj+wZE9h0QcmFTIKqKuhHJEHK3IUcDrt74rZfKkEIrLwGVHbNOQDQ eVQLyYpl52OpozicStzikb/rRuCPrYqBV544i/+K+o1QjcB/o68f+udOj5yKn8ej9s BAzdYRJdKDLXqmR6fvNakNYy6HhYWEXlRb2HvWg8= To: Miguel Ojeda , Alex Gaynor , Pavel Machek , Lee Jones , Jean Delvare Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Fiona Behrens , Mark Pearson , Peter Koch , rust-for-linux@vger.kernel.org, linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v2 4/5] rust: add I/O port abstractions with resource management Date: Mon, 13 Jan 2025 13:16:19 +0100 Message-ID: <20250113121620.21598-5-me@kloenk.dev> In-Reply-To: <20250113121620.21598-1-me@kloenk.dev> References: <20250113121620.21598-1-me@kloenk.dev> Precedence: bulk X-Mailing-List: linux-leds@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 This patch introduces abstractions for working with I/O port resources, including the `Resource` and `Region` types. These abstractions facilitate interaction with `ioport_resource`, enabling safe management of resource reservations and memory accesses. Additionally, helper functions such as `outb`, `outw`, and `outl` have been provided to write values to these resources, with matching read functions such as `inb`, `inw`, and `inl` for accessing the port memory. Signed-off-by: Fiona Behrens --- rust/helpers/helpers.c | 1 + rust/helpers/ioport.c | 42 ++++++++++ rust/kernel/ioport.rs | 169 +++++++++++++++++++++++++++++++++++++++++ rust/kernel/lib.rs | 2 + 4 files changed, 214 insertions(+) create mode 100644 rust/helpers/ioport.c create mode 100644 rust/kernel/ioport.rs diff --git a/rust/helpers/helpers.c b/rust/helpers/helpers.c index dcf827a61b52..b40aee82fa0f 100644 --- a/rust/helpers/helpers.c +++ b/rust/helpers/helpers.c @@ -14,6 +14,7 @@ #include "cred.c" #include "err.c" #include "fs.c" +#include "ioport.c" #include "jump_label.c" #include "kunit.c" #include "mutex.c" diff --git a/rust/helpers/ioport.c b/rust/helpers/ioport.c new file mode 100644 index 000000000000..d9c9e2093b98 --- /dev/null +++ b/rust/helpers/ioport.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include + +struct resource *rust_helper_request_region(resource_size_t start, + resource_size_t n, const char *name) +{ + return request_region(start, n, name); +} + +struct resource *rust_helper_request_muxed_region(resource_size_t start, + resource_size_t n, + const char *name) +{ + return request_muxed_region(start, n, name); +} + +void rust_helper_release_region(resource_size_t start, resource_size_t n) +{ + release_region(start, n); +} + +#define define_rust_helper_out(name, type) \ + void rust_helper_##name(type value, unsigned long addr) \ + { \ + (name)(value, addr); \ + } + +define_rust_helper_out(outb, u8); +define_rust_helper_out(outw, u16); +define_rust_helper_out(outl, u32); + +#define define_rust_helper_in(name, type) \ + type rust_helper_##name(unsigned long addr) \ + { \ + return (name)(addr); \ + } + +define_rust_helper_in(inb, u8); +define_rust_helper_in(inw, u16); +define_rust_helper_in(inl, u32); diff --git a/rust/kernel/ioport.rs b/rust/kernel/ioport.rs new file mode 100644 index 000000000000..9bc342cb4663 --- /dev/null +++ b/rust/kernel/ioport.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Abstractions of routines for detecting, reserving and +//! allocating system resources. +//! +//! C header: [`include/linux/ioport.h`](srctree/include/linux/ioport.h) + +use core::ops::Deref; +use core::ptr; + +use crate::prelude::*; +use crate::types::Opaque; + +/// Resource Size type. +/// This is a type alias to `u64` +/// depending on the config option `CONFIG_PHYS_ADDR_T_64BIT`. +#[cfg(CONFIG_PHYS_ADDR_T_64BIT)] +pub type ResourceSize = u64; + +/// Resource Size type. +/// This is a type alias to `u32` +/// depending on the config option `CONFIG_PHYS_ADDR_T_64BIT`. +#[cfg(not(CONFIG_PHYS_ADDR_T_64BIT))] +pub type ResourceSize = u32; + +/// Resource node. +#[repr(transparent)] +pub struct Resource(Opaque); + +impl Resource { + /// Convert a raw C `struct resource` pointer to a `&'a Resource`. + /// + /// # Safety + /// + /// Callers must ensure that `ptr` is valid, non-null. + pub unsafe fn as_ref<'a>(ptr: *mut bindings::resource) -> &'a Self { + // SAFETY: Guaranteed by the safety requirements of the function. + unsafe { &*ptr.cast() } + } + + /// Return raw pointer to the resource. + pub fn as_raw(&self) -> *mut bindings::resource { + self.0.get() + } + + /// Get name of the resource. + pub fn name(&self) -> &CStr { + // SAFETY: self.get is valid and of type `bindings::resource` + let name_ptr = unsafe { ptr::read(ptr::addr_of!((*self.as_raw()).name)) }; + // SAFETY: `name_ptr` is a valid char pointer from the resource + unsafe { CStr::from_char_ptr(name_ptr) } + } + + /// Get the start of the [`Resource`]. + #[inline] + pub fn start(&self) -> ResourceSize { + // SAFETY: self.get is valid and of type `bindings::resource` + unsafe { ptr::read(ptr::addr_of!((*self.as_raw()).start)) } + } + + /// Get the end of the [`Resource`]. + #[inline] + pub fn end(&self) -> ResourceSize { + // SAFETY: self.get is valid and of type `bindings::resource` + unsafe { ptr::read(ptr::addr_of!((*self.as_raw()).start)) } + } + + /// Get the length of the Resource. + // Empty resource (len = 0) does not make any sense.` + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> ResourceSize { + self.end() - self.start() + 1 + } +} + +/// Requested region using `request_region`. This will release the region when dropped. +/// +/// This uses port memory from the `ioport_resource` parent resource. +pub struct Region<'name>(&'name Resource); + +impl<'name> Region<'name> { + /// Request a new muxed region from the `ioport_resource` region. + #[inline] + pub fn request_muxed(start: ResourceSize, n: ResourceSize, name: &'name CStr) -> Option { + // SAFETY: C ffi call. `name` is valid for lifetime of `Self` + unsafe { Self::from_resource(bindings::request_muxed_region(start, n, name.as_char_ptr())) } + } + + /// Request a new region from the `ioport_resource` region. + #[inline] + pub fn request(start: ResourceSize, n: ResourceSize, name: &'name CStr) -> Option { + // SAFETY: C ffi call. `name` is valid for lifetime of `Self` + unsafe { Self::from_resource(bindings::request_region(start, n, name.as_char_ptr())) } + } + + /// Get a resource pointer and return a resource if the pointer is non-null. + /// + /// Helper for `request` and `request_muxed`. + /// + /// # Safety + /// + /// `resource` has to be a valid or null pointer to a resource. + unsafe fn from_resource(resource: *mut bindings::resource) -> Option { + if resource.is_null() { + None + } else { + // SAFETY: resource is a valid resource by the function requirements and non-null. + Some(Self(unsafe { Resource::as_ref(resource) })) + } + } +} + +macro_rules! define_out { + ($name:ident => $type:ty) => { + #[doc = concat!("Write [`", stringify!($type), "`] value into port memory region at the given offset.")] + pub fn $name(&self, value: $type, offset: ResourceSize) { + let address = self.start() + offset; + debug_assert!((address + (core::mem::size_of::<$type>() as ResourceSize)) <= (self.end() + 1)); + // SAFETY: ffi call, address is in the region + unsafe { bindings::$name(value, address) }; + } + }; + ($($name:ident => $type:ty;)*) => { + $(define_out! { $name => $type })* + }; +} + +macro_rules! define_in { + ($name:ident => $type:ty) => { + #[doc = concat!("Read [`", stringify!($type), "`] value from port memory region at the given offset.")] + pub fn $name(&self, offset: ResourceSize) -> $type { + let address = self.start() + offset; + debug_assert!((address + (core::mem::size_of::<$type>() as ResourceSize)) <= (self.end() + 1)); + // SAFETY: ffi call, address is in the region + unsafe { bindings::$name(address) } + } + }; + ($($name:ident => $type:ty;)*) => { + $(define_in! { $name => $type })* + }; +} + +impl Region<'_> { + define_out! { + outb => u8; + outw => u16; + outl => u32; + } + define_in! { + inb => u8; + inw => u16; + inl => u32; + } +} + +impl Drop for Region<'_> { + fn drop(&mut self) { + // SAFETY: ffi call, resource is valid. + unsafe { bindings::release_region(self.start(), self.len()) }; + } +} + +impl Deref for Region<'_> { + type Target = Resource; + + fn deref(&self) -> &Self::Target { + self.0 + } +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index 8895a1683f82..99cd706c40e7 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -43,6 +43,8 @@ pub mod fs; pub mod init; pub mod ioctl; +#[cfg(CONFIG_HAS_IOPORT)] +pub mod ioport; pub mod jump_label; #[cfg(CONFIG_KUNIT)] pub mod kunit; From patchwork Mon Jan 13 12:16:20 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fiona Behrens X-Patchwork-Id: 857378 Received: from gimli.kloenk.de (gimli.kloenk.de [49.12.72.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8FD6A1CAA8B; Mon, 13 Jan 2025 12:16:47 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=49.12.72.200 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736770609; cv=none; b=ZwUaxy+F1W1GwL9Rc9rSCIrbHGuD8XEqVE7v99D+2DLZ8+8tf2QhXLChQ3P8nMEiIRwT4iIYYI1TDfC++hvetlzRfrnFeAZB1CXqEPG/f974qzUuFo8Y/Ih/Mtz8PxqOXYoMWkAji7QtPQJMR7uFl9W5GQpevIPdmGPTZTwa7uc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1736770609; c=relaxed/simple; bh=UvvrAR9ApRUqao8isFWxMEwk6PPS7V9ANdkH0NAcR6M=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=mdkSam7AsrTwHxw3tJRAE98q0myvdVqoHTnOhORJfqBtQY5Ojg7NJ7ZcfpXjgY+rlAwLIzEx6+Sf4677yG0NGWIxG8MILy5jwYHfe4sBomGP7QdJ2SwFod3BcRozy7wcTOqT3B0Up8IDIJA4FjHAtk/niffxm2i7OQ0QLPL3DvE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev; spf=pass smtp.mailfrom=kloenk.dev; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b=XQQptS2L; arc=none smtp.client-ip=49.12.72.200 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kloenk.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=kloenk.dev header.i=@kloenk.dev header.b="XQQptS2L" From: Fiona Behrens DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kloenk.dev; s=mail; t=1736770605; bh=U8U0PrI/d9CyD4YsL382N2BEX1fb3wg8wSgKoWM6dhY=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=XQQptS2Lz+1j0+jxKSfPAeWoz4SS5m4NIPeCQm5hRamsSV4jaX9ULL3EV5Ej14317 w3vDwzKmrlmmwLuhP2rA/++3B48bmhnc0sGWksRtrRaOPA6PbSeUdVULmLpMga5fh1 XCTn8WPv31L4/l2PQvUgDMUwSTAVfW2EXsGe9QSI= To: Miguel Ojeda , Alex Gaynor , Pavel Machek , Lee Jones , Jean Delvare Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Fiona Behrens , Mark Pearson , Peter Koch , rust-for-linux@vger.kernel.org, linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v2 5/5] leds: leds_lenovo_se10: LED driver for Lenovo SE10 platform Date: Mon, 13 Jan 2025 13:16:20 +0100 Message-ID: <20250113121620.21598-6-me@kloenk.dev> In-Reply-To: <20250113121620.21598-1-me@kloenk.dev> References: <20250113121620.21598-1-me@kloenk.dev> Precedence: bulk X-Mailing-List: linux-leds@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Add driver for the Lenovo ThinkEdge SE10 LED. This driver supports controlling the red LED located on the front panel of the Lenovo SE10 hardware. Additionally, it supports the hardware-triggered functionality of the LED, which by default is tied to the WWAN trigger. The driver is written in Rust and adds basic LED support for the SE10 platform. Signed-off-by: Fiona Behrens --- drivers/leds/Kconfig | 10 +++ drivers/leds/Makefile | 1 + drivers/leds/leds_lenovo_se10.rs | 132 +++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 drivers/leds/leds_lenovo_se10.rs diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index b784bb74a837..89d9e98189d6 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -223,6 +223,16 @@ config LEDS_TURRIS_OMNIA side of CZ.NIC's Turris Omnia router. There are 12 RGB LEDs on the front panel. +config LEDS_LENOVO_SE10 + tristate "LED support for Lenovo ThinkEdge SE10" + depends on RUST + depends on (X86 && DMI) || COMPILE_TEST + depends on HAS_IOPORT + imply LEDS_TRIGGERS + help + This option enables basic support for the LED found on the front of + Lenovo's SE10 ThinkEdge. There is one user controlable LED on the front panel. + config LEDS_LM3530 tristate "LCD Backlight driver for LM3530" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 18afbb5a23ee..2cff22cbafcf 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -37,6 +37,7 @@ obj-$(CONFIG_LEDS_IP30) += leds-ip30.o obj-$(CONFIG_LEDS_IPAQ_MICRO) += leds-ipaq-micro.o obj-$(CONFIG_LEDS_IS31FL319X) += leds-is31fl319x.o obj-$(CONFIG_LEDS_IS31FL32XX) += leds-is31fl32xx.o +obj-$(CONFIG_LEDS_LENOVO_SE10) += leds_lenovo_se10.o obj-$(CONFIG_LEDS_LM3530) += leds-lm3530.o obj-$(CONFIG_LEDS_LM3532) += leds-lm3532.o obj-$(CONFIG_LEDS_LM3533) += leds-lm3533.o diff --git a/drivers/leds/leds_lenovo_se10.rs b/drivers/leds/leds_lenovo_se10.rs new file mode 100644 index 000000000000..d704125610a4 --- /dev/null +++ b/drivers/leds/leds_lenovo_se10.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +//! LED driver for Lenovo ThinkEdge SE10. + +use kernel::ioport::{Region, ResourceSize}; +#[cfg(CONFIG_LEDS_TRIGGERS)] +use kernel::leds::triggers; +use kernel::leds::{Led, LedConfig, Operations}; +use kernel::prelude::*; +use kernel::time::Delta; +use kernel::{c_str, dmi_device_table}; + +module! { + type: SE10, + name: "leds_lenovo_se10", + author: "Fiona Behrens ", + description: "LED driver for Lenovo ThinkEdge SE10", + license: "GPL", +} + +dmi_device_table!(5, SE10_DMI_TABLE, [ + "LENOVO-SE10": [SysVendor: "LENOVO", ProductName: "12NH"], + "LENOVO-SE10": [SysVendor: "LENOVO", ProductName: "12NJ"], + "LENOVO-SE10": [SysVendor: "LENOVO", ProductName: "12NK"], + "LENOVO-SE10": [SysVendor: "LENOVO", ProductName: "12NL"], + "LENOVO-SE10": [SysVendor: "LENOVO", ProductName: "12NM"], +]); + +struct SE10 { + /// Led registration + _led: Pin>>, +} + +impl kernel::Module for SE10 { + fn init(_module: &'static ThisModule) -> Result { + if SE10_DMI_TABLE.check_system().is_none() { + return Err(ENODEV); + } + + let led = KBox::try_pin_init( + Led::register( + None, + LedConfig { + name: Some(c_str!("platform:red:user")), + #[cfg(CONFIG_LEDS_TRIGGERS)] + hardware_trigger: Some(kernel::sync::Arc::pin_init( + triggers::Hardware::register(c_str!("wwan")), + GFP_KERNEL, + )?), + ..LedConfig::new(kernel::leds::Color::Red, LedSE10) + }, + ), + GFP_KERNEL, + )?; + + Ok(Self { _led: led }) + } +} + +/// Valid led commands. +#[repr(u8)] +#[allow(missing_docs)] +enum LedCommand { + #[cfg(CONFIG_LEDS_TRIGGERS)] + Trigger = 0xB2, + Off = 0xB3, + On = 0xB4, + Blink = 0xB5, +} + +struct LedSE10; + +impl LedSE10 { + /// Base address of the command port. + const CMD_PORT: ResourceSize = 0x6C; + /// Length of the command port. + const CMD_LEN: ResourceSize = 1; + /// Blink duration the hardware supports. + const HW_DURATION: Delta = Delta::from_millis(1000); + + /// Request led region. + fn request_cmd_region(&self) -> Result> { + Region::request_muxed(Self::CMD_PORT, Self::CMD_LEN, c_str!("leds_lenovo_se10")) + .ok_or(EBUSY) + } + + /// Send command. + fn send_cmd(&self, cmd: LedCommand) -> Result { + let region = self.request_cmd_region()?; + region.outb(cmd as u8, 0); + Ok(()) + } +} + +#[vtable] +impl Operations for LedSE10 { + type This = Led; + + const MAX_BRIGHTNESS: u8 = 1; + + fn brightness_set(this: &mut Self::This, brightness: u8) { + if let Err(e) = if brightness == 0 { + this.data.send_cmd(LedCommand::Off) + } else { + this.data.send_cmd(LedCommand::On) + } { + pr_warn!("Failed to set led: {e:?}\n)") + } + } + + fn blink_set( + this: &mut Self::This, + delay_on: Delta, + delay_off: Delta, + ) -> Result<(Delta, Delta)> { + if !(delay_on.is_zero() && delay_off.is_zero() + || delay_on == Self::HW_DURATION && delay_off == Self::HW_DURATION) + { + return Err(EINVAL); + } + + this.data.send_cmd(LedCommand::Blink)?; + Ok((Self::HW_DURATION, Self::HW_DURATION)) + } +} + +#[vtable] +#[cfg(CONFIG_LEDS_TRIGGERS)] +impl triggers::HardwareOperations for LedSE10 { + fn activate(this: &mut Self::This) -> Result { + this.data.send_cmd(LedCommand::Trigger) + } +}