|
| 1 | +// SPDX-License-Identifier: GPL-2.0-only |
| 2 | +/* |
| 3 | + * Copyright (C) 2017-2025 Loongson Technology Corporation Limited. |
| 4 | + * |
| 5 | + * Loongson PWM driver |
| 6 | + * |
| 7 | + * For Loongson's PWM IP block documentation please refer Chapter 11 of |
| 8 | + * Reference Manual: https://loongson.github.io/LoongArch-Documentation/Loongson-7A1000-usermanual-EN.pdf |
| 9 | + * |
| 10 | + * Author: Juxin Gao <[email protected]> |
| 11 | + * Further cleanup and restructuring by: |
| 12 | + |
| 13 | + * |
| 14 | + * Limitations: |
| 15 | + * - If both DUTY and PERIOD are set to 0, the output is a constant low signal. |
| 16 | + * - When disabled the output is driven to 0 independent of the configured |
| 17 | + * polarity. |
| 18 | + * - If the register is reconfigured while PWM is running, it does not complete |
| 19 | + * the currently running period. |
| 20 | + * - Disabling the PWM stops the output immediately (without waiting for current |
| 21 | + * period to complete first). |
| 22 | + */ |
| 23 | + |
| 24 | +#include <linux/acpi.h> |
| 25 | +#include <linux/clk.h> |
| 26 | +#include <linux/device.h> |
| 27 | +#include <linux/init.h> |
| 28 | +#include <linux/io.h> |
| 29 | +#include <linux/kernel.h> |
| 30 | +#include <linux/module.h> |
| 31 | +#include <linux/platform_device.h> |
| 32 | +#include <linux/pwm.h> |
| 33 | +#include <linux/units.h> |
| 34 | + |
| 35 | +/* Loongson PWM registers */ |
| 36 | +#define LOONGSON_PWM_REG_DUTY 0x4 /* Low Pulse Buffer Register */ |
| 37 | +#define LOONGSON_PWM_REG_PERIOD 0x8 /* Pulse Period Buffer Register */ |
| 38 | +#define LOONGSON_PWM_REG_CTRL 0xc /* Control Register */ |
| 39 | + |
| 40 | +/* Control register bits */ |
| 41 | +#define LOONGSON_PWM_CTRL_REG_EN BIT(0) /* Counter Enable Bit */ |
| 42 | +#define LOONGSON_PWM_CTRL_REG_OE BIT(3) /* Pulse Output Enable Control Bit, Valid Low */ |
| 43 | +#define LOONGSON_PWM_CTRL_REG_SINGLE BIT(4) /* Single Pulse Control Bit */ |
| 44 | +#define LOONGSON_PWM_CTRL_REG_INTE BIT(5) /* Interrupt Enable Bit */ |
| 45 | +#define LOONGSON_PWM_CTRL_REG_INT BIT(6) /* Interrupt Bit */ |
| 46 | +#define LOONGSON_PWM_CTRL_REG_RST BIT(7) /* Counter Reset Bit */ |
| 47 | +#define LOONGSON_PWM_CTRL_REG_CAPTE BIT(8) /* Measurement Pulse Enable Bit */ |
| 48 | +#define LOONGSON_PWM_CTRL_REG_INVERT BIT(9) /* Output flip-flop Enable Bit */ |
| 49 | +#define LOONGSON_PWM_CTRL_REG_DZONE BIT(10) /* Anti-dead Zone Enable Bit */ |
| 50 | + |
| 51 | +/* default input clk frequency for the ACPI case */ |
| 52 | +#define LOONGSON_PWM_FREQ_DEFAULT 50000 /* Hz */ |
| 53 | + |
| 54 | +struct pwm_loongson_ddata { |
| 55 | + struct clk *clk; |
| 56 | + void __iomem *base; |
| 57 | + u64 clk_rate; |
| 58 | +}; |
| 59 | + |
| 60 | +static inline __pure struct pwm_loongson_ddata *to_pwm_loongson_ddata(struct pwm_chip *chip) |
| 61 | +{ |
| 62 | + return pwmchip_get_drvdata(chip); |
| 63 | +} |
| 64 | + |
| 65 | +static inline u32 pwm_loongson_readl(struct pwm_loongson_ddata *ddata, u32 offset) |
| 66 | +{ |
| 67 | + return readl(ddata->base + offset); |
| 68 | +} |
| 69 | + |
| 70 | +static inline void pwm_loongson_writel(struct pwm_loongson_ddata *ddata, |
| 71 | + u32 val, u32 offset) |
| 72 | +{ |
| 73 | + writel(val, ddata->base + offset); |
| 74 | +} |
| 75 | + |
| 76 | +static int pwm_loongson_set_polarity(struct pwm_chip *chip, struct pwm_device *pwm, |
| 77 | + enum pwm_polarity polarity) |
| 78 | +{ |
| 79 | + u16 val; |
| 80 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 81 | + |
| 82 | + val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| 83 | + |
| 84 | + if (polarity == PWM_POLARITY_INVERSED) |
| 85 | + /* Duty cycle defines LOW period of PWM */ |
| 86 | + val |= LOONGSON_PWM_CTRL_REG_INVERT; |
| 87 | + else |
| 88 | + /* Duty cycle defines HIGH period of PWM */ |
| 89 | + val &= ~LOONGSON_PWM_CTRL_REG_INVERT; |
| 90 | + |
| 91 | + pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| 92 | + |
| 93 | + return 0; |
| 94 | +} |
| 95 | + |
| 96 | +static void pwm_loongson_disable(struct pwm_chip *chip, struct pwm_device *pwm) |
| 97 | +{ |
| 98 | + u32 val; |
| 99 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 100 | + |
| 101 | + val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| 102 | + val &= ~LOONGSON_PWM_CTRL_REG_EN; |
| 103 | + pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| 104 | +} |
| 105 | + |
| 106 | +static int pwm_loongson_enable(struct pwm_chip *chip, struct pwm_device *pwm) |
| 107 | +{ |
| 108 | + u32 val; |
| 109 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 110 | + |
| 111 | + val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| 112 | + val |= LOONGSON_PWM_CTRL_REG_EN; |
| 113 | + pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL); |
| 114 | + |
| 115 | + return 0; |
| 116 | +} |
| 117 | + |
| 118 | +static int pwm_loongson_config(struct pwm_chip *chip, struct pwm_device *pwm, |
| 119 | + u64 duty_ns, u64 period_ns) |
| 120 | +{ |
| 121 | + u32 duty, period; |
| 122 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 123 | + |
| 124 | + /* duty = duty_ns * ddata->clk_rate / NSEC_PER_SEC */ |
| 125 | + duty = mul_u64_u64_div_u64(duty_ns, ddata->clk_rate, NSEC_PER_SEC); |
| 126 | + if (duty > U32_MAX) |
| 127 | + duty = U32_MAX; |
| 128 | + |
| 129 | + /* period = period_ns * ddata->clk_rate / NSEC_PER_SEC */ |
| 130 | + period = mul_u64_u64_div_u64(period_ns, ddata->clk_rate, NSEC_PER_SEC); |
| 131 | + if (period > U32_MAX) |
| 132 | + period = U32_MAX; |
| 133 | + |
| 134 | + pwm_loongson_writel(ddata, duty, LOONGSON_PWM_REG_DUTY); |
| 135 | + pwm_loongson_writel(ddata, period, LOONGSON_PWM_REG_PERIOD); |
| 136 | + |
| 137 | + return 0; |
| 138 | +} |
| 139 | + |
| 140 | +static int pwm_loongson_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
| 141 | + const struct pwm_state *state) |
| 142 | +{ |
| 143 | + int ret; |
| 144 | + bool enabled = pwm->state.enabled; |
| 145 | + |
| 146 | + if (!state->enabled) { |
| 147 | + if (enabled) |
| 148 | + pwm_loongson_disable(chip, pwm); |
| 149 | + return 0; |
| 150 | + } |
| 151 | + |
| 152 | + ret = pwm_loongson_set_polarity(chip, pwm, state->polarity); |
| 153 | + if (ret) |
| 154 | + return ret; |
| 155 | + |
| 156 | + ret = pwm_loongson_config(chip, pwm, state->duty_cycle, state->period); |
| 157 | + if (ret) |
| 158 | + return ret; |
| 159 | + |
| 160 | + if (!enabled && state->enabled) |
| 161 | + ret = pwm_loongson_enable(chip, pwm); |
| 162 | + |
| 163 | + return ret; |
| 164 | +} |
| 165 | + |
| 166 | +static int pwm_loongson_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
| 167 | + struct pwm_state *state) |
| 168 | +{ |
| 169 | + u32 duty, period, ctrl; |
| 170 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 171 | + |
| 172 | + duty = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY); |
| 173 | + period = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD); |
| 174 | + ctrl = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL); |
| 175 | + |
| 176 | + /* duty & period have a max of 2^32, so we can't overflow */ |
| 177 | + state->duty_cycle = DIV64_U64_ROUND_UP((u64)duty * NSEC_PER_SEC, ddata->clk_rate); |
| 178 | + state->period = DIV64_U64_ROUND_UP((u64)period * NSEC_PER_SEC, ddata->clk_rate); |
| 179 | + state->polarity = (ctrl & LOONGSON_PWM_CTRL_REG_INVERT) ? PWM_POLARITY_INVERSED : |
| 180 | + PWM_POLARITY_NORMAL; |
| 181 | + state->enabled = (ctrl & LOONGSON_PWM_CTRL_REG_EN) ? true : false; |
| 182 | + |
| 183 | + return 0; |
| 184 | +} |
| 185 | + |
| 186 | +static const struct pwm_ops pwm_loongson_ops = { |
| 187 | + .apply = pwm_loongson_apply, |
| 188 | + .get_state = pwm_loongson_get_state, |
| 189 | +}; |
| 190 | + |
| 191 | +static int pwm_loongson_probe(struct platform_device *pdev) |
| 192 | +{ |
| 193 | + int ret; |
| 194 | + struct pwm_chip *chip; |
| 195 | + struct pwm_loongson_ddata *ddata; |
| 196 | + struct device *dev = &pdev->dev; |
| 197 | + |
| 198 | + chip = devm_pwmchip_alloc(dev, 1, sizeof(*ddata)); |
| 199 | + if (IS_ERR(chip)) |
| 200 | + return PTR_ERR(chip); |
| 201 | + ddata = to_pwm_loongson_ddata(chip); |
| 202 | + |
| 203 | + ddata->base = devm_platform_ioremap_resource(pdev, 0); |
| 204 | + if (IS_ERR(ddata->base)) |
| 205 | + return PTR_ERR(ddata->base); |
| 206 | + |
| 207 | + ddata->clk = devm_clk_get_optional_enabled(dev, NULL); |
| 208 | + if (IS_ERR(ddata->clk)) |
| 209 | + return dev_err_probe(dev, PTR_ERR(ddata->clk), |
| 210 | + "Failed to get pwm clock\n"); |
| 211 | + if (ddata->clk) { |
| 212 | + ret = devm_clk_rate_exclusive_get(dev, ddata->clk); |
| 213 | + if (ret) |
| 214 | + return dev_err_probe(dev, PTR_ERR(ddata->clk), |
| 215 | + "Failed to get exclusive rate\n"); |
| 216 | + |
| 217 | + ddata->clk_rate = clk_get_rate(ddata->clk); |
| 218 | + if (!ddata->clk_rate) |
| 219 | + return dev_err_probe(dev, -EINVAL, |
| 220 | + "Failed to get frequency\n"); |
| 221 | + } else { |
| 222 | + ddata->clk_rate = LOONGSON_PWM_FREQ_DEFAULT; |
| 223 | + } |
| 224 | + |
| 225 | + /* This check is done to prevent an overflow in .apply */ |
| 226 | + if (ddata->clk_rate > NSEC_PER_SEC) |
| 227 | + return dev_err_probe(dev, -EINVAL, "PWM clock out of range\n"); |
| 228 | + |
| 229 | + chip->ops = &pwm_loongson_ops; |
| 230 | + chip->atomic = true; |
| 231 | + dev_set_drvdata(dev, chip); |
| 232 | + |
| 233 | + ret = devm_pwmchip_add(dev, chip); |
| 234 | + if (ret < 0) |
| 235 | + return dev_err_probe(dev, ret, "Failed to add PWM chip\n"); |
| 236 | + |
| 237 | + return 0; |
| 238 | +} |
| 239 | + |
| 240 | +static int pwm_loongson_suspend(struct device *dev) |
| 241 | +{ |
| 242 | + struct pwm_chip *chip = dev_get_drvdata(dev); |
| 243 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 244 | + struct pwm_device *pwm = &chip->pwms[0]; |
| 245 | + |
| 246 | + if (pwm->state.enabled) |
| 247 | + return -EBUSY; |
| 248 | + |
| 249 | + clk_disable_unprepare(ddata->clk); |
| 250 | + |
| 251 | + return 0; |
| 252 | +} |
| 253 | + |
| 254 | +static int pwm_loongson_resume(struct device *dev) |
| 255 | +{ |
| 256 | + struct pwm_chip *chip = dev_get_drvdata(dev); |
| 257 | + struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip); |
| 258 | + |
| 259 | + return clk_prepare_enable(ddata->clk); |
| 260 | +} |
| 261 | + |
| 262 | +static DEFINE_SIMPLE_DEV_PM_OPS(pwm_loongson_pm_ops, pwm_loongson_suspend, |
| 263 | + pwm_loongson_resume); |
| 264 | + |
| 265 | +static const struct of_device_id pwm_loongson_of_ids[] = { |
| 266 | + { .compatible = "loongson,ls7a-pwm" }, |
| 267 | + { /* sentinel */ }, |
| 268 | +}; |
| 269 | +MODULE_DEVICE_TABLE(of, pwm_loongson_of_ids); |
| 270 | + |
| 271 | +static const struct acpi_device_id pwm_loongson_acpi_ids[] = { |
| 272 | + { "LOON0006" }, |
| 273 | + { } |
| 274 | +}; |
| 275 | +MODULE_DEVICE_TABLE(acpi, pwm_loongson_acpi_ids); |
| 276 | + |
| 277 | +static struct platform_driver pwm_loongson_driver = { |
| 278 | + .probe = pwm_loongson_probe, |
| 279 | + .driver = { |
| 280 | + .name = "loongson-pwm", |
| 281 | + .pm = pm_ptr(&pwm_loongson_pm_ops), |
| 282 | + .of_match_table = pwm_loongson_of_ids, |
| 283 | + .acpi_match_table = pwm_loongson_acpi_ids, |
| 284 | + }, |
| 285 | +}; |
| 286 | +module_platform_driver(pwm_loongson_driver); |
| 287 | + |
| 288 | +MODULE_DESCRIPTION("Loongson PWM driver"); |
| 289 | +MODULE_AUTHOR("Loongson Technology Corporation Limited."); |
| 290 | +MODULE_LICENSE("GPL"); |
0 commit comments