mirror of https://github.com/torvalds/linux.git
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:
commit
77956cf364
|
|
@ -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>;
|
||||||
|
};
|
||||||
|
};
|
||||||
10
MAINTAINERS
10
MAINTAINERS
|
|
@ -20871,6 +20871,14 @@ F: include/linux/pwm.h
|
||||||
F: include/linux/pwm_backlight.h
|
F: include/linux/pwm_backlight.h
|
||||||
K: pwm_(config|apply_might_sleep|apply_atomic|ops)
|
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
|
PXA GPIO DRIVER
|
||||||
M: Robert Jarzmik <robert.jarzmik@free.fr>
|
M: Robert Jarzmik <robert.jarzmik@free.fr>
|
||||||
L: linux-gpio@vger.kernel.org
|
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/mailbox/thead,th1520-mbox.yaml
|
||||||
F: Documentation/devicetree/bindings/net/thead,th1520-gmac.yaml
|
F: Documentation/devicetree/bindings/net/thead,th1520-gmac.yaml
|
||||||
F: Documentation/devicetree/bindings/pinctrl/thead,th1520-pinctrl.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: Documentation/devicetree/bindings/reset/thead,th1520-reset.yaml
|
||||||
F: arch/riscv/boot/dts/thead/
|
F: arch/riscv/boot/dts/thead/
|
||||||
F: drivers/clk/thead/clk-th1520-ap.c
|
F: drivers/clk/thead/clk-th1520-ap.c
|
||||||
|
|
@ -22303,6 +22312,7 @@ F: drivers/pinctrl/pinctrl-th1520.c
|
||||||
F: drivers/pmdomain/thead/
|
F: drivers/pmdomain/thead/
|
||||||
F: drivers/power/reset/th1520-aon-reboot.c
|
F: drivers/power/reset/th1520-aon-reboot.c
|
||||||
F: drivers/power/sequencing/pwrseq-thead-gpu.c
|
F: drivers/power/sequencing/pwrseq-thead-gpu.c
|
||||||
|
F: drivers/pwm/pwm_th1520.rs
|
||||||
F: drivers/reset/reset-th1520.c
|
F: drivers/reset/reset-th1520.c
|
||||||
F: include/dt-bindings/clock/thead,th1520-clk-ap.h
|
F: include/dt-bindings/clock/thead,th1520-clk-ap.h
|
||||||
F: include/dt-bindings/power/thead,th1520-power.h
|
F: include/dt-bindings/power/thead,th1520-power.h
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,16 @@ config PWM_ADP5585
|
||||||
This option enables support for the PWM function found in the Analog
|
This option enables support for the PWM function found in the Analog
|
||||||
Devices ADP5585.
|
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
|
config PWM_APPLE
|
||||||
tristate "Apple SoC PWM support"
|
tristate "Apple SoC PWM support"
|
||||||
depends on ARCH_APPLE || COMPILE_TEST
|
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
|
To compile this driver as a module, choose M here: the module
|
||||||
will be called pwm-tegra.
|
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
|
config PWM_TIECAP
|
||||||
tristate "ECAP PWM support"
|
tristate "ECAP PWM support"
|
||||||
depends on ARCH_OMAP2PLUS || ARCH_DAVINCI_DA8XX || ARCH_KEYSTONE || ARCH_K3 || COMPILE_TEST
|
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
|
To compile this driver as a module, choose M here: the module
|
||||||
will be called pwm-xilinx.
|
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
|
endif
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
obj-$(CONFIG_PWM) += core.o
|
obj-$(CONFIG_PWM) += core.o
|
||||||
obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o
|
obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o
|
||||||
obj-$(CONFIG_PWM_ADP5585) += pwm-adp5585.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_APPLE) += pwm-apple.o
|
||||||
obj-$(CONFIG_PWM_ARGON_FAN_HAT) += pwm-argon-fan-hat.o
|
obj-$(CONFIG_PWM_ARGON_FAN_HAT) += pwm-argon-fan-hat.o
|
||||||
obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.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_SUN4I) += pwm-sun4i.o
|
||||||
obj-$(CONFIG_PWM_SUNPLUS) += pwm-sunplus.o
|
obj-$(CONFIG_PWM_SUNPLUS) += pwm-sunplus.o
|
||||||
obj-$(CONFIG_PWM_TEGRA) += pwm-tegra.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_TIECAP) += pwm-tiecap.o
|
||||||
obj-$(CONFIG_PWM_TIEHRPWM) += pwm-tiehrpwm.o
|
obj-$(CONFIG_PWM_TIEHRPWM) += pwm-tiehrpwm.o
|
||||||
obj-$(CONFIG_PWM_TWL) += pwm-twl.o
|
obj-$(CONFIG_PWM_TWL) += pwm-twl.o
|
||||||
|
|
|
||||||
|
|
@ -1608,12 +1608,13 @@ void pwmchip_put(struct pwm_chip *chip)
|
||||||
}
|
}
|
||||||
EXPORT_SYMBOL_GPL(pwmchip_put);
|
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);
|
struct pwm_chip *chip = pwmchip_from_dev(pwmchip_dev);
|
||||||
|
|
||||||
kfree(chip);
|
kfree(chip);
|
||||||
}
|
}
|
||||||
|
EXPORT_SYMBOL_GPL(pwmchip_release);
|
||||||
|
|
||||||
struct pwm_chip *pwmchip_alloc(struct device *parent, unsigned int npwm, size_t sizeof_priv)
|
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;
|
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,
|
(char *)s->private, chip->id,
|
||||||
pwmchip_parent(chip)->bus ? pwmchip_parent(chip)->bus->name : "no-bus",
|
pwmchip_parent(chip)->bus ? pwmchip_parent(chip)->bus->name : "no-bus",
|
||||||
dev_name(pwmchip_parent(chip)), chip->npwm,
|
dev_name(pwmchip_parent(chip)), chip->npwm);
|
||||||
(chip->npwm != 1) ? "s" : "");
|
|
||||||
|
|
||||||
pwm_dbg_show(chip, s);
|
pwm_dbg_show(chip, s);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -34,29 +34,6 @@ static inline struct bcm2835_pwm *to_bcm2835_pwm(struct pwm_chip *chip)
|
||||||
return pwmchip_get_drvdata(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,
|
static int bcm2835_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
const struct pwm_state *state)
|
const struct pwm_state *state)
|
||||||
{
|
{
|
||||||
|
|
@ -102,6 +79,9 @@ static int bcm2835_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
/* set polarity */
|
/* set polarity */
|
||||||
val = readl(pc->base + PWM_CONTROL);
|
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)
|
if (state->polarity == PWM_POLARITY_NORMAL)
|
||||||
val &= ~(PWM_POLARITY << PWM_CONTROL_SHIFT(pwm->hwpwm));
|
val &= ~(PWM_POLARITY << PWM_CONTROL_SHIFT(pwm->hwpwm));
|
||||||
else
|
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 = {
|
static const struct pwm_ops bcm2835_pwm_ops = {
|
||||||
.request = bcm2835_pwm_request,
|
|
||||||
.free = bcm2835_pwm_free,
|
|
||||||
.apply = bcm2835_pwm_apply,
|
.apply = bcm2835_pwm_apply,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ static int max7360_pwm_round_waveform_tohw(struct pwm_chip *chip,
|
||||||
duty_steps = MAX7360_PWM_MAX - 1;
|
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;
|
wfhw->enabled = !!wf->period_length_ns;
|
||||||
|
|
||||||
if (wf->period_length_ns && wf->period_length_ns < MAX7360_PWM_PERIOD_NS)
|
if (wf->period_length_ns && wf->period_length_ns < MAX7360_PWM_PERIOD_NS)
|
||||||
|
|
|
||||||
|
|
@ -135,50 +135,51 @@ static inline u32 pwm_mediatek_readl(struct pwm_mediatek_chip *chip,
|
||||||
num * chip->soc->chanreg_width + offset);
|
num * chip->soc->chanreg_width + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pwm_mediatek_enable(struct pwm_chip *chip, struct pwm_device *pwm)
|
struct pwm_mediatek_waveform {
|
||||||
{
|
u32 enable;
|
||||||
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
|
u32 con;
|
||||||
u32 value;
|
u32 width;
|
||||||
|
u32 thres;
|
||||||
value = readl(pc->regs);
|
};
|
||||||
value |= BIT(pwm->hwpwm);
|
|
||||||
writel(value, pc->regs);
|
static int pwm_mediatek_round_waveform_tohw(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
}
|
const struct pwm_waveform *wf, void *_wfhw)
|
||||||
|
|
||||||
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 *wfhw = _wfhw;
|
||||||
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
|
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
|
||||||
u32 clkdiv, enable;
|
u32 clkdiv, enable;
|
||||||
u32 reg_width = PWMDWIDTH, reg_thres = PWMTHRES;
|
|
||||||
u64 cnt_period, cnt_duty;
|
u64 cnt_period, cnt_duty;
|
||||||
unsigned long clk_rate;
|
unsigned long clk_rate;
|
||||||
int ret;
|
int ret = 0;
|
||||||
|
|
||||||
ret = pwm_mediatek_clk_enable(pc, pwm->hwpwm);
|
if (wf->period_length_ns == 0) {
|
||||||
if (ret < 0)
|
*wfhw = (typeof(*wfhw)){
|
||||||
return ret;
|
.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;
|
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 */
|
cnt_period = mul_u64_u64_div_u64(wf->period_length_ns, clk_rate, NSEC_PER_SEC);
|
||||||
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);
|
|
||||||
if (cnt_period == 0) {
|
if (cnt_period == 0) {
|
||||||
ret = -ERANGE;
|
cnt_period = 1;
|
||||||
goto out;
|
ret = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cnt_period > FIELD_MAX(PWMDWIDTH_PERIOD) + 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;
|
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)
|
if (cnt_duty > cnt_period)
|
||||||
cnt_duty = cnt_period;
|
cnt_duty = cnt_period;
|
||||||
|
|
||||||
|
|
@ -206,26 +207,173 @@ static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
|
|
||||||
cnt_period -= 1;
|
cnt_period -= 1;
|
||||||
|
|
||||||
dev_dbg(&chip->dev, "pwm#%u: %lld/%lld @%lu -> CON: %x, PERIOD: %llx, DUTY: %llx\n",
|
dev_dbg(&chip->dev, "pwm#%u: %lld/%lld @%lu -> ENABLE: %x, CON: %x, PERIOD: %llx, DUTY: %llx\n",
|
||||||
pwm->hwpwm, duty_ns, period_ns, clk_rate, clkdiv, cnt_period, cnt_duty);
|
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);
|
||||||
|
|
||||||
if (pc->soc->pwm45_fixup && pwm->hwpwm > 2) {
|
|
||||||
/*
|
/*
|
||||||
* PWM[4,5] has distinct offset for PWMDWIDTH and PWMTHRES
|
* cnt_period is a 13 bit value, NSEC_PER_SEC is 30 bits wide
|
||||||
* from the other PWMs on MT7623.
|
* and clkdiv is less than 8, so the multiplication doesn't
|
||||||
|
* overflow an u64.
|
||||||
*/
|
*/
|
||||||
reg_width = PWM45DWIDTH_FIXUP;
|
*wf = (typeof(*wf)){
|
||||||
reg_thres = PWM45THRES_FIXUP;
|
.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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pwm_mediatek_writel(pc, pwm->hwpwm, PWMCON, BIT(15) | clkdiv);
|
dev_dbg(&chip->dev, "pwm#%u: ENABLE: %x, CLKDIV: %x, PERIOD: %x, DUTY: %x @%lu -> %lld/%lld\n",
|
||||||
pwm_mediatek_writel(pc, pwm->hwpwm, reg_width, cnt_period);
|
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 (enable) {
|
||||||
pwm_mediatek_writel(pc, pwm->hwpwm, reg_thres, cnt_duty);
|
if (pc->soc->pwm45_fixup && pwm->hwpwm > 2) {
|
||||||
pwm_mediatek_enable(chip, pwm);
|
/*
|
||||||
|
* PWM[4,5] has distinct offset for PWMDWIDTH and PWMTHRES
|
||||||
|
* from the other PWMs on MT7623.
|
||||||
|
*/
|
||||||
|
reg_width = PWM45DWIDTH_FIXUP;
|
||||||
|
reg_thres = PWM45THRES_FIXUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
*wfhw = (typeof(*wfhw)){
|
||||||
|
.enable = enable,
|
||||||
|
.con = BIT(15) | clkdiv,
|
||||||
|
.width = cnt_period,
|
||||||
|
.thres = cnt_duty,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
pwm_mediatek_disable(chip, pwm);
|
*wfhw = (typeof(*wfhw)){
|
||||||
|
.enable = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
/*
|
||||||
|
* PWM[4,5] has distinct offset for PWMDWIDTH and PWMTHRES
|
||||||
|
* from the other PWMs on MT7623.
|
||||||
|
*/
|
||||||
|
reg_width = PWM45DWIDTH_FIXUP;
|
||||||
|
reg_thres = PWM45THRES_FIXUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(ctrl & BIT(pwm->hwpwm))) {
|
||||||
|
/*
|
||||||
|
* The clks are already on, just increasing the usage
|
||||||
|
* counter doesn't fail.
|
||||||
|
*/
|
||||||
|
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:
|
out:
|
||||||
|
|
@ -234,93 +382,12 @@ static int pwm_mediatek_config(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
return ret;
|
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)
|
|
||||||
{
|
|
||||||
struct pwm_mediatek_chip *pc = to_pwm_mediatek_chip(chip);
|
|
||||||
int ret;
|
|
||||||
u32 enable;
|
|
||||||
u32 reg_width = PWMDWIDTH, reg_thres = PWMTHRES;
|
|
||||||
|
|
||||||
if (pc->soc->pwm45_fixup && pwm->hwpwm > 2) {
|
|
||||||
/*
|
|
||||||
* PWM[4,5] has distinct offset for PWMDWIDTH and PWMTHRES
|
|
||||||
* from the other PWMs on MT7623.
|
|
||||||
*/
|
|
||||||
reg_width = PWM45DWIDTH_FIXUP;
|
|
||||||
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));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pwm_mediatek_clk_disable(pc, pwm->hwpwm);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct pwm_ops pwm_mediatek_ops = {
|
static const struct pwm_ops pwm_mediatek_ops = {
|
||||||
.apply = pwm_mediatek_apply,
|
.sizeof_wfhw = sizeof(struct pwm_mediatek_waveform),
|
||||||
.get_state = pwm_mediatek_get_state,
|
.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)
|
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);
|
soc = of_device_get_match_data(&pdev->dev);
|
||||||
|
|
||||||
chip = devm_pwmchip_alloc(&pdev->dev, soc->num_pwms,
|
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))
|
if (IS_ERR(chip))
|
||||||
return PTR_ERR(chip);
|
return PTR_ERR(chip);
|
||||||
pc = to_pwm_mediatek_chip(chip);
|
pc = to_pwm_mediatek_chip(chip);
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@ static inline unsigned int rzg2l_gpt_subchannel(unsigned int hwpwm)
|
||||||
return hwpwm & 0x1;
|
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)
|
static void rzg2l_gpt_write(struct rzg2l_gpt_chip *rzg2l_gpt, u32 reg, u32 data)
|
||||||
{
|
{
|
||||||
writel(data, rzg2l_gpt->mmio + reg);
|
writel(data, rzg2l_gpt->mmio + reg);
|
||||||
|
|
@ -271,10 +276,14 @@ static int rzg2l_gpt_config(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
* in use with different settings.
|
* in use with different settings.
|
||||||
*/
|
*/
|
||||||
if (rzg2l_gpt->channel_request_count[ch] > 1) {
|
if (rzg2l_gpt->channel_request_count[ch] > 1) {
|
||||||
if (period_ticks < rzg2l_gpt->period_ticks[ch])
|
u8 sibling_ch = rzg2l_gpt_sibling(pwm->hwpwm);
|
||||||
return -EBUSY;
|
|
||||||
else
|
if (rzg2l_gpt_is_ch_enabled(rzg2l_gpt, sibling_ch)) {
|
||||||
|
if (period_ticks < rzg2l_gpt->period_ticks[ch])
|
||||||
|
return -EBUSY;
|
||||||
|
|
||||||
period_ticks = rzg2l_gpt->period_ticks[ch];
|
period_ticks = rzg2l_gpt->period_ticks[ch];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prescale = rzg2l_gpt_calculate_prescale(rzg2l_gpt, period_ticks);
|
prescale = rzg2l_gpt_calculate_prescale(rzg2l_gpt, period_ticks);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
@ -488,6 +488,12 @@ int __pwmchip_add(struct pwm_chip *chip, struct module *owner);
|
||||||
#define pwmchip_add(chip) __pwmchip_add(chip, THIS_MODULE)
|
#define pwmchip_add(chip) __pwmchip_add(chip, THIS_MODULE)
|
||||||
void pwmchip_remove(struct pwm_chip *chip);
|
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);
|
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)
|
#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
|
#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 pwm_lookup {
|
||||||
struct list_head list;
|
struct list_head list;
|
||||||
const char *provider;
|
const char *provider;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
#include <linux/pm_opp.h>
|
#include <linux/pm_opp.h>
|
||||||
#include <linux/poll.h>
|
#include <linux/poll.h>
|
||||||
#include <linux/property.h>
|
#include <linux/property.h>
|
||||||
|
#include <linux/pwm.h>
|
||||||
#include <linux/random.h>
|
#include <linux/random.h>
|
||||||
#include <linux/refcount.h>
|
#include <linux/refcount.h>
|
||||||
#include <linux/regulator/consumer.h>
|
#include <linux/regulator/consumer.h>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
#include "poll.c"
|
#include "poll.c"
|
||||||
#include "processor.c"
|
#include "processor.c"
|
||||||
#include "property.c"
|
#include "property.c"
|
||||||
|
#include "pwm.c"
|
||||||
#include "rbtree.c"
|
#include "rbtree.c"
|
||||||
#include "rcu.c"
|
#include "rcu.c"
|
||||||
#include "refcount.c"
|
#include "refcount.c"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -125,6 +125,8 @@
|
||||||
pub mod print;
|
pub mod print;
|
||||||
pub mod processor;
|
pub mod processor;
|
||||||
pub mod ptr;
|
pub mod ptr;
|
||||||
|
#[cfg(CONFIG_RUST_PWM_ABSTRACTIONS)]
|
||||||
|
pub mod pwm;
|
||||||
pub mod rbtree;
|
pub mod rbtree;
|
||||||
pub mod regulator;
|
pub mod regulator;
|
||||||
pub mod revocable;
|
pub mod revocable;
|
||||||
|
|
|
||||||
|
|
@ -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 period’s 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"],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -98,6 +98,7 @@ struct ModuleInfo {
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
alias: Option<Vec<String>>,
|
alias: Option<Vec<String>>,
|
||||||
firmware: Option<Vec<String>>,
|
firmware: Option<Vec<String>>,
|
||||||
|
imports_ns: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleInfo {
|
impl ModuleInfo {
|
||||||
|
|
@ -112,6 +113,7 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
|
||||||
"license",
|
"license",
|
||||||
"alias",
|
"alias",
|
||||||
"firmware",
|
"firmware",
|
||||||
|
"imports_ns",
|
||||||
];
|
];
|
||||||
const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
|
const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
|
||||||
let mut seen_keys = Vec::new();
|
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),
|
"license" => info.license = expect_string_ascii(it),
|
||||||
"alias" => info.alias = Some(expect_string_array(it)),
|
"alias" => info.alias = Some(expect_string_array(it)),
|
||||||
"firmware" => info.firmware = 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:?}."),
|
_ => panic!("Unknown key \"{key}\". Valid keys are: {EXPECTED_KEYS:?}."),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +198,11 @@ pub(crate) fn module(ts: TokenStream) -> TokenStream {
|
||||||
modinfo.emit("firmware", &fw);
|
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.
|
// Built-in modules also export the `file` modinfo string.
|
||||||
let file =
|
let file =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue