blob: 2e1d6f5f991cfe335a854f0e13a1fb6a9624f244 [file] [log] [blame]
/* drivers/misc/twl6040-vib.c
*
* Copyright (C) 2010 Texas Instruments, Inc.
* Copyright (C) 2008 Google, Inc.
* Author: Dan Murphy <dmurphy@ti.com>
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Derived from: vib-gpio.c
* Additional derivation from: twl6040-vibra.c
*/
#include <linux/err.h>
#include <linux/hrtimer.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>
#include <linux/i2c/twl.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/twl6040-vib.h>
#include <linux/mfd/twl6040-codec.h>
#include "../staging/android/timed_output.h"
/* milliseconds */
#define TWL6040_VIB_POWER_DOWN_DELAY 5000
struct vib_data {
struct timed_output_dev dev;
struct work_struct vib_work;
struct delayed_work power_work;
struct hrtimer timer;
spinlock_t lock;
struct mutex io_mutex;
struct mutex power_mutex;
struct twl4030_codec_vibra_data *pdata;
struct twl6040 *twl6040;
int vib_power_state;
int vib_state;
bool powered;
};
static struct vib_data *misc_data;
static irqreturn_t twl6040_vib_irq_handler(int irq, void *data)
{
struct vib_data *misc_data = data;
struct twl6040 *twl6040 = misc_data->twl6040;
u8 intid = 0, status = 0;
intid = twl6040_reg_read(twl6040, TWL6040_REG_INTID);
if (intid & TWL6040_VIBINT) {
status = twl6040_reg_read(twl6040, TWL6040_REG_STATUS);
if (status & TWL6040_VIBLOCDET) {
pr_warn("Vibra left overcurrent detected\n");
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLL,
TWL6040_VIBENAL);
}
if (status & TWL6040_VIBROCDET) {
pr_warn("Vibra right overcurrent detected\n");
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLR,
TWL6040_VIBENAR);
}
}
return IRQ_HANDLED;
}
static void twl6040_vib_power_work(struct work_struct *work)
{
mutex_lock(&misc_data->power_mutex);
if (misc_data->powered) {
twl6040_disable(misc_data->twl6040);
misc_data->powered = false;
}
mutex_unlock(&misc_data->power_mutex);
}
static int twl6040_vib_power(bool on)
{
int ret = 0;
mutex_lock(&misc_data->power_mutex);
cancel_delayed_work_sync(&misc_data->power_work);
if (on == misc_data->powered)
goto out;
if (on) {
/* vibra power-up is immediate */
ret = twl6040_enable(misc_data->twl6040);
if (ret)
goto out;
misc_data->powered = true;
} else {
/* vibra power-down is deferred */
schedule_delayed_work(&misc_data->power_work,
msecs_to_jiffies(TWL6040_VIB_POWER_DOWN_DELAY));
}
out:
mutex_unlock(&misc_data->power_mutex);
return ret;
}
static void vib_set(int const new_power_state)
{
struct twl6040 *twl6040 = misc_data->twl6040;
u8 speed = misc_data->pdata->voltage_raise_speed;
int ret;
mutex_lock(&misc_data->io_mutex);
/* already in requested state */
if (new_power_state == misc_data->vib_power_state)
goto out;
/**
* @warning VIBDATx registers MUST be setted BEFORE VIBENAx bit
* setted in corresponding VIBCTLx registers
*/
if (new_power_state) {
ret = twl6040_vib_power(true);
if (ret)
goto out;
if (speed == 0x00)
speed = 0x32;
twl6040_reg_write(twl6040, TWL6040_REG_VIBDATL, speed);
twl6040_reg_write(twl6040, TWL6040_REG_VIBDATR, speed);
/*
* ERRATA: Disable overcurrent protection for at least
* 2.5ms when enabling vibrator drivers to avoid false
* overcurrent detection
*/
twl6040_set_bits(twl6040, TWL6040_REG_VIBCTLL,
TWL6040_VIBENAL | TWL6040_VIBCTRLLP);
twl6040_set_bits(twl6040, TWL6040_REG_VIBCTLR,
TWL6040_VIBENAR | TWL6040_VIBCTRLRP);
mdelay(4);
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLL,
TWL6040_VIBCTRLLP);
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLR,
TWL6040_VIBCTRLRP);
} else {
twl6040_reg_write(twl6040, TWL6040_REG_VIBDATL, 0x00);
twl6040_reg_write(twl6040, TWL6040_REG_VIBDATR, 0x00);
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLL,
TWL6040_VIBENAL);
twl6040_clear_bits(twl6040, TWL6040_REG_VIBCTLR,
TWL6040_VIBENAR);
twl6040_vib_power(false);
}
misc_data->vib_power_state = new_power_state;
out:
mutex_unlock(&misc_data->io_mutex);
}
static void vib_update(struct work_struct *work)
{
vib_set(misc_data->vib_state);
}
static enum hrtimer_restart vib_timer_func(struct hrtimer *timer)
{
struct vib_data *data =
container_of(timer, struct vib_data, timer);
data->vib_state = 0;
schedule_work(&data->vib_work);
return HRTIMER_NORESTART;
}
static int vib_get_time(struct timed_output_dev *dev)
{
struct vib_data *data =
container_of(dev, struct vib_data, dev);
if (hrtimer_active(&data->timer)) {
ktime_t r = hrtimer_get_remaining(&data->timer);
struct timeval t = ktime_to_timeval(r);
return t.tv_sec * 1000 + t.tv_usec / 1000;
} else
return 0;
}
static void vib_enable(struct timed_output_dev *dev, int value)
{
struct vib_data *data = container_of(dev, struct vib_data, dev);
unsigned long flags;
if (value < 0) {
pr_err("%s: Invalid vibrator timer value\n", __func__);
return;
}
spin_lock_irqsave(&data->lock, flags);
hrtimer_cancel(&data->timer);
if (value == 0)
data->vib_state = 0;
else {
value = (value > data->pdata->max_timeout ?
data->pdata->max_timeout : value);
/* add hardware power-up time to requested timeout */
if (!misc_data->powered)
value += TWL6040_POWER_UP_TIME;
data->vib_state = 1;
hrtimer_start(&data->timer,
ktime_set(value / 1000, (value % 1000) * 1000000),
HRTIMER_MODE_REL);
}
spin_unlock_irqrestore(&data->lock, flags);
schedule_work(&data->vib_work);
}
/*
* This is a temporary solution until a more global haptics soltion is
* available for haptics that need to occur in any application
*/
void vibrator_haptic_fire(int value)
{
vib_enable(&misc_data->dev, value);
}
#if CONFIG_PM
static int vib_suspend(struct device *dev)
{
hrtimer_cancel(&misc_data->timer);
cancel_work_sync(&misc_data->vib_work);
vib_set(0);
flush_delayed_work_sync(&misc_data->power_work);
return 0;
}
#else
#define vib_suspend NULL
#endif
static const struct dev_pm_ops vib_pm_ops = {
.suspend = vib_suspend,
};
static int vib_probe(struct platform_device *pdev)
{
struct twl4030_codec_vibra_data *pdata = pdev->dev.platform_data;
struct vib_data *data;
int ret = 0;
if (!pdata) {
ret = -EBUSY;
goto err0;
}
data = kzalloc(sizeof(struct vib_data), GFP_KERNEL);
if (!data) {
ret = -ENOMEM;
goto err0;
}
data->pdata = pdata;
data->twl6040 = dev_get_drvdata(pdev->dev.parent);
INIT_WORK(&data->vib_work, vib_update);
INIT_DELAYED_WORK(&data->power_work, twl6040_vib_power_work);
hrtimer_init(&data->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
data->timer.function = vib_timer_func;
spin_lock_init(&data->lock);
data->dev.name = "vibrator";
data->dev.get_time = vib_get_time;
data->dev.enable = vib_enable;
ret = timed_output_dev_register(&data->dev);
if (ret < 0)
goto err1;
if (data->pdata->init)
ret = data->pdata->init();
if (ret < 0)
goto err2;
misc_data = data;
platform_set_drvdata(pdev, data);
mutex_init(&misc_data->io_mutex);
mutex_init(&misc_data->power_mutex);
ret = twl6040_request_irq(data->twl6040, TWL6040_IRQ_VIB,
twl6040_vib_irq_handler, 0,
"twl6040_irq_vib", data);
if (ret) {
pr_err("%s: VIB IRQ request failed: %d\n", __func__, ret);
goto err2;
}
vib_enable(&data->dev, data->pdata->initial_vibrate);
return 0;
err2:
timed_output_dev_unregister(&data->dev);
err1:
kfree(data);
err0:
return ret;
}
static int vib_remove(struct platform_device *pdev)
{
struct vib_data *data = platform_get_drvdata(pdev);
if (data->pdata->exit)
data->pdata->exit();
hrtimer_cancel(&data->timer);
vib_set(0);
flush_delayed_work_sync(&data->power_work);
twl6040_free_irq(data->twl6040, TWL6040_IRQ_VIB, data);
timed_output_dev_unregister(&data->dev);
kfree(data);
return 0;
}
/* TO DO: Need to make this drivers own platform data entries */
static struct platform_driver twl6040_vib_driver = {
.probe = vib_probe,
.remove = vib_remove,
.driver = {
.name = VIB_NAME,
.owner = THIS_MODULE,
.pm = &vib_pm_ops,
},
};
static int __init twl6040_vib_init(void)
{
return platform_driver_register(&twl6040_vib_driver);
}
static void __exit twl6040_vib_exit(void)
{
platform_driver_unregister(&twl6040_vib_driver);
}
module_init(twl6040_vib_init);
module_exit(twl6040_vib_exit);
MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>");
MODULE_DESCRIPTION("TWL6040 Vibrator Driver");
MODULE_LICENSE("GPL");