forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
6.0 KiB
234 lines
6.0 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Intel Keem Bay PWM driver |
|
* |
|
* Copyright (C) 2020 Intel Corporation |
|
* Authors: Lai Poey Seng <[email protected]> |
|
* Vineetha G. Jaya Kumaran <[email protected]> |
|
* |
|
* Limitations: |
|
* - Upon disabling a channel, the currently running |
|
* period will not be completed. However, upon |
|
* reconfiguration of the duty cycle/period, the |
|
* currently running period will be completed first. |
|
*/ |
|
|
|
#include <linux/bitfield.h> |
|
#include <linux/clk.h> |
|
#include <linux/io.h> |
|
#include <linux/mod_devicetable.h> |
|
#include <linux/module.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/pwm.h> |
|
#include <linux/regmap.h> |
|
|
|
#define KMB_TOTAL_PWM_CHANNELS 6 |
|
#define KMB_PWM_COUNT_MAX U16_MAX |
|
#define KMB_PWM_EN_BIT BIT(31) |
|
|
|
/* Mask */ |
|
#define KMB_PWM_HIGH_MASK GENMASK(31, 16) |
|
#define KMB_PWM_LOW_MASK GENMASK(15, 0) |
|
#define KMB_PWM_LEADIN_MASK GENMASK(30, 0) |
|
|
|
/* PWM Register offset */ |
|
#define KMB_PWM_LEADIN_OFFSET(ch) (0x00 + 4 * (ch)) |
|
#define KMB_PWM_HIGHLOW_OFFSET(ch) (0x20 + 4 * (ch)) |
|
|
|
struct keembay_pwm { |
|
struct pwm_chip chip; |
|
struct device *dev; |
|
struct clk *clk; |
|
void __iomem *base; |
|
}; |
|
|
|
static inline struct keembay_pwm *to_keembay_pwm_dev(struct pwm_chip *chip) |
|
{ |
|
return container_of(chip, struct keembay_pwm, chip); |
|
} |
|
|
|
static void keembay_clk_unprepare(void *data) |
|
{ |
|
clk_disable_unprepare(data); |
|
} |
|
|
|
static int keembay_clk_enable(struct device *dev, struct clk *clk) |
|
{ |
|
int ret; |
|
|
|
ret = clk_prepare_enable(clk); |
|
if (ret) |
|
return ret; |
|
|
|
return devm_add_action_or_reset(dev, keembay_clk_unprepare, clk); |
|
} |
|
|
|
/* |
|
* With gcc 10, CONFIG_CC_OPTIMIZE_FOR_SIZE and only "inline" instead of |
|
* "__always_inline" this fails to compile because the compiler doesn't notice |
|
* for all valid masks (e.g. KMB_PWM_LEADIN_MASK) that they are ok. |
|
*/ |
|
static __always_inline void keembay_pwm_update_bits(struct keembay_pwm *priv, u32 mask, |
|
u32 val, u32 offset) |
|
{ |
|
u32 buff = readl(priv->base + offset); |
|
|
|
buff = u32_replace_bits(buff, val, mask); |
|
writel(buff, priv->base + offset); |
|
} |
|
|
|
static void keembay_pwm_enable(struct keembay_pwm *priv, int ch) |
|
{ |
|
keembay_pwm_update_bits(priv, KMB_PWM_EN_BIT, 1, |
|
KMB_PWM_LEADIN_OFFSET(ch)); |
|
} |
|
|
|
static void keembay_pwm_disable(struct keembay_pwm *priv, int ch) |
|
{ |
|
keembay_pwm_update_bits(priv, KMB_PWM_EN_BIT, 0, |
|
KMB_PWM_LEADIN_OFFSET(ch)); |
|
} |
|
|
|
static void keembay_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
|
struct pwm_state *state) |
|
{ |
|
struct keembay_pwm *priv = to_keembay_pwm_dev(chip); |
|
unsigned long long high, low; |
|
unsigned long clk_rate; |
|
u32 highlow; |
|
|
|
clk_rate = clk_get_rate(priv->clk); |
|
|
|
/* Read channel enabled status */ |
|
highlow = readl(priv->base + KMB_PWM_LEADIN_OFFSET(pwm->hwpwm)); |
|
if (highlow & KMB_PWM_EN_BIT) |
|
state->enabled = true; |
|
else |
|
state->enabled = false; |
|
|
|
/* Read period and duty cycle */ |
|
highlow = readl(priv->base + KMB_PWM_HIGHLOW_OFFSET(pwm->hwpwm)); |
|
low = FIELD_GET(KMB_PWM_LOW_MASK, highlow) * NSEC_PER_SEC; |
|
high = FIELD_GET(KMB_PWM_HIGH_MASK, highlow) * NSEC_PER_SEC; |
|
state->duty_cycle = DIV_ROUND_UP_ULL(high, clk_rate); |
|
state->period = DIV_ROUND_UP_ULL(high + low, clk_rate); |
|
state->polarity = PWM_POLARITY_NORMAL; |
|
} |
|
|
|
static int keembay_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
|
const struct pwm_state *state) |
|
{ |
|
struct keembay_pwm *priv = to_keembay_pwm_dev(chip); |
|
struct pwm_state current_state; |
|
unsigned long long div; |
|
unsigned long clk_rate; |
|
u32 pwm_count = 0; |
|
u16 high, low; |
|
|
|
if (state->polarity != PWM_POLARITY_NORMAL) |
|
return -EINVAL; |
|
|
|
/* |
|
* Configure the pwm repeat count as infinite at (15:0) and leadin |
|
* low time as 0 at (30:16), which is in terms of clock cycles. |
|
*/ |
|
keembay_pwm_update_bits(priv, KMB_PWM_LEADIN_MASK, 0, |
|
KMB_PWM_LEADIN_OFFSET(pwm->hwpwm)); |
|
|
|
keembay_pwm_get_state(chip, pwm, ¤t_state); |
|
|
|
if (!state->enabled) { |
|
if (current_state.enabled) |
|
keembay_pwm_disable(priv, pwm->hwpwm); |
|
return 0; |
|
} |
|
|
|
/* |
|
* The upper 16 bits and lower 16 bits of the KMB_PWM_HIGHLOW_OFFSET |
|
* register contain the high time and low time of waveform accordingly. |
|
* All the values are in terms of clock cycles. |
|
*/ |
|
|
|
clk_rate = clk_get_rate(priv->clk); |
|
div = clk_rate * state->duty_cycle; |
|
div = DIV_ROUND_DOWN_ULL(div, NSEC_PER_SEC); |
|
if (div > KMB_PWM_COUNT_MAX) |
|
return -ERANGE; |
|
|
|
high = div; |
|
div = clk_rate * state->period; |
|
div = DIV_ROUND_DOWN_ULL(div, NSEC_PER_SEC); |
|
div = div - high; |
|
if (div > KMB_PWM_COUNT_MAX) |
|
return -ERANGE; |
|
|
|
low = div; |
|
|
|
pwm_count = FIELD_PREP(KMB_PWM_HIGH_MASK, high) | |
|
FIELD_PREP(KMB_PWM_LOW_MASK, low); |
|
|
|
writel(pwm_count, priv->base + KMB_PWM_HIGHLOW_OFFSET(pwm->hwpwm)); |
|
|
|
if (state->enabled && !current_state.enabled) |
|
keembay_pwm_enable(priv, pwm->hwpwm); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct pwm_ops keembay_pwm_ops = { |
|
.owner = THIS_MODULE, |
|
.apply = keembay_pwm_apply, |
|
.get_state = keembay_pwm_get_state, |
|
}; |
|
|
|
static int keembay_pwm_probe(struct platform_device *pdev) |
|
{ |
|
struct device *dev = &pdev->dev; |
|
struct keembay_pwm *priv; |
|
int ret; |
|
|
|
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
|
if (!priv) |
|
return -ENOMEM; |
|
|
|
priv->clk = devm_clk_get(dev, NULL); |
|
if (IS_ERR(priv->clk)) |
|
return dev_err_probe(dev, PTR_ERR(priv->clk), "Failed to get clock\n"); |
|
|
|
priv->base = devm_platform_ioremap_resource(pdev, 0); |
|
if (IS_ERR(priv->base)) |
|
return PTR_ERR(priv->base); |
|
|
|
ret = keembay_clk_enable(dev, priv->clk); |
|
if (ret) |
|
return ret; |
|
|
|
priv->chip.dev = dev; |
|
priv->chip.ops = &keembay_pwm_ops; |
|
priv->chip.npwm = KMB_TOTAL_PWM_CHANNELS; |
|
|
|
ret = devm_pwmchip_add(dev, &priv->chip); |
|
if (ret) |
|
return dev_err_probe(dev, ret, "Failed to add PWM chip\n"); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct of_device_id keembay_pwm_of_match[] = { |
|
{ .compatible = "intel,keembay-pwm" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, keembay_pwm_of_match); |
|
|
|
static struct platform_driver keembay_pwm_driver = { |
|
.probe = keembay_pwm_probe, |
|
.driver = { |
|
.name = "pwm-keembay", |
|
.of_match_table = keembay_pwm_of_match, |
|
}, |
|
}; |
|
module_platform_driver(keembay_pwm_driver); |
|
|
|
MODULE_ALIAS("platform:pwm-keembay"); |
|
MODULE_DESCRIPTION("Intel Keem Bay PWM driver"); |
|
MODULE_LICENSE("GPL v2");
|
|
|