[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