pwm: Add PWM driver for SiFive SoC
Adds a PWM driver for PWM chip present in SiFive's HiFive Unleashed SoC
This driver is simple port of Linux pwm sifive driver from Linux v5.6
commit: 9e37a53eb051 ("pwm: sifive: Add a driver for SiFive SoC PWM")
Signed-off-by: Yash Shah <yash.shah@sifive.com>
Reviewed-by: Heiko Schocher <hs@denx.de>
diff --git a/drivers/pwm/pwm-sifive.c b/drivers/pwm/pwm-sifive.c
new file mode 100644
index 0000000..77bc659
--- /dev/null
+++ b/drivers/pwm/pwm-sifive.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2020 SiFive, Inc
+ * For SiFive's PWM IP block documentation please refer Chapter 14 of
+ * Reference Manual : https://static.dev.sifive.com/FU540-C000-v1.0.pdf
+ *
+ * Limitations:
+ * - When changing both duty cycle and period, we cannot prevent in
+ * software that the output might produce a period with mixed
+ * settings (new period length and old duty cycle).
+ * - The hardware cannot generate a 100% duty cycle.
+ * - The hardware generates only inverted output.
+ */
+
+#include <common.h>
+#include <clk.h>
+#include <div64.h>
+#include <dm.h>
+#include <pwm.h>
+#include <regmap.h>
+#include <linux/io.h>
+#include <linux/log2.h>
+#include <linux/bitfield.h>
+
+/* PWMCFG fields */
+#define PWM_SIFIVE_PWMCFG_SCALE GENMASK(3, 0)
+#define PWM_SIFIVE_PWMCFG_STICKY BIT(8)
+#define PWM_SIFIVE_PWMCFG_ZERO_CMP BIT(9)
+#define PWM_SIFIVE_PWMCFG_DEGLITCH BIT(10)
+#define PWM_SIFIVE_PWMCFG_EN_ALWAYS BIT(12)
+#define PWM_SIFIVE_PWMCFG_EN_ONCE BIT(13)
+#define PWM_SIFIVE_PWMCFG_CENTER BIT(16)
+#define PWM_SIFIVE_PWMCFG_GANG BIT(24)
+#define PWM_SIFIVE_PWMCFG_IP BIT(28)
+
+/* PWM_SIFIVE_SIZE_PWMCMP is used to calculate offset for pwmcmpX registers */
+#define PWM_SIFIVE_SIZE_PWMCMP 4
+#define PWM_SIFIVE_CMPWIDTH 16
+
+DECLARE_GLOBAL_DATA_PTR;
+
+struct pwm_sifive_regs {
+ unsigned long cfg;
+ unsigned long cnt;
+ unsigned long pwms;
+ unsigned long cmp0;
+};
+
+struct pwm_sifive_data {
+ struct pwm_sifive_regs regs;
+};
+
+struct pwm_sifive_priv {
+ void __iomem *base;
+ ulong freq;
+ const struct pwm_sifive_data *data;
+};
+
+static int pwm_sifive_set_config(struct udevice *dev, uint channel,
+ uint period_ns, uint duty_ns)
+{
+ struct pwm_sifive_priv *priv = dev_get_priv(dev);
+ const struct pwm_sifive_regs *regs = &priv->data->regs;
+ unsigned long scale_pow;
+ unsigned long long num;
+ u32 scale, val = 0, frac;
+
+ debug("%s: period_ns=%u, duty_ns=%u\n", __func__, period_ns, duty_ns);
+
+ /*
+ * The PWM unit is used with pwmzerocmp=0, so the only way to modify the
+ * period length is using pwmscale which provides the number of bits the
+ * counter is shifted before being feed to the comparators. A period
+ * lasts (1 << (PWM_SIFIVE_CMPWIDTH + pwmscale)) clock ticks.
+ * (1 << (PWM_SIFIVE_CMPWIDTH + scale)) * 10^9/rate = period
+ */
+ scale_pow = lldiv((uint64_t)priv->freq * period_ns, 1000000000);
+ scale = clamp(ilog2(scale_pow) - PWM_SIFIVE_CMPWIDTH, 0, 0xf);
+ val |= FIELD_PREP(PWM_SIFIVE_PWMCFG_SCALE, scale);
+
+ /*
+ * The problem of output producing mixed setting as mentioned at top,
+ * occurs here. To minimize the window for this problem, we are
+ * calculating the register values first and then writing them
+ * consecutively
+ */
+ num = (u64)duty_ns * (1U << PWM_SIFIVE_CMPWIDTH);
+ frac = DIV_ROUND_CLOSEST_ULL(num, period_ns);
+ frac = min(frac, (1U << PWM_SIFIVE_CMPWIDTH) - 1);
+
+ writel(val, priv->base + regs->cfg);
+ writel(frac, priv->base + regs->cmp0 + channel *
+ PWM_SIFIVE_SIZE_PWMCMP);
+
+ return 0;
+}
+
+static int pwm_sifive_set_enable(struct udevice *dev, uint channel, bool enable)
+{
+ struct pwm_sifive_priv *priv = dev_get_priv(dev);
+ const struct pwm_sifive_regs *regs = &priv->data->regs;
+ u32 val;
+
+ debug("%s: Enable '%s'\n", __func__, dev->name);
+
+ if (enable) {
+ val = readl(priv->base + regs->cfg);
+ val |= PWM_SIFIVE_PWMCFG_EN_ALWAYS;
+ writel(val, priv->base + regs->cfg);
+ } else {
+ writel(0, priv->base + regs->cmp0 + channel *
+ PWM_SIFIVE_SIZE_PWMCMP);
+ }
+
+ return 0;
+}
+
+static int pwm_sifive_ofdata_to_platdata(struct udevice *dev)
+{
+ struct pwm_sifive_priv *priv = dev_get_priv(dev);
+
+ priv->base = dev_read_addr_ptr(dev);
+
+ return 0;
+}
+
+static int pwm_sifive_probe(struct udevice *dev)
+{
+ struct pwm_sifive_priv *priv = dev_get_priv(dev);
+ struct clk clk;
+ int ret = 0;
+
+ ret = clk_get_by_index(dev, 0, &clk);
+ if (ret < 0) {
+ debug("%s get clock fail!\n", __func__);
+ return -EINVAL;
+ }
+
+ priv->freq = clk_get_rate(&clk);
+ priv->data = (struct pwm_sifive_data *)dev_get_driver_data(dev);
+
+ return 0;
+}
+
+static const struct pwm_ops pwm_sifive_ops = {
+ .set_config = pwm_sifive_set_config,
+ .set_enable = pwm_sifive_set_enable,
+};
+
+static const struct pwm_sifive_data pwm_data = {
+ .regs = {
+ .cfg = 0x00,
+ .cnt = 0x08,
+ .pwms = 0x10,
+ .cmp0 = 0x20,
+ },
+};
+
+static const struct udevice_id pwm_sifive_ids[] = {
+ { .compatible = "sifive,pwm0", .data = (ulong)&pwm_data},
+ { }
+};
+
+U_BOOT_DRIVER(pwm_sifive) = {
+ .name = "pwm_sifive",
+ .id = UCLASS_PWM,
+ .of_match = pwm_sifive_ids,
+ .ops = &pwm_sifive_ops,
+ .ofdata_to_platdata = pwm_sifive_ofdata_to_platdata,
+ .probe = pwm_sifive_probe,
+ .priv_auto_alloc_size = sizeof(struct pwm_sifive_priv),
+};