| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * simple driver for PWM (Pulse Width Modulator) controller | |
| * | |
| * Derived from pxa PWM driver by eric miao <eric.miao@marvell.com> | |
| */ | |
| #include <linux/bitfield.h> | |
| #include <linux/bitops.h> | |
| #include <linux/clk.h> | |
| #include <linux/delay.h> | |
| #include <linux/err.h> | |
| #include <linux/io.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/of_device.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/pwm.h> | |
| #include <linux/slab.h> | |
| #define MX1_PWMC 0x00 /* PWM Control Register */ | |
| #define MX1_PWMS 0x04 /* PWM Sample Register */ | |
| #define MX1_PWMP 0x08 /* PWM Period Register */ | |
| #define MX1_PWMC_EN BIT(4) | |
| struct pwm_imx1_chip { | |
| struct clk *clk_ipg; | |
| struct clk *clk_per; | |
| void __iomem *mmio_base; | |
| struct pwm_chip chip; | |
| }; | |
| #define to_pwm_imx1_chip(chip) container_of(chip, struct pwm_imx1_chip, chip) | |
| static int pwm_imx1_clk_prepare_enable(struct pwm_chip *chip) | |
| { | |
| struct pwm_imx1_chip *imx = to_pwm_imx1_chip(chip); | |
| int ret; | |
| ret = clk_prepare_enable(imx->clk_ipg); | |
| if (ret) | |
| return ret; | |
| ret = clk_prepare_enable(imx->clk_per); | |
| if (ret) { | |
| clk_disable_unprepare(imx->clk_ipg); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static void pwm_imx1_clk_disable_unprepare(struct pwm_chip *chip) | |
| { | |
| struct pwm_imx1_chip *imx = to_pwm_imx1_chip(chip); | |
| clk_disable_unprepare(imx->clk_per); | |
| clk_disable_unprepare(imx->clk_ipg); | |
| } | |
| static int pwm_imx1_config(struct pwm_chip *chip, | |
| struct pwm_device *pwm, u64 duty_ns, u64 period_ns) | |
| { | |
| struct pwm_imx1_chip *imx = to_pwm_imx1_chip(chip); | |
| u32 max, p; | |
| /* | |
| * The PWM subsystem allows for exact frequencies. However, | |
| * I cannot connect a scope on my device to the PWM line and | |
| * thus cannot provide the program the PWM controller | |
| * exactly. Instead, I'm relying on the fact that the | |
| * Bootloader (u-boot or WinCE+haret) has programmed the PWM | |
| * function group already. So I'll just modify the PWM sample | |
| * register to follow the ratio of duty_ns vs. period_ns | |
| * accordingly. | |
| * | |
| * This is good enough for programming the brightness of | |
| * the LCD backlight. | |
| * | |
| * The real implementation would divide PERCLK[0] first by | |
| * both the prescaler (/1 .. /128) and then by CLKSEL | |
| * (/2 .. /16). | |
| */ | |
| max = readl(imx->mmio_base + MX1_PWMP); | |
| p = mul_u64_u64_div_u64(max, duty_ns, period_ns); | |
| writel(max - p, imx->mmio_base + MX1_PWMS); | |
| return 0; | |
| } | |
| static int pwm_imx1_enable(struct pwm_chip *chip, struct pwm_device *pwm) | |
| { | |
| struct pwm_imx1_chip *imx = to_pwm_imx1_chip(chip); | |
| u32 value; | |
| int ret; | |
| ret = pwm_imx1_clk_prepare_enable(chip); | |
| if (ret < 0) | |
| return ret; | |
| value = readl(imx->mmio_base + MX1_PWMC); | |
| value |= MX1_PWMC_EN; | |
| writel(value, imx->mmio_base + MX1_PWMC); | |
| return 0; | |
| } | |
| static void pwm_imx1_disable(struct pwm_chip *chip, struct pwm_device *pwm) | |
| { | |
| struct pwm_imx1_chip *imx = to_pwm_imx1_chip(chip); | |
| u32 value; | |
| value = readl(imx->mmio_base + MX1_PWMC); | |
| value &= ~MX1_PWMC_EN; | |
| writel(value, imx->mmio_base + MX1_PWMC); | |
| pwm_imx1_clk_disable_unprepare(chip); | |
| } | |
| static int pwm_imx1_apply(struct pwm_chip *chip, struct pwm_device *pwm, | |
| const struct pwm_state *state) | |
| { | |
| int err; | |
| if (state->polarity != PWM_POLARITY_NORMAL) | |
| return -EINVAL; | |
| if (!state->enabled) { | |
| if (pwm->state.enabled) | |
| pwm_imx1_disable(chip, pwm); | |
| return 0; | |
| } | |
| err = pwm_imx1_config(chip, pwm, state->duty_cycle, state->period); | |
| if (err) | |
| return err; | |
| if (!pwm->state.enabled) | |
| return pwm_imx1_enable(chip, pwm); | |
| return 0; | |
| } | |
| static const struct pwm_ops pwm_imx1_ops = { | |
| .apply = pwm_imx1_apply, | |
| .owner = THIS_MODULE, | |
| }; | |
| static const struct of_device_id pwm_imx1_dt_ids[] = { | |
| { .compatible = "fsl,imx1-pwm", }, | |
| { /* sentinel */ } | |
| }; | |
| MODULE_DEVICE_TABLE(of, pwm_imx1_dt_ids); | |
| static int pwm_imx1_probe(struct platform_device *pdev) | |
| { | |
| struct pwm_imx1_chip *imx; | |
| imx = devm_kzalloc(&pdev->dev, sizeof(*imx), GFP_KERNEL); | |
| if (!imx) | |
| return -ENOMEM; | |
| imx->clk_ipg = devm_clk_get(&pdev->dev, "ipg"); | |
| if (IS_ERR(imx->clk_ipg)) | |
| return dev_err_probe(&pdev->dev, PTR_ERR(imx->clk_ipg), | |
| "getting ipg clock failed\n"); | |
| imx->clk_per = devm_clk_get(&pdev->dev, "per"); | |
| if (IS_ERR(imx->clk_per)) | |
| return dev_err_probe(&pdev->dev, PTR_ERR(imx->clk_per), | |
| "failed to get peripheral clock\n"); | |
| imx->chip.ops = &pwm_imx1_ops; | |
| imx->chip.dev = &pdev->dev; | |
| imx->chip.npwm = 1; | |
| imx->mmio_base = devm_platform_ioremap_resource(pdev, 0); | |
| if (IS_ERR(imx->mmio_base)) | |
| return PTR_ERR(imx->mmio_base); | |
| return devm_pwmchip_add(&pdev->dev, &imx->chip); | |
| } | |
| static struct platform_driver pwm_imx1_driver = { | |
| .driver = { | |
| .name = "pwm-imx1", | |
| .of_match_table = pwm_imx1_dt_ids, | |
| }, | |
| .probe = pwm_imx1_probe, | |
| }; | |
| module_platform_driver(pwm_imx1_driver); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_AUTHOR("Sascha Hauer <s.hauer@pengutronix.de>"); |