diff --git a/Documentation/devicetree/bindings/pwm/thead,th1520-pwm.yaml b/Documentation/devicetree/bindings/pwm/thead,th1520-pwm.yaml new file mode 100644 index 00000000000000..855aec59ac53c4 --- /dev/null +++ b/Documentation/devicetree/bindings/pwm/thead,th1520-pwm.yaml @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/pwm/thead,th1520-pwm.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: T-HEAD TH1520 PWM controller + +maintainers: + - Michal Wilczynski + +allOf: + - $ref: pwm.yaml# + +properties: + compatible: + const: thead,th1520-pwm + + reg: + maxItems: 1 + + clocks: + items: + - description: SoC PWM clock + + "#pwm-cells": + const: 3 + +required: + - compatible + - reg + - clocks + +unevaluatedProperties: false + +examples: + - | + #include + soc { + #address-cells = <2>; + #size-cells = <2>; + pwm@ffec01c000 { + compatible = "thead,th1520-pwm"; + reg = <0xff 0xec01c000 0x0 0x4000>; + clocks = <&clk CLK_PWM>; + #pwm-cells = <3>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 69511c3b2b76fb..411b8fcaad4c94 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19614,6 +19614,12 @@ F: include/linux/pwm.h F: include/linux/pwm_backlight.h K: pwm_(config|apply_might_sleep|apply_atomic|ops) +PWM SUBSYSTEM BINDINGS [RUST] +M: Michal Wilczynski +S: Maintained +F: rust/helpers/pwm.c +F: rust/kernel/pwm.rs + PXA GPIO DRIVER M: Robert Jarzmik L: linux-gpio@vger.kernel.org @@ -20909,6 +20915,7 @@ F: Documentation/devicetree/bindings/firmware/thead,th1520-aon.yaml F: Documentation/devicetree/bindings/mailbox/thead,th1520-mbox.yaml F: Documentation/devicetree/bindings/net/thead,th1520-gmac.yaml F: Documentation/devicetree/bindings/pinctrl/thead,th1520-pinctrl.yaml +F: Documentation/devicetree/bindings/pwm/thead,th1520-pwm.yaml F: arch/riscv/boot/dts/thead/ F: drivers/clk/thead/clk-th1520-ap.c F: drivers/firmware/thead,th1520-aon.c @@ -20916,6 +20923,7 @@ F: drivers/mailbox/mailbox-th1520.c F: drivers/net/ethernet/stmicro/stmmac/dwmac-thead.c F: drivers/pinctrl/pinctrl-th1520.c F: drivers/pmdomain/thead/ +F: drivers/pwm/pwm_th1520.rs F: include/dt-bindings/clock/thead,th1520-clk-ap.h F: include/dt-bindings/power/thead,th1520-power.h F: include/linux/firmware/thead/thead,th1520-aon.h diff --git a/arch/riscv/boot/dts/thead/th1520-lichee-pi-4a.dts b/arch/riscv/boot/dts/thead/th1520-lichee-pi-4a.dts index 4020c727f09e8e..c58c2085ca92a3 100644 --- a/arch/riscv/boot/dts/thead/th1520-lichee-pi-4a.dts +++ b/arch/riscv/boot/dts/thead/th1520-lichee-pi-4a.dts @@ -28,9 +28,76 @@ chosen { stdout-path = "serial0:115200n8"; }; + + thermal-zones { + cpu-thermal { + polling-delay = <1000>; + polling-delay-passive = <1000>; + thermal-sensors = <&pvt 0>; + + trips { + fan_config0: fan-trip0 { + temperature = <39000>; + hysteresis = <5000>; + type = "active"; + }; + + fan_config1: fan-trip1 { + temperature = <50000>; + hysteresis = <5000>; + type = "active"; + }; + + fan_config2: fan-trip2 { + temperature = <60000>; + hysteresis = <5000>; + type = "active"; + }; + }; + + cooling-maps { + map-active-0 { + cooling-device = <&fan 1 1>; + trip = <&fan_config0>; + }; + + map-active-1 { + cooling-device = <&fan 2 2>; + trip = <&fan_config1>; + }; + + map-active-2 { + cooling-device = <&fan 3 3>; + trip = <&fan_config2>; + }; + }; + }; + }; + + fan: pwm-fan { + pinctrl-names = "default"; + pinctrl-0 = <&fan_pins>; + compatible = "pwm-fan"; + #cooling-cells = <2>; + pwms = <&pwm 1 10000000 0>; + cooling-levels = <0 66 196 255>; + }; + }; &padctrl0_apsys { + fan_pins: fan-0 { + pwm1-pins { + pins = "GPIO3_3"; /* PWM1 */ + function = "pwm"; + bias-disable; + drive-strength = <25>; + input-disable; + input-schmitt-disable; + slew-rate = <0>; + }; + }; + uart0_pins: uart0-0 { tx-pins { pins = "UART0_TXD"; diff --git a/arch/riscv/boot/dts/thead/th1520.dtsi b/arch/riscv/boot/dts/thead/th1520.dtsi index 527336417765d8..faf5c3aaf209b2 100644 --- a/arch/riscv/boot/dts/thead/th1520.dtsi +++ b/arch/riscv/boot/dts/thead/th1520.dtsi @@ -482,6 +482,13 @@ status = "disabled"; }; + pwm: pwm@ffec01c000 { + compatible = "thead,th1520-pwm"; + reg = <0xff 0xec01c000 0x0 0x4000>; + clocks = <&clk CLK_PWM>; + #pwm-cells = <3>; + }; + clk: clock-controller@ffef010000 { compatible = "thead,th1520-clk-ap"; reg = <0xff 0xef010000 0x0 0x1000>; @@ -641,6 +648,17 @@ thead,pad-group = <1>; }; + pvt: pvt@fffff4e000 { + compatible = "moortec,mr75203"; + reg = <0xff 0xfff4e000 0x0 0x80>, + <0xff 0xfff4e080 0x0 0x100>, + <0xff 0xfff4e180 0x0 0x680>, + <0xff 0xfff4e800 0x0 0x600>; + reg-names = "common", "ts", "pd", "vm"; + clocks = <&aonsys_clk>; + #thermal-sensor-cells = <1>; + }; + gpio@fffff52000 { compatible = "snps,dw-apb-gpio"; reg = <0xff 0xfff52000 0x0 0x1000>; diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 4731d5b90d7edc..796fcd8343b7c8 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -684,6 +684,12 @@ config PWM_TEGRA To compile this driver as a module, choose M here: the module will be called pwm-tegra. +config PWM_TH1520_RUST + tristate "TH1520 PWM support (Rust)" + depends on RUST_PWM_ABSTRACTIONS + help + Generic PWM framework driver for TH1520 SoC. + config PWM_TIECAP tristate "ECAP PWM support" depends on ARCH_OMAP2PLUS || ARCH_DAVINCI_DA8XX || ARCH_KEYSTONE || ARCH_K3 || COMPILE_TEST @@ -755,4 +761,12 @@ config PWM_XILINX To compile this driver as a module, choose M here: the module will be called pwm-xilinx. + config RUST_PWM_ABSTRACTIONS + bool "Rust PWM abstractions support" + depends on RUST + depends on PWM=y + help + Adds support needed for PWM drivers written in Rust. It provides a + wrapper around the C pwm core. + endif diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index 539e0def3f82fc..6890f860ada6f1 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -70,3 +70,4 @@ obj-$(CONFIG_PWM_TWL_LED) += pwm-twl-led.o obj-$(CONFIG_PWM_VISCONTI) += pwm-visconti.o obj-$(CONFIG_PWM_VT8500) += pwm-vt8500.o obj-$(CONFIG_PWM_XILINX) += pwm-xilinx.o +obj-$(CONFIG_PWM_TH1520_RUST) += pwm_th1520.o diff --git a/drivers/pwm/pwm_th1520.rs b/drivers/pwm/pwm_th1520.rs new file mode 100644 index 00000000000000..4665e293e8d0bd --- /dev/null +++ b/drivers/pwm/pwm_th1520.rs @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2025 Samsung Electronics Co., Ltd. +// Author: Michal Wilczynski + +//! Rust T-HEAD TH1520 PWM driver +use kernel::{c_str, clk::Clk, device, io::mem::IoMem, of, platform, prelude::*, pwm, time}; + +const MAX_PWM_NUM: u32 = 6; + +const fn th1520_pwm_chn_base(n: u32) -> u32 { + n * 0x20 +} +const fn th1520_pwm_ctrl(n: u32) -> u32 { + th1520_pwm_chn_base(n) + 0x00 +} +const fn th1520_pwm_per(n: u32) -> u32 { + th1520_pwm_chn_base(n) + 0x08 +} +const fn th1520_pwm_fp(n: u32) -> u32 { + th1520_pwm_chn_base(n) + 0x0c +} + +const PWM_START: u32 = 1 << 0; +const PWM_CFG_UPDATE: u32 = 1 << 2; +const PWM_CONTINUOUS_MODE: u32 = 1 << 5; +const PWM_FPOUT: u32 = 1 << 8; +const PWM_INFACTOUT: u32 = 1 << 9; + +struct Th1520PwmChipData { + clk: Clk, + iomem: kernel::devres::Devres>, +} + +impl Th1520PwmChipData { + fn _config( + &self, + hwpwm: u32, + duty_ns: u64, + period_ns: u64, + target_polarity: pwm::Polarity, + ) -> Result { + let regs = self.iomem.try_access().ok_or_else(|| { + pr_err!("PWM-{}: Failed to access I/O memory in _config\n", hwpwm); + EBUSY + })?; + + // Calculate cycle values + let rate_hz_u64 = self.clk.rate().as_hz() as u64; + + if duty_ns > period_ns { + pr_err!( + "PWM-{}: Duty {}ns > period {}ns\n", + hwpwm, + duty_ns, + period_ns + ); + return Err(EINVAL); + } + if period_ns == 0 { + pr_err!("PWM-{}: Period is zero\n", hwpwm); + return Err(EINVAL); + } + + let mut period_cycle = mul_div_u64(period_ns, rate_hz_u64, time::NSEC_PER_SEC as u64); + if period_cycle > u32::MAX as u64 { + period_cycle = u32::MAX as u64; + } + if period_cycle == 0 { + pr_err!( + "PWM-{}: Calculated period_cycle is zero, not allowed by HW\n", + hwpwm + ); + return Err(EINVAL); + } + + let mut duty_cycle = mul_div_u64(duty_ns, rate_hz_u64, time::NSEC_PER_SEC as u64); + if duty_cycle > u32::MAX as u64 { + duty_cycle = u32::MAX as u64; + } + + let mut base_ctrl_val = PWM_INFACTOUT | PWM_CONTINUOUS_MODE; + if target_polarity == pwm::Polarity::Normal { + // FPOUT=1 for Normal + base_ctrl_val |= PWM_FPOUT; + } else { + // Inversed, FPOUT=0 + base_ctrl_val &= !PWM_FPOUT; + } + regs.try_write32(base_ctrl_val, th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _config: Initial CTRL write (polarity, mode): 0x{:x}\n", + hwpwm, + base_ctrl_val + ); + + // Write period and duty registers + regs.try_write32(period_cycle as u32, th1520_pwm_per(hwpwm) as usize)?; + regs.try_write32(duty_cycle as u32, th1520_pwm_fp(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _config: Period_cyc={}, Duty_cyc={}\n", + hwpwm, + period_cycle, + duty_cycle + ); + + // Apply period/duty by toggling CFG_UPDATE from 0 to 1. + // The `base_ctrl_val` (just written to HW) has CFG_UPDATE=0. Now set it. + let ctrl_val_for_update = base_ctrl_val | PWM_CFG_UPDATE; + regs.try_write32(ctrl_val_for_update, th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _config: CTRL write with CFG_UPDATE: 0x{:x}\n", + hwpwm, + ctrl_val_for_update + ); + + Ok(ctrl_val_for_update) + } + + fn _enable(&self, hwpwm: u32, ctrl_val_after_config: u32) -> Result { + let regs = self.iomem.try_access().ok_or_else(|| { + pr_err!("PWM-{}: Failed to access I/O memory in _enable\n", hwpwm); + EBUSY + })?; + + // ctrl_val_after_config already has mode, polarity, and CFG_UPDATE correctly set. + // Now add the START bit. START bit auto-clears. + let ctrl_to_start = ctrl_val_after_config | PWM_START; + regs.try_write32(ctrl_to_start, th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _enable: CTRL write with START: 0x{:x}\n", + hwpwm, + ctrl_to_start + ); + Ok(()) + } + + fn _disable(&self, hwpwm: u32) -> Result<()> { + let regs = self.iomem.try_access().ok_or_else(|| { + pr_err!("PWM-{}: Failed to access I/O memory in _disable\n", hwpwm); + EINVAL + })?; + + let mut ctrl_val = regs.try_read32(th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!("PWM-{}: _disable: Read CTRL: 0x{:x}\n", hwpwm, ctrl_val); + + // Ensure CFG_UPDATE is 0 before updating duty (Limitation #4) + if (ctrl_val & PWM_CFG_UPDATE) != 0 { + ctrl_val &= !PWM_CFG_UPDATE; + regs.try_write32(ctrl_val, th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _disable: Cleared CFG_UPDATE, wrote CTRL: 0x{:x}\n", + hwpwm, + ctrl_val + ); + } + + // Set duty cycle to 0 + regs.try_write32(0, th1520_pwm_fp(hwpwm) as usize)?; + pr_debug!("PWM-{}: _disable: Wrote 0 to DUTY (FP) register\n", hwpwm); + + // Apply the 0% duty by toggling CFG_UPDATE from 0 to 1 + // Use the ctrl_val that has CFG_UPDATE cleared (or was already clear) + ctrl_val |= PWM_CFG_UPDATE; + regs.try_write32(ctrl_val, th1520_pwm_ctrl(hwpwm) as usize)?; + pr_debug!( + "PWM-{}: _disable: Set CFG_UPDATE, wrote CTRL: 0x{:x}\n", + hwpwm, + ctrl_val + ); + + Ok(()) + } +} + +impl pwm::PwmOps for Th1520PwmChipData { + // This driver implements get_state + fn apply( + pwm_chip_ref: &mut pwm::Chip, + pwm_dev: &mut pwm::Device, + target_state: &pwm::State, + ) -> Result { + let data: &Th1520PwmChipData = pwm_chip_ref.get_drvdata().ok_or(EINVAL)?; + let hwpwm = pwm_dev.hwpwm(); + + if !target_state.enabled() { + if pwm_dev.state().enabled() { + data._disable(hwpwm)?; + } + + return Ok(()); + } + + // Configure period, duty, and polarity. + // This function also latches period/duty with CFG_UPDATE. + // It returns the control value that was written with CFG_UPDATE set. + let ctrl_val_after_config = data._config( + hwpwm, + target_state.duty_cycle(), + target_state.period(), + target_state.polarity(), + )?; + + // Enable by setting START bit if it wasn't enabled before this apply call + if !pwm_dev.state().enabled() { + data._enable(hwpwm, ctrl_val_after_config)?; + } + + Ok(()) + } +} + +impl Drop for Th1520PwmChipData { + fn drop(&mut self) { + self.clk.disable_unprepare(); + } +} + +fn mul_div_u64(a: u64, b: u64, c: u64) -> u64 { + if c == 0 { + return 0; + } + a.wrapping_mul(b) / c +} + +static TH1520_PWM_OPS: pwm::PwmOpsVTable = pwm::create_pwm_ops::(); + +struct Th1520PwmPlatformDriver; + +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ::IdInfo, + [(of::DeviceId::new(c_str!("thead,th1520-pwm")), ())] +); + +impl platform::Driver for Th1520PwmPlatformDriver { + type IdInfo = (); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe( + pdev: &platform::Device, + _id_info: Option<&Self::IdInfo>, + ) -> Result>> { + let resource = pdev.resource(0).ok_or(ENODEV)?; + let iomem = pdev.ioremap_resource(&resource)?; + + let clk = Clk::get(pdev.as_ref(), None)?; + + clk.prepare_enable()?; + let driver_data = KBox::new(Th1520PwmChipData { clk, iomem }, GFP_KERNEL)?; + let pwm_chip = pwm::devm_chip_alloc(pdev.as_ref(), MAX_PWM_NUM, 0)?; + + let result = pwm::devm_chip_add(pdev.as_ref(), pwm_chip, &TH1520_PWM_OPS); + if result.is_err() { + pr_err!("Failed to add PWM chip: {:?}\n", result); + return Err(EIO); + } + + pwm_chip.set_drvdata(driver_data); + pr_info!("T-HEAD TH1520 PWM probed correctly\n"); + + Ok(KBox::new(Self, GFP_KERNEL)?.into()) + } +} + +kernel::module_platform_driver! { + type: Th1520PwmPlatformDriver, + name: "pwm_th1520", + author: "Michal Wilczynski", + description: "T-HEAD TH1520 PWM driver", + license: "GPL v2", +} diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h index ab37e1d35c70d5..066bb07ec1396b 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include diff --git a/rust/helpers/helpers.c b/rust/helpers/helpers.c index 1e7c84df725211..efec8fe728d0c6 100644 --- a/rust/helpers/helpers.c +++ b/rust/helpers/helpers.c @@ -25,6 +25,7 @@ #include "platform.c" #include "pci.c" #include "pid_namespace.c" +#include "pwm.c" #include "rbtree.c" #include "rcu.c" #include "refcount.c" diff --git a/rust/helpers/pwm.c b/rust/helpers/pwm.c new file mode 100644 index 00000000000000..d75c588863685d --- /dev/null +++ b/rust/helpers/pwm.c @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2025 Samsung Electronics Co., Ltd. +// Author: Michal Wilczynski + +#include + +struct device *rust_helper_pwmchip_parent(const struct pwm_chip *chip) +{ + return pwmchip_parent(chip); +} + +void *rust_helper_pwmchip_get_drvdata(struct pwm_chip *chip) +{ + return pwmchip_get_drvdata(chip); +} + +void rust_helper_pwmchip_set_drvdata(struct pwm_chip *chip, void *data) +{ + pwmchip_set_drvdata(chip, data); +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index de07aadd1ff5fe..cf6c4392ae4150 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -77,6 +77,8 @@ pub mod security; pub mod seq_file; pub mod sizes; mod static_assert; +#[cfg(CONFIG_RUST_PWM_ABSTRACTIONS)] +pub mod pwm; #[doc(hidden)] pub mod std_vendor; pub mod str; diff --git a/rust/kernel/pwm.rs b/rust/kernel/pwm.rs new file mode 100644 index 00000000000000..357fda46faa99c --- /dev/null +++ b/rust/kernel/pwm.rs @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2025 Samsung Electronics Co., Ltd. +// Author: Michal Wilczynski + +//! PWM (Pulse Width Modulator) abstractions. +//! +//! This module provides safe Rust abstractions for working with the Linux +//! kernel's PWM subsystem, leveraging types generated by `bindgen` +//! from `` and `drivers/pwm/core.c`. + +use crate::{ + bindings, + device::Device as CoreDevice, + error::*, + prelude::*, + str::CStr, + types::{ForeignOwnable, Opaque}, +}; +use core::marker::PhantomData; + +/// PWM polarity. Mirrors `enum pwm_polarity`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Polarity { + /// Normal polarity (duty cycle defines the high period of the signal) + Normal, + /// Inversed polarity (duty cycle defines the low period of the signal) + Inversed, +} + +impl From for Polarity { + fn from(polarity: bindings::pwm_polarity) -> Self { + match polarity { + bindings::pwm_polarity_PWM_POLARITY_NORMAL => Polarity::Normal, + bindings::pwm_polarity_PWM_POLARITY_INVERSED => Polarity::Inversed, + _ => { + pr_warn!( + "Unknown pwm_polarity value {}, defaulting to Normal\n", + polarity + ); + Polarity::Normal + } + } + } +} + +impl From for bindings::pwm_polarity { + fn from(polarity: Polarity) -> Self { + match polarity { + Polarity::Normal => bindings::pwm_polarity_PWM_POLARITY_NORMAL, + Polarity::Inversed => bindings::pwm_polarity_PWM_POLARITY_INVERSED, + } + } +} + +/// Wrapper for board-dependent PWM arguments (`struct pwm_args`). +#[repr(transparent)] +pub struct Args(Opaque); + +impl Args { + /// Creates an `Args` wrapper from the C struct reference. + fn from_c_ref(c_args: &bindings::pwm_args) -> Self { + // SAFETY: Pointer is valid, construct Opaque wrapper. We copy the data. + Args(Opaque::new(*c_args)) + } + + /// Returns the period of the PWM signal in nanoseconds. + pub fn period(&self) -> u64 { + // SAFETY: Reading from the valid pointer obtained by `get()`. + unsafe { (*self.0.get()).period } + } + + /// Returns the polarity of the PWM signal. + pub fn polarity(&self) -> Polarity { + // SAFETY: Reading from the valid pointer obtained by `get()`. + Polarity::from(unsafe { (*self.0.get()).polarity }) + } +} + +/// Wrapper for PWM state (`struct pwm_state`). +#[repr(transparent)] +pub struct State(Opaque); + +impl State { + /// Creates a new zeroed `State`. + pub fn new() -> Self { + State(Opaque::new(bindings::pwm_state::default())) + } + + /// Creates a `State` wrapper around a copy of a C `pwm_state`. + pub(crate) fn from_c(c_state: bindings::pwm_state) -> Self { + State(Opaque::new(c_state)) + } + + /// Creates a `State` wrapper around a reference to a C `pwm_state`. + fn from_c_ref(c_state: &bindings::pwm_state) -> &Self { + // SAFETY: Pointer is valid, lifetime tied to input ref. Cast pointer type. + unsafe { &*(c_state as *const bindings::pwm_state as *const Self) } + } + + /// Gets the period of the PWM signal in nanoseconds. + pub fn period(&self) -> u64 { + unsafe { (*self.0.get()).period } + } + + /// Sets the period of the PWM signal in nanoseconds. + pub fn set_period(&mut self, period_ns: u64) { + unsafe { + (*self.0.get()).period = period_ns; + } + } + + /// Gets the duty cycle of the PWM signal in nanoseconds. + pub fn duty_cycle(&self) -> u64 { + unsafe { (*self.0.get()).duty_cycle } + } + + /// Sets the duty cycle of the PWM signal in nanoseconds. + pub fn set_duty_cycle(&mut self, duty_ns: u64) { + unsafe { + (*self.0.get()).duty_cycle = duty_ns; + } + } + + /// Returns `true` if the PWM signal is enabled. + pub fn enabled(&self) -> bool { + unsafe { (*self.0.get()).enabled } + } + + /// Sets the enabled state of the PWM signal. + pub fn set_enabled(&mut self, enabled: bool) { + unsafe { + (*self.0.get()).enabled = enabled; + } + } + + /// Gets the polarity of the PWM signal. + pub fn polarity(&self) -> Polarity { + Polarity::from(unsafe { (*self.0.get()).polarity }) + } + + /// Sets the polarity of the PWM signal. + pub fn set_polarity(&mut self, polarity: Polarity) { + unsafe { + (*self.0.get()).polarity = polarity.into(); + } + } + + /// Returns `true` if the PWM signal is configured for power usage hint. + pub fn usage_power(&self) -> bool { + unsafe { (*self.0.get()).usage_power } + } + + /// Sets the power usage hint for the PWM signal. + pub fn set_usage_power(&mut self, usage_power: bool) { + unsafe { + (*self.0.get()).usage_power = usage_power; + } + } +} + +/// Wrapper for a PWM device/channel (`struct pwm_device`). +#[repr(transparent)] +pub struct Device(Opaque); + +impl Device { + pub(crate) unsafe fn from_ptr<'a>(ptr: *mut bindings::pwm_device) -> &'a mut Self { + unsafe { &mut *ptr.cast::() } + } + + fn as_ptr(&self) -> *mut bindings::pwm_device { + self.0.get() + } + + /// Gets the hardware PWM index for this device within its chip. + pub fn hwpwm(&self) -> u32 { + unsafe { (*self.as_ptr()).hwpwm } + } + + /// Gets a reference to the parent `Chip` that this device belongs to. + pub fn chip(&self) -> &Chip { + unsafe { Chip::from_ptr((*self.as_ptr()).chip) } + } + + /// Gets the label for this PWM device, if any. + pub fn label(&self) -> Option<&CStr> { + let label_ptr = unsafe { (*self.as_ptr()).label }; + if label_ptr.is_null() { + None + } else { + Some(unsafe { CStr::from_char_ptr(label_ptr) }) + } + } + + /// Gets a copy of the board-dependent arguments for this PWM device. + pub fn args(&self) -> Args { + Args::from_c_ref(unsafe { &(*self.as_ptr()).args }) + } + + /// Gets a copy of the current state of this PWM device. + pub fn state(&self) -> State { + State::from_c(unsafe { (*self.as_ptr()).state }) + } + + /// Returns `true` if the PWM signal is currently enabled based on its state. + pub fn is_enabled(&self) -> bool { + self.state().enabled() + } +} + +/// Wrapper for a PWM chip/controller (`struct pwm_chip`). +#[repr(transparent)] +pub struct Chip(Opaque); + +impl Chip { + /// Creates a `Chip` reference from a raw pointer. (Safety notes apply) + pub(crate) unsafe fn from_ptr<'a>(ptr: *mut bindings::pwm_chip) -> &'a mut Self { + unsafe { &mut *ptr.cast::() } + } + + /// Returns a raw pointer to the underlying `pwm_chip`. + pub(crate) fn as_ptr(&self) -> *mut bindings::pwm_chip { + self.0.get() + } + + /// Gets the number of PWM channels (hardware PWMs) on this chip. + pub fn npwm(&self) -> u32 { + unsafe { (*self.as_ptr()).npwm } + } + + /// Returns `true` if the chip supports atomic operations for configuration. + pub fn is_atomic(&self) -> bool { + unsafe { (*self.as_ptr()).atomic } + } + + /// Returns a reference to the embedded `struct device` abstraction (`CoreDevice`). + pub fn device(&self) -> &CoreDevice { + // SAFETY: `dev` field exists and points to the embedded device. + let dev_ptr = unsafe { &(*self.as_ptr()).dev as *const _ as *mut bindings::device }; + unsafe { &*(dev_ptr as *mut CoreDevice) } + } + + /// Returns a reference to the parent device (`struct device`) of this PWM chip's device. + pub fn parent_device(&self) -> Option<&CoreDevice> { + // SAFETY: Accessing fields via assumed-valid pointer and bindgen layout. + let parent_ptr = unsafe { bindings::pwmchip_parent(self.as_ptr()) }; + if parent_ptr.is_null() { + None + } else { + // SAFETY: Pointer is non-null, assume valid device managed by kernel. + Some(unsafe { &*(parent_ptr as *mut CoreDevice) }) + } + } + + /// Gets the *typed* driver-specific data associated with this chip's embedded device. + pub fn get_drvdata(&self) -> Option<&T> { + let ptr = unsafe { bindings::pwmchip_get_drvdata(self.as_ptr()) }; + if ptr.is_null() { + None + } else { + unsafe { Some(&*(ptr as *const T)) } + } + } + + /// Sets the *typed* driver-specific data associated with this chip's embedded device. + pub fn set_drvdata(&mut self, data: T) { + unsafe { bindings::pwmchip_set_drvdata(self.as_ptr(), data.into_foreign() as _) } + } +} + +/// Allocates a PWM chip structure using device resource management. Mirrors `devm_pwmchip_alloc`. +pub fn devm_chip_alloc<'a>( + parent: &'a CoreDevice, + npwm: u32, + sizeof_priv: usize, +) -> Result<&'a mut Chip> { + // SAFETY: `devm_pwmchip_alloc` called with valid args. Returns valid ptr or ERR_PTR. + let parent_ptr = parent as *const CoreDevice as *mut bindings::device; + let chip_ptr = unsafe { bindings::devm_pwmchip_alloc(parent_ptr, npwm, sizeof_priv) }; + if unsafe { bindings::IS_ERR(chip_ptr as *const core::ffi::c_void) } { + let err = unsafe { bindings::PTR_ERR(chip_ptr as *const core::ffi::c_void) }; + pr_err!("devm_pwmchip_alloc failed: {}\n", err); + Err(Error::from_errno(err as i32)) + } else { + // SAFETY: `chip_ptr` valid, lifetime managed by `devm` tied to `parent`. + Ok(unsafe { &mut *(chip_ptr as *mut Chip) }) + } +} + +/// Registers a PWM chip with the PWM subsystem. Mirrors `__pwmchip_add`. +pub fn chip_add(chip: &mut Chip, ops: &'static PwmOpsVTable) -> Result { + // SAFETY: Pointers are valid. `__pwmchip_add` requires ops to be set. + unsafe { + let chip_ptr = chip.as_ptr(); + // Assign the ops pointer directly to the C struct field + (*chip_ptr).ops = ops.as_ptr(); + to_result(bindings::__pwmchip_add( + chip_ptr, + core::ptr::null_mut() + )) + } +} + +/// Registers a PWM chip using device resource management. Mirrors `__devm_pwmchip_add`. +pub fn devm_chip_add(parent: &CoreDevice, chip: &mut Chip, ops: &'static PwmOpsVTable) -> Result { + // SAFETY: Pointers are valid. `__devm_pwmchip_add` requires ops to be set. + unsafe { + let chip_ptr = chip.as_ptr(); + // Assign the ops pointer directly to the C struct field + (*chip_ptr).ops = ops.as_ptr(); + let parent_ptr = parent as *const CoreDevice as *mut bindings::device; + to_result(bindings::__devm_pwmchip_add( + parent_ptr, + chip_ptr, + core::ptr::null_mut() + )) + } +} + +/// Trait defining the operations for a PWM driver. Mirrors relevant parts of `struct pwm_ops`. +pub trait PwmOps: 'static { + /// Atomically apply a new state to the PWM device. Mirrors `pwm_ops->apply`. + fn apply(chip: &mut Chip, pwm: &mut Device, state: &State) -> Result; + + // TODO: Add other ops like request, free, capture, waveform ops if needed. +} + +/// Holds the vtable for PwmOps implementations. +struct Adapter { + _p: PhantomData, +} + +impl Adapter { + // Trampoline for `apply`. + unsafe extern "C" fn apply_callback( + chip: *mut bindings::pwm_chip, + pwm: *mut bindings::pwm_device, + state: *const bindings::pwm_state, // Input state is const + ) -> core::ffi::c_int { + // SAFETY: Pointers from core are valid. Create temporary wrappers. + let chip_ref = unsafe { Chip::from_ptr(chip) }; + let pwm_ref = unsafe { Device::from_ptr(pwm) }; + // Use the reference wrapper for the const C state + let state_ref = State::from_c_ref(unsafe { &*state }); + + match T::apply(chip_ref, pwm_ref, state_ref) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } + } +} + +/// VTable structure wrapper for PWM operations. Mirrors `struct pwm_ops`. +#[repr(transparent)] +pub struct PwmOpsVTable(Opaque); + +// SAFETY: Holds function pointers, no accessible mutable state via &self. +unsafe impl Sync for PwmOpsVTable {} + +impl PwmOpsVTable { + /// Returns a raw pointer to the underlying `pwm_ops` struct. + pub(crate) fn as_ptr(&self) -> *const bindings::pwm_ops { + self.0.get() + } +} + +/// Creates a PWM operations vtable for a type `T` that implements `PwmOps`. +/// +/// This is used to bridge Rust trait implementations to the C `struct pwm_ops` +/// expected by the kernel. +pub const fn create_pwm_ops() -> PwmOpsVTable { + let mut ops: bindings::pwm_ops = unsafe { core::mem::zeroed() }; + + ops.apply = Some(Adapter::::apply_callback); + + PwmOpsVTable(Opaque::new(ops)) +}