pwm: Changes for v6.19-rc1

Additional to the usual mix of core cleanups, driver changes, minor
 fixes and device tree updates the highlight this cycle is Rust support
 for the core and a first Rust driver both provided by Michal Wilczynski.
 Michal wrote about these changes on
 https://mwilczynski.dev/posts/bringing-rust-to-the-pwm-subsystem/ which
 is a nice read.
 -----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCgAdFiEEP4GsaTp6HlmJrf7Tj4D7WH0S/k4FAmktTeIACgkQj4D7WH0S
 /k5iuQf+KXBvcSYimI53UNQsT2e9uZc794w6tpQrauHuDp/szFyiGo6XaM+Hir3I
 PS2F/2knI6puRlIyPFIxedlgSzNfpU5mXfNM86CmeuNefWVvOGBTU4lLg3ifEgGD
 CU7mbV0HMVLX749CDTbrYxIPmSNnIx8bj5V4YIm78QtR3j2L6iTZIaXeF2Ip2FAI
 AhNjkar3jk6apg5rbtSLC8At9OG5/JVgEdTuXCaEjCtnEkPz9Z4VPMOwzxzCK24q
 EHPTBpAM+psBBr3KOTSMjAuVqfrx07L47e7597XitzcdGg+zVaOnFZpuWER1/t7+
 1EAf8wlcYhi3GnUyXbaBzYq1FfhfEA==
 =x9B8
 -----END PGP SIGNATURE-----

Merge tag 'pwm/for-6.19-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/ukleinek/linux

Pull pwm updates from Uwe Kleine-König:
 "In addition to the usual mix of core cleanups, driver changes, minor
  fixes and device tree updates the highlight this cycle is Rust support
  for the core and a first Rust driver both provided by Michal
  Wilczynski.

  Michal wrote about these changes on

    https://mwilczynski.dev/posts/bringing-rust-to-the-pwm-subsystem/

  which is a nice read"

* tag 'pwm/for-6.19-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/ukleinek/linux: (22 commits)
  pwm: rzg2l-gpt: Allow checking period_tick cache value only if sibling channel is enabled
  pwm: bcm2835: Make sure the channel is enabled after pwm_request()
  pwm: mediatek: Make use of struct_size macro
  pwm: mediatek: Remove unneeded semicolon
  pwm: airoha: Add support for EN7581 SoC
  pwm: mediatek: Convert to waveform API
  pwm: max7360: Clean MAX7360 code
  pwm: Drop unused function pwm_apply_args()
  pwm: Use %u to printf unsigned int pwm_chip::npwm and pwm_chip::id
  pwm: Simplify printf to emit chip->npwm in $debugfs/pwm
  pwm: th1520: Use module_pwm_platform_driver! macro
  pwm: th1520: Fix clippy warning for redundant struct field init
  pwm: Fix Rust formatting
  dt-bindings: pwm: thead: Add T-HEAD TH1520 PWM controller
  pwm: Add Rust driver for T-HEAD TH1520 SoC
  rust: pwm: Fix broken intra-doc link
  rust: pwm: Drop wrapping of PWM polarity and state
  rust: pwm: Add module_pwm_platform_driver! macro
  rust: pwm: Add complete abstraction layer
  rust: pwm: Add Kconfig and basic data structures
  ...
This commit is contained in:
Linus Torvalds 2025-12-04 11:04:38 -08:00
commit 77956cf364
18 changed files with 2096 additions and 200 deletions

View File

@ -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 <m.wilczynski@samsung.com>
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 <dt-bindings/clock/thead,th1520-clk-ap.h>
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>;
};
};

View File

@ -20871,6 +20871,14 @@ 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 <m.wilczynski@samsung.com>
L: linux-pwm@vger.kernel.org
L: rust-for-linux@vger.kernel.org
S: Maintained
F: rust/helpers/pwm.c
F: rust/kernel/pwm.rs
PXA GPIO DRIVER
M: Robert Jarzmik <robert.jarzmik@free.fr>
L: linux-gpio@vger.kernel.org
@ -22293,6 +22301,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: Documentation/devicetree/bindings/reset/thead,th1520-reset.yaml
F: arch/riscv/boot/dts/thead/
F: drivers/clk/thead/clk-th1520-ap.c
@ -22303,6 +22312,7 @@ F: drivers/pinctrl/pinctrl-th1520.c
F: drivers/pmdomain/thead/
F: drivers/power/reset/th1520-aon-reboot.c
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

View File

@ -63,6 +63,16 @@ config PWM_ADP5585
This option enables support for the PWM function found in the Analog
Devices ADP5585.
config PWM_AIROHA
tristate "Airoha PWM support"
depends on ARCH_AIROHA || COMPILE_TEST
select REGMAP_MMIO
help
Generic PWM framework driver for Airoha SoC.
To compile this driver as a module, choose M here: the module
will be called pwm-airoha.
config PWM_APPLE
tristate "Apple SoC PWM support"
depends on ARCH_APPLE || COMPILE_TEST
@ -748,6 +758,17 @@ config PWM_TEGRA
To compile this driver as a module, choose M here: the module
will be called pwm-tegra.
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
@ -819,4 +840,16 @@ 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
depends on RUST
help
This option enables the safe Rust abstraction layer for the PWM
subsystem. It provides idiomatic wrappers and traits necessary for
writing PWM controller drivers in Rust.
The abstractions handle resource management (like memory and reference
counting) and provide safe interfaces to the underlying C core,
allowing driver logic to be written in safe Rust.
endif

View File

@ -2,6 +2,7 @@
obj-$(CONFIG_PWM) += core.o
obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o
obj-$(CONFIG_PWM_ADP5585) += pwm-adp5585.o
obj-$(CONFIG_PWM_AIROHA) += pwm-airoha.o
obj-$(CONFIG_PWM_APPLE) += pwm-apple.o
obj-$(CONFIG_PWM_ARGON_FAN_HAT) += pwm-argon-fan-hat.o
obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o
@ -68,6 +69,7 @@ obj-$(CONFIG_PWM_STMPE) += pwm-stmpe.o
obj-$(CONFIG_PWM_SUN4I) += pwm-sun4i.o
obj-$(CONFIG_PWM_SUNPLUS) += pwm-sunplus.o
obj-$(CONFIG_PWM_TEGRA) += pwm-tegra.o
obj-$(CONFIG_PWM_TH1520) += pwm_th1520.o
obj-$(CONFIG_PWM_TIECAP) += pwm-tiecap.o
obj-$(CONFIG_PWM_TIEHRPWM) += pwm-tiehrpwm.o
obj-$(CONFIG_PWM_TWL) += pwm-twl.o

View File

@ -1608,12 +1608,13 @@ void pwmchip_put(struct pwm_chip *chip)
}
EXPORT_SYMBOL_GPL(pwmchip_put);
static void pwmchip_release(struct device *pwmchip_dev)
void pwmchip_release(struct device *pwmchip_dev)
{
struct pwm_chip *chip = pwmchip_from_dev(pwmchip_dev);
kfree(chip);
}
EXPORT_SYMBOL_GPL(pwmchip_release);
struct pwm_chip *pwmchip_alloc(struct device *parent, unsigned int npwm, size_t sizeof_priv)
{
@ -2696,11 +2697,10 @@ static int pwm_seq_show(struct seq_file *s, void *v)
{
struct pwm_chip *chip = v;
seq_printf(s, "%s%d: %s/%s, %d PWM device%s\n",
seq_printf(s, "%s%u: %s/%s, npwm: %u\n",
(char *)s->private, chip->id,
pwmchip_parent(chip)->bus ? pwmchip_parent(chip)->bus->name : "no-bus",
dev_name(pwmchip_parent(chip)), chip->npwm,
(chip->npwm != 1) ? "s" : "");
dev_name(pwmchip_parent(chip)), chip->npwm);
pwm_dbg_show(chip, s);

622
drivers/pwm/pwm-airoha.c Normal file
View File

@ -0,0 +1,622 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2022 Markus Gothe <markus.gothe@genexis.eu>
* Copyright 2025 Christian Marangi <ansuelsmth@gmail.com>
*
* Limitations:
* - Only 8 concurrent waveform generators are available for 8 combinations of
* duty_cycle and period. Waveform generators are shared between 16 GPIO
* pins and 17 SIPO GPIO pins.
* - Supports only normal polarity.
* - On configuration the currently running period is completed.
* - Minimum supported period is 4 ms
* - Maximum supported period is 1s
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
#include <linux/bitmap.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/math64.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/regmap.h>
#include <linux/types.h>
#define AIROHA_PWM_REG_SGPIO_LED_DATA 0x0024
#define AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG BIT(31)
#define AIROHA_PWM_SGPIO_LED_DATA_DATA GENMASK(16, 0)
#define AIROHA_PWM_REG_SGPIO_CLK_DIVR 0x0028
#define AIROHA_PWM_SGPIO_CLK_DIVR GENMASK(1, 0)
#define AIROHA_PWM_SGPIO_CLK_DIVR_32 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 3)
#define AIROHA_PWM_SGPIO_CLK_DIVR_16 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 2)
#define AIROHA_PWM_SGPIO_CLK_DIVR_8 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 1)
#define AIROHA_PWM_SGPIO_CLK_DIVR_4 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 0)
#define AIROHA_PWM_REG_SGPIO_CLK_DLY 0x002c
#define AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG 0x0030
#define AIROHA_PWM_SERIAL_GPIO_FLASH_MODE BIT(1)
#define AIROHA_PWM_SERIAL_GPIO_MODE_74HC164 BIT(0)
#define AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(_n) (0x003c + (4 * (_n)))
#define AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(_n) (16 * (_n))
#define AIROHA_PWM_GPIO_FLASH_PRD_LOW GENMASK(15, 8)
#define AIROHA_PWM_GPIO_FLASH_PRD_HIGH GENMASK(7, 0)
#define AIROHA_PWM_REG_GPIO_FLASH_MAP(_n) (0x004c + (4 * (_n)))
#define AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(_n) (4 * (_n))
#define AIROHA_PWM_GPIO_FLASH_EN BIT(3)
#define AIROHA_PWM_GPIO_FLASH_SET_ID GENMASK(2, 0)
/* Register map is equal to GPIO flash map */
#define AIROHA_PWM_REG_SIPO_FLASH_MAP(_n) (0x0054 + (4 * (_n)))
#define AIROHA_PWM_REG_CYCLE_CFG_VALUE(_n) (0x0098 + (4 * (_n)))
#define AIROHA_PWM_REG_CYCLE_CFG_SHIFT(_n) (8 * (_n))
#define AIROHA_PWM_WAVE_GEN_CYCLE GENMASK(7, 0)
/* GPIO/SIPO flash map handles 8 pins in one register */
#define AIROHA_PWM_PINS_PER_FLASH_MAP 8
/* Cycle(Period) registers handles 4 generators in one 32-bit register */
#define AIROHA_PWM_BUCKET_PER_CYCLE_CFG 4
/* Flash(Duty) producer handles 2 generators in one 32-bit register */
#define AIROHA_PWM_BUCKET_PER_FLASH_PROD 2
#define AIROHA_PWM_NUM_BUCKETS 8
/*
* The first 16 GPIO pins, GPIO0-GPIO15, are mapped into 16 PWM channels, 0-15.
* The SIPO GPIO pins are 17 pins which are mapped into 17 PWM channels, 16-32.
* However, we've only got 8 concurrent waveform generators and can therefore
* only use up to 8 different combinations of duty cycle and period at a time.
*/
#define AIROHA_PWM_NUM_GPIO 16
#define AIROHA_PWM_NUM_SIPO 17
#define AIROHA_PWM_MAX_CHANNELS (AIROHA_PWM_NUM_GPIO + AIROHA_PWM_NUM_SIPO)
struct airoha_pwm_bucket {
/* Concurrent access protected by PWM core */
int used;
u32 period_ticks;
u32 duty_ticks;
};
struct airoha_pwm {
struct regmap *regmap;
DECLARE_BITMAP(initialized, AIROHA_PWM_MAX_CHANNELS);
struct airoha_pwm_bucket buckets[AIROHA_PWM_NUM_BUCKETS];
/* Cache bucket used by each pwm channel */
u8 channel_bucket[AIROHA_PWM_MAX_CHANNELS];
};
/* The PWM hardware supports periods between 4 ms and 1 s */
#define AIROHA_PWM_PERIOD_TICK_NS (4 * NSEC_PER_MSEC)
#define AIROHA_PWM_PERIOD_MAX_NS (1 * NSEC_PER_SEC)
/* It is represented internally as 1/250 s between 1 and 250. Unit is ticks. */
#define AIROHA_PWM_PERIOD_MIN 1
#define AIROHA_PWM_PERIOD_MAX 250
/* Duty cycle is relative with 255 corresponding to 100% */
#define AIROHA_PWM_DUTY_FULL 255
static void airoha_pwm_get_flash_map_addr_and_shift(unsigned int hwpwm,
u32 *addr, u32 *shift)
{
unsigned int offset, hwpwm_bit;
if (hwpwm >= AIROHA_PWM_NUM_GPIO) {
unsigned int sipohwpwm = hwpwm - AIROHA_PWM_NUM_GPIO;
offset = sipohwpwm / AIROHA_PWM_PINS_PER_FLASH_MAP;
hwpwm_bit = sipohwpwm % AIROHA_PWM_PINS_PER_FLASH_MAP;
/* One FLASH_MAP register handles 8 pins */
*shift = AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(hwpwm_bit);
*addr = AIROHA_PWM_REG_SIPO_FLASH_MAP(offset);
} else {
offset = hwpwm / AIROHA_PWM_PINS_PER_FLASH_MAP;
hwpwm_bit = hwpwm % AIROHA_PWM_PINS_PER_FLASH_MAP;
/* One FLASH_MAP register handles 8 pins */
*shift = AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(hwpwm_bit);
*addr = AIROHA_PWM_REG_GPIO_FLASH_MAP(offset);
}
}
static u32 airoha_pwm_get_period_ticks_from_ns(u32 period_ns)
{
return period_ns / AIROHA_PWM_PERIOD_TICK_NS;
}
static u32 airoha_pwm_get_duty_ticks_from_ns(u32 period_ns, u32 duty_ns)
{
return mul_u64_u32_div(duty_ns, AIROHA_PWM_DUTY_FULL, period_ns);
}
static u32 airoha_pwm_get_period_ns_from_ticks(u32 period_tick)
{
return period_tick * AIROHA_PWM_PERIOD_TICK_NS;
}
static u32 airoha_pwm_get_duty_ns_from_ticks(u32 period_tick, u32 duty_tick)
{
u32 period_ns = period_tick * AIROHA_PWM_PERIOD_TICK_NS;
/*
* Overflow can't occur in multiplication as duty_tick is just 8 bit
* and period_ns is clamped to AIROHA_PWM_PERIOD_MAX_NS and fit in a
* u64.
*/
return DIV_U64_ROUND_UP(duty_tick * period_ns, AIROHA_PWM_DUTY_FULL);
}
static int airoha_pwm_get_bucket(struct airoha_pwm *pc, int bucket,
u64 *period_ns, u64 *duty_ns)
{
struct regmap *map = pc->regmap;
u32 period_tick, duty_tick;
unsigned int offset;
u32 shift, val;
int ret;
offset = bucket / AIROHA_PWM_BUCKET_PER_CYCLE_CFG;
shift = bucket % AIROHA_PWM_BUCKET_PER_CYCLE_CFG;
shift = AIROHA_PWM_REG_CYCLE_CFG_SHIFT(shift);
ret = regmap_read(map, AIROHA_PWM_REG_CYCLE_CFG_VALUE(offset), &val);
if (ret)
return ret;
period_tick = FIELD_GET(AIROHA_PWM_WAVE_GEN_CYCLE, val >> shift);
*period_ns = airoha_pwm_get_period_ns_from_ticks(period_tick);
offset = bucket / AIROHA_PWM_BUCKET_PER_FLASH_PROD;
shift = bucket % AIROHA_PWM_BUCKET_PER_FLASH_PROD;
shift = AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(shift);
ret = regmap_read(map, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset),
&val);
if (ret)
return ret;
duty_tick = FIELD_GET(AIROHA_PWM_GPIO_FLASH_PRD_HIGH, val >> shift);
*duty_ns = airoha_pwm_get_duty_ns_from_ticks(period_tick, duty_tick);
return 0;
}
static int airoha_pwm_get_generator(struct airoha_pwm *pc, u32 duty_ticks,
u32 period_ticks)
{
int best = -ENOENT, unused = -ENOENT;
u32 duty_ns, best_duty_ns = 0;
u32 best_period_ticks = 0;
unsigned int i;
duty_ns = airoha_pwm_get_duty_ns_from_ticks(period_ticks, duty_ticks);
for (i = 0; i < ARRAY_SIZE(pc->buckets); i++) {
struct airoha_pwm_bucket *bucket = &pc->buckets[i];
u32 bucket_period_ticks = bucket->period_ticks;
u32 bucket_duty_ticks = bucket->duty_ticks;
/* If found, save an unused bucket to return it later */
if (!bucket->used) {
unused = i;
continue;
}
/* We found a matching bucket, exit early */
if (duty_ticks == bucket_duty_ticks &&
period_ticks == bucket_period_ticks)
return i;
/*
* Unlike duty cycle zero, which can be handled by
* disabling PWM, a generator is needed for full duty
* cycle but it can be reused regardless of period
*/
if (duty_ticks == AIROHA_PWM_DUTY_FULL &&
bucket_duty_ticks == AIROHA_PWM_DUTY_FULL)
return i;
/*
* With an unused bucket available, skip searching for
* a bucket to recycle (closer to the requested period/duty)
*/
if (unused >= 0)
continue;
/* Ignore bucket with invalid period */
if (bucket_period_ticks > period_ticks)
continue;
/*
* Search for a bucket closer to the requested period
* that has the maximal possible period that isn't bigger
* than the requested period. For that period pick the maximal
* duty cycle that isn't bigger than the requested duty_cycle.
*/
if (bucket_period_ticks >= best_period_ticks) {
u32 bucket_duty_ns = airoha_pwm_get_duty_ns_from_ticks(bucket_period_ticks,
bucket_duty_ticks);
/* Skip bucket that goes over the requested duty */
if (bucket_duty_ns > duty_ns)
continue;
if (bucket_duty_ns > best_duty_ns) {
best_period_ticks = bucket_period_ticks;
best_duty_ns = bucket_duty_ns;
best = i;
}
}
}
/* Return an unused bucket or the best one found (if ever) */
return unused >= 0 ? unused : best;
}
static void airoha_pwm_release_bucket_config(struct airoha_pwm *pc,
unsigned int hwpwm)
{
int bucket;
/* Nothing to clear, PWM channel never used */
if (!test_bit(hwpwm, pc->initialized))
return;
bucket = pc->channel_bucket[hwpwm];
pc->buckets[bucket].used--;
}
static int airoha_pwm_apply_bucket_config(struct airoha_pwm *pc, unsigned int bucket,
u32 duty_ticks, u32 period_ticks)
{
u32 mask, shift, val;
u32 offset;
int ret;
offset = bucket / AIROHA_PWM_BUCKET_PER_CYCLE_CFG;
shift = bucket % AIROHA_PWM_BUCKET_PER_CYCLE_CFG;
shift = AIROHA_PWM_REG_CYCLE_CFG_SHIFT(shift);
/* Configure frequency divisor */
mask = AIROHA_PWM_WAVE_GEN_CYCLE << shift;
val = FIELD_PREP(AIROHA_PWM_WAVE_GEN_CYCLE, period_ticks) << shift;
ret = regmap_update_bits(pc->regmap, AIROHA_PWM_REG_CYCLE_CFG_VALUE(offset),
mask, val);
if (ret)
return ret;
offset = bucket / AIROHA_PWM_BUCKET_PER_FLASH_PROD;
shift = bucket % AIROHA_PWM_BUCKET_PER_FLASH_PROD;
shift = AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(shift);
/* Configure duty cycle */
mask = AIROHA_PWM_GPIO_FLASH_PRD_HIGH << shift;
val = FIELD_PREP(AIROHA_PWM_GPIO_FLASH_PRD_HIGH, duty_ticks) << shift;
ret = regmap_update_bits(pc->regmap, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset),
mask, val);
if (ret)
return ret;
mask = AIROHA_PWM_GPIO_FLASH_PRD_LOW << shift;
val = FIELD_PREP(AIROHA_PWM_GPIO_FLASH_PRD_LOW,
AIROHA_PWM_DUTY_FULL - duty_ticks) << shift;
return regmap_update_bits(pc->regmap, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset),
mask, val);
}
static int airoha_pwm_consume_generator(struct airoha_pwm *pc,
u32 duty_ticks, u32 period_ticks,
unsigned int hwpwm)
{
bool config_bucket = false;
int bucket, ret;
/*
* Search for a bucket that already satisfies duty and period
* or an unused one.
* If not found, -ENOENT is returned.
*/
bucket = airoha_pwm_get_generator(pc, duty_ticks, period_ticks);
if (bucket < 0)
return bucket;
/* Release previous used bucket (if any) */
airoha_pwm_release_bucket_config(pc, hwpwm);
if (!pc->buckets[bucket].used)
config_bucket = true;
pc->buckets[bucket].used++;
if (config_bucket) {
pc->buckets[bucket].period_ticks = period_ticks;
pc->buckets[bucket].duty_ticks = duty_ticks;
ret = airoha_pwm_apply_bucket_config(pc, bucket,
duty_ticks,
period_ticks);
if (ret) {
pc->buckets[bucket].used--;
return ret;
}
}
return bucket;
}
static int airoha_pwm_sipo_init(struct airoha_pwm *pc)
{
u32 val;
int ret;
ret = regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG,
AIROHA_PWM_SERIAL_GPIO_MODE_74HC164);
if (ret)
return ret;
/* Configure shift register chip clock timings, use 32x divisor */
ret = regmap_write(pc->regmap, AIROHA_PWM_REG_SGPIO_CLK_DIVR,
AIROHA_PWM_SGPIO_CLK_DIVR_32);
if (ret)
return ret;
/*
* Configure the shift register chip clock delay. This needs
* to be configured based on the chip characteristics when the SoC
* apply the shift register configuration.
* This doesn't affect actual PWM operation and is only specific to
* the shift register chip.
*
* For 74HC164 we set it to 0.
*
* For reference, the actual delay applied is the internal clock
* feed to the SGPIO chip + 1.
*
* From documentation is specified that clock delay should not be
* greater than (AIROHA_PWM_REG_SGPIO_CLK_DIVR / 2) - 1.
*/
ret = regmap_write(pc->regmap, AIROHA_PWM_REG_SGPIO_CLK_DLY, 0);
if (ret)
return ret;
/*
* It is necessary to explicitly shift out all zeros after muxing
* to initialize the shift register before enabling PWM
* mode because in PWM mode SIPO will not start shifting until
* it needs to output a non-zero value (bit 31 of led_data
* indicates shifting in progress and it must return to zero
* before led_data can be written or PWM mode can be set).
*/
ret = regmap_read_poll_timeout(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA, val,
!(val & AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG),
10, 200 * USEC_PER_MSEC);
if (ret)
return ret;
ret = regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA,
AIROHA_PWM_SGPIO_LED_DATA_DATA);
if (ret)
return ret;
ret = regmap_read_poll_timeout(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA, val,
!(val & AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG),
10, 200 * USEC_PER_MSEC);
if (ret)
return ret;
/* Set SIPO in PWM mode */
return regmap_set_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG,
AIROHA_PWM_SERIAL_GPIO_FLASH_MODE);
}
static int airoha_pwm_config_flash_map(struct airoha_pwm *pc,
unsigned int hwpwm, int index)
{
unsigned int addr;
u32 shift;
int ret;
airoha_pwm_get_flash_map_addr_and_shift(hwpwm, &addr, &shift);
/* negative index means disable PWM channel */
if (index < 0) {
/*
* If we need to disable the PWM, we just put low the
* GPIO. No need to setup buckets.
*/
return regmap_clear_bits(pc->regmap, addr,
AIROHA_PWM_GPIO_FLASH_EN << shift);
}
ret = regmap_update_bits(pc->regmap, addr,
AIROHA_PWM_GPIO_FLASH_SET_ID << shift,
FIELD_PREP(AIROHA_PWM_GPIO_FLASH_SET_ID, index) << shift);
if (ret)
return ret;
return regmap_set_bits(pc->regmap, addr, AIROHA_PWM_GPIO_FLASH_EN << shift);
}
static int airoha_pwm_config(struct airoha_pwm *pc, struct pwm_device *pwm,
u32 period_ticks, u32 duty_ticks)
{
unsigned int hwpwm = pwm->hwpwm;
int bucket, ret;
bucket = airoha_pwm_consume_generator(pc, duty_ticks, period_ticks,
hwpwm);
if (bucket < 0)
return bucket;
ret = airoha_pwm_config_flash_map(pc, hwpwm, bucket);
if (ret) {
pc->buckets[bucket].used--;
return ret;
}
__set_bit(hwpwm, pc->initialized);
pc->channel_bucket[hwpwm] = bucket;
/*
* SIPO are special GPIO attached to a shift register chip. The handling
* of this chip is internal to the SoC that takes care of applying the
* values based on the flash map. To apply a new flash map, it's needed
* to trigger a refresh on the shift register chip.
* If a SIPO is getting configuring , always reinit the shift register
* chip to make sure the correct flash map is applied.
* Skip reconfiguring the shift register if the related hwpwm
* is disabled (as it doesn't need to be mapped).
*/
if (hwpwm >= AIROHA_PWM_NUM_GPIO) {
ret = airoha_pwm_sipo_init(pc);
if (ret) {
airoha_pwm_release_bucket_config(pc, hwpwm);
return ret;
}
}
return 0;
}
static void airoha_pwm_disable(struct airoha_pwm *pc, struct pwm_device *pwm)
{
/* Disable PWM and release the bucket */
airoha_pwm_config_flash_map(pc, pwm->hwpwm, -1);
airoha_pwm_release_bucket_config(pc, pwm->hwpwm);
__clear_bit(pwm->hwpwm, pc->initialized);
/* If no SIPO is used, disable the shift register chip */
if (!bitmap_read(pc->initialized,
AIROHA_PWM_NUM_GPIO, AIROHA_PWM_NUM_SIPO))
regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG,
AIROHA_PWM_SERIAL_GPIO_FLASH_MODE);
}
static int airoha_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_state *state)
{
struct airoha_pwm *pc = pwmchip_get_drvdata(chip);
u32 period_ticks, duty_ticks;
u32 period_ns, duty_ns;
if (!state->enabled) {
airoha_pwm_disable(pc, pwm);
return 0;
}
/* Only normal polarity is supported */
if (state->polarity == PWM_POLARITY_INVERSED)
return -EINVAL;
/* Exit early if period is less than minimum supported */
if (state->period < AIROHA_PWM_PERIOD_TICK_NS)
return -EINVAL;
/* Clamp period to MAX supported value */
if (state->period > AIROHA_PWM_PERIOD_MAX_NS)
period_ns = AIROHA_PWM_PERIOD_MAX_NS;
else
period_ns = state->period;
/* Validate duty to configured period */
if (state->duty_cycle > period_ns)
duty_ns = period_ns;
else
duty_ns = state->duty_cycle;
/* Convert period ns to ticks */
period_ticks = airoha_pwm_get_period_ticks_from_ns(period_ns);
/* Convert period ticks to ns again for cosistent duty tick calculation */
period_ns = airoha_pwm_get_period_ns_from_ticks(period_ticks);
duty_ticks = airoha_pwm_get_duty_ticks_from_ns(period_ns, duty_ns);
return airoha_pwm_config(pc, pwm, period_ticks, duty_ticks);
}
static int airoha_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state)
{
struct airoha_pwm *pc = pwmchip_get_drvdata(chip);
int ret, hwpwm = pwm->hwpwm;
u32 addr, shift, val;
u8 bucket;
airoha_pwm_get_flash_map_addr_and_shift(hwpwm, &addr, &shift);
ret = regmap_read(pc->regmap, addr, &val);
if (ret)
return ret;
state->enabled = FIELD_GET(AIROHA_PWM_GPIO_FLASH_EN, val >> shift);
if (!state->enabled)
return 0;
state->polarity = PWM_POLARITY_NORMAL;
bucket = FIELD_GET(AIROHA_PWM_GPIO_FLASH_SET_ID, val >> shift);
return airoha_pwm_get_bucket(pc, bucket, &state->period,
&state->duty_cycle);
}
static const struct pwm_ops airoha_pwm_ops = {
.apply = airoha_pwm_apply,
.get_state = airoha_pwm_get_state,
};
static int airoha_pwm_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct airoha_pwm *pc;
struct pwm_chip *chip;
int ret;
chip = devm_pwmchip_alloc(dev, AIROHA_PWM_MAX_CHANNELS, sizeof(*pc));
if (IS_ERR(chip))
return PTR_ERR(chip);
chip->ops = &airoha_pwm_ops;
pc = pwmchip_get_drvdata(chip);
pc->regmap = device_node_to_regmap(dev_of_node(dev->parent));
if (IS_ERR(pc->regmap))
return dev_err_probe(dev, PTR_ERR(pc->regmap), "Failed to get PWM regmap\n");
ret = devm_pwmchip_add(dev, chip);
if (ret)
return dev_err_probe(dev, ret, "Failed to add PWM chip\n");
return 0;
}
static const struct of_device_id airoha_pwm_of_match[] = {
{ .compatible = "airoha,en7581-pwm" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, airoha_pwm_of_match);
static struct platform_driver airoha_pwm_driver = {
.driver = {
.name = "pwm-airoha",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.of_match_table = airoha_pwm_of_match,
},
.probe = airoha_pwm_probe,
};
module_platform_driver(airoha_pwm_driver);
MODULE_AUTHOR("Lorenzo Bianconi <lorenzo@kernel.org>");
MODULE_AUTHOR("Markus Gothe <markus.gothe@genexis.eu>");
MODULE_AUTHOR("Benjamin Larsson <benjamin.larsson@genexis.eu>");
MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>");
MODULE_DESCRIPTION("Airoha EN7581 PWM driver");
MODULE_LICENSE("GPL");

View File

@ -34,29 +34,6 @@ static inline struct bcm2835_pwm *to_bcm2835_pwm(struct pwm_chip *chip)
return pwmchip_get_drvdata(chip);
}
static int bcm2835_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm)
{
struct bcm2835_pwm *pc = to_bcm2835_pwm(chip);
u32 value;
value = readl(pc->base + PWM_CONTROL);
value &= ~(PWM_CONTROL_MASK << PWM_CONTROL_SHIFT(pwm->hwpwm));
value |= (PWM_MODE << PWM_CONTROL_SHIFT(pwm->hwpwm));
writel(value, pc->base + PWM_CONTROL);
return 0;
}
static void bcm2835_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm)
{
struct bcm2835_pwm *pc = to_bcm2835_pwm(chip);
u32 value;
value = readl(pc->base + PWM_CONTROL);
value &= ~(PWM_CONTROL_MASK << PWM_CONTROL_SHIFT(pwm->hwpwm));
writel(value, pc->base + PWM_CONTROL);
}
static int bcm2835_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_state *state)
{
@ -102,6 +79,9 @@ static int bcm2835_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
/* set polarity */
val = readl(pc->base + PWM_CONTROL);
val &= ~(PWM_CONTROL_MASK << PWM_CONTROL_SHIFT(pwm->hwpwm));
val |= PWM_MODE << PWM_CONTROL_SHIFT(pwm->hwpwm);
if (state->polarity == PWM_POLARITY_NORMAL)
val &= ~(PWM_POLARITY << PWM_CONTROL_SHIFT(pwm->hwpwm));
else
@ -119,8 +99,6 @@ static int bcm2835_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
}
static const struct pwm_ops bcm2835_pwm_ops = {
.request = bcm2835_pwm_request,
.free = bcm2835_pwm_free,
.apply = bcm2835_pwm_apply,
};

View File

@ -75,7 +75,7 @@ static int max7360_pwm_round_waveform_tohw(struct pwm_chip *chip,
duty_steps = MAX7360_PWM_MAX - 1;
}
wfhw->duty_steps = min(MAX7360_PWM_MAX, duty_steps);
wfhw->duty_steps = duty_steps;
wfhw->enabled = !!wf->period_length_ns;
if (wf->period_length_ns && wf->period_length_ns < MAX7360_PWM_PERIOD_NS)

View File

@ -135,50 +135,51 @@ static inline u32 pwm_mediatek_readl(struct pwm_mediatek_chip *chip,
num * chip->soc->chanreg_width + offset);
}
static void pwm_mediatek_enable(struct pwm_chip *chip, struct pwm_device *pwm)
{
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 value;
value = readl(pc->regs);
value |= BIT(pwm->hwpwm);
writel(value, pc->regs);
}
static void pwm_mediatek_disable(struct pwm_chip *chip, struct pwm_device *pwm)
{
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 value;
value = readl(pc->regs);
value &= ~BIT(pwm->hwpwm);
writel(value, pc->regs);
}
static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
u64 duty_ns, u64 period_ns)
struct pwm_mediatek_waveform {
u32 enable;
u32 con;
u32 width;
u32 thres;
};
static int pwm_mediatek_round_waveform_tohw(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_waveform *wf, void *_wfhw)
{
struct pwm_mediatek_waveform *wfhw = _wfhw;
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 clkdiv, enable;
u32 reg_width = PWMDWIDTH, reg_thres = PWMTHRES;
u64 cnt_period, cnt_duty;
unsigned long clk_rate;
int ret;
int ret = 0;
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
if (ret < 0)
if (wf->period_length_ns == 0) {
*wfhw = (typeof(*wfhw)){
.enable = 0,
};
return 0;
}
if (!pc->clk_pwms[pwm->hwpwm].rate) {
struct clk *clk = pc->clk_pwms[pwm->hwpwm].clk;
ret = clk_prepare_enable(clk);
if (ret)
return ret;
pc->clk_pwms[pwm->hwpwm].rate = clk_get_rate(clk);
clk_disable_unprepare(clk);
}
clk_rate = pc->clk_pwms[pwm->hwpwm].rate;
if (clk_rate == 0 || clk_rate > 1000000000)
return -EINVAL;
/* Make sure we use the bus clock and not the 26MHz clock */
if (pc->soc->pwm_ck_26m_sel_reg)
writel(0, pc->regs + pc->soc->pwm_ck_26m_sel_reg);
cnt_period = mul_u64_u64_div_u64(period_ns, clk_rate, NSEC_PER_SEC);
cnt_period = mul_u64_u64_div_u64(wf->period_length_ns, clk_rate, NSEC_PER_SEC);
if (cnt_period == 0) {
ret = -ERANGE;
goto out;
cnt_period = 1;
ret = 1;
}
if (cnt_period > FIELD_MAX(PWMDWIDTH_PERIOD) + 1) {
@ -193,7 +194,7 @@ static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
clkdiv = 0;
}
cnt_duty = mul_u64_u64_div_u64(duty_ns, clk_rate, NSEC_PER_SEC) >> clkdiv;
cnt_duty = mul_u64_u64_div_u64(wf->duty_length_ns, clk_rate, NSEC_PER_SEC) >> clkdiv;
if (cnt_duty > cnt_period)
cnt_duty = cnt_period;
@ -206,9 +207,90 @@ static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
cnt_period -= 1;
dev_dbg(&chip->dev, "pwm#%u: %lld/%lld @%lu -> CON: %x, PERIOD: %llx, DUTY: %llx\n",
pwm->hwpwm, duty_ns, period_ns, clk_rate, clkdiv, cnt_period, cnt_duty);
dev_dbg(&chip->dev, "pwm#%u: %lld/%lld @%lu -> ENABLE: %x, CON: %x, PERIOD: %llx, DUTY: %llx\n",
pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns, clk_rate,
enable, clkdiv, cnt_period, cnt_duty);
*wfhw = (typeof(*wfhw)){
.enable = enable,
.con = clkdiv,
.width = cnt_period,
.thres = cnt_duty,
};
return ret;
}
static int pwm_mediatek_round_waveform_fromhw(struct pwm_chip *chip, struct pwm_device *pwm,
const void *_wfhw, struct pwm_waveform *wf)
{
const struct pwm_mediatek_waveform *wfhw = _wfhw;
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 clkdiv, cnt_period, cnt_duty;
unsigned long clk_rate;
/*
* When _wfhw was populated, the clock was on, so .rate is
* already set appropriately.
*/
clk_rate = pc->clk_pwms[pwm->hwpwm].rate;
if (wfhw->enable) {
clkdiv = FIELD_GET(PWMCON_CLKDIV, wfhw->con);
cnt_period = FIELD_GET(PWMDWIDTH_PERIOD, wfhw->width);
cnt_duty = FIELD_GET(PWMTHRES_DUTY, wfhw->thres);
/*
* cnt_period is a 13 bit value, NSEC_PER_SEC is 30 bits wide
* and clkdiv is less than 8, so the multiplication doesn't
* overflow an u64.
*/
*wf = (typeof(*wf)){
.period_length_ns =
DIV_ROUND_UP_ULL((u64)(cnt_period + 1) * NSEC_PER_SEC << clkdiv, clk_rate),
.duty_length_ns =
DIV_ROUND_UP_ULL((u64)(cnt_duty + 1) * NSEC_PER_SEC << clkdiv, clk_rate),
};
} else {
clkdiv = 0;
cnt_period = 0;
cnt_duty = 0;
/*
* .enable = 0 is also used for too small duty_cycle values, so
* report the HW as being enabled to communicate the minimal
* period.
*/
*wf = (typeof(*wf)){
.period_length_ns =
DIV_ROUND_UP_ULL(NSEC_PER_SEC, clk_rate),
.duty_length_ns = 0,
};
}
dev_dbg(&chip->dev, "pwm#%u: ENABLE: %x, CLKDIV: %x, PERIOD: %x, DUTY: %x @%lu -> %lld/%lld\n",
pwm->hwpwm, wfhw->enable, clkdiv, cnt_period, cnt_duty, clk_rate,
wf->duty_length_ns, wf->period_length_ns);
return 0;
}
static int pwm_mediatek_read_waveform(struct pwm_chip *chip,
struct pwm_device *pwm, void *_wfhw)
{
struct pwm_mediatek_waveform *wfhw = _wfhw;
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 enable, clkdiv, cnt_period, cnt_duty;
u32 reg_width = PWMDWIDTH, reg_thres = PWMTHRES;
int ret;
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
if (ret < 0)
return ret;
enable = readl(pc->regs) & BIT(pwm->hwpwm);
if (enable) {
if (pc->soc->pwm45_fixup && pwm->hwpwm > 2) {
/*
* PWM[4,5] has distinct offset for PWMDWIDTH and PWMTHRES
@ -218,56 +300,42 @@ static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
reg_thres = PWM45THRES_FIXUP;
}
pwm_mediatek_writel(pc, pwm->hwpwm, PWMCON, BIT(15) | clkdiv);
pwm_mediatek_writel(pc, pwm->hwpwm, reg_width, cnt_period);
clkdiv = FIELD_GET(PWMCON_CLKDIV, pwm_mediatek_readl(pc, pwm->hwpwm, PWMCON));
cnt_period = FIELD_GET(PWMDWIDTH_PERIOD, pwm_mediatek_readl(pc, pwm->hwpwm, reg_width));
cnt_duty = FIELD_GET(PWMTHRES_DUTY, pwm_mediatek_readl(pc, pwm->hwpwm, reg_thres));
if (enable) {
pwm_mediatek_writel(pc, pwm->hwpwm, reg_thres, cnt_duty);
pwm_mediatek_enable(chip, pwm);
*wfhw = (typeof(*wfhw)){
.enable = enable,
.con = BIT(15) | clkdiv,
.width = cnt_period,
.thres = cnt_duty,
};
} else {
pwm_mediatek_disable(chip, pwm);
*wfhw = (typeof(*wfhw)){
.enable = 0,
};
}
out:
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
return ret;
}
static int pwm_mediatek_apply(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_state *state)
{
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
int err;
if (state->polarity != PWM_POLARITY_NORMAL)
return -EINVAL;
if (!state->enabled) {
if (pwm->state.enabled) {
pwm_mediatek_disable(chip, pwm);
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
}
return 0;
}
err = pwm_mediatek_config(chip, pwm, state->duty_cycle, state->period);
if (err)
return err;
if (!pwm->state.enabled)
err = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
return err;
}
static int pwm_mediatek_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state)
static int pwm_mediatek_write_waveform(struct pwm_chip *chip,
struct pwm_device *pwm, const void *_wfhw)
{
const struct pwm_mediatek_waveform *wfhw = _wfhw;
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
u32 ctrl;
int ret;
u32 enable;
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
if (ret < 0)
return ret;
ctrl = readl(pc->regs);
if (wfhw->enable) {
u32 reg_width = PWMDWIDTH, reg_thres = PWMTHRES;
if (pc->soc->pwm45_fixup && pwm->hwpwm > 2) {
@ -279,48 +347,47 @@ static int pwm_mediatek_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
reg_thres = PWM45THRES_FIXUP;
}
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
if (ret < 0)
return ret;
enable = readl(pc->regs);
if (enable & BIT(pwm->hwpwm)) {
u32 clkdiv, cnt_period, cnt_duty;
unsigned long clk_rate;
clk_rate = pc->clk_pwms[pwm->hwpwm].rate;
state->enabled = true;
state->polarity = PWM_POLARITY_NORMAL;
clkdiv = FIELD_GET(PWMCON_CLKDIV,
pwm_mediatek_readl(pc, pwm->hwpwm, PWMCON));
cnt_period = FIELD_GET(PWMDWIDTH_PERIOD,
pwm_mediatek_readl(pc, pwm->hwpwm, reg_width));
cnt_duty = FIELD_GET(PWMTHRES_DUTY,
pwm_mediatek_readl(pc, pwm->hwpwm, reg_thres));
if (!(ctrl & BIT(pwm->hwpwm))) {
/*
* cnt_period is a 13 bit value, NSEC_PER_SEC is 30 bits wide
* and clkdiv is less than 8, so the multiplication doesn't
* overflow an u64.
* The clks are already on, just increasing the usage
* counter doesn't fail.
*/
state->period =
DIV_ROUND_UP_ULL((u64)cnt_period * NSEC_PER_SEC << clkdiv, clk_rate);
state->duty_cycle =
DIV_ROUND_UP_ULL((u64)cnt_duty * NSEC_PER_SEC << clkdiv, clk_rate);
} else {
state->enabled = false;
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
if (unlikely(ret < 0))
goto out;
ctrl |= BIT(pwm->hwpwm);
writel(ctrl, pc->regs);
}
/* Make sure we use the bus clock and not the 26MHz clock */
if (pc->soc->pwm_ck_26m_sel_reg)
writel(0, pc->regs + pc->soc->pwm_ck_26m_sel_reg);
pwm_mediatek_writel(pc, pwm->hwpwm, PWMCON, BIT(15) | wfhw->con);
pwm_mediatek_writel(pc, pwm->hwpwm, reg_width, wfhw->width);
pwm_mediatek_writel(pc, pwm->hwpwm, reg_thres, wfhw->thres);
} else {
if (ctrl & BIT(pwm->hwpwm)) {
ctrl &= ~BIT(pwm->hwpwm);
writel(ctrl, pc->regs);
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
}
}
out:
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
return ret;
}
static const struct pwm_ops pwm_mediatek_ops = {
.apply = pwm_mediatek_apply,
.get_state = pwm_mediatek_get_state,
.sizeof_wfhw = sizeof(struct pwm_mediatek_waveform),
.round_waveform_tohw = pwm_mediatek_round_waveform_tohw,
.round_waveform_fromhw = pwm_mediatek_round_waveform_fromhw,
.read_waveform = pwm_mediatek_read_waveform,
.write_waveform = pwm_mediatek_write_waveform,
};
static int pwm_mediatek_init_used_clks(struct pwm_mediatek_chip *pc)
@ -377,7 +444,7 @@ static int pwm_mediatek_probe(struct platform_device *pdev)
soc = of_device_get_match_data(&pdev->dev);
chip = devm_pwmchip_alloc(&pdev->dev, soc->num_pwms,
sizeof(*pc) + soc->num_pwms * sizeof(*pc->clk_pwms));
struct_size(pc, clk_pwms, soc->num_pwms));
if (IS_ERR(chip))
return PTR_ERR(chip);
pc = to_pwm_mediatek_chip(chip);

View File

@ -96,6 +96,11 @@ static inline unsigned int rzg2l_gpt_subchannel(unsigned int hwpwm)
return hwpwm & 0x1;
}
static inline unsigned int rzg2l_gpt_sibling(unsigned int hwpwm)
{
return hwpwm ^ 0x1;
}
static void rzg2l_gpt_write(struct rzg2l_gpt_chip *rzg2l_gpt, u32 reg, u32 data)
{
writel(data, rzg2l_gpt->mmio + reg);
@ -271,11 +276,15 @@ static int rzg2l_gpt_config(struct pwm_chip *chip, struct pwm_device *pwm,
* in use with different settings.
*/
if (rzg2l_gpt->channel_request_count[ch] > 1) {
u8 sibling_ch = rzg2l_gpt_sibling(pwm->hwpwm);
if (rzg2l_gpt_is_ch_enabled(rzg2l_gpt, sibling_ch)) {
if (period_ticks < rzg2l_gpt->period_ticks[ch])
return -EBUSY;
else
period_ticks = rzg2l_gpt->period_ticks[ch];
}
}
prescale = rzg2l_gpt_calculate_prescale(rzg2l_gpt, period_ticks);
pv = rzg2l_gpt_calculate_pv_or_dc(period_ticks, prescale);

387
drivers/pwm/pwm_th1520.rs Normal file
View File

@ -0,0 +1,387 @@
// 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>,
) -> Result<Pin<KBox<Self>>> {
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(KBox::new(Th1520PwmPlatformDriver, GFP_KERNEL)?.into())
}
}
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",
}

View File

@ -488,6 +488,12 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner);
#define pwmchip_add(chip) __pwmchip_add(chip, THIS_MODULE)
void pwmchip_remove(struct pwm_chip *chip);
/*
* For FFI wrapper use only:
* The Rust PWM abstraction needs this to properly free the pwm_chip.
*/
void pwmchip_release(struct device *dev);
int __devm_pwmchip_add(struct device *dev, struct pwm_chip *chip, struct module *owner);
#define devm_pwmchip_add(dev, chip) __devm_pwmchip_add(dev, chip, THIS_MODULE)
@ -611,39 +617,6 @@ devm_fwnode_pwm_get(struct device *dev, struct fwnode_handle *fwnode,
}
#endif
static inline void pwm_apply_args(struct pwm_device *pwm)
{
struct pwm_state state = { };
/*
* PWM users calling pwm_apply_args() expect to have a fresh config
* where the polarity and period are set according to pwm_args info.
* The problem is, polarity can only be changed when the PWM is
* disabled.
*
* PWM drivers supporting hardware readout may declare the PWM device
* as enabled, and prevent polarity setting, which changes from the
* existing behavior, where all PWM devices are declared as disabled
* at startup (even if they are actually enabled), thus authorizing
* polarity setting.
*
* To fulfill this requirement, we apply a new state which disables
* the PWM device and set the reference period and polarity config.
*
* Note that PWM users requiring a smooth handover between the
* bootloader and the kernel (like critical regulators controlled by
* PWM devices) will have to switch to the atomic API and avoid calling
* pwm_apply_args().
*/
state.enabled = false;
state.polarity = pwm->args.polarity;
state.period = pwm->args.period;
state.usage_power = false;
pwm_apply_might_sleep(pwm, &state);
}
struct pwm_lookup {
struct list_head list;
const char *provider;

View File

@ -72,6 +72,7 @@
#include <linux/pm_opp.h>
#include <linux/poll.h>
#include <linux/property.h>
#include <linux/pwm.h>
#include <linux/random.h>
#include <linux/refcount.h>
#include <linux/regulator/consumer.h>

View File

@ -43,6 +43,7 @@
#include "poll.c"
#include "processor.c"
#include "property.c"
#include "pwm.c"
#include "rbtree.c"
#include "rcu.c"
#include "refcount.c"

20
rust/helpers/pwm.c Normal file
View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2025 Samsung Electronics Co., Ltd.
// Author: Michal Wilczynski <m.wilczynski@samsung.com>
#include <linux/pwm.h>
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);
}

View File

@ -125,6 +125,8 @@
pub mod print;
pub mod processor;
pub mod ptr;
#[cfg(CONFIG_RUST_PWM_ABSTRACTIONS)]
pub mod pwm;
pub mod rbtree;
pub mod regulator;
pub mod revocable;

735
rust/kernel/pwm.rs Normal file
View File

@ -0,0 +1,735 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2025 Samsung Electronics Co., Ltd.
// Author: Michal Wilczynski <m.wilczynski@samsung.com>
//! PWM subsystem abstractions.
//!
//! C header: [`include/linux/pwm.h`](srctree/include/linux/pwm.h).
use crate::{
bindings,
container_of,
device::{self, Bound},
devres,
error::{self, to_result},
prelude::*,
types::{ARef, AlwaysRefCounted, Opaque}, //
};
use core::{marker::PhantomData, ptr::NonNull};
/// Represents a PWM waveform configuration.
/// Mirrors struct [`struct pwm_waveform`](srctree/include/linux/pwm.h).
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Waveform {
/// Total duration of one complete PWM cycle, in nanoseconds.
pub period_length_ns: u64,
/// Duty-cycle active time, in nanoseconds.
///
/// For a typical normal polarity configuration (active-high) this is the
/// high time of the signal.
pub duty_length_ns: u64,
/// Duty-cycle start offset, in nanoseconds.
///
/// Delay from the beginning of the period to the first active edge.
/// In most simple PWM setups this is `0`, so the duty cycle starts
/// immediately at each periods start.
pub duty_offset_ns: u64,
}
impl From<bindings::pwm_waveform> for Waveform {
fn from(wf: bindings::pwm_waveform) -> Self {
Waveform {
period_length_ns: wf.period_length_ns,
duty_length_ns: wf.duty_length_ns,
duty_offset_ns: wf.duty_offset_ns,
}
}
}
impl From<Waveform> for bindings::pwm_waveform {
fn from(wf: Waveform) -> Self {
bindings::pwm_waveform {
period_length_ns: wf.period_length_ns,
duty_length_ns: wf.duty_length_ns,
duty_offset_ns: wf.duty_offset_ns,
}
}
}
/// Describes the outcome of a `round_waveform` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoundingOutcome {
/// The requested waveform was achievable exactly or by rounding values down.
ExactOrRoundedDown,
/// The requested waveform could only be achieved by rounding up.
RoundedUp,
}
/// Wrapper for a PWM device [`struct pwm_device`](srctree/include/linux/pwm.h).
#[repr(transparent)]
pub struct Device(Opaque<bindings::pwm_device>);
impl Device {
/// Creates a reference to a [`Device`] from a valid C pointer.
///
/// # Safety
///
/// The caller must ensure that `ptr` is valid and remains valid for the lifetime of the
/// returned [`Device`] reference.
pub(crate) unsafe fn from_raw<'a>(ptr: *mut bindings::pwm_device) -> &'a Self {
// SAFETY: The safety requirements guarantee the validity of the dereference, while the
// `Device` type being transparent makes the cast ok.
unsafe { &*ptr.cast::<Self>() }
}
/// Returns a raw pointer to the underlying `pwm_device`.
fn as_raw(&self) -> *mut bindings::pwm_device {
self.0.get()
}
/// Gets the hardware PWM index for this device within its chip.
pub fn hwpwm(&self) -> u32 {
// SAFETY: `self.as_raw()` provides a valid pointer for `self`'s lifetime.
unsafe { (*self.as_raw()).hwpwm }
}
/// Gets a reference to the parent `Chip` that this device belongs to.
pub fn chip<T: PwmOps>(&self) -> &Chip<T> {
// SAFETY: `self.as_raw()` provides a valid pointer. (*self.as_raw()).chip
// is assumed to be a valid pointer to `pwm_chip` managed by the kernel.
// Chip::from_raw's safety conditions must be met.
unsafe { Chip::<T>::from_raw((*self.as_raw()).chip) }
}
/// Gets the label for this PWM device, if any.
pub fn label(&self) -> Option<&CStr> {
// SAFETY: self.as_raw() provides a valid pointer.
let label_ptr = unsafe { (*self.as_raw()).label };
if label_ptr.is_null() {
return None;
}
// SAFETY: label_ptr is non-null and points to a C string
// managed by the kernel, valid for the lifetime of the PWM device.
Some(unsafe { CStr::from_char_ptr(label_ptr) })
}
/// Sets the PWM waveform configuration and enables the PWM signal.
pub fn set_waveform(&self, wf: &Waveform, exact: bool) -> Result {
let c_wf = bindings::pwm_waveform::from(*wf);
// SAFETY: `self.as_raw()` provides a valid `*mut pwm_device` pointer.
// `&c_wf` is a valid pointer to a `pwm_waveform` struct. The C function
// handles all necessary internal locking.
let ret = unsafe { bindings::pwm_set_waveform_might_sleep(self.as_raw(), &c_wf, exact) };
to_result(ret)
}
/// Queries the hardware for the configuration it would apply for a given
/// request.
pub fn round_waveform(&self, wf: &mut Waveform) -> Result<RoundingOutcome> {
let mut c_wf = bindings::pwm_waveform::from(*wf);
// SAFETY: `self.as_raw()` provides a valid `*mut pwm_device` pointer.
// `&mut c_wf` is a valid pointer to a mutable `pwm_waveform` struct that
// the C function will update.
let ret = unsafe { bindings::pwm_round_waveform_might_sleep(self.as_raw(), &mut c_wf) };
to_result(ret)?;
*wf = Waveform::from(c_wf);
if ret == 1 {
Ok(RoundingOutcome::RoundedUp)
} else {
Ok(RoundingOutcome::ExactOrRoundedDown)
}
}
/// Reads the current waveform configuration directly from the hardware.
pub fn get_waveform(&self) -> Result<Waveform> {
let mut c_wf = bindings::pwm_waveform::default();
// SAFETY: `self.as_raw()` is a valid pointer. We provide a valid pointer
// to a stack-allocated `pwm_waveform` struct for the kernel to fill.
let ret = unsafe { bindings::pwm_get_waveform_might_sleep(self.as_raw(), &mut c_wf) };
to_result(ret)?;
Ok(Waveform::from(c_wf))
}
}
/// The result of a `round_waveform_tohw` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RoundedWaveform<WfHw> {
/// A status code, 0 for success or 1 if values were rounded up.
pub status: c_int,
/// The driver-specific hardware representation of the waveform.
pub hardware_waveform: WfHw,
}
/// Trait defining the operations for a PWM driver.
pub trait PwmOps: 'static + Sized {
/// The driver-specific hardware representation of a waveform.
///
/// This type must be [`Copy`], [`Default`], and fit within `PWM_WFHWSIZE`.
type WfHw: Copy + Default;
/// Optional hook for when a PWM device is requested.
fn request(_chip: &Chip<Self>, _pwm: &Device, _parent_dev: &device::Device<Bound>) -> Result {
Ok(())
}
/// Optional hook for capturing a PWM signal.
fn capture(
_chip: &Chip<Self>,
_pwm: &Device,
_result: &mut bindings::pwm_capture,
_timeout: usize,
_parent_dev: &device::Device<Bound>,
) -> Result {
Err(ENOTSUPP)
}
/// Convert a generic waveform to the hardware-specific representation.
/// This is typically a pure calculation and does not perform I/O.
fn round_waveform_tohw(
_chip: &Chip<Self>,
_pwm: &Device,
_wf: &Waveform,
) -> Result<RoundedWaveform<Self::WfHw>> {
Err(ENOTSUPP)
}
/// Convert a hardware-specific representation back to a generic waveform.
/// This is typically a pure calculation and does not perform I/O.
fn round_waveform_fromhw(
_chip: &Chip<Self>,
_pwm: &Device,
_wfhw: &Self::WfHw,
_wf: &mut Waveform,
) -> Result {
Err(ENOTSUPP)
}
/// Read the current hardware configuration into the hardware-specific representation.
fn read_waveform(
_chip: &Chip<Self>,
_pwm: &Device,
_parent_dev: &device::Device<Bound>,
) -> Result<Self::WfHw> {
Err(ENOTSUPP)
}
/// Write a hardware-specific waveform configuration to the hardware.
fn write_waveform(
_chip: &Chip<Self>,
_pwm: &Device,
_wfhw: &Self::WfHw,
_parent_dev: &device::Device<Bound>,
) -> Result {
Err(ENOTSUPP)
}
}
/// Bridges Rust `PwmOps` to the C `pwm_ops` vtable.
struct Adapter<T: PwmOps> {
_p: PhantomData<T>,
}
impl<T: PwmOps> Adapter<T> {
const VTABLE: PwmOpsVTable = create_pwm_ops::<T>();
/// # Safety
///
/// `wfhw_ptr` must be valid for writes of `size_of::<T::WfHw>()` bytes.
unsafe fn serialize_wfhw(wfhw: &T::WfHw, wfhw_ptr: *mut c_void) -> Result {
let size = core::mem::size_of::<T::WfHw>();
build_assert!(size <= bindings::PWM_WFHWSIZE as usize);
// SAFETY: The caller ensures `wfhw_ptr` is valid for `size` bytes.
unsafe {
core::ptr::copy_nonoverlapping(
core::ptr::from_ref::<T::WfHw>(wfhw).cast::<u8>(),
wfhw_ptr.cast::<u8>(),
size,
);
}
Ok(())
}
/// # Safety
///
/// `wfhw_ptr` must be valid for reads of `size_of::<T::WfHw>()` bytes.
unsafe fn deserialize_wfhw(wfhw_ptr: *const c_void) -> Result<T::WfHw> {
let size = core::mem::size_of::<T::WfHw>();
build_assert!(size <= bindings::PWM_WFHWSIZE as usize);
let mut wfhw = T::WfHw::default();
// SAFETY: The caller ensures `wfhw_ptr` is valid for `size` bytes.
unsafe {
core::ptr::copy_nonoverlapping(
wfhw_ptr.cast::<u8>(),
core::ptr::from_mut::<T::WfHw>(&mut wfhw).cast::<u8>(),
size,
);
}
Ok(wfhw)
}
/// # Safety
///
/// `dev` must be a valid pointer to a `bindings::device` embedded within a
/// `bindings::pwm_chip`. This function is called by the device core when the
/// last reference to the device is dropped.
unsafe extern "C" fn release_callback(dev: *mut bindings::device) {
// SAFETY: The function's contract guarantees that `dev` points to a `device`
// field embedded within a valid `pwm_chip`. `container_of!` can therefore
// safely calculate the address of the containing struct.
let c_chip_ptr = unsafe { container_of!(dev, bindings::pwm_chip, dev) };
// SAFETY: `c_chip_ptr` is a valid pointer to a `pwm_chip` as established
// above. Calling this FFI function is safe.
let drvdata_ptr = unsafe { bindings::pwmchip_get_drvdata(c_chip_ptr) };
// SAFETY: The driver data was initialized in `new`. We run its destructor here.
unsafe { core::ptr::drop_in_place(drvdata_ptr.cast::<T>()) };
// Now, call the original release function to free the `pwm_chip` itself.
// SAFETY: `dev` is the valid pointer passed into this callback, which is
// the expected argument for `pwmchip_release`.
unsafe {
bindings::pwmchip_release(dev);
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn request_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
) -> c_int {
// SAFETY: PWM core guarentees `chip_ptr` and `pwm_ptr` are valid pointers.
let (chip, pwm) = unsafe { (Chip::<T>::from_raw(chip_ptr), Device::from_raw(pwm_ptr)) };
// SAFETY: The PWM core guarantees the parent device exists and is bound during callbacks.
let bound_parent = unsafe { chip.bound_parent_device() };
match T::request(chip, pwm, bound_parent) {
Ok(()) => 0,
Err(e) => e.to_errno(),
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn capture_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
res: *mut bindings::pwm_capture,
timeout: usize,
) -> c_int {
// SAFETY: Relies on the function's contract that `chip_ptr` and `pwm_ptr` are valid
// pointers.
let (chip, pwm, result) = unsafe {
(
Chip::<T>::from_raw(chip_ptr),
Device::from_raw(pwm_ptr),
&mut *res,
)
};
// SAFETY: The PWM core guarantees the parent device exists and is bound during callbacks.
let bound_parent = unsafe { chip.bound_parent_device() };
match T::capture(chip, pwm, result, timeout, bound_parent) {
Ok(()) => 0,
Err(e) => e.to_errno(),
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn round_waveform_tohw_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
wf_ptr: *const bindings::pwm_waveform,
wfhw_ptr: *mut c_void,
) -> c_int {
// SAFETY: Relies on the function's contract that `chip_ptr` and `pwm_ptr` are valid
// pointers.
let (chip, pwm, wf) = unsafe {
(
Chip::<T>::from_raw(chip_ptr),
Device::from_raw(pwm_ptr),
Waveform::from(*wf_ptr),
)
};
match T::round_waveform_tohw(chip, pwm, &wf) {
Ok(rounded) => {
// SAFETY: `wfhw_ptr` is valid per this function's safety contract.
if unsafe { Self::serialize_wfhw(&rounded.hardware_waveform, wfhw_ptr) }.is_err() {
return EINVAL.to_errno();
}
rounded.status
}
Err(e) => e.to_errno(),
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn round_waveform_fromhw_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
wfhw_ptr: *const c_void,
wf_ptr: *mut bindings::pwm_waveform,
) -> c_int {
// SAFETY: Relies on the function's contract that `chip_ptr` and `pwm_ptr` are valid
// pointers.
let (chip, pwm) = unsafe { (Chip::<T>::from_raw(chip_ptr), Device::from_raw(pwm_ptr)) };
// SAFETY: `deserialize_wfhw`'s safety contract is met by this function's contract.
let wfhw = match unsafe { Self::deserialize_wfhw(wfhw_ptr) } {
Ok(v) => v,
Err(e) => return e.to_errno(),
};
let mut rust_wf = Waveform::default();
match T::round_waveform_fromhw(chip, pwm, &wfhw, &mut rust_wf) {
Ok(()) => {
// SAFETY: `wf_ptr` is guaranteed valid by the C caller.
unsafe {
*wf_ptr = rust_wf.into();
};
0
}
Err(e) => e.to_errno(),
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn read_waveform_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
wfhw_ptr: *mut c_void,
) -> c_int {
// SAFETY: Relies on the function's contract that `chip_ptr` and `pwm_ptr` are valid
// pointers.
let (chip, pwm) = unsafe { (Chip::<T>::from_raw(chip_ptr), Device::from_raw(pwm_ptr)) };
// SAFETY: The PWM core guarantees the parent device exists and is bound during callbacks.
let bound_parent = unsafe { chip.bound_parent_device() };
match T::read_waveform(chip, pwm, bound_parent) {
// SAFETY: `wfhw_ptr` is valid per this function's safety contract.
Ok(wfhw) => match unsafe { Self::serialize_wfhw(&wfhw, wfhw_ptr) } {
Ok(()) => 0,
Err(e) => e.to_errno(),
},
Err(e) => e.to_errno(),
}
}
/// # Safety
///
/// Pointers from C must be valid.
unsafe extern "C" fn write_waveform_callback(
chip_ptr: *mut bindings::pwm_chip,
pwm_ptr: *mut bindings::pwm_device,
wfhw_ptr: *const c_void,
) -> c_int {
// SAFETY: Relies on the function's contract that `chip_ptr` and `pwm_ptr` are valid
// pointers.
let (chip, pwm) = unsafe { (Chip::<T>::from_raw(chip_ptr), Device::from_raw(pwm_ptr)) };
// SAFETY: The PWM core guarantees the parent device exists and is bound during callbacks.
let bound_parent = unsafe { chip.bound_parent_device() };
// SAFETY: `wfhw_ptr` is valid per this function's safety contract.
let wfhw = match unsafe { Self::deserialize_wfhw(wfhw_ptr) } {
Ok(v) => v,
Err(e) => return e.to_errno(),
};
match T::write_waveform(chip, pwm, &wfhw, bound_parent) {
Ok(()) => 0,
Err(e) => e.to_errno(),
}
}
}
/// VTable structure wrapper for PWM operations.
/// Mirrors [`struct pwm_ops`](srctree/include/linux/pwm.h).
#[repr(transparent)]
pub struct PwmOpsVTable(bindings::pwm_ops);
// SAFETY: PwmOpsVTable is Send. The vtable contains only function pointers
// and a size, which are simple data types that can be safely moved across
// threads. The thread-safety of calling these functions is handled by the
// kernel's locking mechanisms.
unsafe impl Send for PwmOpsVTable {}
// SAFETY: PwmOpsVTable is Sync. The vtable is immutable after it is created,
// so it can be safely referenced and accessed concurrently by multiple threads
// e.g. to read the function pointers.
unsafe impl Sync for PwmOpsVTable {}
impl PwmOpsVTable {
/// Returns a raw pointer to the underlying `pwm_ops` struct.
pub(crate) fn as_raw(&self) -> *const bindings::pwm_ops {
&self.0
}
}
/// 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<T: PwmOps>() -> PwmOpsVTable {
// SAFETY: `core::mem::zeroed()` is unsafe. For `pwm_ops`, all fields are
// `Option<extern "C" fn(...)>` or data, so a zeroed pattern (None/0) is valid initially.
let mut ops: bindings::pwm_ops = unsafe { core::mem::zeroed() };
ops.request = Some(Adapter::<T>::request_callback);
ops.capture = Some(Adapter::<T>::capture_callback);
ops.round_waveform_tohw = Some(Adapter::<T>::round_waveform_tohw_callback);
ops.round_waveform_fromhw = Some(Adapter::<T>::round_waveform_fromhw_callback);
ops.read_waveform = Some(Adapter::<T>::read_waveform_callback);
ops.write_waveform = Some(Adapter::<T>::write_waveform_callback);
ops.sizeof_wfhw = core::mem::size_of::<T::WfHw>();
PwmOpsVTable(ops)
}
/// Wrapper for a PWM chip/controller ([`struct pwm_chip`](srctree/include/linux/pwm.h)).
#[repr(transparent)]
pub struct Chip<T: PwmOps>(Opaque<bindings::pwm_chip>, PhantomData<T>);
impl<T: PwmOps> Chip<T> {
/// Creates a reference to a [`Chip`] from a valid pointer.
///
/// # Safety
///
/// The caller must ensure that `ptr` is valid and remains valid for the lifetime of the
/// returned [`Chip`] reference.
pub(crate) unsafe fn from_raw<'a>(ptr: *mut bindings::pwm_chip) -> &'a Self {
// SAFETY: The safety requirements guarantee the validity of the dereference, while the
// `Chip` type being transparent makes the cast ok.
unsafe { &*ptr.cast::<Self>() }
}
/// Returns a raw pointer to the underlying `pwm_chip`.
pub(crate) fn as_raw(&self) -> *mut bindings::pwm_chip {
self.0.get()
}
/// Gets the number of PWM channels (hardware PWMs) on this chip.
pub fn num_channels(&self) -> u32 {
// SAFETY: `self.as_raw()` provides a valid pointer for `self`'s lifetime.
unsafe { (*self.as_raw()).npwm }
}
/// Returns `true` if the chip supports atomic operations for configuration.
pub fn is_atomic(&self) -> bool {
// SAFETY: `self.as_raw()` provides a valid pointer for `self`'s lifetime.
unsafe { (*self.as_raw()).atomic }
}
/// Returns a reference to the embedded `struct device` abstraction.
pub fn device(&self) -> &device::Device {
// SAFETY:
// - `self.as_raw()` provides a valid pointer to `bindings::pwm_chip`.
// - The `dev` field is an instance of `bindings::device` embedded
// within `pwm_chip`.
// - Taking a pointer to this embedded field is valid.
// - `device::Device` is `#[repr(transparent)]`.
// - The lifetime of the returned reference is tied to `self`.
unsafe { device::Device::from_raw(&raw mut (*self.as_raw()).dev) }
}
/// Gets the typed driver specific data associated with this chip's embedded device.
pub fn drvdata(&self) -> &T {
// SAFETY: `pwmchip_get_drvdata` returns the pointer to the private data area,
// which we know holds our `T`. The pointer is valid for the lifetime of `self`.
unsafe { &*bindings::pwmchip_get_drvdata(self.as_raw()).cast::<T>() }
}
/// Returns a reference to the parent device of this PWM chip's device.
///
/// # Safety
///
/// The caller must guarantee that the parent device exists and is bound.
/// This is guaranteed by the PWM core during `PwmOps` callbacks.
unsafe fn bound_parent_device(&self) -> &device::Device<Bound> {
// SAFETY: Per the function's safety contract, the parent device exists.
let parent = unsafe { self.device().parent().unwrap_unchecked() };
// SAFETY: Per the function's safety contract, the parent device is bound.
// This is guaranteed by the PWM core during `PwmOps` callbacks.
unsafe { parent.as_bound() }
}
/// Allocates and wraps a PWM chip using `bindings::pwmchip_alloc`.
///
/// Returns an [`ARef<Chip>`] managing the chip's lifetime via refcounting
/// on its embedded `struct device`.
pub fn new(
parent_dev: &device::Device,
num_channels: u32,
data: impl pin_init::PinInit<T, Error>,
) -> Result<ARef<Self>> {
let sizeof_priv = core::mem::size_of::<T>();
// SAFETY: `pwmchip_alloc` allocates memory for the C struct and our private data.
let c_chip_ptr_raw =
unsafe { bindings::pwmchip_alloc(parent_dev.as_raw(), num_channels, sizeof_priv) };
let c_chip_ptr: *mut bindings::pwm_chip = error::from_err_ptr(c_chip_ptr_raw)?;
// SAFETY: The `drvdata` pointer is the start of the private area, which is where
// we will construct our `T` object.
let drvdata_ptr = unsafe { bindings::pwmchip_get_drvdata(c_chip_ptr) };
// SAFETY: We construct the `T` object in-place in the allocated private memory.
unsafe { data.__pinned_init(drvdata_ptr.cast())? };
// SAFETY: `c_chip_ptr` points to a valid chip.
unsafe {
(*c_chip_ptr).dev.release = Some(Adapter::<T>::release_callback);
}
// SAFETY: `c_chip_ptr` points to a valid chip.
// The `Adapter`'s `VTABLE` has a 'static lifetime, so the pointer
// returned by `as_raw()` is always valid.
unsafe {
(*c_chip_ptr).ops = Adapter::<T>::VTABLE.as_raw();
}
// Cast the `*mut bindings::pwm_chip` to `*mut Chip`. This is valid because
// `Chip` is `repr(transparent)` over `Opaque<bindings::pwm_chip>`, and
// `Opaque<T>` is `repr(transparent)` over `T`.
let chip_ptr_as_self = c_chip_ptr.cast::<Self>();
// SAFETY: `chip_ptr_as_self` points to a valid `Chip` (layout-compatible with
// `bindings::pwm_chip`) whose embedded device has refcount 1.
// `ARef::from_raw` takes this pointer and manages it via `AlwaysRefCounted`.
Ok(unsafe { ARef::from_raw(NonNull::new_unchecked(chip_ptr_as_self)) })
}
}
// SAFETY: Implements refcounting for `Chip` using the embedded `struct device`.
unsafe impl<T: PwmOps> AlwaysRefCounted for Chip<T> {
#[inline]
fn inc_ref(&self) {
// SAFETY: `self.0.get()` points to a valid `pwm_chip` because `self` exists.
// The embedded `dev` is valid. `get_device` increments its refcount.
unsafe {
bindings::get_device(&raw mut (*self.0.get()).dev);
}
}
#[inline]
unsafe fn dec_ref(obj: NonNull<Chip<T>>) {
let c_chip_ptr = obj.cast::<bindings::pwm_chip>().as_ptr();
// SAFETY: `obj` is a valid pointer to a `Chip` (and thus `bindings::pwm_chip`)
// with a non-zero refcount. `put_device` handles decrement and final release.
unsafe {
bindings::put_device(&raw mut (*c_chip_ptr).dev);
}
}
}
// SAFETY: `Chip` is a wrapper around `*mut bindings::pwm_chip`. The underlying C
// structure's state is managed and synchronized by the kernel's device model
// and PWM core locking mechanisms. Therefore, it is safe to move the `Chip`
// wrapper (and the pointer it contains) across threads.
unsafe impl<T: PwmOps + Send> Send for Chip<T> {}
// SAFETY: It is safe for multiple threads to have shared access (`&Chip`) because
// the `Chip` data is immutable from the Rust side without holding the appropriate
// kernel locks, which the C core is responsible for. Any interior mutability is
// handled and synchronized by the C kernel code.
unsafe impl<T: PwmOps + Sync> Sync for Chip<T> {}
/// A resource guard that ensures `pwmchip_remove` is called on drop.
///
/// This struct is intended to be managed by the `devres` framework by transferring its ownership
/// via [`devres::register`]. This ties the lifetime of the PWM chip registration
/// to the lifetime of the underlying device.
pub struct Registration<T: PwmOps> {
chip: ARef<Chip<T>>,
}
impl<T: 'static + PwmOps + Send + Sync> Registration<T> {
/// Registers a PWM chip with the PWM subsystem.
///
/// Transfers its ownership to the `devres` framework, which ties its lifetime
/// to the parent device.
/// On unbind of the parent device, the `devres` entry will be dropped, automatically
/// calling `pwmchip_remove`. This function should be called from the driver's `probe`.
pub fn register(dev: &device::Device<Bound>, chip: ARef<Chip<T>>) -> Result {
let chip_parent = chip.device().parent().ok_or(EINVAL)?;
if dev.as_raw() != chip_parent.as_raw() {
return Err(EINVAL);
}
let c_chip_ptr = chip.as_raw();
// SAFETY: `c_chip_ptr` points to a valid chip with its ops initialized.
// `__pwmchip_add` is the C function to register the chip with the PWM core.
unsafe {
to_result(bindings::__pwmchip_add(c_chip_ptr, core::ptr::null_mut()))?;
}
let registration = Registration { chip };
devres::register(dev, registration, GFP_KERNEL)
}
}
impl<T: PwmOps> Drop for Registration<T> {
fn drop(&mut self) {
let chip_raw = self.chip.as_raw();
// SAFETY: `chip_raw` points to a chip that was successfully registered.
// `bindings::pwmchip_remove` is the correct C function to unregister it.
// This `drop` implementation is called automatically by `devres` on driver unbind.
unsafe {
bindings::pwmchip_remove(chip_raw);
}
}
}
/// Declares a kernel module that exposes a single PWM driver.
///
/// # Examples
///
///```ignore
/// kernel::module_pwm_platform_driver! {
/// type: MyDriver,
/// name: "Module name",
/// authors: ["Author name"],
/// description: "Description",
/// license: "GPL v2",
/// }
///```
#[macro_export]
macro_rules! module_pwm_platform_driver {
($($user_args:tt)*) => {
$crate::module_platform_driver! {
$($user_args)*
imports_ns: ["PWM"],
}
};
}

View File

@ -98,6 +98,7 @@ struct ModuleInfo {
description: Option<String>,
alias: Option<Vec<String>>,
firmware: Option<Vec<String>>,
imports_ns: Option<Vec<String>>,
}
impl ModuleInfo {
@ -112,6 +113,7 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
"license",
"alias",
"firmware",
"imports_ns",
];
const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
let mut seen_keys = Vec::new();
@ -137,6 +139,7 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
"license" => info.license = expect_string_ascii(it),
"alias" => info.alias = Some(expect_string_array(it)),
"firmware" => info.firmware = Some(expect_string_array(it)),
"imports_ns" => info.imports_ns = Some(expect_string_array(it)),
_ => panic!("Unknown key \"{key}\". Valid keys are: {EXPECTED_KEYS:?}."),
}
@ -195,6 +198,11 @@ pub(crate) fn module(ts: TokenStream) -> TokenStream {
modinfo.emit("firmware", &fw);
}
}
if let Some(imports) = info.imports_ns {
for ns in imports {
modinfo.emit("import_ns", &ns);
}
}
// Built-in modules also export the `file` modinfo string.
let file =