blob: de0f8b077b7c2a919a8ca9ca2f1188ae7b6166d0 [file] [log] [blame]
/**
* linux/drivers/parrot/pwm/p7mu-pwm.c - Parrot7 power management unit PWM
* implementation
*
* Copyright (C) 2012 Parrot S.A.
*
* author: Gregor Boirie <gregor.boirie@parrot.com>
* date: 16-Nov-2012
*
* This file is released under the GPL
*
* TODO:
* - review
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/err.h>
#include <linux/seq_file.h>
#include <linux/pwm.h>
#include <mfd/p7mu-pin.h>
#include <mfd/p7mu-clk.h>
#define P7MU_PWM_RATIO ((u16) 0)
#define P7MU_PWM_DIV ((u16) 1)
#define P7MU_PWM_CTRL ((u16) 2)
#define P7MU_PWM_EN ((u16) (1U << 0))
#define P7MU_PWM_LOAD ((u16) (1U << 1))
#ifdef DEBUG
#define P7MU_ASSERT_PWM(_chip, _pwm) \
BUG_ON(! _chip); \
BUG_ON(! _pwm); \
BUG_ON(_chip != &p7mu_pwm_chip); \
BUG_ON(_chip != _pwm->chip); \
BUG_ON(_chip->npwm != P7MU_PWMS_MAX); \
BUG_ON(! _chip->pwms); \
BUG_ON(_pwm->hwpwm >= chip->npwm); \
BUG_ON(! p7mu_pwm_data[_pwm->hwpwm].res); \
BUG_ON(IS_ERR(p7mu_pwm_data[_pwm->hwpwm].res))
#else
#define P7MU_ASSERT_PWM(_chip, _pwm)
#endif
struct p7mu_pwm {
u16 div;
u16 ratio;
struct resource const* res;
};
static struct pwm_chip p7mu_pwm_chip;
static struct p7mu_pwm p7mu_pwm_data[P7MU_PWMS_MAX];
/* Multiplex requested PWM block to pin setup by platform. */
static int p7mu_request_pwm(struct pwm_chip* chip, struct pwm_device* pwm)
{
int err;
P7MU_ASSERT_PWM(chip, pwm);
/* Forward call to P7MU pin management layer. */
err = p7mu_request_pin((unsigned int) p7mu_pdata()->pwm_pins[pwm->hwpwm],
P7MU_PWM_MUX);
if (! err) {
dev_dbg(chip->dev, "requested %d[%d]\n", pwm->pwm, pwm->hwpwm);
return 0;
}
dev_err(chip->dev,
"failed to request %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
err);
return err;
}
static void p7mu_free_pwm(struct pwm_chip* chip, struct pwm_device* pwm)
{
struct p7mu_pwm* const data = &p7mu_pwm_data[pwm->hwpwm];
int err;
P7MU_ASSERT_PWM(chip, pwm);
err = p7mu_write16(data->res->start + P7MU_PWM_RATIO, 0);
if (err) {
dev_dbg(chip->dev, "failed to clear ratio (%d)\n", err);
goto err;
}
err = p7mu_mod16(data->res->start + P7MU_PWM_CTRL,
P7MU_PWM_LOAD,
P7MU_PWM_LOAD);
if (err) {
dev_dbg(chip->dev, "failed to reload ratio (%d)\n", err);
goto err;
}
else
data->ratio = 0;
err = p7mu_write16(data->res->start + P7MU_PWM_CTRL, 0);
if (err) {
dev_dbg(chip->dev, "failed to stop (%d)\n", err);
goto err;
}
/* Forward call to P7MU pin management layer. */
p7mu_free_pin((unsigned int) p7mu_pdata()->pwm_pins[pwm->hwpwm]);
dev_dbg(chip->dev, "released %d[%d]\n", pwm->pwm, pwm->hwpwm);
err:
dev_err(chip->dev,
"failed to release %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
err);
}
static unsigned long p7mu_pwm_period(uint16_t div, unsigned long hz)
{
return ((256 * ((unsigned long) div + 1)) *
((unsigned long) USEC_PER_SEC / 100UL)) /
(hz / 100UL);
}
static unsigned long p7mu_pwm_duty(unsigned long period, uint16_t ratio)
{
return (period * (unsigned long) ratio) / 256;
}
/*
* PWM layer guarantees duty_ns <= period_ns & period_ns > 0.
* Compute using usecs to avoid overflows. Periods range:
* 128000 nsec -> 0xffffffff nsec (~ 4.3 sec)
* 7128 Hz -> ~ 0.23 Hz
*/
static int p7mu_config_pwm(struct pwm_chip* chip,
struct pwm_device* pwm,
int duty_ns,
int period_ns)
{
struct p7mu_pwm* const data = &p7mu_pwm_data[pwm->hwpwm];
unsigned long const us = (unsigned long) period_ns / NSEC_PER_USEC;
uint16_t div, ratio;
unsigned long phz;
int err;
P7MU_ASSERT_PWM(chip, pwm);
if (! us) {
dev_dbg(chip->dev, "invalid period requested\n");
err = -EINVAL;
goto err;
}
/*
* Get parent clock frequency in Hz. It must be (at least) 256 times the target
* frequency since this is the minimum divisor value.
*/
phz = p7mu_round_pwmclk_rate((256UL * (unsigned long) USEC_PER_SEC) / us);
if ((int) phz < 0) {
dev_dbg(chip->dev, "out of range period requested\n");
err = -ERANGE;
goto err;
}
/*
* Compute in msecs since this migth otherwise overflow numerator. This
* implies we may very rarely compute a frequency a bit higher than the
* requested one because numerator is rounded down when dividing (in
* addition to loosing precision).
* Consider this as acceptable for now...
*/
div = (uint16_t) DIV_ROUND_UP((us / 10UL) * (phz / 100UL),
256UL * (unsigned long) MSEC_PER_SEC) - 1;
ratio = (uint16_t) DIV_ROUND_CLOSEST((unsigned long) duty_ns,
(unsigned long) period_ns / 256UL);
#ifdef DEBUG
BUG_ON(ratio & ~((u16) 0x1ff));
#endif
if (div != data->div || ratio != data->ratio) {
/* Avoid useless I2C accesses. */
err = p7mu_write16(data->res->start + P7MU_PWM_RATIO, ratio);
if (err) {
dev_dbg(chip->dev, "failed to set ratio\n");
goto err;
}
err = p7mu_write16(data->res->start + P7MU_PWM_DIV, div);
if (err) {
dev_dbg(chip->dev, "failed to set period\n");
goto err;
}
err = p7mu_set_pwmclk_rate(pwm->hwpwm, phz);
if (err) {
dev_dbg(chip->dev, "failed to set parent clock frequency\n");
goto err;
}
err = p7mu_mod16(data->res->start + P7MU_PWM_CTRL,
P7MU_PWM_LOAD,
P7MU_PWM_LOAD);
if (err) {
dev_dbg(chip->dev, "failed to load ratop / period\n");
goto err;
}
#ifdef DEBUG
{
unsigned long const t = p7mu_pwm_period(div, phz);
dev_dbg(chip->dev, "configured %d[%d] period=%luusec, duty=%luusec\n",
pwm->pwm,
pwm->hwpwm,
t,
p7mu_pwm_duty(t, ratio));
}
#endif
}
data->div = div;
data->ratio = ratio;
return 0;
err:
dev_err(chip->dev,
"failed to setup %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
err);
return err;
}
static int p7mu_enable_pwm(struct pwm_chip* chip, struct pwm_device* pwm)
{
int err;
struct p7mu_pwm const* const data = &p7mu_pwm_data[pwm->hwpwm];
P7MU_ASSERT_PWM(chip, pwm);
err = p7mu_mod16(data->res->start + P7MU_PWM_CTRL,
P7MU_PWM_EN,
P7MU_PWM_EN);
if (err)
dev_err(chip->dev,
"failed to enable %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
err);
else
dev_dbg(chip->dev,
"enabled %d[%d]\n",
pwm->pwm,
pwm->hwpwm);
return err;
}
static void p7mu_disable_pwm(struct pwm_chip* chip, struct pwm_device* pwm)
{
int err;
struct p7mu_pwm const* const data = &p7mu_pwm_data[pwm->hwpwm];
P7MU_ASSERT_PWM(chip, pwm);
err = p7mu_mod16(data->res->start + P7MU_PWM_CTRL,
0,
P7MU_PWM_EN);
if (err)
dev_err(chip->dev,
"failed to disable %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
err);
else
dev_dbg(chip->dev,
"disabled %d[%d]\n",
pwm->pwm,
pwm->hwpwm);
}
#ifdef CONFIG_DEBUG_FS
static int p7mu_pwm_info(struct pwm_device const* pwm,
unsigned long* period,
unsigned long* duty)
{
struct p7mu_pwm const* const data = &p7mu_pwm_data[pwm->hwpwm];
unsigned long const phz = p7mu_get_pwmclk_rate(pwm->hwpwm);
if ((int) phz < 0) {
dev_warn(pwm->chip->dev,
"failed to get parent clock rate for %d[%d] (%d)\n",
pwm->pwm,
pwm->hwpwm,
(int) phz);
return (int) phz;
}
if (data->ratio == (u16) ~(0U))
/* Not configured yet. */
return -EBADFD;
*period = p7mu_pwm_period(data->div, phz);
*duty = p7mu_pwm_duty(*period, data->ratio);
return 0;
}
static void p7mu_show_pwm(struct pwm_chip* chip, struct seq_file* s)
{
unsigned int p;
for (p = 0; p < chip->npwm; p++) {
struct pwm_device const* const pwm = &chip->pwms[p];
bool const req = test_bit(PWMF_REQUESTED, &pwm->flags);
bool const en = test_bit(PWMF_ENABLED, &pwm->flags);
unsigned long period, duty;
P7MU_ASSERT_PWM(chip, pwm);
seq_printf(s, " pwm-%-3d (%-20.20s)", p, pwm->label);
if (req || en) {
int const err = p7mu_pwm_info(pwm, &period, &duty);
if (! err) {
if (req)
seq_printf(s, ": requested @ %lu / %lu usec", duty, period);
else if (en)
seq_printf(s, ": enabled @ %lu / %lu usec", duty, period);
}
}
seq_printf(s, "\n");
}
}
#define P7MU_SHOW_PWM p7mu_show_pwm
#else /* CONFIG_DEBUG_FS */
#define P7MU_SHOW_PWM NULL
#endif /* CONFIG_DEBUG_FS */
static struct pwm_ops const p7mu_pwm_ops = {
.request = p7mu_request_pwm,
.free = p7mu_free_pwm,
.config = p7mu_config_pwm,
.enable = p7mu_enable_pwm,
.disable = p7mu_disable_pwm,
#ifdef CONFIG_DEBUG_FS
.dbg_show = P7MU_SHOW_PWM,
#endif
.owner = THIS_MODULE
};
static struct pwm_chip p7mu_pwm_chip = {
.ops = &p7mu_pwm_ops,
.base = P7MU_FIRST_PWM,
.npwm = P7MU_PWMS_MAX
};
static int __devinit p7mu_probe_pwm(struct platform_device* pdev)
{
unsigned int r;
unsigned int nr;
int err;
for (r = 0, nr = 0; r < pdev->num_resources; r++) {
struct resource const* const res = &pdev->resource[r];
if (resource_type(res) != IORESOURCE_IO)
continue;
p7mu_pwm_data[nr].ratio = (u16) ~(0U);
p7mu_pwm_data[nr].res = p7mu_request_region(pdev, res);
if (! IS_ERR(p7mu_pwm_data[nr].res))
nr++;
}
if (nr != P7MU_PWMS_MAX) {
dev_err(&pdev->dev, "failed to find I/O regions\n");
err = -ENXIO;
goto err;
}
p7mu_pwm_chip.dev = &pdev->dev;
err = pwmchip_add(&p7mu_pwm_chip);
if (! err)
return 0;
err:
for (r = 0; r < nr; r++)
p7mu_release_region(p7mu_pwm_data[r].res);
dev_err(&pdev->dev,
"failed to register chip (%d)\n",
err);
return err;
}
static int __devexit p7mu_remove_pwm(struct platform_device* pdev)
{
unsigned int r;
int const err = pwmchip_remove(&p7mu_pwm_chip);
if (err) {
dev_err(&pdev->dev,
"failed to unregister chip (%d)\n",
err);
return err;
}
for (r = 0; r < P7MU_PWMS_MAX; r++) {
#ifdef DEBUG
BUG_ON(! p7mu_pwm_data[r].res);
BUG_ON(IS_ERR(p7mu_pwm_data[r].res));
#endif
p7mu_release_region(p7mu_pwm_data[r].res);
}
return 0;
}
#define P7MU_PWM_DRV_NAME "p7mu-pwm"
static struct platform_driver p7mu_pwm_driver = {
.driver = {
.name = P7MU_PWM_DRV_NAME,
.owner = THIS_MODULE,
},
.probe = p7mu_probe_pwm,
.remove = __devexit_p(&p7mu_remove_pwm),
};
module_platform_driver(p7mu_pwm_driver);
MODULE_DESCRIPTION("Parrot Power Management Unit PWM driver");
MODULE_AUTHOR("Grégor Boirie <gregor.boirie@parrot.com>");
MODULE_LICENSE("GPL");