Signed-off-by: Nicolas Frattaroli --- MAINTAINERS | 1 + drivers/counter/Kconfig | 13 ++ drivers/counter/Makefile | 1 + drivers/counter/rockchip-pwm-capture.c | 341 +++++++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 3ddd245fd4ad8d9ed2e762910a7a1f6436f93e34..e5d26256d05a04a9642371cf3dbb4dd0c1c34e68 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20891,6 +20891,7 @@ L: linux-rockchip@lists.infradead.org L: linux-pwm@vger.kernel.org S: Maintained F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml +F: drivers/counter/rockchip-pwm-capture.c F: drivers/pwm/pwm-rockchip-v4.c F: drivers/soc/rockchip/mfpwm.c F: include/soc/rockchip/mfpwm.h diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig index d30d22dfe57741b145a45632b6325d5f9680590e..01b4f5c326478c73b518041830ee0d65b37f6833 100644 --- a/drivers/counter/Kconfig +++ b/drivers/counter/Kconfig @@ -90,6 +90,19 @@ config MICROCHIP_TCB_CAPTURE To compile this driver as a module, choose M here: the module will be called microchip-tcb-capture. +config ROCKCHIP_PWM_CAPTURE + tristate "Rockchip PWM Counter Capture driver" + depends on ARCH_ROCKCHIP || COMPILE_TEST + depends on ROCKCHIP_MFPWM + depends on HAS_IOMEM + help + Generic counter framework driver for the multi-function PWM on + Rockchip SoCs such as the RK3576. + + Uses the Rockchip Multi-function PWM controller driver infrastructure + to guarantee exclusive operation with other functions of the same + device implemented by drivers in other subsystems. + config RZ_MTU3_CNT tristate "Renesas RZ/G2L MTU3a counter driver" depends on RZ_MTU3 diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile index fa3c1d08f7068835aa912aa13bc92bcfd44d16fb..2bfcfc2c584bd174a9885064746a98f15b204aec 100644 --- a/drivers/counter/Makefile +++ b/drivers/counter/Makefile @@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o obj-$(CONFIG_INTEL_QEP) += intel-qep.o obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o +obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o diff --git a/drivers/counter/rockchip-pwm-capture.c b/drivers/counter/rockchip-pwm-capture.c new file mode 100644 index 0000000000000000000000000000000000000000..b2bfa2c6e04dfa0410fa0d7ef1c395217e4a9db2 --- /dev/null +++ b/drivers/counter/rockchip-pwm-capture.c @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2025 Collabora Ltd. + * + * A counter driver for the Pulse-Width-Modulation (PWM) hardware found on + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". It + * allows for measuring the period and duty cycle in nanoseconds through the + * generic counter framework, while guaranteeing exclusive use over the MFPWM + * device while the counter is enabled. + * + * Authors: + * Nicolas Frattaroli + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC) +/* + * amount of jiffies between no PWM signal change and us deciding PWM is off. + * PWM signals with a period longer than this won't be detected by us, which is + * the trade-off we make to have a faster response time when a signal is turned + * off. + */ +#define RKPWMC_CLEAR_DELAY (max(msecs_to_jiffies(100), 1)) + +struct rockchip_pwm_capture { + struct rockchip_mfpwm_func *pwmf; + bool is_enabled; + spinlock_t enable_lock; + atomic_t lpc; + atomic_t hpc; + atomic_t captures_left; + struct delayed_work clear_capture; +}; + +static struct counter_signal rkpwmc_signals[] = { + { + .id = 0, + .name = "Channel 1" + }, +}; + +static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = { + COUNTER_SYNAPSE_ACTION_BOTH_EDGES, + +}; + +static struct counter_synapse rkpwmc_pwm_synapses[] = { + { + .actions_list = rkpwmc_hpc_lpc_actions, + .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions), + .signal = &rkpwmc_signals[0] + }, +}; + +static const enum counter_function rkpwmc_functions[] = { + COUNTER_FUNCTION_INCREASE, +}; + +static int rkpwmc_enable_read(struct counter_device *counter, + struct counter_count *count, + u8 *enable) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + + guard(spinlock)(&pc->enable_lock); + + *enable = pc->is_enabled; + + return 0; +} + +static int rkpwmc_enable_write(struct counter_device *counter, + struct counter_count *count, + u8 enable) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + int ret; + + guard(spinlock)(&pc->enable_lock); + + if (!!enable != pc->is_enabled) { + ret = mfpwm_acquire(pc->pwmf); + if (ret) + return ret; + + if (enable) { + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(false)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, + PWMV4_CTRL_CAP_FLAGS); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN, + PWMV4_INT_LPC_W(true) | + PWMV4_INT_HPC_W(true)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(true) | PWMV4_CLK_EN(true)); + + ret = mfpwm_pwmclk_enable(pc->pwmf); + if (ret) + goto err_release; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + goto err_disable_pwm_clk; + + atomic_set(&pc->captures_left, 4); + pc->is_enabled = true; + } else { + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN, + PWMV4_INT_LPC_W(false) | + PWMV4_INT_HPC_W(false)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(false) | PWMV4_CLK_EN(false)); + /* + * Do not use cancel_delayed_work here unless you want + * to cause the interrupt handler, which may still be + * running at this point, to stall. Similarly, don't do + * flush_delayed_work since it may sleep. + */ + mod_delayed_work(system_bh_wq, &pc->clear_capture, 0); + mfpwm_pwmclk_disable(pc->pwmf); + pc->is_enabled = false; + mfpwm_release(pc->pwmf); + } + + mfpwm_release(pc->pwmf); + } + + return 0; + +err_disable_pwm_clk: + mfpwm_pwmclk_disable(pc->pwmf); +err_release: + mfpwm_release(pc->pwmf); + + return ret; +} + +static struct counter_comp rkpwmc_ext[] = { + COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write), +}; + +enum rkpwmc_count_id { + COUNT_PERIOD = 0, + COUNT_DUTY = 1, +}; + +static struct counter_count rkpwmc_counts[] = { + { + .id = COUNT_PERIOD, + .name = "Period in nanoseconds", + .functions_list = rkpwmc_functions, + .num_functions = ARRAY_SIZE(rkpwmc_functions), + .synapses = rkpwmc_pwm_synapses, + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses), + .ext = rkpwmc_ext, + .num_ext = ARRAY_SIZE(rkpwmc_ext), + }, + { + .id = COUNT_DUTY, + .name = "Duty cycle in nanoseconds", + .functions_list = rkpwmc_functions, + .num_functions = ARRAY_SIZE(rkpwmc_functions), + .synapses = rkpwmc_pwm_synapses, + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses), + .ext = rkpwmc_ext, + .num_ext = ARRAY_SIZE(rkpwmc_ext), + }, +}; + +static int rkpwmc_count_read(struct counter_device *counter, + struct counter_count *count, u64 *value) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + unsigned long rate; + u64 period; + u64 lpc; + u64 hpc; + int ret = 0; + + if (count->id != COUNT_PERIOD && count->id != COUNT_DUTY) + return -EINVAL; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + return ret; + + rate = mfpwm_clk_get_rate(pc->pwmf->parent); + if (!rate) { + ret = -EINVAL; + goto out_release; + } + + hpc = (u32) atomic_read(&pc->hpc); + + if (count->id == COUNT_PERIOD) { + lpc = (u32) atomic_read(&pc->lpc); + period = mult_frac((hpc + lpc), NSEC_PER_SEC, rate); + *value = period; + } else { + *value = mult_frac(hpc, NSEC_PER_SEC, rate); + } + +out_release: + mfpwm_release(pc->pwmf); + + return ret; +} + +static const struct counter_ops rkpwmc_ops = { + .count_read = rkpwmc_count_read, +}; + +static irqreturn_t rkpwmc_irq_handler(int irq, void *data) +{ + struct rockchip_pwm_capture *pc = data; + u32 intsts; + u32 clr = 0; + u32 lpc; + u32 hpc; + + intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS); + + if (!(intsts & RKPWMC_INT_MASK)) + return IRQ_NONE; + + if (intsts & PWMV4_INT_LPC) { + clr |= PWMV4_INT_LPC; + atomic_dec_if_positive(&pc->captures_left); + } + + if (intsts & PWMV4_INT_HPC) { + clr |= PWMV4_INT_HPC; + atomic_dec_if_positive(&pc->captures_left); + } + + /* After 4 interrupts, reset to 4 captures left and read the regs */ + if (!atomic_cmpxchg(&pc->captures_left, 0, 4)) { + lpc = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC); + hpc = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC); + + atomic_set(&pc->lpc, lpc); + atomic_set(&pc->hpc, hpc); + mod_delayed_work(system_bh_wq, &pc->clear_capture, + RKPWMC_CLEAR_DELAY); + } + + if (clr) + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr); + + if (intsts ^ clr) + return IRQ_NONE; + + return IRQ_HANDLED; +} + +static void rkpwmc_clear_capture_worker(struct work_struct *work) +{ + struct rockchip_pwm_capture *pc = container_of( + work, struct rockchip_pwm_capture, clear_capture.work); + + atomic_set(&pc->hpc, 0); + atomic_set(&pc->lpc, 0); +} + +static int rockchip_pwm_capture_probe(struct platform_device *pdev) +{ + struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev); + struct rockchip_pwm_capture *pc; + struct counter_device *counter; + int ret; + + counter = devm_counter_alloc(&pdev->dev, sizeof(*pc)); + if (IS_ERR(counter)) + return PTR_ERR(counter); + + pc = counter_priv(counter); + pc->pwmf = pwmf; + spin_lock_init(&pc->enable_lock); + + platform_set_drvdata(pdev, pc); + + ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler, + IRQF_SHARED, pdev->name, pc); + if (ret) + return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n"); + + ret = devm_delayed_work_autocancel(&pdev->dev, &pc->clear_capture, + rkpwmc_clear_capture_worker); + + counter->name = pdev->name; + counter->signals = rkpwmc_signals; + counter->num_signals = ARRAY_SIZE(rkpwmc_signals); + counter->ops = &rkpwmc_ops; + counter->counts = rkpwmc_counts; + counter->num_counts = ARRAY_SIZE(rkpwmc_counts); + + ret = devm_counter_add(&pdev->dev, counter); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n"); + + return 0; +} + +static void rockchip_pwm_capture_remove(struct platform_device *pdev) +{ + struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev); + + mfpwm_remove_func(pwmf); +} + +static const struct platform_device_id rockchip_pwm_capture_id_table[] = { + { .name = "rockchip-pwm-capture", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table); + +static struct platform_driver rockchip_pwm_capture_driver = { + .probe = rockchip_pwm_capture_probe, + .remove = rockchip_pwm_capture_remove, + .id_table = rockchip_pwm_capture_id_table, + .driver = { + .name = "rockchip-pwm-capture", + }, +}; +module_platform_driver(rockchip_pwm_capture_driver); + +MODULE_AUTHOR("Nicolas Frattaroli "); +MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("ROCKCHIP_MFPWM"); +MODULE_IMPORT_NS("COUNTER"); -- 2.49.0