diff --git a/MAINTAINERS b/MAINTAINERS index b0296f822ac6dc..e4e9365ef3e324 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3422,6 +3422,7 @@ L: linux-pwm@vger.kernel.org S: Supported W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/pwm/adi,axi-pwmgen.yaml +F: drivers/pwm/pwm-axi-pwmgen.c AXI SPI ENGINE M: Michael Hennerich diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 4b956d661755d6..4caa4d8aa3ef1f 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -98,6 +98,18 @@ config PWM_ATMEL_TCB To compile this driver as a module, choose M here: the module will be called pwm-atmel-tcb. +config PWM_AXI_PWMGEN + tristate "Analog Devices AXI PWM generator" + depends on HAS_IOMEM + help + This enables support for the Analog Devices AXI PWM generator. + + This is a single output configurable PWM generator with variable + pulse width and period. + + To compile this driver as a module, choose M here: the module will be + called pwm-axi-pwmgen. + config PWM_BCM_IPROC tristate "iProc PWM support" depends on ARCH_BCM_IPROC || COMPILE_TEST diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index c5ec9e168ee7c5..8322089954e997 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_PWM_APPLE) += pwm-apple.o obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o obj-$(CONFIG_PWM_ATMEL_HLCDC_PWM) += pwm-atmel-hlcdc.o obj-$(CONFIG_PWM_ATMEL_TCB) += pwm-atmel-tcb.o +obj-$(CONFIG_PWM_AXI_PWMGEN) += pwm-axi-pwmgen.o obj-$(CONFIG_PWM_BCM_IPROC) += pwm-bcm-iproc.o obj-$(CONFIG_PWM_BCM_KONA) += pwm-bcm-kona.o obj-$(CONFIG_PWM_BCM2835) += pwm-bcm2835.o diff --git a/drivers/pwm/pwm-axi-pwmgen.c b/drivers/pwm/pwm-axi-pwmgen.c new file mode 100644 index 00000000000000..df05f469c430c8 --- /dev/null +++ b/drivers/pwm/pwm-axi-pwmgen.c @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Analog Devices AXI PWM generator + * + * Copyright 2023 Analog Devices Inc. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#define AXI_PWMGEN_NPWM 4 +#define AXI_PWMGEN_REG_CORE_VERSION 0x00 +#define AXI_PWMGEN_REG_ID 0x04 +#define AXI_PWMGEN_REG_SCRATCHPAD 0x08 +#define AXI_PWMGEN_REG_CORE_MAGIC 0x0C +#define AXI_PWMGEN_REG_CONFIG 0x10 +#define AXI_PWMGEN_REG_NPWM 0x14 +#define AXI_PWMGEN_CH_PERIOD_BASE 0x40 +#define AXI_PWMGEN_CH_DUTY_BASE 0x44 +#define AXI_PWMGEN_CH_OFFSET_BASE 0x48 +#define AXI_PWMGEN_CHX_PERIOD(ch) (AXI_PWMGEN_CH_PERIOD_BASE + (12 * (ch))) +#define AXI_PWMGEN_CHX_DUTY(ch) (AXI_PWMGEN_CH_DUTY_BASE + (12 * (ch))) +#define AXI_PWMGEN_CHX_OFFSET(ch) (AXI_PWMGEN_CH_OFFSET_BASE + (12 * (ch))) +#define AXI_PWMGEN_TEST_DATA 0x5A0F0081 +#define AXI_PWMGEN_LOAD_CONIG BIT(1) +#define AXI_PWMGEN_RESET BIT(0) + +struct axi_pwmgen { + struct pwm_chip chip; + struct clk *clk; + void __iomem *base; + + /* Used to store the period when the channel is disabled */ + unsigned int ch_period[AXI_PWMGEN_NPWM]; + bool ch_enabled[AXI_PWMGEN_NPWM]; +}; + +static inline unsigned int axi_pwmgen_read(struct axi_pwmgen *pwm, + unsigned int reg) +{ + return readl(pwm->base + reg); +} + +static inline void axi_pwmgen_write(struct axi_pwmgen *pwm, + unsigned int reg, + unsigned int value) +{ + writel(value, pwm->base + reg); +} + +static void axi_pwmgen_write_mask(struct axi_pwmgen *pwm, + unsigned int reg, + unsigned int mask, + unsigned int value) +{ + unsigned int temp; + + temp = axi_pwmgen_read(pwm, reg); + axi_pwmgen_write(pwm, reg, (temp & ~mask) | value); +} + +static struct axi_pwmgen *to_axi_pwmgen(struct pwm_chip *chip) +{ + return container_of(chip, struct axi_pwmgen, chip); +} + +static int axi_pwmgen_apply(struct pwm_chip *chip, struct pwm_device *device, + const struct pwm_state *state) +{ + struct axi_pwmgen *pwm = to_axi_pwmgen(chip); + unsigned long clk_rate = clk_get_rate(pwm->clk); + unsigned int ch = device->hwpwm; + u64 period_cnt, duty_cnt; + + period_cnt = DIV_ROUND_UP_ULL(state->period * clk_rate, NSEC_PER_SEC); + if (period_cnt > UINT_MAX) + return -EINVAL; + + pwm->ch_period[ch] = period_cnt; + pwm->ch_enabled[ch] = state->enabled; + axi_pwmgen_write(pwm, AXI_PWMGEN_CHX_PERIOD(ch), + state->enabled ? period_cnt : 0); + + duty_cnt = DIV_ROUND_UP_ULL(state->duty_cycle * clk_rate, NSEC_PER_SEC); + axi_pwmgen_write(pwm, AXI_PWMGEN_CHX_DUTY(ch), duty_cnt); + + axi_pwmgen_write(pwm, AXI_PWMGEN_REG_CONFIG, AXI_PWMGEN_LOAD_CONIG); + + return 0; +} + +static int axi_pwmgen_get_state(struct pwm_chip *chip, struct pwm_device *pwm, + struct pwm_state *state) +{ + struct axi_pwmgen *pwmgen = to_axi_pwmgen(chip); + unsigned long rate = clk_get_rate(pwmgen->clk); + size_t ch = pwm->hwpwm; + unsigned int cnt; + + state->enabled = pwmgen->ch_enabled[ch]; + + if (state->enabled) + cnt = axi_pwmgen_read(pwmgen, AXI_PWMGEN_CHX_PERIOD(ch)); + else + cnt = pwmgen->ch_period[ch]; + + state->period = DIV_ROUND_CLOSEST_ULL((u64)cnt * NSEC_PER_SEC, rate); + + cnt = axi_pwmgen_read(pwmgen, AXI_PWMGEN_CHX_DUTY(ch)); + state->duty_cycle = DIV_ROUND_CLOSEST_ULL((u64)cnt * NSEC_PER_SEC, rate); + + return 0; +} + +static const struct pwm_ops axi_pwmgen_pwm_ops = { + .apply = axi_pwmgen_apply, + .get_state = axi_pwmgen_get_state, +}; + +static int axi_pwmgen_setup(struct pwm_chip *chip) +{ + struct axi_pwmgen *pwm; + unsigned int reg; + int idx; + + pwm = to_axi_pwmgen(chip); + axi_pwmgen_write(pwm, AXI_PWMGEN_REG_SCRATCHPAD, AXI_PWMGEN_TEST_DATA); + reg = axi_pwmgen_read(pwm, AXI_PWMGEN_REG_SCRATCHPAD); + if (reg != AXI_PWMGEN_TEST_DATA) + return dev_err_probe(chip->dev, -EIO, + "failed to access the device registers\n"); + + pwm->chip.npwm = axi_pwmgen_read(pwm, AXI_PWMGEN_REG_NPWM); + if (pwm->chip.npwm > AXI_PWMGEN_NPWM) + return -EINVAL; + + /* Disable all the outputs */ + for (idx = 0; idx < pwm->chip.npwm; idx++) { + axi_pwmgen_write(pwm, AXI_PWMGEN_CHX_PERIOD(idx), 0); + axi_pwmgen_write(pwm, AXI_PWMGEN_CHX_DUTY(idx), 0); + axi_pwmgen_write(pwm, AXI_PWMGEN_CHX_OFFSET(idx), 0); + } + + /* Enable the core */ + axi_pwmgen_write_mask(pwm, AXI_PWMGEN_REG_CONFIG, AXI_PWMGEN_RESET, 0); + + return 0; +} + +static int axi_pwmgen_probe(struct platform_device *pdev) +{ + struct axi_pwmgen *pwm; + int ret; + + pwm = devm_kzalloc(&pdev->dev, sizeof(*pwm), GFP_KERNEL); + if (!pwm) + return -ENOMEM; + + pwm->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(pwm->base)) + return PTR_ERR(pwm->base); + + pwm->clk = devm_clk_get_enabled(&pdev->dev, NULL); + if (IS_ERR(pwm->clk)) + return PTR_ERR(pwm->clk); + + pwm->chip.dev = &pdev->dev; + pwm->chip.ops = &axi_pwmgen_pwm_ops; + pwm->chip.base = -1; + + ret = axi_pwmgen_setup(&pwm->chip); + if (ret < 0) + return ret; + + return devm_pwmchip_add(&pdev->dev, &pwm->chip); +} + +static const struct of_device_id axi_pwmgen_ids[] = { + { .compatible = "adi,axi-pwmgen" }, + { } +}; +MODULE_DEVICE_TABLE(of, axi_pwmgen_ids); + +static struct platform_driver axi_pwmgen_driver = { + .driver = { + .name = "axi-pwmgen", + .of_match_table = axi_pwmgen_ids, + }, + .probe = axi_pwmgen_probe, +}; + +module_platform_driver(axi_pwmgen_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Sergiu Cuciurean "); +MODULE_DESCRIPTION("Driver for the Analog Devices AXI PWM generator");