1
0
Fork 0
alistair23-linux/drivers/pwm/pwm-lpss.c

279 lines
7.3 KiB
C
Raw Permalink Normal View History

// SPDX-License-Identifier: GPL-2.0-only
/*
* Intel Low Power Subsystem PWM controller driver
*
* Copyright (C) 2014, Intel Corporation
* Author: Mika Westerberg <mika.westerberg@linux.intel.com>
* Author: Chew Kean Ho <kean.ho.chew@intel.com>
* Author: Chang Rebecca Swee Fun <rebecca.swee.fun.chang@intel.com>
* Author: Chew Chiau Ee <chiau.ee.chew@intel.com>
* Author: Alan Cox <alan@linux.intel.com>
*/
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pm_runtime.h>
#include <linux/time.h>
#include "pwm-lpss.h"
#define PWM 0x00000000
#define PWM_ENABLE BIT(31)
#define PWM_SW_UPDATE BIT(30)
#define PWM_BASE_UNIT_SHIFT 8
#define PWM_ON_TIME_DIV_MASK 0x000000ff
/* Size of each PWM register space if multiple */
#define PWM_SIZE 0x400
static inline struct pwm_lpss_chip *to_lpwm(struct pwm_chip *chip)
{
return container_of(chip, struct pwm_lpss_chip, chip);
}
static inline u32 pwm_lpss_read(const struct pwm_device *pwm)
{
struct pwm_lpss_chip *lpwm = to_lpwm(pwm->chip);
return readl(lpwm->regs + pwm->hwpwm * PWM_SIZE + PWM);
}
static inline void pwm_lpss_write(const struct pwm_device *pwm, u32 value)
{
struct pwm_lpss_chip *lpwm = to_lpwm(pwm->chip);
writel(value, lpwm->regs + pwm->hwpwm * PWM_SIZE + PWM);
}
static int pwm_lpss_wait_for_update(struct pwm_device *pwm)
{
struct pwm_lpss_chip *lpwm = to_lpwm(pwm->chip);
const void __iomem *addr = lpwm->regs + pwm->hwpwm * PWM_SIZE + PWM;
const unsigned int ms = 500 * USEC_PER_MSEC;
u32 val;
int err;
/*
* PWM Configuration register has SW_UPDATE bit that is set when a new
* configuration is written to the register. The bit is automatically
* cleared at the start of the next output cycle by the IP block.
*
* If one writes a new configuration to the register while it still has
* the bit enabled, PWM may freeze. That is, while one can still write
* to the register, it won't have an effect. Thus, we try to sleep long
* enough that the bit gets cleared and make sure the bit is not
* enabled while we update the configuration.
*/
err = readl_poll_timeout(addr, val, !(val & PWM_SW_UPDATE), 40, ms);
if (err)
dev_err(pwm->chip->dev, "PWM_SW_UPDATE was not cleared\n");
return err;
}
static inline int pwm_lpss_is_updating(struct pwm_device *pwm)
{
return (pwm_lpss_read(pwm) & PWM_SW_UPDATE) ? -EBUSY : 0;
}
static void pwm_lpss_prepare(struct pwm_lpss_chip *lpwm, struct pwm_device *pwm,
int duty_ns, int period_ns)
{
unsigned long long on_time_div;
unsigned long c = lpwm->info->clk_rate, base_unit_range;
unsigned long long base_unit, freq = NSEC_PER_SEC;
pwm: lpss: Only set update bit if we are actually changing the settings According to the datasheet the update bit must be set if the on-time-div or the base-unit changes. Now that we properly order device resume on Cherry Trail so that the GFX0 _PS0 method no longer exits with an error, we end up with a sequence of events where we are writing the same values twice in a row. First the _PS0 method restores the duty cycle of 0% the GPU driver set on suspend and then the GPU driver first updates just the enabled bit in the pwm_state from 0 to 1, causing us to write the same values again, before restoring the pre-suspend duty-cycle in a separate pwm_apply call. When writing the update bit the second time, without changing any of the values the update bit clears immediately / instantly, instead of staying 1 for a while as usual. After this the next setting of the update bit seems to be ignored, causing the restoring of the pre-suspend duty-cycle to not get applied. This makes the backlight come up with a 0% dutycycle after suspend/resume. Any further brightness changes after this do work. This commit moves the setting of the update bit into pwm_lpss_prepare() and only sets the bit if we have actually changed any of the values. This avoids the setting of the update bit the second time we configure the PWM to 0% dutycycle, this fixes the backlight coming up with 0% duty-cycle after a suspend/resume. Signed-off-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
2018-10-14 09:12:02 -06:00
u32 orig_ctrl, ctrl;
do_div(freq, period_ns);
/*
* The equation is:
* base_unit = round(base_unit_range * freq / c)
*/
base_unit_range = BIT(lpwm->info->base_unit_bits);
freq *= base_unit_range;
base_unit = DIV_ROUND_CLOSEST_ULL(freq, c);
pwm: lpss: Add range limit check for the base_unit register value [ Upstream commit ef9f60daab309558c8bb3e086a9a11ee40bd6061 ] When the user requests a high enough period ns value, then the calculations in pwm_lpss_prepare() might result in a base_unit value of 0. But according to the data-sheet the way the PWM controller works is that each input clock-cycle the base_unit gets added to a N bit counter and that counter overflowing determines the PWM output frequency. Adding 0 to the counter is a no-op. The data-sheet even explicitly states that writing 0 to the base_unit bits will result in the PWM outputting a continuous 0 signal. When the user requestes a low enough period ns value, then the calculations in pwm_lpss_prepare() might result in a base_unit value which is bigger then base_unit_range - 1. Currently the codes for this deals with this by applying a mask: base_unit &= (base_unit_range - 1); But this means that we let the value overflow the range, we throw away the higher bits and store whatever value is left in the lower bits into the register leading to a random output frequency, rather then clamping the output frequency to the highest frequency which the hardware can do. This commit fixes both issues by clamping the base_unit value to be between 1 and (base_unit_range - 1). Fixes: 684309e5043e ("pwm: lpss: Avoid potential overflow of base_unit") Reviewed-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> Acked-by: Thierry Reding <thierry.reding@gmail.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com> Link: https://patchwork.freedesktop.org/patch/msgid/20200903112337.4113-5-hdegoede@redhat.com Signed-off-by: Sasha Levin <sashal@kernel.org>
2020-09-03 05:23:24 -06:00
/* base_unit must not be 0 and we also want to avoid overflowing it */
base_unit = clamp_val(base_unit, 1, base_unit_range - 1);
on_time_div = 255ULL * duty_ns;
do_div(on_time_div, period_ns);
on_time_div = 255ULL - on_time_div;
pwm: lpss: Only set update bit if we are actually changing the settings According to the datasheet the update bit must be set if the on-time-div or the base-unit changes. Now that we properly order device resume on Cherry Trail so that the GFX0 _PS0 method no longer exits with an error, we end up with a sequence of events where we are writing the same values twice in a row. First the _PS0 method restores the duty cycle of 0% the GPU driver set on suspend and then the GPU driver first updates just the enabled bit in the pwm_state from 0 to 1, causing us to write the same values again, before restoring the pre-suspend duty-cycle in a separate pwm_apply call. When writing the update bit the second time, without changing any of the values the update bit clears immediately / instantly, instead of staying 1 for a while as usual. After this the next setting of the update bit seems to be ignored, causing the restoring of the pre-suspend duty-cycle to not get applied. This makes the backlight come up with a 0% dutycycle after suspend/resume. Any further brightness changes after this do work. This commit moves the setting of the update bit into pwm_lpss_prepare() and only sets the bit if we have actually changed any of the values. This avoids the setting of the update bit the second time we configure the PWM to 0% dutycycle, this fixes the backlight coming up with 0% duty-cycle after a suspend/resume. Signed-off-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
2018-10-14 09:12:02 -06:00
orig_ctrl = ctrl = pwm_lpss_read(pwm);
ctrl &= ~PWM_ON_TIME_DIV_MASK;
ctrl &= ~((base_unit_range - 1) << PWM_BASE_UNIT_SHIFT);
ctrl |= (u32) base_unit << PWM_BASE_UNIT_SHIFT;
ctrl |= on_time_div;
pwm: lpss: Only set update bit if we are actually changing the settings According to the datasheet the update bit must be set if the on-time-div or the base-unit changes. Now that we properly order device resume on Cherry Trail so that the GFX0 _PS0 method no longer exits with an error, we end up with a sequence of events where we are writing the same values twice in a row. First the _PS0 method restores the duty cycle of 0% the GPU driver set on suspend and then the GPU driver first updates just the enabled bit in the pwm_state from 0 to 1, causing us to write the same values again, before restoring the pre-suspend duty-cycle in a separate pwm_apply call. When writing the update bit the second time, without changing any of the values the update bit clears immediately / instantly, instead of staying 1 for a while as usual. After this the next setting of the update bit seems to be ignored, causing the restoring of the pre-suspend duty-cycle to not get applied. This makes the backlight come up with a 0% dutycycle after suspend/resume. Any further brightness changes after this do work. This commit moves the setting of the update bit into pwm_lpss_prepare() and only sets the bit if we have actually changed any of the values. This avoids the setting of the update bit the second time we configure the PWM to 0% dutycycle, this fixes the backlight coming up with 0% duty-cycle after a suspend/resume. Signed-off-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
2018-10-14 09:12:02 -06:00
if (orig_ctrl != ctrl) {
pwm_lpss_write(pwm, ctrl);
pwm_lpss_write(pwm, ctrl | PWM_SW_UPDATE);
}
}
static inline void pwm_lpss_cond_enable(struct pwm_device *pwm, bool cond)
{
if (cond)
pwm_lpss_write(pwm, pwm_lpss_read(pwm) | PWM_ENABLE);
}
static int pwm_lpss_apply(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_state *state)
{
struct pwm_lpss_chip *lpwm = to_lpwm(chip);
int ret;
if (state->enabled) {
if (!pwm_is_enabled(pwm)) {
pm_runtime_get_sync(chip->dev);
ret = pwm_lpss_is_updating(pwm);
if (ret) {
pm_runtime_put(chip->dev);
return ret;
}
pwm_lpss_prepare(lpwm, pwm, state->duty_cycle, state->period);
pwm_lpss_cond_enable(pwm, lpwm->info->bypass == false);
ret = pwm_lpss_wait_for_update(pwm);
if (ret) {
pm_runtime_put(chip->dev);
return ret;
}
pwm_lpss_cond_enable(pwm, lpwm->info->bypass == true);
} else {
ret = pwm_lpss_is_updating(pwm);
if (ret)
return ret;
pwm_lpss_prepare(lpwm, pwm, state->duty_cycle, state->period);
return pwm_lpss_wait_for_update(pwm);
}
} else if (pwm_is_enabled(pwm)) {
pwm_lpss_write(pwm, pwm_lpss_read(pwm) & ~PWM_ENABLE);
pm_runtime_put(chip->dev);
}
return 0;
}
/* This function gets called once from pwmchip_add to get the initial state */
static void pwm_lpss_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state)
{
struct pwm_lpss_chip *lpwm = to_lpwm(chip);
unsigned long base_unit_range;
unsigned long long base_unit, freq, on_time_div;
u32 ctrl;
base_unit_range = BIT(lpwm->info->base_unit_bits);
ctrl = pwm_lpss_read(pwm);
on_time_div = 255 - (ctrl & PWM_ON_TIME_DIV_MASK);
base_unit = (ctrl >> PWM_BASE_UNIT_SHIFT) & (base_unit_range - 1);
freq = base_unit * lpwm->info->clk_rate;
do_div(freq, base_unit_range);
if (freq == 0)
state->period = NSEC_PER_SEC;
else
state->period = NSEC_PER_SEC / (unsigned long)freq;
on_time_div *= state->period;
do_div(on_time_div, 255);
state->duty_cycle = on_time_div;
state->polarity = PWM_POLARITY_NORMAL;
state->enabled = !!(ctrl & PWM_ENABLE);
if (state->enabled)
pm_runtime_get(chip->dev);
}
static const struct pwm_ops pwm_lpss_ops = {
.apply = pwm_lpss_apply,
.get_state = pwm_lpss_get_state,
.owner = THIS_MODULE,
};
struct pwm_lpss_chip *pwm_lpss_probe(struct device *dev, struct resource *r,
const struct pwm_lpss_boardinfo *info)
{
struct pwm_lpss_chip *lpwm;
unsigned long c;
int ret;
if (WARN_ON(info->npwm > MAX_PWMS))
return ERR_PTR(-ENODEV);
lpwm = devm_kzalloc(dev, sizeof(*lpwm), GFP_KERNEL);
if (!lpwm)
return ERR_PTR(-ENOMEM);
lpwm->regs = devm_ioremap_resource(dev, r);
if (IS_ERR(lpwm->regs))
pwm: lpss: Fix const qualifier and sparse warnings Fixes the following warnings reported by the 0-DAY kernel build testing backend: drivers/pwm/pwm-lpss.c: In function 'pwm_lpss_probe_pci': >> drivers/pwm/pwm-lpss.c:192:2: warning: passing argument 3 of 'pwm_lpss_probe' discards 'const' qualifier from pointer target type [enabled by default] lpwm = pwm_lpss_probe(&pdev->dev, &pdev->resource[0], info); ^ drivers/pwm/pwm-lpss.c:130:30: note: expected 'struct pwm_lpss_boardinfo *' but argument is of type 'const struct pwm_lpss_boardinfo *' static struct pwm_lpss_chip *pwm_lpss_probe(struct device *dev, ^ >> drivers/pwm/pwm-lpss.c:143:28: sparse: incorrect type in return expression (different address spaces) drivers/pwm/pwm-lpss.c:143:28: expected struct pwm_lpss_chip * drivers/pwm/pwm-lpss.c:143:28: got void [noderef] <asn:2>*regs >> drivers/pwm/pwm-lpss.c:192:63: sparse: incorrect type in argument 3 (different modifiers) drivers/pwm/pwm-lpss.c:192:63: expected struct pwm_lpss_boardinfo *info drivers/pwm/pwm-lpss.c:192:63: got struct pwm_lpss_boardinfo const *[assigned] info drivers/pwm/pwm-lpss.c: In function 'pwm_lpss_probe_pci': drivers/pwm/pwm-lpss.c:192:2: warning: passing argument 3 of 'pwm_lpss_probe' discards 'const' qualifier from pointer target type [enabled by default] lpwm = pwm_lpss_probe(&pdev->dev, &pdev->resource[0], info); ^ drivers/pwm/pwm-lpss.c:130:30: note: expected 'struct pwm_lpss_boardinfo *' but argument is of type 'const struct pwm_lpss_boardinfo *' static struct pwm_lpss_chip *pwm_lpss_probe(struct device *dev, ^ Reported-by: kbuild test robot <fengguang.wu@intel.com> Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
2014-05-07 02:27:57 -06:00
return ERR_CAST(lpwm->regs);
lpwm->info = info;
c = lpwm->info->clk_rate;
if (!c)
return ERR_PTR(-EINVAL);
lpwm->chip.dev = dev;
lpwm->chip.ops = &pwm_lpss_ops;
lpwm->chip.base = -1;
lpwm->chip.npwm = info->npwm;
ret = pwmchip_add(&lpwm->chip);
if (ret) {
dev_err(dev, "failed to add PWM chip: %d\n", ret);
return ERR_PTR(ret);
}
return lpwm;
}
EXPORT_SYMBOL_GPL(pwm_lpss_probe);
int pwm_lpss_remove(struct pwm_lpss_chip *lpwm)
{
int i;
for (i = 0; i < lpwm->info->npwm; i++) {
if (pwm_is_enabled(&lpwm->chip.pwms[i]))
pm_runtime_put(lpwm->chip.dev);
}
return pwmchip_remove(&lpwm->chip);
}
EXPORT_SYMBOL_GPL(pwm_lpss_remove);
int pwm_lpss_suspend(struct device *dev)
{
struct pwm_lpss_chip *lpwm = dev_get_drvdata(dev);
int i;
for (i = 0; i < lpwm->info->npwm; i++)
lpwm->saved_ctrl[i] = readl(lpwm->regs + i * PWM_SIZE + PWM);
return 0;
}
EXPORT_SYMBOL_GPL(pwm_lpss_suspend);
int pwm_lpss_resume(struct device *dev)
{
struct pwm_lpss_chip *lpwm = dev_get_drvdata(dev);
int i;
for (i = 0; i < lpwm->info->npwm; i++)
writel(lpwm->saved_ctrl[i], lpwm->regs + i * PWM_SIZE + PWM);
return 0;
}
EXPORT_SYMBOL_GPL(pwm_lpss_resume);
MODULE_DESCRIPTION("PWM driver for Intel LPSS");
MODULE_AUTHOR("Mika Westerberg <mika.westerberg@linux.intel.com>");
MODULE_LICENSE("GPL v2");