blob: dde418c03468c5b067e31b274e2113d15689c73e [file] [log] [blame]
/*
* Copyright (C) 2007-2010 Motorola, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307, USA
*/
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/irq.h>
#include <linux/regulator/machine.h>
#include <linux/spi/spi.h>
#include <linux/spi/cpcap.h>
#include <linux/spi/cpcap-regbits.h>
#include <linux/uaccess.h>
#include <linux/reboot.h>
#include <linux/notifier.h>
#include <linux/delay.h>
struct cpcap_driver_info {
struct list_head list;
struct platform_device *pdev;
};
static long ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static int cpcap_probe(struct spi_device *spi);
static int cpcap_remove(struct spi_device *spi);
const static struct file_operations cpcap_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = ioctl,
};
static struct miscdevice cpcap_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = CPCAP_DEV_NAME,
.fops = &cpcap_fops,
};
static struct spi_driver cpcap_driver = {
.driver = {
.name = "cpcap",
.bus = &spi_bus_type,
.owner = THIS_MODULE,
},
.probe = cpcap_probe,
.remove = cpcap_remove,
};
static struct platform_device cpcap_adc_device = {
.name = "cpcap_adc",
.id = -1,
.dev.platform_data = NULL,
};
static struct platform_device cpcap_key_device = {
.name = "cpcap_key",
.id = -1,
.dev.platform_data = NULL,
};
static struct platform_device cpcap_batt_device = {
.name = "cpcap_battery",
.id = -1,
.dev.platform_data = NULL,
};
static struct platform_device cpcap_uc_device = {
.name = "cpcap_uc",
.id = -1,
.dev.platform_data = NULL,
};
static struct platform_device cpcap_rtc_device = {
.name = "cpcap_rtc",
.id = -1,
.dev.platform_data = NULL,
};
/* List of required CPCAP devices that will ALWAYS be present.
*
* DO NOT ADD NEW DEVICES TO THIS LIST! You must use cpcap_driver_register()
* for any new drivers for non-core functionality of CPCAP.
*/
static struct platform_device *cpcap_devices[] = {
&cpcap_uc_device,
&cpcap_adc_device,
&cpcap_key_device,
&cpcap_batt_device,
&cpcap_rtc_device,
};
static struct cpcap_device *misc_cpcap;
static LIST_HEAD(cpcap_device_list);
static DEFINE_MUTEX(cpcap_driver_lock);
static int cpcap_reboot(struct notifier_block *this, unsigned long code,
void *cmd)
{
int ret = -1;
int result = NOTIFY_DONE;
char *mode = cmd;
unsigned short value;
unsigned short counter = 0;
/* Disable the USB transceiver */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_USBC2, 0,
CPCAP_BIT_USBXCVREN);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Disable Transciever failure.\n");
result = NOTIFY_BAD;
}
if (code == SYS_RESTART) {
if (mode != NULL && !strncmp("outofcharge", mode, 12)) {
/* Set the outofcharge bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
CPCAP_BIT_OUT_CHARGE_ONLY,
CPCAP_BIT_OUT_CHARGE_ONLY);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"outofcharge cpcap set failure.\n");
result = NOTIFY_BAD;
}
/* Set the soft reset bit in the cpcap */
cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
CPCAP_BIT_SOFT_RESET,
CPCAP_BIT_SOFT_RESET);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"reset cpcap set failure.\n");
result = NOTIFY_BAD;
}
}
/* Check if we are starting recovery mode */
if (mode != NULL && !strncmp("recovery", mode, 9)) {
/* Set the fota (recovery mode) bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
CPCAP_BIT_FOTA_MODE, CPCAP_BIT_FOTA_MODE);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Recovery cpcap set failure.\n");
result = NOTIFY_BAD;
}
} else {
/* Set the fota (recovery mode) bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1, 0,
CPCAP_BIT_FOTA_MODE);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Recovery cpcap clear failure.\n");
result = NOTIFY_BAD;
}
}
/* Check if we are going into fast boot mode */
if (mode != NULL && !strncmp("bootloader", mode, 11)) {
/* Set the bootmode bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
CPCAP_BIT_BOOT_MODE, CPCAP_BIT_BOOT_MODE);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Boot mode cpcap set failure.\n");
result = NOTIFY_BAD;
}
}
cpcap_regacc_write(misc_cpcap, CPCAP_REG_MI2, 0, 0xFFFF);
} else {
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
0,
CPCAP_BIT_OUT_CHARGE_ONLY);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"outofcharge cpcap set failure.\n");
result = NOTIFY_BAD;
}
/* Clear the soft reset bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1, 0,
CPCAP_BIT_SOFT_RESET);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"SW Reset cpcap set failure.\n");
result = NOTIFY_BAD;
}
/* Clear the fota (recovery mode) bit in the cpcap */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1, 0,
CPCAP_BIT_FOTA_MODE);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Recovery cpcap clear failure.\n");
result = NOTIFY_BAD;
}
}
/* Always clear the kpanic bit */
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_VAL1,
0, CPCAP_BIT_AP_KERNEL_PANIC);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Clear kernel panic bit failure.\n");
result = NOTIFY_BAD;
}
/* Always clear the power cut bit on SW Shutdown*/
ret = cpcap_regacc_write(misc_cpcap, CPCAP_REG_PC1,
0, CPCAP_BIT_PC1_PCEN);
if (ret) {
dev_err(&(misc_cpcap->spi->dev),
"Clear Power Cut bit failure.\n");
result = NOTIFY_BAD;
}
cpcap_regacc_write(misc_cpcap, CPCAP_REG_CRM, 0x0300, 0x3FFF);
(void)cpcap_regacc_read(misc_cpcap, CPCAP_REG_INTS2, &value);
if (!(value & CPCAP_BIT_VBUSVLD_S)) {
while ((value & CPCAP_BIT_SESSVLD_S) && (counter < 100)) {
mdelay(10);
counter++;
(void)cpcap_regacc_read(misc_cpcap, CPCAP_REG_INTS2,
&value);
}
}
/* Clear the charger and charge path settings to avoid a false turn on
* event in caused by CPCAP. After clearing these settings, 100ms is
* needed to before SYSRSTRTB is pulled low to avoid the false turn on
* event.
*/
cpcap_regacc_write(misc_cpcap, CPCAP_REG_CRM, 0, 0x3FFF);
mdelay(100);
return result;
}
static struct notifier_block cpcap_reboot_notifier = {
.notifier_call = cpcap_reboot,
};
static int __init cpcap_init(void)
{
return spi_register_driver(&cpcap_driver);
}
static void cpcap_vendor_read(struct cpcap_device *cpcap)
{
unsigned short value;
(void)cpcap_regacc_read(cpcap, CPCAP_REG_VERSC1, &value);
cpcap->vendor = (enum cpcap_vendor)((value >> 6) & 0x0007);
cpcap->revision = (enum cpcap_revision)(((value >> 3) & 0x0007) |
((value << 3) & 0x0038));
}
int cpcap_device_unregister(struct platform_device *pdev)
{
struct cpcap_driver_info *info;
struct cpcap_driver_info *tmp;
int found;
found = 0;
mutex_lock(&cpcap_driver_lock);
list_for_each_entry_safe(info, tmp, &cpcap_device_list, list) {
if (info->pdev == pdev) {
list_del(&info->list);
/*
* misc_cpcap != NULL suggests pdev
* already registered
*/
if (misc_cpcap) {
printk(KERN_INFO "CPCAP: unregister %s\n",
pdev->name);
platform_device_unregister(pdev);
}
info->pdev = NULL;
kfree(info);
found = 1;
}
}
mutex_unlock(&cpcap_driver_lock);
BUG_ON(!found);
return 0;
}
int cpcap_device_register(struct platform_device *pdev)
{
int retval;
struct cpcap_driver_info *info;
retval = 0;
info = kzalloc(sizeof(*info), GFP_KERNEL);
if (!info) {
printk(KERN_ERR "Cannot save device %s\n", pdev->name);
return -ENOMEM;
}
mutex_lock(&cpcap_driver_lock);
info->pdev = pdev;
list_add_tail(&info->list, &cpcap_device_list);
/* If misc_cpcap is valid, the CPCAP driver has already been probed.
* Therefore, call platform_device_register() to probe the device.
*/
if (misc_cpcap) {
dev_info(&(misc_cpcap->spi->dev),
"Probing CPCAP device %s\n", pdev->name);
/*
* platform_data is non-empty indicates
* CPCAP client devices need to pass their own data
* In that case we put cpcap data in driver_data
*/
if (pdev->dev.platform_data != NULL)
platform_set_drvdata(pdev, misc_cpcap);
else
pdev->dev.platform_data = misc_cpcap;
retval = platform_device_register(pdev);
} else
printk(KERN_INFO "CPCAP: delaying %s probe\n",
pdev->name);
mutex_unlock(&cpcap_driver_lock);
return retval;
}
static int cpcap_probe(struct spi_device *spi)
{
int i, retval = -EINVAL;
struct cpcap_device *cpcap;
struct cpcap_platform_data *data;
unsigned int irq;
struct cpcap_driver_info *info;
cpcap = kzalloc(sizeof(*cpcap), GFP_KERNEL);
if (cpcap == NULL)
return -ENOMEM;
cpcap->spi = spi;
data = cpcap_get_plat_data(cpcap);
if (data == NULL) {
dev_info(&(spi->dev), "No platform data found for CPCAP\n");
goto free_mem;
}
/* fixup struct spi_device for backward compatibility */
spi->controller_data = data;
spi_set_drvdata(spi, cpcap);
/* setup IRQ GPIO (previously done in board file) */
retval = gpio_request(data->irq_gpio, "cpcap-irq");
if (retval)
goto free_mem;
retval = gpio_direction_input(data->irq_gpio);
if (retval)
goto free_gpio;
irq = gpio_to_irq(data->irq_gpio);
irq_set_irq_type(irq, IRQ_TYPE_EDGE_RISING);
spi->irq = irq;
retval = cpcap_regacc_init(cpcap);
if (retval < 0)
goto free_gpio;
retval = cpcap_irq_init(cpcap);
if (retval < 0)
goto free_cpcap_irq;
cpcap_vendor_read(cpcap);
for (i = 0; i < ARRAY_SIZE(cpcap_devices); i++)
cpcap_devices[i]->dev.platform_data = cpcap;
retval = misc_register(&cpcap_dev);
if (retval < 0)
goto free_cpcap_irq;
/* loop twice becuase cpcap_regulator_probe may refer to other devices
* in this list to handle dependencies between regulators. Create them
* all and then add them */
for (i = 0; i < CPCAP_NUM_REGULATORS; i++) {
struct platform_device *pdev;
pdev = platform_device_alloc("cpcap-regltr", i);
if (!pdev) {
dev_err(&(spi->dev), "Cannot create regulator\n");
continue;
}
pdev->dev.parent = &(spi->dev);
pdev->dev.platform_data = &data->regulator_init[i];
platform_set_drvdata(pdev, cpcap);
cpcap->regulator_pdev[i] = pdev;
}
for (i = 0; i < CPCAP_NUM_REGULATORS; i++) {
/* vusb has to be added after sw5 so skip it for now,
* it will be added from probe of sw5 */
if (i == CPCAP_VUSB)
continue;
platform_device_add(cpcap->regulator_pdev[i]);
}
platform_add_devices(cpcap_devices, ARRAY_SIZE(cpcap_devices));
mutex_lock(&cpcap_driver_lock);
misc_cpcap = cpcap; /* kept for misc device */
list_for_each_entry(info, &cpcap_device_list, list) {
int ret = 0;
dev_info(&(spi->dev), "Probing CPCAP device %s\n",
info->pdev->name);
if (info->pdev->dev.platform_data != NULL)
platform_set_drvdata(info->pdev, cpcap);
else
info->pdev->dev.platform_data = cpcap;
ret = platform_device_register(info->pdev);
}
mutex_unlock(&cpcap_driver_lock);
register_reboot_notifier(&cpcap_reboot_notifier);
return 0;
free_cpcap_irq:
cpcap_irq_shutdown(cpcap);
free_gpio:
gpio_free(data->irq_gpio);
free_mem:
kfree(cpcap);
return retval;
}
static int cpcap_remove(struct spi_device *spi)
{
struct cpcap_device *cpcap = spi_get_drvdata(spi);
struct cpcap_driver_info *info;
int i;
unregister_reboot_notifier(&cpcap_reboot_notifier);
mutex_lock(&cpcap_driver_lock);
list_for_each_entry(info, &cpcap_device_list, list) {
dev_info(&(spi->dev), "Removing CPCAP device %s\n",
info->pdev->name);
platform_device_unregister(info->pdev);
}
misc_cpcap = NULL;
mutex_unlock(&cpcap_driver_lock);
for (i = ARRAY_SIZE(cpcap_devices); i > 0; i--)
platform_device_unregister(cpcap_devices[i-1]);
for (i = 0; i < CPCAP_NUM_REGULATORS; i++)
platform_device_unregister(cpcap->regulator_pdev[i]);
misc_deregister(&cpcap_dev);
cpcap_irq_shutdown(cpcap);
kfree(cpcap);
return 0;
}
static int test_ioctl(unsigned int cmd, unsigned long arg)
{
int retval = -EINVAL;
struct cpcap_regacc read_data;
struct cpcap_regacc write_data;
switch (cmd) {
case CPCAP_IOCTL_TEST_READ_REG:
if (copy_from_user((void *)&read_data, (void *)arg,
sizeof(read_data)))
return -EFAULT;
retval = cpcap_regacc_read(misc_cpcap, read_data.reg,
&read_data.value);
if (retval < 0)
return retval;
if (copy_to_user((void *)arg, (void *)&read_data,
sizeof(read_data)))
return -EFAULT;
return 0;
break;
case CPCAP_IOCTL_TEST_WRITE_REG:
if (copy_from_user((void *) &write_data,
(void *) arg,
sizeof(write_data)))
return -EFAULT;
retval = cpcap_regacc_write(misc_cpcap, write_data.reg,
write_data.value, write_data.mask);
break;
default:
retval = -ENOTTY;
break;
}
return retval;
}
static int adc_ioctl(unsigned int cmd, unsigned long arg)
{
int retval = -EINVAL;
struct cpcap_adc_phase phase;
switch (cmd) {
case CPCAP_IOCTL_ADC_PHASE:
if (copy_from_user((void *) &phase, (void *) arg,
sizeof(phase)))
return -EFAULT;
cpcap_adc_phase(misc_cpcap, &phase);
retval = 0;
break;
default:
retval = -ENOTTY;
break;
}
return retval;
}
#if defined(CONFIG_LEDS_FLASH_RESET)
int cpcap_direct_misc_write(unsigned short reg, unsigned short value,\
unsigned short mask)
{
int retval = -EINVAL;
retval = cpcap_regacc_write(misc_cpcap, reg, value, mask);
return retval;
}
#endif
static long ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int retval = -ENOTTY;
unsigned int cmd_num;
cmd_num = _IOC_NR(cmd);
if ((cmd_num > CPCAP_IOCTL_NUM_TEST__START) &&
(cmd_num < CPCAP_IOCTL_NUM_TEST__END)) {
retval = test_ioctl(cmd, arg);
}
if ((cmd_num > CPCAP_IOCTL_NUM_ADC__START) &&
(cmd_num < CPCAP_IOCTL_NUM_ADC__END)) {
retval = adc_ioctl(cmd, arg);
}
return retval;
}
static void cpcap_shutdown(void)
{
spi_unregister_driver(&cpcap_driver);
}
int cpcap_disable_offmode_wakeups(bool disable)
{
int retval = 0;
return retval;
}
subsys_initcall(cpcap_init);
module_exit(cpcap_shutdown);
MODULE_ALIAS("platform:cpcap");
MODULE_DESCRIPTION("CPCAP driver");
MODULE_AUTHOR("Motorola");
MODULE_LICENSE("GPL");