// 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 * Further cleanup and restructuring by: * Binbin Zhou * * 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 #include #include #include #include #include #include #include #include #include /* 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");