mirror of https://github.com/OpenIPC/firmware.git
492 lines
12 KiB
Diff
492 lines
12 KiB
Diff
diff -drupN a/drivers/pwm/pwm-ingenic.c b/drivers/pwm/pwm-ingenic.c
|
|
--- a/drivers/pwm/pwm-ingenic.c 1970-01-01 03:00:00.000000000 +0300
|
|
+++ b/drivers/pwm/pwm-ingenic.c 2022-06-09 05:02:33.000000000 +0300
|
|
@@ -0,0 +1,487 @@
|
|
+/*
|
|
+ * Copyright (c) 2007 Ben Dooks
|
|
+ * Copyright (c) 2008 Simtec Electronics
|
|
+ * Ben Dooks <ben@simtec.co.uk>, <ben-linux@fluff.org>
|
|
+ * Copyright (c) 2013 Tomasz Figa <tomasz.figa@gmail.com>
|
|
+ *
|
|
+ * PWM driver for Samsung SoCs
|
|
+ *
|
|
+ * 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.
|
|
+ */
|
|
+
|
|
+#include <linux/bitops.h>
|
|
+#include <linux/clk.h>
|
|
+#include <linux/export.h>
|
|
+#include <linux/err.h>
|
|
+#include <linux/io.h>
|
|
+#include <linux/kernel.h>
|
|
+#include <linux/module.h>
|
|
+#include <linux/of.h>
|
|
+#include <linux/platform_device.h>
|
|
+#include <linux/pwm.h>
|
|
+#include <linux/slab.h>
|
|
+#include <linux/spinlock.h>
|
|
+#include <linux/time.h>
|
|
+
|
|
+#include <linux/mfd/core.h>
|
|
+#include <linux/mfd/ingenic-tcu.h>
|
|
+
|
|
+/**
|
|
+ * struct ingenic_pwm_channel - private data of PWM channel
|
|
+ * @period_ns: current period in nanoseconds programmed to the hardware
|
|
+ * @duty_ns: current duty time in nanoseconds programmed to the hardware
|
|
+ */
|
|
+struct ingenic_pwm_channel {
|
|
+ struct ingenic_tcu_chn *chan;
|
|
+ u32 period_ns;
|
|
+ u32 duty_ns;
|
|
+};
|
|
+
|
|
+/**
|
|
+ * struct ingenic_pwm_chip - private data of PWM chip
|
|
+ * @chip: generic PWM chip
|
|
+ * @rtc_clk: external RTC clock(can be ERR_PTR if not present)
|
|
+ * @ext_clk: external EXT clock(can be ERR_PTR if not present)
|
|
+ * @output_mask: output status for all channels - one bit per channel
|
|
+ */
|
|
+struct ingenic_pwm_chip {
|
|
+ struct pwm_chip chip;
|
|
+ struct clk *rtc_clk;
|
|
+ struct clk *ext_clk;
|
|
+ u32 output_mask;
|
|
+};
|
|
+
|
|
+static inline
|
|
+struct ingenic_pwm_chip *to_ingenic_pwm_chip(struct pwm_chip *chip)
|
|
+{
|
|
+ return container_of(chip, struct ingenic_pwm_chip, chip);
|
|
+}
|
|
+static inline
|
|
+struct ingenic_tcu_chn *to_ingenic_tcu_channel(struct pwm_device *pwm)
|
|
+{
|
|
+ return ((struct ingenic_pwm_channel *)pwm_get_chip_data(pwm))->chan;
|
|
+}
|
|
+
|
|
+static int pwm_get_period_num(struct ingenic_pwm_chip *chip,
|
|
+ struct ingenic_tcu_chn *tcu_chan, int period_ns)
|
|
+{
|
|
+ unsigned long long rate;
|
|
+ unsigned long long tmp;
|
|
+ int div = 0;
|
|
+ int period = 0;
|
|
+
|
|
+ if(tcu_chan->clk_src == TCU_CLKSRC_EXT) {
|
|
+ rate = (unsigned long long)clk_get_rate(chip->ext_clk);
|
|
+ } else if(tcu_chan->clk_src == TCU_CLKSRC_RTC) {
|
|
+ rate = (unsigned long long)clk_get_rate(chip->ext_clk);
|
|
+ } else {
|
|
+ printk("not support clk soruce\n");
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ tmp = rate * period_ns;
|
|
+ do_div(tmp, 1000000000);
|
|
+ period = tmp;
|
|
+
|
|
+ while (period > 0xffff && div < 6) {
|
|
+ period >>= 2;
|
|
+ ++div;
|
|
+ }
|
|
+
|
|
+ if (div == 6)
|
|
+ return -EINVAL;
|
|
+
|
|
+ tcu_chan->full_num = period;
|
|
+ tcu_chan->clk_div = div;
|
|
+ return period;
|
|
+}
|
|
+static int pwm_ingenic_get_period(struct ingenic_pwm_chip *chip,
|
|
+ struct pwm_device *pwm, int period_ns)
|
|
+{
|
|
+ struct ingenic_tcu_chn *tcu_chan;
|
|
+ int period;
|
|
+
|
|
+ tcu_chan = to_ingenic_tcu_channel(pwm);
|
|
+ /*
|
|
+ * Compare minimum PWM frequency that can be achieved with possible
|
|
+ * divider settings and choose the lowest divisor that can generate
|
|
+ * frequencies lower than requested.
|
|
+ */
|
|
+ tcu_chan->clk_src = TCU_CLKSRC_EXT;
|
|
+ period = pwm_get_period_num(chip, tcu_chan, period_ns);
|
|
+ if(period < 0) {
|
|
+ tcu_chan->clk_src = TCU_CLKSRC_RTC;
|
|
+ period = pwm_get_period_num(chip, tcu_chan, period_ns);
|
|
+ if(period < 0) {
|
|
+ dev_err(chip->chip.dev,"can not find right div\n");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return period;
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_request(struct pwm_chip *chip, struct pwm_device *pwm)
|
|
+{
|
|
+ struct ingenic_pwm_chip *our_chip = to_ingenic_pwm_chip(chip);
|
|
+ struct ingenic_pwm_channel *our_chan;
|
|
+ struct mfd_cell *cell;
|
|
+
|
|
+ if (!(our_chip->output_mask & BIT(pwm->hwpwm))) {
|
|
+ dev_warn(chip->dev,
|
|
+ "tried to request PWM channel %d without output\n",
|
|
+ pwm->hwpwm);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ cell = request_cell(pwm->hwpwm);
|
|
+ if(!cell){
|
|
+ dev_err(chip->dev, "tried to request tcu channel %d busy\n",
|
|
+ pwm->hwpwm);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ our_chan = devm_kzalloc(chip->dev, sizeof(*our_chan), GFP_KERNEL);
|
|
+ if (!our_chan)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ our_chan->chan = cell->platform_data;
|
|
+ our_chan->chan->enable(cell->id);
|
|
+
|
|
+ pwm_set_chip_data(pwm, our_chan);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void pwm_ingenic_free(struct pwm_chip *chip, struct pwm_device *pwm)
|
|
+{
|
|
+ struct ingenic_pwm_channel *our_chan;
|
|
+ int id;
|
|
+
|
|
+ our_chan = pwm_get_chip_data(pwm);
|
|
+
|
|
+ id = pwm->hwpwm;
|
|
+ our_chan->chan->disable(id);
|
|
+ free_cell(id);
|
|
+ devm_kfree(chip->dev, our_chan);
|
|
+ pwm_set_chip_data(pwm, NULL);
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_enable(struct pwm_chip *chip, struct pwm_device *pwm)
|
|
+{
|
|
+ struct ingenic_tcu_chn *tcu_chan;
|
|
+ tcu_chan = to_ingenic_tcu_channel(pwm);
|
|
+ ingenic_tcu_counter_begin(tcu_chan);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void pwm_ingenic_disable(struct pwm_chip *chip, struct pwm_device *pwm)
|
|
+{
|
|
+ struct ingenic_tcu_chn *tcu_chan;
|
|
+ tcu_chan = to_ingenic_tcu_channel(pwm);
|
|
+ ingenic_tcu_counter_stop(tcu_chan);
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_config(struct pwm_chip *chip, struct pwm_device *pwm,
|
|
+ int duty_ns, int period_ns)
|
|
+{
|
|
+ struct ingenic_pwm_chip *our_chip = to_ingenic_pwm_chip(chip);
|
|
+ struct ingenic_pwm_channel *our_chan = pwm_get_chip_data(pwm);
|
|
+ struct ingenic_tcu_chn *tcu_chan = our_chan->chan;
|
|
+ u16 tfull = tcu_chan->full_num, thalf;
|
|
+
|
|
+ /*
|
|
+ * We currently avoid using 64bit arithmetic by using the
|
|
+ * fact that anything faster than 1Hz is easily representable
|
|
+ * by 32bits.
|
|
+ */
|
|
+ if (period_ns > NSEC_PER_SEC)
|
|
+ return -ERANGE;
|
|
+
|
|
+ if (period_ns == our_chan->period_ns && duty_ns == our_chan->duty_ns)
|
|
+ return 0;
|
|
+
|
|
+ /* Check to see if we are changing the clock rate of the PWM. */
|
|
+ if (our_chan->period_ns != period_ns) {
|
|
+ int period;
|
|
+
|
|
+ period = pwm_ingenic_get_period(our_chip, pwm, period_ns);
|
|
+ dev_dbg(our_chip->chip.dev, "duty_ns=%d, period_ns=%d (%u)\n",
|
|
+ duty_ns, period_ns, period);
|
|
+ if(period < 0)
|
|
+ return -ERANGE;
|
|
+ tfull = period;
|
|
+ }
|
|
+ /* Period is too short. */
|
|
+ if (tfull <= 1)
|
|
+ return -ERANGE;
|
|
+
|
|
+ /* Note that counters count down. */
|
|
+ {
|
|
+ unsigned long long tmp;
|
|
+ tmp = (unsigned long long)tfull * duty_ns;
|
|
+ do_div(tmp, period_ns);
|
|
+ thalf = tmp;
|
|
+ }
|
|
+
|
|
+ /* 0% duty is not available */
|
|
+ if (!thalf)
|
|
+ ++thalf;
|
|
+
|
|
+ thalf = tfull - thalf;
|
|
+ if(!thalf){
|
|
+ /* set pwm output 1 */
|
|
+ }
|
|
+
|
|
+ dev_info(our_chip->chip.dev,
|
|
+ "thalf=%u/%u\n", thalf, tfull);
|
|
+
|
|
+ tcu_chan->irq_type = NULL_IRQ_MODE;
|
|
+ tcu_chan->is_pwm = 1;
|
|
+ tcu_chan->full_num = tfull;
|
|
+ tcu_chan->half_num = thalf;
|
|
+ tcu_chan->shutdown_mode = 0;
|
|
+
|
|
+ ingenic_tcu_config(tcu_chan);
|
|
+
|
|
+ our_chan->period_ns = period_ns;
|
|
+ our_chan->duty_ns = duty_ns;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_set_polarity(struct pwm_chip *chip,
|
|
+ struct pwm_device *pwm,
|
|
+ enum pwm_polarity polarity)
|
|
+{
|
|
+ struct ingenic_tcu_chn *tcu_chan;
|
|
+ bool invert = (polarity == PWM_POLARITY_NORMAL);
|
|
+
|
|
+ tcu_chan = to_ingenic_tcu_channel(pwm);
|
|
+ if (invert) {
|
|
+ tcu_chan->init_level = 1;
|
|
+ } else {
|
|
+ tcu_chan->init_level = 0;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static const struct pwm_ops pwm_ingenic_ops = {
|
|
+ .request = pwm_ingenic_request,
|
|
+ .free = pwm_ingenic_free,
|
|
+ .enable = pwm_ingenic_enable,
|
|
+ .disable = pwm_ingenic_disable,
|
|
+ .config = pwm_ingenic_config,
|
|
+ .set_polarity = pwm_ingenic_set_polarity,
|
|
+ .owner = THIS_MODULE,
|
|
+};
|
|
+
|
|
+#ifdef CONFIG_OF
|
|
+/**
|
|
+ * of_irq_find_parent - Given a device node, find its interrupt parent node
|
|
+ * @child: pointer to device node
|
|
+ *
|
|
+ * Returns a pointer to the interrupt parent node, or NULL if the interrupt
|
|
+ * parent could not be determined.
|
|
+ */
|
|
+struct device_node *of_pwm_find_parent(struct device_node *child)
|
|
+{
|
|
+ struct device_node *p;
|
|
+ const __be32 *parp;
|
|
+
|
|
+ if (!of_node_get(child))
|
|
+ return NULL;
|
|
+
|
|
+ do {
|
|
+ parp = of_get_property(child, "ingenic,timer-parent", NULL);
|
|
+ if (parp == NULL)
|
|
+ p = of_get_parent(child);
|
|
+ else {
|
|
+ p = of_find_node_by_phandle(be32_to_cpup(parp));
|
|
+ }
|
|
+ of_node_put(child);
|
|
+ } while (!p);
|
|
+
|
|
+ return p;
|
|
+}
|
|
+
|
|
+static void pwm_ingenic_parse_dt(struct ingenic_pwm_chip *chip)
|
|
+{
|
|
+ struct device_node *np = chip->chip.dev->of_node;
|
|
+ struct device_node *child, *parent;
|
|
+ int enable_pwm_num = 0;
|
|
+
|
|
+ for_each_child_of_node(np, child) {
|
|
+ const char *status;
|
|
+ parent = of_pwm_find_parent(child);
|
|
+ of_property_read_string(child, "status", &status);
|
|
+
|
|
+ if(!strcmp(status, "okay") ){
|
|
+ unsigned int info, id;
|
|
+ of_property_read_u32(parent, "ingenic,channel-info", &info);
|
|
+ id = info & (CHANNEL_BASE_OFF*2 - 1);
|
|
+ chip->output_mask |= BIT(id);
|
|
+ enable_pwm_num ++;
|
|
+ }
|
|
+ }
|
|
+ chip->chip.of_pwm_n_cells = enable_pwm_num;
|
|
+}
|
|
+static const struct of_device_id ingenic_pwm_matches[] = {
|
|
+ { .compatible = "ingenic,pwm", .data = NULL },
|
|
+ {},
|
|
+};
|
|
+MODULE_DEVICE_TABLE(of, ingenic_pwm_matches);
|
|
+#else
|
|
+static int pwm_ingenic_parse_dt(struct ingenic_pwm_chip *chip)
|
|
+{
|
|
+ return -ENODEV;
|
|
+}
|
|
+#endif
|
|
+
|
|
+static int pwm_ingenic_probe(struct platform_device *pdev)
|
|
+{
|
|
+ struct device *dev = &pdev->dev;
|
|
+ struct ingenic_pwm_chip *chip;
|
|
+ struct device_node *np;
|
|
+ unsigned int pwmnum;
|
|
+ int ret = 0;
|
|
+
|
|
+ chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
|
|
+ if (chip == NULL)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ np = pdev->dev.of_node;
|
|
+ pwmnum = of_get_child_count(np);
|
|
+ chip->chip.dev = &pdev->dev;
|
|
+ chip->chip.dev->of_node = np;
|
|
+ chip->chip.ops = &pwm_ingenic_ops;
|
|
+ chip->chip.base = -1;
|
|
+ chip->chip.npwm = pwmnum;
|
|
+
|
|
+ if (IS_ENABLED(CONFIG_OF) && pdev->dev.of_node) {
|
|
+ pwm_ingenic_parse_dt(chip);
|
|
+ /* chip->chip.of_xlate = of_pwm_xlate_with_flags; */
|
|
+ } else {
|
|
+ if (!pdev->dev.platform_data) {
|
|
+ dev_err(&pdev->dev, "no platform data specified\n");
|
|
+ return -EINVAL;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ chip->ext_clk = devm_clk_get(&pdev->dev, "ext");
|
|
+ chip->rtc_clk = devm_clk_get(&pdev->dev, "rtc");
|
|
+ if (IS_ERR(chip->ext_clk) || IS_ERR(chip->rtc_clk)) {
|
|
+ dev_err(dev, "failed to get timer base clk\n");
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ platform_set_drvdata(pdev, chip);
|
|
+
|
|
+ ret = pwmchip_add(&chip->chip);
|
|
+ if (ret < 0) {
|
|
+ dev_err(dev, "failed to register PWM chip\n");
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_remove(struct platform_device *pdev)
|
|
+{
|
|
+ struct ingenic_pwm_chip *chip = platform_get_drvdata(pdev);
|
|
+ int ret;
|
|
+
|
|
+ ret = pwmchip_remove(&chip->chip);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ clk_disable_unprepare(chip->ext_clk);
|
|
+ clk_disable_unprepare(chip->rtc_clk);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+
|
|
+#ifdef CONFIG_PM_SLEEP
|
|
+static int pwm_ingenic_suspend(struct device *dev)
|
|
+{
|
|
+ struct ingenic_pwm_chip *chip = dev_get_drvdata(dev);
|
|
+ unsigned int i;
|
|
+
|
|
+ /*
|
|
+ * No one preserves these values during suspend so reset them.
|
|
+ * Otherwise driver leaves PWM unconfigured if same values are
|
|
+ * passed to pwm_config() next time.
|
|
+ */
|
|
+ for (i = 0; i < chip->chip.npwm; ++i) {
|
|
+ struct pwm_device *pwm = &chip->chip.pwms[i];
|
|
+ struct ingenic_pwm_channel *chan = pwm_get_chip_data(pwm);
|
|
+ int id;
|
|
+ if (!chan)
|
|
+ continue;
|
|
+
|
|
+ id = pwm->hwpwm;
|
|
+ chan->chan->disable(id);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int pwm_ingenic_resume(struct device *dev)
|
|
+{
|
|
+ struct ingenic_pwm_chip *chip = dev_get_drvdata(dev);
|
|
+ unsigned int chan;
|
|
+
|
|
+ /*
|
|
+ * Inverter setting must be preserved across suspend/resume
|
|
+ * as nobody really seems to configure it more than once.
|
|
+ */
|
|
+ for (chan = 0; chan < chip->chip.npwm; ++chan) {
|
|
+ if (chip->output_mask & BIT(chan)) {
|
|
+ struct pwm_device *pwm = &chip->chip.pwms[chan];
|
|
+ struct ingenic_pwm_channel *chan = pwm_get_chip_data(pwm);
|
|
+ int id;
|
|
+ if (!chan)
|
|
+ continue;
|
|
+
|
|
+ id = pwm->hwpwm;
|
|
+ chan->chan->enable(id);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+#endif
|
|
+
|
|
+static SIMPLE_DEV_PM_OPS(pwm_ingenic_pm_ops, pwm_ingenic_suspend,
|
|
+ pwm_ingenic_resume);
|
|
+
|
|
+static struct platform_driver pwm_ingenic_driver = {
|
|
+ .driver = {
|
|
+ .name = "ingenic,pwm",
|
|
+ .pm = &pwm_ingenic_pm_ops,
|
|
+ .of_match_table = of_match_ptr(ingenic_pwm_matches),
|
|
+ },
|
|
+ .probe = pwm_ingenic_probe,
|
|
+ .remove = pwm_ingenic_remove,
|
|
+};
|
|
+
|
|
+static int __init ingenic_pwm_init(void)
|
|
+{
|
|
+ return platform_driver_register(&pwm_ingenic_driver);
|
|
+}
|
|
+
|
|
+static void __exit ingenic_pwm_exit(void)
|
|
+{
|
|
+ platform_driver_unregister(&pwm_ingenic_driver);
|
|
+}
|
|
+
|
|
+device_initcall_sync(ingenic_pwm_init);
|
|
+module_exit(ingenic_pwm_exit);
|
|
+
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_AUTHOR("bo.liu <bo.liu@ingenic.com>");
|
|
+MODULE_ALIAS("platform:ingenic-pwm");
|