From 8f1f099e7eaacc77675b71ffae00e0d87e832859 Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:14:55 +0200 Subject: [PATCH 1/6] rust: Add basic PWM abstractions Introduce initial Rust abstractions for the Linux PWM subsystem. These abstractions provide safe wrappers around the core C data structures and functions, enabling the development of PWM chip drivers in Rust. The main components added are: - A Kconfig option RUST_PWM_ABSTRACTIONS - C helper functions in rust/helpers/pwm.c to provide stable callable interfaces for Rust, for pwmchip_parent, pwmchip_get_drvdata, and pwmchip_set_drvdata - A new Rust module rust/kernel/pwm.rs containing: - Safe wrappers for struct pwm_chip, struct pwm_device, struct pwm_state, and struct pwm_args - An enum Polarity for type safe polarity handling - Functions devm_chip_alloc and devm_chip_add which wrap the kernel's device-managed APIs for PWM chip allocation and registration. - A PwmOps trait and create_pwm_ops function to allow Rust drivers to define their PWM operations, initially supporting the .apply callback. This foundational layer will be used by subsequent patches to implement a specific PWM chip driver in Rust. It focuses on the pwm_chip provider APIs necessary for such a driver. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- MAINTAINERS | 6 + drivers/pwm/Kconfig | 8 + rust/bindings/bindings_helper.h | 1 + rust/helpers/helpers.c | 1 + rust/helpers/pwm.c | 20 ++ rust/kernel/lib.rs | 2 + rust/kernel/pwm.rs | 376 ++++++++++++++++++++++++++++++++ 7 files changed, 414 insertions(+) create mode 100644 rust/helpers/pwm.c create mode 100644 rust/kernel/pwm.rs diff --git a/MAINTAINERS b/MAINTAINERS index 69511c3b2b76fb..0fc4fc1dab29a7 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 diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 4731d5b90d7edc..b5bd5c13b3a5e5 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -755,4 +755,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/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)) +} From ba379044deead4ccf078e1ed45f70f15877a6856 Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:14:56 +0200 Subject: [PATCH 2/6] pwm: Add Rust driver for T-HEAD TH1520 SoC Introduce a PWM driver for the T-HEAD TH1520 SoC written in Rust. It utilizes the Rust PWM abstractions added in the previous commit. The driver implements the standard PwmOps for the PWM framework, supporting configuration of period, duty cycle, and polarity for the TH1520's PWM channels. It uses devm managed resources for the PWM chip itself and Rust DevRes for I/O memory. Clock management is handled using Rust's RAII pattern. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- MAINTAINERS | 1 + drivers/pwm/Kconfig | 6 + drivers/pwm/Makefile | 1 + drivers/pwm/pwm_th1520.rs | 272 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 drivers/pwm/pwm_th1520.rs diff --git a/MAINTAINERS b/MAINTAINERS index 0fc4fc1dab29a7..2ebd7f7ffe2311 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20922,6 +20922,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/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index b5bd5c13b3a5e5..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 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", +} From e8194646c9fbb510fd0d1d46bec3e0a27843bb35 Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:14:57 +0200 Subject: [PATCH 3/6] dt-bindings: pwm: thead: Add T-HEAD TH1520 PWM controller Add the Device Tree binding documentation for the T-HEAD TH1520 SoC PWM controller. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- .../bindings/pwm/thead,th1520-pwm.yaml | 48 +++++++++++++++++++ MAINTAINERS | 1 + 2 files changed, 49 insertions(+) create mode 100644 Documentation/devicetree/bindings/pwm/thead,th1520-pwm.yaml 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 2ebd7f7ffe2311..411b8fcaad4c94 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20915,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 From 6ebc53e40e75a4ea61ba374d3dfa92a551f70ff1 Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:14:58 +0200 Subject: [PATCH 4/6] riscv: dts: thead: Add PWM controller node Add the Device Tree node for the T-HEAD TH1520 SoC's PWM controller. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- arch/riscv/boot/dts/thead/th1520.dtsi | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/arch/riscv/boot/dts/thead/th1520.dtsi b/arch/riscv/boot/dts/thead/th1520.dtsi index 527336417765d8..f24e12d7259fab 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>; From 27864b527dd882457e74a5222348f4d264266c8e Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:14:59 +0200 Subject: [PATCH 5/6] riscv: dts: thead: Add PVT node Add PVT DT node for thermal sensor. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- arch/riscv/boot/dts/thead/th1520.dtsi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/arch/riscv/boot/dts/thead/th1520.dtsi b/arch/riscv/boot/dts/thead/th1520.dtsi index f24e12d7259fab..faf5c3aaf209b2 100644 --- a/arch/riscv/boot/dts/thead/th1520.dtsi +++ b/arch/riscv/boot/dts/thead/th1520.dtsi @@ -648,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>; From 16d8947d9d8ced2a809542246ba251d9b2026f41 Mon Sep 17 00:00:00 2001 From: Michal Wilczynski Date: Sat, 24 May 2025 23:15:00 +0200 Subject: [PATCH 6/6] riscv: dts: thead: Add PWM fan and thermal control Add Device Tree nodes to enable a PWM controlled fan and it's associated thermal management for the Lichee Pi 4A board. This enables temperature-controlled active cooling for the Lichee Pi 4A board based on SoC temperature. Signed-off-by: Michal Wilczynski Signed-off-by: Linux RISC-V bot --- .../boot/dts/thead/th1520-lichee-pi-4a.dts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) 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";