[Letux-kernel] [RFC 10/28] pwm: introduce jz4730 pwm driver
H. Nikolaus Schaller
hns at goldelico.com
Sat Jan 23 17:28:36 CET 2021
From: Paul Boddie <paul at boddie.org.uk>
Since the JZ4730 has dedicated PWM registers, it appears easier to
maintain a separate driver instead of accommodating the JZ4730 in
the JZ4740 PWM driver.
Signed-off-by: Paul Boddie <paul at boddie.org.uk>
Signed-off-by: H. Nikolaus Schaller <hns at goldelico.com>
---
drivers/pwm/Kconfig | 10 ++
drivers/pwm/Makefile | 1 +
drivers/pwm/pwm-jz4730.c | 248 +++++++++++++++++++++++++++++++++++++++
3 files changed, 259 insertions(+)
create mode 100644 drivers/pwm/pwm-jz4730.c
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 0937e1c047acb..c8e7bbbe4eb77 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -270,6 +270,16 @@ config PWM_IQS620A
To compile this driver as a module, choose M here: the module will
be called pwm-iqs620a.
+config PWM_JZ4730
+ tristate "Ingenic JZ4730 PWM support"
+ depends on MACH_JZ4730
+ help
+ Generic PWM framework driver for Ingenic JZ4730 based
+ machines.
+
+ To compile this driver as a module, choose M here: the module
+ will be called pwm-jz4730.
+
config PWM_JZ4740
tristate "Ingenic JZ47xx PWM support"
depends on MIPS
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 18b89d7fd092a..cdb0066679ec1 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -23,6 +23,7 @@ obj-$(CONFIG_PWM_IMX27) += pwm-imx27.o
obj-$(CONFIG_PWM_IMX_TPM) += pwm-imx-tpm.o
obj-$(CONFIG_PWM_INTEL_LGM) += pwm-intel-lgm.o
obj-$(CONFIG_PWM_IQS620A) += pwm-iqs620a.o
+obj-$(CONFIG_PWM_JZ4730) += pwm-jz4730.o
obj-$(CONFIG_PWM_JZ4740) += pwm-jz4740.o
obj-$(CONFIG_PWM_KEEMBAY) += pwm-keembay.o
obj-$(CONFIG_PWM_LP3943) += pwm-lp3943.o
diff --git a/drivers/pwm/pwm-jz4730.c b/drivers/pwm/pwm-jz4730.c
new file mode 100644
index 0000000000000..28366ef874929
--- /dev/null
+++ b/drivers/pwm/pwm-jz4730.c
@@ -0,0 +1,248 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2010, Lars-Peter Clausen <lars at metafoo.de>
+ * Copyright (C) 2017, 2019 Paul Boddie <paul at boddie.org.uk>
+ * JZ4730 platform PWM support
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ */
+
+#include <linux/clk.h>
+#include <linux/err.h>
+#include <linux/gpio.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+
+#define NUM_PWM 2
+
+#define JZ4730_PWM_REG_CTR 0x00 /* 8-bit */
+#define JZ4730_PWM_REG_PER 0x04 /* 16-bit */
+#define JZ4730_PWM_REG_DUT 0x08 /* 16-bit */
+
+#define JZ4730_PWM_OFFSET 0x1000 /* register spacing */
+
+#define JZ4730_PWM_CTR_EN 0x80 /* enable */
+#define JZ4730_PWM_CTR_SD 0x40 /* shutdown */
+#define JZ4730_PWM_CTR_PRESCALE 0x3f /* mask */
+#define JZ4730_PWM_CTR_PRESCALE_LIMIT 0x40 /* actual limit, not encoded */
+
+#define JZ4730_PWM_PER_PERIOD 0x3ff /* mask */
+#define JZ4730_PWM_PER_PERIOD_LIMIT 0x400 /* actual limit, not encoded */
+
+#define JZ4730_PWM_DUT_FULL 0x400
+#define JZ4730_PWM_DUT_DUTY 0x3ff /* mask */
+
+struct jz4730_pwm_chip {
+ struct pwm_chip chip;
+ struct regmap *map;
+ struct clk *clk;
+};
+
+static inline struct jz4730_pwm_chip *to_jz4730(struct pwm_chip *chip)
+{
+ return container_of(chip, struct jz4730_pwm_chip, chip);
+}
+
+/* Update the register by loading, updating and storing the register value. */
+
+static void pwm_jz4730_update_reg(struct pwm_chip *chip, struct pwm_device *pwm,
+ u32 reg, u32 affected, u32 value)
+{
+ struct jz4730_pwm_chip *jzpc = to_jz4730(chip);
+ u32 offset = pwm->pwm * JZ4730_PWM_OFFSET;
+
+ regmap_update_bits(jzpc->map, offset + reg, affected, value);
+}
+
+static int jz4730_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+ pwm_jz4730_update_reg(chip, pwm, JZ4730_PWM_REG_CTR,
+ JZ4730_PWM_CTR_EN, JZ4730_PWM_CTR_EN);
+ return 0;
+}
+
+static void jz4730_pwm_disable(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+ pwm_jz4730_update_reg(chip, pwm, JZ4730_PWM_REG_CTR,
+ JZ4730_PWM_CTR_EN, 0);
+}
+
+static int jz4730_pwm_is_enabled(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+ struct jz4730_pwm_chip *jzpc = to_jz4730(chip);
+ u32 offset = pwm->pwm * JZ4730_PWM_OFFSET;
+ unsigned int val;
+
+ regmap_read(jzpc->map, offset + JZ4730_PWM_REG_CTR, &val);
+ return !!(val & JZ4730_PWM_CTR_EN);
+}
+
+static int jz4730_pwm_config(struct pwm_chip *chip, struct pwm_device *pwm,
+ int duty_ns, int period_ns)
+{
+ struct jz4730_pwm_chip *jzpc = to_jz4730(chip);
+
+ /* The prescaler seems to be a simple quantity, not a 4**n
+ * progression as seen in the JZ4740 TCSR:PRESCALE field.
+ *
+ * Thus, the PWM frequency can be calculated as follows, given
+ * observed values:
+ *
+ * PWM frequency = clock frequency / prescaler / period count
+ * = 3686400Hz / 64 / 300
+ * = 192Hz
+ *
+ * The above formula can be rewritten as follows:
+ *
+ * PWM frequency = clock frequency / (prescaler * period count)
+ * clock period * prescaler * period count = PWM period
+ * prescaler * period count = PWM period / clock period
+ * prescaler * period count = PWM period * clock frequency
+ *
+ * With the prescaler and period count apparently acting
+ * together as a larger quantity, the prescaler needs populating
+ * when the period count would otherwise exceed the limit of its
+ * field.
+ */
+
+ unsigned long long tmp;
+ unsigned long period, duty;
+ unsigned int prescaler = 1;
+ bool is_enabled;
+
+ /* Get the PWM period as a multiple of the clock period. */
+
+ tmp = (unsigned long long)clk_get_rate(jzpc->clk) * period_ns;
+ do_div(tmp, 1000000000);
+ period = tmp;
+
+ if (period > JZ4730_PWM_PER_PERIOD_LIMIT) {
+ prescaler = period / JZ4730_PWM_PER_PERIOD_LIMIT;
+ period = period % JZ4730_PWM_PER_PERIOD_LIMIT;
+ }
+
+ if (prescaler > JZ4730_PWM_CTR_PRESCALE_LIMIT)
+ return -EINVAL;
+
+ /* Get the duty duration as a multiple of the clock period. */
+
+ tmp = (unsigned long long)period * duty_ns;
+ do_div(tmp, period_ns);
+ duty = tmp;
+
+ if (duty >= period)
+ duty = period;
+
+ is_enabled = jz4730_pwm_is_enabled(chip, pwm);
+ if (is_enabled)
+ jz4730_pwm_disable(chip, pwm);
+
+ /* It may be the case that setting duty to period still causes
+ * one cycle to be low. This is what happens with output compare
+ * on PIC32, for instance.
+ */
+
+ pwm_jz4730_update_reg(chip, pwm, JZ4730_PWM_REG_DUT,
+ JZ4730_PWM_DUT_DUTY, duty);
+
+ /* The period and prescaler encoding may employ an offset, with
+ * a stored value of zero representing one.
+ */
+
+ pwm_jz4730_update_reg(chip, pwm, JZ4730_PWM_REG_PER,
+ JZ4730_PWM_PER_PERIOD, period - 1);
+
+ pwm_jz4730_update_reg(chip, pwm, JZ4730_PWM_REG_CTR,
+ JZ4730_PWM_CTR_PRESCALE, prescaler - 1);
+
+ if (is_enabled)
+ jz4730_pwm_enable(chip, pwm);
+
+ return 0;
+}
+
+static const struct regmap_config ingenic_pwm_regmap_config = {
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+};
+
+static const struct pwm_ops jz4730_pwm_ops = {
+ .config = jz4730_pwm_config,
+ .enable = jz4730_pwm_enable,
+ .disable = jz4730_pwm_disable,
+ .owner = THIS_MODULE,
+};
+
+static int jz4730_pwm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct jz4730_pwm_chip *jzpc;
+ void __iomem *base;
+
+ jzpc = devm_kzalloc(&pdev->dev, sizeof(*jzpc), GFP_KERNEL);
+ if (!jzpc)
+ return -ENOMEM;
+
+ jzpc->clk = devm_clk_get(&pdev->dev, "ext");
+ if (IS_ERR(jzpc->clk))
+ return PTR_ERR(jzpc->clk);
+
+ base = devm_ioremap_resource(dev,
+ platform_get_resource(pdev, IORESOURCE_MEM, 0));
+ if (IS_ERR(base)) {
+ dev_err(dev, "Failed to ioremap registers\n");
+ return PTR_ERR(base);
+ }
+
+ jzpc->map = devm_regmap_init_mmio(dev, base,
+ &ingenic_pwm_regmap_config);
+ if (IS_ERR(jzpc->map)) {
+ dev_err(dev, "Failed to create regmap\n");
+ return PTR_ERR(jzpc->map);
+ }
+
+ jzpc->chip.dev = &pdev->dev;
+ jzpc->chip.ops = &jz4730_pwm_ops;
+ jzpc->chip.npwm = NUM_PWM;
+ jzpc->chip.base = 0;
+
+ platform_set_drvdata(pdev, jzpc);
+
+ return pwmchip_add(&jzpc->chip);
+}
+
+static int jz4730_pwm_remove(struct platform_device *pdev)
+{
+ struct jz4730_pwm_chip *jzpc = platform_get_drvdata(pdev);
+
+ return pwmchip_remove(&jzpc->chip);
+}
+
+static const struct of_device_id jz4730_pwm_dt_ids[] = {
+ { .compatible = "ingenic,jz4730-pwm", },
+ {},
+};
+MODULE_DEVICE_TABLE(of, jz4730_pwm_dt_ids);
+
+static struct platform_driver jz4730_pwm_driver = {
+ .driver = {
+ .name = "jz4730-pwm",
+ .of_match_table = of_match_ptr(jz4730_pwm_dt_ids),
+ },
+ .probe = jz4730_pwm_probe,
+ .remove = jz4730_pwm_remove,
+};
+module_platform_driver(jz4730_pwm_driver);
+
+MODULE_AUTHOR("Paul Boddie <paul at boddie.org.uk>");
+MODULE_DESCRIPTION("Ingenic JZ4730 PWM driver");
+MODULE_ALIAS("platform:jz4730-pwm");
+MODULE_LICENSE("GPL");
--
2.26.2
More information about the Letux-kernel
mailing list