| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2017-2025 Loongson Technology Corporation Limited. |
| * |
| * Loongson PWM driver |
| * |
| * For Loongson's PWM IP block documentation please refer Chapter 11 of |
| * Reference Manual: https://loongson.github.io/LoongArch-Documentation/Loongson-7A1000-usermanual-EN.pdf |
| * |
| * Author: Juxin Gao <gaojuxin@loongson.cn> |
| * Further cleanup and restructuring by: |
| * Binbin Zhou <zhoubinbin@loongson.cn> |
| * |
| * Limitations: |
| * - If both DUTY and PERIOD are set to 0, the output is a constant low signal. |
| * - When disabled the output is driven to 0 independent of the configured |
| * polarity. |
| * - If the register is reconfigured while PWM is running, it does not complete |
| * the currently running period. |
| * - Disabling the PWM stops the output immediately (without waiting for current |
| * period to complete first). |
| */ |
| |
| #include <linux/acpi.h> |
| #include <linux/clk.h> |
| #include <linux/device.h> |
| #include <linux/init.h> |
| #include <linux/io.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/pwm.h> |
| #include <linux/units.h> |
| |
| /* Loongson PWM registers */ |
| #define LOONGSON_PWM_REG_DUTY 0x4 /* Low Pulse Buffer Register */ |
| #define LOONGSON_PWM_REG_PERIOD 0x8 /* Pulse Period Buffer Register */ |
| #define LOONGSON_PWM_REG_CTRL 0xc /* Control Register */ |
| |
| /* Control register bits */ |
| #define LOONGSON_PWM_CTRL_REG_EN BIT(0) /* Counter Enable Bit */ |
| #define LOONGSON_PWM_CTRL_REG_OE BIT(3) /* Pulse Output Enable Control Bit, Valid Low */ |
| #define LOONGSON_PWM_CTRL_REG_SINGLE BIT(4) /* Single Pulse Control Bit */ |
| #define LOONGSON_PWM_CTRL_REG_INTE BIT(5) /* Interrupt Enable Bit */ |
| #define LOONGSON_PWM_CTRL_REG_INT BIT(6) /* Interrupt Bit */ |
| #define LOONGSON_PWM_CTRL_REG_RST BIT(7) /* Counter Reset Bit */ |
| #define LOONGSON_PWM_CTRL_REG_CAPTE BIT(8) /* Measurement Pulse Enable Bit */ |
| #define LOONGSON_PWM_CTRL_REG_INVERT BIT(9) /* Output flip-flop Enable Bit */ |
| #define LOONGSON_PWM_CTRL_REG_DZONE BIT(10) /* Anti-dead Zone Enable Bit */ |
| |
| /* default input clk frequency for the ACPI case */ |
| #define LOONGSON_PWM_FREQ_DEFAULT 50000 /* Hz */ |
| |
| struct pwm_loongson_ddata { |
| struct clk *clk; |
| void __iomem *base; |
| u64 clk_rate; |
| }; |
| |
| static inline __pure struct pwm_loongson_ddata *to_pwm_loongson_ddata(struct pwm_chip *chip) |
| { |
| return pwmchip_get_drvdata(chip); |
| } |
| |
| static inline u32 pwm_loongson_readl(struct pwm_loongson_ddata *ddata, u32 offset) |
| { |
| return readl(ddata->base + offset); |
| } |
| |
| static inline void pwm_loongson_writel(struct pwm_loongson_ddata *ddata, |
| u32 val, u32 offset) |
| { |
| writel(val, ddata->base + offset); |
| } |
| |
| static int pwm_loongson_set_polarity(struct pwm_chip *chip, struct pwm_device *pwm, |
| enum pwm_polarity polarity) |
| { |
| u16 val; |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| |
| if (polarity == PWM_POLARITY_INVERSED) |
| /* Duty cycle defines LOW period of PWM */ |
| val |= LOONGSON_PWM_CTRL_REG_INVERT; |
| else |
| /* Duty cycle defines HIGH period of PWM */ |
| val &= ~LOONGSON_PWM_CTRL_REG_INVERT; |
| |
| pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| |
| return 0; |
| } |
| |
| static void pwm_loongson_disable(struct pwm_chip *chip, struct pwm_device *pwm) |
| { |
| u32 val; |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| val &= ~LOONGSON_PWM_CTRL_REG_EN; |
| pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| } |
| |
| static int pwm_loongson_enable(struct pwm_chip *chip, struct pwm_device *pwm) |
| { |
| u32 val; |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| val |= LOONGSON_PWM_CTRL_REG_EN; |
| pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| |
| return 0; |
| } |
| |
| static int pwm_loongson_config(struct pwm_chip *chip, struct pwm_device *pwm, |
| u64 duty_ns, u64 period_ns) |
| { |
| u64 duty, period; |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| /* duty = duty_ns * ddata->clk_rate / NSEC_PER_SEC */ |
| duty = mul_u64_u64_div_u64(duty_ns, ddata->clk_rate, NSEC_PER_SEC); |
| if (duty > U32_MAX) |
| duty = U32_MAX; |
| |
| /* period = period_ns * ddata->clk_rate / NSEC_PER_SEC */ |
| period = mul_u64_u64_div_u64(period_ns, ddata->clk_rate, NSEC_PER_SEC); |
| if (period > U32_MAX) |
| period = U32_MAX; |
| |
| pwm_loongson_writel(ddata, duty, LOONGSON_PWM_REG_DUTY); |
| pwm_loongson_writel(ddata, period, LOONGSON_PWM_REG_PERIOD); |
| |
| return 0; |
| } |
| |
| static int pwm_loongson_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
| const struct pwm_state *state) |
| { |
| int ret; |
| bool enabled = pwm->state.enabled; |
| |
| if (!state->enabled) { |
| if (enabled) |
| pwm_loongson_disable(chip, pwm); |
| return 0; |
| } |
| |
| ret = pwm_loongson_set_polarity(chip, pwm, state->polarity); |
| if (ret) |
| return ret; |
| |
| ret = pwm_loongson_config(chip, pwm, state->duty_cycle, state->period); |
| if (ret) |
| return ret; |
| |
| if (!enabled && state->enabled) |
| ret = pwm_loongson_enable(chip, pwm); |
| |
| return ret; |
| } |
| |
| static int pwm_loongson_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
| struct pwm_state *state) |
| { |
| u32 duty, period, ctrl; |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| duty = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY); |
| period = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD); |
| ctrl = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| |
| /* duty & period have a max of 2^32, so we can't overflow */ |
| state->duty_cycle = DIV64_U64_ROUND_UP((u64)duty * NSEC_PER_SEC, ddata->clk_rate); |
| state->period = DIV64_U64_ROUND_UP((u64)period * NSEC_PER_SEC, ddata->clk_rate); |
| state->polarity = (ctrl & LOONGSON_PWM_CTRL_REG_INVERT) ? PWM_POLARITY_INVERSED : |
| PWM_POLARITY_NORMAL; |
| state->enabled = (ctrl & LOONGSON_PWM_CTRL_REG_EN) ? true : false; |
| |
| return 0; |
| } |
| |
| static const struct pwm_ops pwm_loongson_ops = { |
| .apply = pwm_loongson_apply, |
| .get_state = pwm_loongson_get_state, |
| }; |
| |
| static int pwm_loongson_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct pwm_chip *chip; |
| struct pwm_loongson_ddata *ddata; |
| struct device *dev = &pdev->dev; |
| |
| chip = devm_pwmchip_alloc(dev, 1, sizeof(*ddata)); |
| if (IS_ERR(chip)) |
| return PTR_ERR(chip); |
| ddata = to_pwm_loongson_ddata(chip); |
| |
| ddata->base = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(ddata->base)) |
| return PTR_ERR(ddata->base); |
| |
| ddata->clk = devm_clk_get_optional_enabled(dev, NULL); |
| if (IS_ERR(ddata->clk)) |
| return dev_err_probe(dev, PTR_ERR(ddata->clk), |
| "Failed to get pwm clock\n"); |
| if (ddata->clk) { |
| ret = devm_clk_rate_exclusive_get(dev, ddata->clk); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Failed to get exclusive rate\n"); |
| |
| ddata->clk_rate = clk_get_rate(ddata->clk); |
| if (!ddata->clk_rate) |
| return dev_err_probe(dev, -EINVAL, |
| "Failed to get frequency\n"); |
| } else { |
| ddata->clk_rate = LOONGSON_PWM_FREQ_DEFAULT; |
| } |
| |
| /* This check is done to prevent an overflow in .apply */ |
| if (ddata->clk_rate > NSEC_PER_SEC) |
| return dev_err_probe(dev, -EINVAL, "PWM clock out of range\n"); |
| |
| chip->ops = &pwm_loongson_ops; |
| chip->atomic = true; |
| dev_set_drvdata(dev, chip); |
| |
| ret = devm_pwmchip_add(dev, chip); |
| if (ret < 0) |
| return dev_err_probe(dev, ret, "Failed to add PWM chip\n"); |
| |
| return 0; |
| } |
| |
| static int pwm_loongson_suspend(struct device *dev) |
| { |
| struct pwm_chip *chip = dev_get_drvdata(dev); |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| struct pwm_device *pwm = &chip->pwms[0]; |
| |
| if (pwm->state.enabled) |
| return -EBUSY; |
| |
| clk_disable_unprepare(ddata->clk); |
| |
| return 0; |
| } |
| |
| static int pwm_loongson_resume(struct device *dev) |
| { |
| struct pwm_chip *chip = dev_get_drvdata(dev); |
| struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| |
| return clk_prepare_enable(ddata->clk); |
| } |
| |
| static DEFINE_SIMPLE_DEV_PM_OPS(pwm_loongson_pm_ops, pwm_loongson_suspend, |
| pwm_loongson_resume); |
| |
| static const struct of_device_id pwm_loongson_of_ids[] = { |
| { .compatible = "loongson,ls7a-pwm" }, |
| { /* sentinel */ }, |
| }; |
| MODULE_DEVICE_TABLE(of, pwm_loongson_of_ids); |
| |
| static const struct acpi_device_id pwm_loongson_acpi_ids[] = { |
| { "LOON0006" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(acpi, pwm_loongson_acpi_ids); |
| |
| static struct platform_driver pwm_loongson_driver = { |
| .probe = pwm_loongson_probe, |
| .driver = { |
| .name = "loongson-pwm", |
| .pm = pm_ptr(&pwm_loongson_pm_ops), |
| .of_match_table = pwm_loongson_of_ids, |
| .acpi_match_table = pwm_loongson_acpi_ids, |
| }, |
| }; |
| module_platform_driver(pwm_loongson_driver); |
| |
| MODULE_DESCRIPTION("Loongson PWM driver"); |
| MODULE_AUTHOR("Loongson Technology Corporation Limited."); |
| MODULE_LICENSE("GPL"); |