mirror of
https://github.com/torvalds/linux.git
synced 2025-12-07 20:06:24 +00:00
Pull driver core updates from Danilo Krummrich:
"Arch Topology:
- Move parse_acpi_topology() from arm64 to common code for reuse in
RISC-V
CPU:
- Expose housekeeping CPUs through /sys/devices/system/cpu/housekeeping
- Print a newline (or 0x0A) instead of '(null)' reading
/sys/devices/system/cpu/nohz_full when nohz_full= is not set
debugfs
- Remove (broken) 'no-mount' mode
- Remove redundant access mode checks in debugfs_get_tree() and
debugfs_create_*() functions
Devres:
- Remove unused devm_free_percpu() helper
- Move devm_alloc_percpu() from device.h to devres.h
Firmware Loader:
- Replace simple_strtol() with kstrtoint()
- Do not call cancel_store() when no upload is in progress
kernfs:
- Increase struct super_block::maxbytes to MAX_LFS_FILESIZE
- Fix a missing unwind path in __kernfs_new_node()
Misc:
- Increase the name size in struct auxiliary_device_id to 40
characters
- Replace system_unbound_wq with system_dfl_wq and add WQ_PERCPU to
alloc_workqueue()
Platform:
- Replace ERR_PTR() with IOMEM_ERR_PTR() in platform ioremap
functions
Rust:
- Auxiliary:
- Unregister auxiliary device on parent device unbind
- Move parent() to impl Device; implement device context aware
parent() for Device<Bound>
- Illustrate how to safely obtain a driver's device private data
when calling from an auxiliary driver into the parant device
driver
- DebugFs:
- Implement support for binary large objects
- Device:
- Let probe() return the driver's device private data as pinned
initializer, i.e. impl PinInit<Self, Error>
- Implement safe accessor for a driver's device private data for
Device<Bound> (returned reference can't out-live driver binding
and guarantees the correct private data type)
- Implement AsBusDevice trait, to be used by class device
abstractions to derive the bus device type of the parent device
- DMA:
- Store raw pointer of allocation as NonNull
- Use start_ptr() and start_ptr_mut() to inherit correct
mutability of self
- FS:
- Add file::Offset type alias
- I2C:
- Add abstractions for I2C device / driver infrastructure
- Implement abstractions for manual I2C device registrations
- I/O:
- Use "kernel vertical" style for imports
- Define ResourceSize as resource_size_t
- Move ResourceSize to top-level I/O module
- Add type alias for phys_addr_t
- Implement Rust version of read_poll_timeout_atomic()
- PCI:
- Use "kernel vertical" style for imports
- Move I/O and IRQ infrastructure to separate files
- Add support for PCI interrupt vectors
- Implement TryInto<IrqRequest<'a>> for IrqVector<'a> to convert
an IrqVector bound to specific pci::Device into an IrqRequest
bound to the same pci::Device's parent Device
- Leverage pin_init_scope() to get rid of redundant Result in IRQ
methods
- PinInit:
- Add {pin_}init_scope() to execute code before creating an
initializer
- Platform:
- Leverage pin_init_scope() to get rid of redundant Result in IRQ
methods
- Timekeeping:
- Implement abstraction of udelay()
- Uaccess:
- Implement read_slice_partial() and read_slice_file() for
UserSliceReader
- Implement write_slice_partial() and write_slice_file() for
UserSliceWriter
sysfs:
- Prepare the constification of struct attribute"
* tag 'driver-core-6.19-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/driver-core/driver-core: (75 commits)
rust: pci: fix build failure when CONFIG_PCI_MSI is disabled
debugfs: Fix default access mode config check
debugfs: Remove broken no-mount mode
debugfs: Remove redundant access mode checks
driver core: Check drivers_autoprobe for all added devices
driver core: WQ_PERCPU added to alloc_workqueue users
driver core: replace use of system_unbound_wq with system_dfl_wq
tick/nohz: Expose housekeeping CPUs in sysfs
tick/nohz: avoid showing '(null)' if nohz_full= not set
sysfs/cpu: Use DEVICE_ATTR_RO for nohz_full attribute
kernfs: fix memory leak of kernfs_iattrs in __kernfs_new_node
fs/kernfs: raise sb->maxbytes to MAX_LFS_FILESIZE
mod_devicetable: Bump auxiliary_device_id name size
sysfs: simplify attribute definition macros
samples/kobject: constify 'struct foo_attribute'
samples/kobject: add is_visible() callback to attribute group
sysfs: attribute_group: enable const variants of is_visible()
sysfs: introduce __SYSFS_FUNCTION_ALTERNATIVE()
sysfs: transparently handle const pointers in ATTRIBUTE_GROUPS()
sysfs: attribute_group: allow registration of const attribute
...
388 lines
12 KiB
Rust
388 lines
12 KiB
Rust
// SPDX-License-Identifier: GPL-2.0
|
|
// Copyright (c) 2025 Samsung Electronics Co., Ltd.
|
|
// Author: Michal Wilczynski <m.wilczynski@samsung.com>
|
|
|
|
//! Rust T-HEAD TH1520 PWM driver
|
|
//!
|
|
//! Limitations:
|
|
//! - The period and duty cycle are controlled by 32-bit hardware registers,
|
|
//! 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 zero 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 = 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 = 1 << 0;
|
|
const TH1520_PWM_CFG_UPDATE: u32 = 1 << 2;
|
|
const TH1520_PWM_CONTINUOUS_MODE: u32 = 1 << 5;
|
|
const TH1520_PWM_FPOUT: u32 = 1 << 8;
|
|
|
|
const TH1520_PWM_REG_SIZE: usize = 0xB0;
|
|
|
|
fn ns_to_cycles(ns: u64, rate_hz: u64) -> u64 {
|
|
const NSEC_PER_SEC_U64: u64 = time::NSEC_PER_SEC as u64;
|
|
|
|
(match ns.checked_mul(rate_hz) {
|
|
Some(product) => product,
|
|
None => u64::MAX,
|
|
}) / NSEC_PER_SEC_U64
|
|
}
|
|
|
|
fn cycles_to_ns(cycles: u64, rate_hz: u64) -> u64 {
|
|
const NSEC_PER_SEC_U64: u64 = time::NSEC_PER_SEC as u64;
|
|
|
|
// TODO: Replace with a kernel helper like `mul_u64_u64_div_u64_roundup`
|
|
// once available in Rust.
|
|
let numerator = 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 managed resources.
|
|
#[pin_data(PinnedDrop)]
|
|
struct Th1520PwmDriverData {
|
|
#[pin]
|
|
iomem: devres::Devres<IoMem<TH1520_PWM_REG_SIZE>>,
|
|
clk: Clk,
|
|
}
|
|
|
|
// This `unsafe` implementation is a temporary necessity because the underlying `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 synchronization
|
|
// 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 = Th1520WfHw;
|
|
|
|
fn round_waveform_tohw(
|
|
chip: &pwm::Chip<Self>,
|
|
_pwm: &pwm::Device,
|
|
wf: &pwm::Waveform,
|
|
) -> Result<pwm::RoundedWaveform<Self::WfHw>> {
|
|
let data = chip.drvdata();
|
|
let mut status = 0;
|
|
|
|
if wf.period_length_ns == 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 = data.clk.rate().as_hz() as u64;
|
|
|
|
let mut period_cycles = ns_to_cycles(wf.period_length_ns, rate_hz).min(u64::from(u32::MAX));
|
|
|
|
if period_cycles == 0 {
|
|
dev_dbg!(
|
|
chip.device(),
|
|
"Requested period {} ns is too small for clock rate {} Hz, rounding up.\n",
|
|
wf.period_length_ns,
|
|
rate_hz
|
|
);
|
|
|
|
period_cycles = 1;
|
|
status = 1;
|
|
}
|
|
|
|
let mut duty_cycles = ns_to_cycles(wf.duty_length_ns, rate_hz).min(u64::from(u32::MAX));
|
|
|
|
let mut ctrl_val = TH1520_PWM_CONTINUOUS_MODE;
|
|
|
|
let is_inversed = wf.duty_length_ns > 0
|
|
&& wf.duty_offset_ns > 0
|
|
&& wf.duty_offset_ns >= wf.period_length_ns.saturating_sub(wf.duty_length_ns);
|
|
if is_inversed {
|
|
duty_cycles = period_cycles - duty_cycles;
|
|
} else {
|
|
ctrl_val |= TH1520_PWM_FPOUT;
|
|
}
|
|
|
|
let wfhw = Th1520WfHw {
|
|
// The cast is safe because the value was clamped with `.min(u64::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,
|
|
hardware_waveform: wfhw,
|
|
})
|
|
}
|
|
|
|
fn round_waveform_fromhw(
|
|
chip: &pwm::Chip<Self>,
|
|
_pwm: &pwm::Device,
|
|
wfhw: &Self::WfHw,
|
|
wf: &mut pwm::Waveform,
|
|
) -> Result {
|
|
let data = chip.drvdata();
|
|
let rate_hz = data.clk.rate().as_hz() as u64;
|
|
|
|
if wfhw.period_cycles == 0 {
|
|
dev_dbg!(
|
|
chip.device(),
|
|
"HW state has zero period, reporting as disabled.\n"
|
|
);
|
|
*wf = pwm::Waveform::default();
|
|
return Ok(());
|
|
}
|
|
|
|
wf.period_length_ns = cycles_to_ns(u64::from(wfhw.period_cycles), rate_hz);
|
|
|
|
let duty_cycles = u64::from(wfhw.duty_cycles);
|
|
|
|
if (wfhw.ctrl_val & TH1520_PWM_FPOUT) != 0 {
|
|
wf.duty_length_ns = cycles_to_ns(duty_cycles, rate_hz);
|
|
wf.duty_offset_ns = 0;
|
|
} else {
|
|
let period_cycles = u64::from(wfhw.period_cycles);
|
|
let original_duty_cycles = period_cycles.saturating_sub(duty_cycles);
|
|
|
|
// For an inverted signal, `duty_length_ns` is the high time (period - low_time).
|
|
wf.duty_length_ns = cycles_to_ns(original_duty_cycles, rate_hz);
|
|
// The offset is the initial low time, which is what the hardware register provides.
|
|
wf.duty_offset_ns = cycles_to_ns(duty_cycles, rate_hz);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn read_waveform(
|
|
chip: &pwm::Chip<Self>,
|
|
pwm: &pwm::Device,
|
|
parent_dev: &Device<Bound>,
|
|
) -> Result<Self::WfHw> {
|
|
let data = chip.drvdata();
|
|
let hwpwm = pwm.hwpwm();
|
|
let iomem_accessor = data.iomem.access(parent_dev)?;
|
|
let iomap = iomem_accessor.deref();
|
|
|
|
let ctrl = iomap.try_read32(th1520_pwm_ctrl(hwpwm))?;
|
|
let period_cycles = iomap.try_read32(th1520_pwm_per(hwpwm))?;
|
|
let duty_cycles = iomap.try_read32(th1520_pwm_fp(hwpwm))?;
|
|
|
|
let wfhw = Th1520WfHw {
|
|
period_cycles,
|
|
duty_cycles,
|
|
ctrl_val: ctrl,
|
|
enabled: duty_cycles != 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<Self>,
|
|
pwm: &pwm::Device,
|
|
wfhw: &Self::WfHw,
|
|
parent_dev: &Device<Bound>,
|
|
) -> Result {
|
|
let data = chip.drvdata();
|
|
let hwpwm = pwm.hwpwm();
|
|
let iomem_accessor = data.iomem.access(parent_dev)?;
|
|
let iomap = iomem_accessor.deref();
|
|
let duty_cycles = iomap.try_read32(th1520_pwm_fp(hwpwm))?;
|
|
let was_enabled = duty_cycles != 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,
|
|
th1520_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_pwm_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,
|
|
<Th1520PwmPlatformDriver as platform::Driver>::IdInfo,
|
|
[(of::DeviceId::new(c_str!("thead,th1520-pwm")), ())]
|
|
);
|
|
|
|
impl platform::Driver for Th1520PwmPlatformDriver {
|
|
type IdInfo = ();
|
|
const OF_ID_TABLE: Option<of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
|
|
|
|
fn probe(
|
|
pdev: &platform::Device<Core>,
|
|
_id_info: Option<&Self::IdInfo>,
|
|
) -> impl PinInit<Self, Error> {
|
|
let dev = pdev.as_ref();
|
|
let request = pdev.io_request_by_index(0).ok_or(ENODEV)?;
|
|
|
|
let clk = Clk::get(dev, None)?;
|
|
|
|
clk.prepare_enable()?;
|
|
|
|
// TODO: Get exclusive ownership of the clock to prevent rate changes.
|
|
// The Rust equivalent of `clk_rate_exclusive_get()` is not yet available.
|
|
// This should be updated once it is implemented.
|
|
let rate_hz = clk.rate().as_hz();
|
|
if rate_hz == 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(EINVAL);
|
|
}
|
|
|
|
let chip = pwm::Chip::new(
|
|
dev,
|
|
TH1520_MAX_PWM_NUM,
|
|
try_pin_init!(Th1520PwmDriverData {
|
|
iomem <- request.iomap_sized::<TH1520_PWM_REG_SIZE>(),
|
|
clk <- clk,
|
|
}),
|
|
)?;
|
|
|
|
pwm::Registration::register(dev, chip)?;
|
|
|
|
Ok(Th1520PwmPlatformDriver)
|
|
}
|
|
}
|
|
|
|
kernel::module_pwm_platform_driver! {
|
|
type: Th1520PwmPlatformDriver,
|
|
name: "pwm-th1520",
|
|
authors: ["Michal Wilczynski <m.wilczynski@samsung.com>"],
|
|
description: "T-HEAD TH1520 PWM driver",
|
|
license: "GPL v2",
|
|
}
|