From nobody Sat Oct 4 06:35:18 2025 Received: from mailout1.w1.samsung.com (mailout1.w1.samsung.com [210.118.77.11]) (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 604A72FBE13 for ; Tue, 30 Sep 2025 12:27:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=210.118.77.11 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1759235265; cv=none; b=EHSun7iel8no4VhO9qCZQ50sBwDKQ2PWbf4r459nmbxQtxtE/k27bX29jLzn5U5xZWAMn+gnccDy1TOgwCMp8VqX0fOttSJHo+rUZHlSjHks9ChYfOQQCxA3JlTb+qetsL/L6OnCzqlxG/3ys455iG2PctJgCeT12Wt71Q+5ksk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1759235265; c=relaxed/simple; bh=f6gIX255v28i5uFhjlIpQzZSrsB5Hk6S6Br1oksl624=; h=From:Date:Subject:MIME-Version:Message-Id:In-Reply-To:To:Cc: Content-Type:References; b=kocQa1YhryuDn6fvgxQHEG+MuIAI9+qtOGUC4SZyGpxRZYVWelSG3R9K7NQgZ053S+zRgtoFPdULj8RAUM+barfF6FZ/RFA/4ildg1988GGJZK9KQ3It4PCOD1DE/FGUjjjrL4CbtjwiAx0CgxZaOldcqk4MvrVwr9Aw+iXthJA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=samsung.com; spf=pass smtp.mailfrom=samsung.com; dkim=pass (1024-bit key) header.d=samsung.com header.i=@samsung.com header.b=nrYR+Spk; arc=none smtp.client-ip=210.118.77.11 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=samsung.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=samsung.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=samsung.com header.i=@samsung.com header.b="nrYR+Spk" Received: from eucas1p2.samsung.com (unknown [182.198.249.207]) by mailout1.w1.samsung.com (KnoxPortal) with ESMTP id 20250930122735euoutp01d05050dba985a61621c96867857fcfc0~qDyTW6jTJ0378403784euoutp01l for ; Tue, 30 Sep 2025 12:27:35 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 mailout1.w1.samsung.com 20250930122735euoutp01d05050dba985a61621c96867857fcfc0~qDyTW6jTJ0378403784euoutp01l DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=samsung.com; s=mail20170921; t=1759235255; bh=fhkdeIOTEqxwSfWdKIOYung7kOzWffgMRFEDumAxt9c=; h=From:Date:Subject:In-Reply-To:To:Cc:References:From; b=nrYR+Spkl5SE9tyW7zjOy5WGTIu7rPl9bTCKKgER021xfL42aZUZ0kHWEsxCHYoAc QFEnugVZPH8F+qwfqZIbP1hd8QyENgyxT/25dPdAcaUBgNzRrPU9j72GeM0PipON7f HRrPEWvI75HDfipR87iDrGbDCI/d1BM2nRya8fD4= Received: from eusmtip2.samsung.com (unknown [203.254.199.222]) by eucas1p2.samsung.com (KnoxPortal) with ESMTPA id 20250930122734eucas1p22bfa9a51f4f47fa9b32cf11ea76de2ca~qDySv3mhV2093520935eucas1p2g; Tue, 30 Sep 2025 12:27:34 +0000 (GMT) Received: from AMDC4942.eu.corp.samsungelectronics.net (unknown [106.210.136.40]) by eusmtip2.samsung.com (KnoxPortal) with ESMTPA id 20250930122733eusmtip2cb8c9f03a3f0a02bac8ef0ed07e5aaa8~qDyRqzkei2138921389eusmtip2m; Tue, 30 Sep 2025 12:27:33 +0000 (GMT) From: Michal Wilczynski Date: Tue, 30 Sep 2025 14:20:35 +0200 Subject: [PATCH v15 4/7] pwm: Add Rust driver for T-HEAD TH1520 SoC Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Message-Id: <20250930-rust-next-pwm-working-fan-for-sending-v15-4-5661c3090877@samsung.com> In-Reply-To: <20250930-rust-next-pwm-working-fan-for-sending-v15-0-5661c3090877@samsung.com> To: =?utf-8?q?Uwe_Kleine-K=C3=B6nig?= , Miguel Ojeda , Alex Gaynor , Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Andreas Hindborg , Alice Ryhl , Trevor Gross , Danilo Krummrich , Michal Wilczynski , Guo Ren , Fu Wei , Rob Herring , Krzysztof Kozlowski , Conor Dooley , Paul Walmsley , Palmer Dabbelt , Albert Ou , Alexandre Ghiti , Marek Szyprowski , Benno Lossin , Michael Turquette , Drew Fustini , Daniel Almeida , Benno Lossin , Drew Fustini Cc: linux-kernel@vger.kernel.org, linux-pwm@vger.kernel.org, rust-for-linux@vger.kernel.org, linux-riscv@lists.infradead.org, devicetree@vger.kernel.org, Elle Rhumsaa X-Mailer: b4 0.15-dev X-CMS-MailID: 20250930122734eucas1p22bfa9a51f4f47fa9b32cf11ea76de2ca X-Msg-Generator: CA Content-Type: text/plain; charset="utf-8" X-RootMTR: 20250930122734eucas1p22bfa9a51f4f47fa9b32cf11ea76de2ca X-EPHeader: CA X-CMS-RootMailID: 20250930122734eucas1p22bfa9a51f4f47fa9b32cf11ea76de2ca References: <20250930-rust-next-pwm-working-fan-for-sending-v15-0-5661c3090877@samsung.com> Introduce a PWM driver for the T-HEAD TH1520 SoC, written in Rust and utilizing the safe PWM abstractions from the preceding commit. The driver implements the pwm::PwmOps trait using the modern waveform API (round_waveform_tohw, write_waveform, etc.) to support configuration of period, duty cycle, and polarity for the TH1520's PWM channels. Resource management is handled using idiomatic Rust patterns. The PWM chip object is allocated via pwm::Chip::new and its registration with the PWM core is managed by the pwm::Registration RAII guard. This ensures pwmchip_remove is always called when the driver unbinds, preventing resource leaks. Device managed resources are used for the MMIO region, and the clock lifecycle is correctly managed in the driver's private data Drop implementation. The driver's core logic is written entirely in safe Rust, with no unsafe blocks, except for the Send and Sync implementations for the driver data, which are explained in the comments. Reviewed-by: Elle Rhumsaa Signed-off-by: Michal Wilczynski --- MAINTAINERS | 1 + drivers/pwm/Kconfig | 11 ++ drivers/pwm/Makefile | 1 + drivers/pwm/pwm_th1520.rs | 382 ++++++++++++++++++++++++++++++++++++++++++= ++++ 4 files changed, 395 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 5d7c0676c1d00a02b3d7db2de88b039c08c99c6e..d79dc21f22d143ca8cde6a06194= 545fbc640e695 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -21741,6 +21741,7 @@ F: drivers/net/ethernet/stmicro/stmmac/dwmac-thead.c F: drivers/pinctrl/pinctrl-th1520.c F: drivers/pmdomain/thead/ F: drivers/power/sequencing/pwrseq-thead-gpu.c +F: drivers/pwm/pwm_th1520.rs F: drivers/reset/reset-th1520.c F: include/dt-bindings/clock/thead,th1520-clk-ap.h F: include/dt-bindings/power/thead,th1520-power.h diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 2b608f4378138775ee3ba4d53f682952e1914118..dd6db01832ee985e2e588a413a1= 3df869a029d3d 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -729,6 +729,17 @@ config PWM_TEGRA To compile this driver as a module, choose M here: the module will be called pwm-tegra. =20 +config PWM_TH1520 + tristate "TH1520 PWM support" + depends on RUST + select RUST_PWM_ABSTRACTIONS + help + This option enables the driver for the PWM controller found on the + T-HEAD TH1520 SoC. + + To compile this driver as a module, choose M here; the module + will be called pwm-th1520. If you are unsure, say N. + 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 ff4f47e5fb7a0dbac72c12de82c3773e5582db6d..5c15c95c6e49143969389198657= eed0ecf4086b2 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -67,6 +67,7 @@ obj-$(CONFIG_PWM_STMPE) +=3D pwm-stmpe.o obj-$(CONFIG_PWM_SUN4I) +=3D pwm-sun4i.o obj-$(CONFIG_PWM_SUNPLUS) +=3D pwm-sunplus.o obj-$(CONFIG_PWM_TEGRA) +=3D pwm-tegra.o +obj-$(CONFIG_PWM_TH1520) +=3D pwm_th1520.o obj-$(CONFIG_PWM_TIECAP) +=3D pwm-tiecap.o obj-$(CONFIG_PWM_TIEHRPWM) +=3D pwm-tiehrpwm.o obj-$(CONFIG_PWM_TWL) +=3D pwm-twl.o diff --git a/drivers/pwm/pwm_th1520.rs b/drivers/pwm/pwm_th1520.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9fd1d8d17bcdb20d20b1b48a9b= 207d7d638bcfd --- /dev/null +++ b/drivers/pwm/pwm_th1520.rs @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2025 Samsung Electronics Co., Ltd. +// Author: Michal Wilczynski + +//! Rust T-HEAD TH1520 PWM driver +//! +//! Limitations: +//! - The period and duty cycle are controlled by 32-bit hardware register= s, +//! limiting the maximum resolution. +//! - The driver supports continuous output mode only; one-shot mode is not +//! implemented. +//! - The controller hardware provides up to 6 PWM channels. +//! - Reconfiguration is glitch free - new period and duty cycle values are +//! latched and take effect at the start of the next period. +//! - Polarity is handled via a simple hardware inversion bit; arbitrary +//! duty cycle offsets are not supported. +//! - Disabling a channel is achieved by configuring its duty cycle to zer= o to +//! produce a static low output. Clearing the `start` does not reliably +//! force the static inactive level defined by the `INACTOUT` bit. Hence +//! this method is not used in this driver. +//! + +use core::ops::Deref; +use kernel::{ + c_str, + clk::Clk, + device::{Bound, Core, Device}, + devres, + io::mem::IoMem, + of, platform, + prelude::*, + pwm, time, +}; + +const TH1520_MAX_PWM_NUM: u32 =3D 6; + +// Register offsets +const fn th1520_pwm_chn_base(n: u32) -> usize { + (n * 0x20) as usize +} + +const fn th1520_pwm_ctrl(n: u32) -> usize { + th1520_pwm_chn_base(n) +} + +const fn th1520_pwm_per(n: u32) -> usize { + th1520_pwm_chn_base(n) + 0x08 +} + +const fn th1520_pwm_fp(n: u32) -> usize { + th1520_pwm_chn_base(n) + 0x0c +} + +// Control register bits +const TH1520_PWM_START: u32 =3D 1 << 0; +const TH1520_PWM_CFG_UPDATE: u32 =3D 1 << 2; +const TH1520_PWM_CONTINUOUS_MODE: u32 =3D 1 << 5; +const TH1520_PWM_FPOUT: u32 =3D 1 << 8; + +const TH1520_PWM_REG_SIZE: usize =3D 0xB0; + +fn ns_to_cycles(ns: u64, rate_hz: u64) -> u64 { + const NSEC_PER_SEC_U64: u64 =3D time::NSEC_PER_SEC as u64; + + (match ns.checked_mul(rate_hz) { + Some(product) =3D> product, + None =3D> u64::MAX, + }) / NSEC_PER_SEC_U64 +} + +fn cycles_to_ns(cycles: u64, rate_hz: u64) -> u64 { + const NSEC_PER_SEC_U64: u64 =3D time::NSEC_PER_SEC as u64; + + // TODO: Replace with a kernel helper like `mul_u64_u64_div_u64_roundu= p` + // once available in Rust. + let numerator =3D cycles + .saturating_mul(NSEC_PER_SEC_U64) + .saturating_add(rate_hz - 1); + + numerator / rate_hz +} + +/// Hardware-specific waveform representation for TH1520. +#[derive(Copy, Clone, Debug, Default)] +struct Th1520WfHw { + period_cycles: u32, + duty_cycles: u32, + ctrl_val: u32, + enabled: bool, +} + +/// The driver's private data struct. It holds all necessary devres manage= d resources. +#[pin_data(PinnedDrop)] +struct Th1520PwmDriverData { + #[pin] + iomem: devres::Devres>, + clk: Clk, +} + +// This `unsafe` implementation is a temporary necessity because the under= lying `kernel::clk::Clk` +// type does not yet expose `Send` and `Sync` implementations. This block = should be removed +// as soon as the clock abstraction provides these guarantees directly. +// TODO: Remove those unsafe impl's when Clk will support them itself. + +// SAFETY: The `devres` framework requires the driver's private data to be= `Send` and `Sync`. +// We can guarantee this because the PWM core synchronizes all callbacks, = preventing concurrent +// access to the contained `iomem` and `clk` resources. +unsafe impl Send for Th1520PwmDriverData {} + +// SAFETY: The same reasoning applies as for `Send`. The PWM core's synchr= onization +// guarantees that it is safe for multiple threads to have shared access (= `&self`) +// to the driver data during callbacks. +unsafe impl Sync for Th1520PwmDriverData {} + +impl pwm::PwmOps for Th1520PwmDriverData { + type WfHw =3D Th1520WfHw; + + fn round_waveform_tohw( + chip: &pwm::Chip, + _pwm: &pwm::Device, + wf: &pwm::Waveform, + ) -> Result> { + let data =3D chip.drvdata(); + + if wf.period_length_ns =3D=3D 0 { + dev_dbg!(chip.device(), "Requested period is 0, disabling PWM.= \n"); + + return Ok(pwm::RoundedWaveform { + status: 0, + hardware_waveform: Th1520WfHw { + enabled: false, + ..Default::default() + }, + }); + } + + let rate_hz =3D data.clk.rate().as_hz() as u64; + + let period_cycles =3D ns_to_cycles(wf.period_length_ns, rate_hz).m= in(u64::from(u32::MAX)); + + if period_cycles =3D=3D 0 { + dev_dbg!( + chip.device(), + "Requested period {} ns is too small for clock rate {} Hz,= disabling PWM.\n", + wf.period_length_ns, + rate_hz + ); + + return Ok(pwm::RoundedWaveform { + status: 0, + hardware_waveform: Th1520WfHw { + enabled: false, + ..Default::default() + }, + }); + } + + let mut duty_cycles =3D ns_to_cycles(wf.duty_length_ns, rate_hz).m= in(u64::from(u32::MAX)); + + let mut ctrl_val =3D TH1520_PWM_CONTINUOUS_MODE; + + let is_inversed =3D wf.duty_length_ns > 0 + && wf.duty_offset_ns > 0 + && wf.duty_offset_ns >=3D wf.period_length_ns.saturating_sub(w= f.duty_length_ns); + if is_inversed { + duty_cycles =3D period_cycles - duty_cycles; + } else { + ctrl_val |=3D TH1520_PWM_FPOUT; + } + + let wfhw =3D Th1520WfHw { + // The cast is safe because the value was clamped with `.min(u= 64::from(u32::MAX))`. + period_cycles: period_cycles as u32, + duty_cycles: duty_cycles as u32, + ctrl_val, + enabled: true, + }; + + dev_dbg!( + chip.device(), + "Requested: {}/{} ns [+{} ns] -> HW: {}/{} cycles, ctrl 0x{:x}= , rate {} Hz\n", + wf.duty_length_ns, + wf.period_length_ns, + wf.duty_offset_ns, + wfhw.duty_cycles, + wfhw.period_cycles, + wfhw.ctrl_val, + rate_hz + ); + + Ok(pwm::RoundedWaveform { + status: 0, + hardware_waveform: wfhw, + }) + } + + fn round_waveform_fromhw( + chip: &pwm::Chip, + _pwm: &pwm::Device, + wfhw: &Self::WfHw, + wf: &mut pwm::Waveform, + ) -> Result { + let data =3D chip.drvdata(); + let rate_hz =3D data.clk.rate().as_hz() as u64; + + if wfhw.period_cycles =3D=3D 0 { + dev_dbg!(chip.device(), "HW state has zero period, reporting a= s disabled.\n"); + *wf =3D pwm::Waveform::default(); + return Ok(()); + } + + wf.period_length_ns =3D cycles_to_ns(u64::from(wfhw.period_cycles)= , rate_hz); + + let duty_cycles =3D u64::from(wfhw.duty_cycles); + + if (wfhw.ctrl_val & TH1520_PWM_FPOUT) !=3D 0 { + wf.duty_length_ns =3D cycles_to_ns(duty_cycles, rate_hz); + wf.duty_offset_ns =3D 0; + } else { + let period_cycles =3D u64::from(wfhw.period_cycles); + let original_duty_cycles =3D period_cycles.saturating_sub(duty= _cycles); + + // For an inverted signal, `duty_length_ns` is the high time (= period - low_time). + wf.duty_length_ns =3D cycles_to_ns(original_duty_cycles, rate_= hz); + // The offset is the initial low time, which is what the hardw= are register provides. + wf.duty_offset_ns =3D cycles_to_ns(duty_cycles, rate_hz); + } + + Ok(()) + } + + fn read_waveform( + chip: &pwm::Chip, + pwm: &pwm::Device, + parent_dev: &Device, + ) -> Result { + let data =3D chip.drvdata(); + let hwpwm =3D pwm.hwpwm(); + let iomem_accessor =3D data.iomem.access(parent_dev)?; + let iomap =3D iomem_accessor.deref(); + + let ctrl =3D iomap.try_read32(th1520_pwm_ctrl(hwpwm))?; + let period_cycles =3D iomap.try_read32(th1520_pwm_per(hwpwm))?; + let duty_cycles =3D iomap.try_read32(th1520_pwm_fp(hwpwm))?; + + let wfhw =3D Th1520WfHw { + period_cycles, + duty_cycles, + ctrl_val: ctrl, + enabled: duty_cycles !=3D 0, + }; + + dev_dbg!( + chip.device(), + "PWM-{}: read_waveform: Read hw state - period: {}, duty: {}, = ctrl: 0x{:x}, enabled: {}", + hwpwm, + wfhw.period_cycles, + wfhw.duty_cycles, + wfhw.ctrl_val, + wfhw.enabled + ); + + Ok(wfhw) + } + + fn write_waveform( + chip: &pwm::Chip, + pwm: &pwm::Device, + wfhw: &Self::WfHw, + parent_dev: &Device, + ) -> Result { + let data =3D chip.drvdata(); + let hwpwm =3D pwm.hwpwm(); + let iomem_accessor =3D data.iomem.access(parent_dev)?; + let iomap =3D iomem_accessor.deref(); + let duty_cycles =3D iomap.try_read32(th1520_pwm_fp(hwpwm))?; + let was_enabled =3D duty_cycles !=3D 0; + + if !wfhw.enabled { + dev_dbg!(chip.device(), "PWM-{}: Disabling channel.\n", hwpwm); + if was_enabled { + iomap.try_write32(wfhw.ctrl_val, th1520_pwm_ctrl(hwpwm))?; + iomap.try_write32(0, th1520_pwm_fp(hwpwm))?; + iomap.try_write32(wfhw.ctrl_val | TH1520_PWM_CFG_UPDATE, t= h1520_pwm_ctrl(hwpwm))?; + } + return Ok(()); + } + + iomap.try_write32(wfhw.ctrl_val, th1520_pwm_ctrl(hwpwm))?; + iomap.try_write32(wfhw.period_cycles, th1520_pwm_per(hwpwm))?; + iomap.try_write32(wfhw.duty_cycles, th1520_pwm_fp(hwpwm))?; + iomap.try_write32(wfhw.ctrl_val | TH1520_PWM_CFG_UPDATE, th1520_pw= m_ctrl(hwpwm))?; + + // The `TH1520_PWM_START` bit must be written in a separate, final= transaction, and + // only when enabling the channel from a disabled state. + if !was_enabled { + iomap.try_write32(wfhw.ctrl_val | TH1520_PWM_START, th1520_pwm= _ctrl(hwpwm))?; + } + + dev_dbg!( + chip.device(), + "PWM-{}: Wrote {}/{} cycles", + hwpwm, + wfhw.duty_cycles, + wfhw.period_cycles, + ); + + Ok(()) + } +} + +#[pinned_drop] +impl PinnedDrop for Th1520PwmDriverData { + fn drop(self: Pin<&mut Self>) { + self.clk.disable_unprepare(); + } +} + +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 =3D (); + const OF_ID_TABLE: Option> =3D Some(&OF_TABL= E); + + fn probe( + pdev: &platform::Device, + _id_info: Option<&Self::IdInfo>, + ) -> Result>> { + let dev =3D pdev.as_ref(); + let request =3D pdev.io_request_by_index(0).ok_or(ENODEV)?; + + let clk =3D Clk::get(dev, None)?; + + clk.prepare_enable()?; + + // TODO: Get exclusive ownership of the clock to prevent rate chan= ges. + // The Rust equivalent of `clk_rate_exclusive_get()` is not yet av= ailable. + // This should be updated once it is implemented. + let rate_hz =3D clk.rate().as_hz(); + if rate_hz =3D=3D 0 { + dev_err!(dev, "Clock rate is zero\n"); + return Err(EINVAL); + } + + if rate_hz > time::NSEC_PER_SEC as usize { + dev_err!( + dev, + "Clock rate {} Hz is too high, not supported.\n", + rate_hz + ); + return Err(ERANGE); + } + + let chip =3D pwm::Chip::new( + dev, + TH1520_MAX_PWM_NUM, + try_pin_init!(Th1520PwmDriverData { + iomem <- request.iomap_sized::(), + clk <- clk, + }), + )?; + + pwm::Registration::register(dev, chip)?; + + Ok(KBox::new(Th1520PwmPlatformDriver, GFP_KERNEL)?.into()) + } +} + +kernel::module_platform_driver! { + type: Th1520PwmPlatformDriver, + name: "pwm-th1520", + authors: ["Michal Wilczynski "], + description: "T-HEAD TH1520 PWM driver", + license: "GPL v2", +} --=20 2.34.1