From 0b60175d1b7db1301e2ad3ad4d64fb0c8d121c0a Mon Sep 17 00:00:00 2001 From: Tim Yamin Date: Sat, 11 Apr 2009 13:05:21 -0700 Subject: [PATCH] MMA7455L driver. --- drivers/input/misc/Kconfig | 9 + drivers/input/misc/Makefile | 1 + drivers/input/misc/mma7455l.c | 614 +++++++++++++++++++++++++++++++++++++++++ include/linux/mma7455l.h | 11 + 4 files changed, 635 insertions(+), 0 deletions(-) diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 6fa9e38..05dca18 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -237,4 +237,13 @@ config INPUT_PCF50633_PMU Say Y to include support for delivering PMU events via input layer on NXP PCF50633. +config INPUT_MMA7455L + tristate "Freescale MMA7455L 3-axis accelerometer" + depends on SPI_MASTER + help + SPI driver for the Freescale MMA7455L 3-axis accelerometer. + + The userspace interface is a 3-axis (X/Y/Z) relative movement + Linux input device, reporting REL_[XYZ] events. + endif diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 2fabcdb..e355d82 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -23,3 +23,4 @@ obj-$(CONFIG_INPUT_TWL4030_PWRBUTTON) += twl4030-pwrbutton.o obj-$(CONFIG_INPUT_APANEL) += apanel.o obj-$(CONFIG_INPUT_SGI_BTNS) += sgi_btns.o obj-$(CONFIG_INPUT_PCF50633_PMU) += pcf50633-input.o +obj-$(CONFIG_INPUT_MMA7455L) += mma7455l.o diff --git a/drivers/input/misc/mma7455l.c b/drivers/input/misc/mma7455l.c new file mode 100644 index 0000000..b33c513 --- /dev/null +++ b/drivers/input/misc/mma7455l.c @@ -0,0 +1,614 @@ +/* Linux kernel driver for the Freescale MMA7455L 3-axis accelerometer + * + * Copyright (C) 2009 by Always Innovating, Inc. + * Author: Gregoire Gentil + * Author: Tim Yamin + * All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * 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 + * + */ + +/* + * What this driver doesn't yet support: + * + * - I2C + * - INT2 handling + * - Pulse detection (and the sysctls to control it) + * - 10-bit measurement + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MMA7455L_WHOAMI_MAGIC 0x55 + +enum mma7455l_reg { + MMA7455L_REG_XOUTL = 0x00, + MMA7455L_REG_XOUTH = 0x01, + MMA7455L_REG_YOUTL = 0x02, + MMA7455L_REG_YOUTH = 0x03, + MMA7455L_REG_ZOUTL = 0x04, + MMA7455L_REG_ZOUTH = 0x05, + MMA7455L_REG_XOUT8 = 0x06, + MMA7455L_REG_YOUT8 = 0x07, + MMA7455L_REG_ZOUT8 = 0x08, + MMA7455L_REG_STATUS = 0x09, + MMA7455L_REG_DETSRC = 0x0a, + MMA7455L_REG_TOUT = 0x0b, + MMA7455L_REG_RESERVED1 = 0x0c, + MMA7455L_REG_I2CAD = 0x0d, + MMA7455L_REG_USRINF = 0x0e, + MMA7455L_REG_WHOAMI = 0x0f, + MMA7455L_REG_XOFFL = 0x10, + MMA7455L_REG_XOFFH = 0x11, + MMA7455L_REG_YOFFL = 0x12, + MMA7455L_REG_YOFFH = 0x13, + MMA7455L_REG_ZOFFL = 0x14, + MMA7455L_REG_ZOFFH = 0x15, + MMA7455L_REG_MCTL = 0x16, + MMA7455L_REG_INTRST = 0x17, + MMA7455L_REG_CTL1 = 0x18, + MMA7455L_REG_CTL2 = 0x19, + MMA7455L_REG_LDTH = 0x1a, + MMA7455L_REG_PDTH = 0x1b, + MMA7455L_REG_PW = 0x1c, + MMA7455L_REG_LT = 0x1d, + MMA7455L_REG_TW = 0x1e, + MMA7455L_REG_RESERVED2 = 0x1f, +}; + +enum mma7455l_reg_status { + MMA7455L_STATUS_XDA = 0x08, + MMA7455L_STATUS_YDA = 0x10, + MMA7455L_STATUS_ZDA = 0x20, +}; + +enum mma7455l_mode { + MMA7455L_MODE_STANDBY = 0, + MMA7455L_MODE_MEASUREMENT = 1, + MMA7455L_MODE_LEVELDETECTION = 0x42, /* Set DRPD to on */ + MMA7455L_MODE_PULSEDETECTION = 0x43, /* Set DRPD to on */ + MMA7455L_MODE_MASK = 0x43, +}; + +enum mma7455l_gselect { + MMA7455L_GSELECT_8 = 0x0, + MMA7455L_GSELECT_2 = 0x4, + MMA7455L_GSELECT_4 = 0x8, + MMA7455L_GSELECT_MASK = 0xC, +}; + +/* FIXME */ +#define MMA7455L_F_FS 0x0020 /* ADC full scale */ + +struct mma7455l_info { + struct spi_device *spi_dev; + struct input_dev *input_dev; + struct mutex lock; + struct delayed_work work; + + u8 mode; + u8 gSelect; + + u8 flags; + u8 working; +}; + +/* lowlevel register access functions */ + +#define WRITE_BIT (1 << 7) +#define ADDR_SHIFT 1 + +static inline u_int8_t __reg_read(struct mma7455l_info *mma, u_int8_t reg) +{ + int rc; + u_int8_t cmd; + + cmd = ((reg & 0x3f) << ADDR_SHIFT); + rc = spi_w8r8(mma->spi_dev, cmd); + + return rc; +} + +static u_int8_t reg_read(struct mma7455l_info *mma, u_int8_t reg) +{ + u_int8_t ret; + + mutex_lock(&mma->lock); + ret = __reg_read(mma, reg); + mutex_unlock(&mma->lock); + + return ret; +} + +static s16 __reg_read_10(struct mma7455l_info *mma, u8 reg1, u8 reg2) +{ + u8 v1, v2; + + v1 = __reg_read(mma, reg1); + v2 = __reg_read(mma, reg2); + + return (v2 & 0x4) << 13 | (v2 & 0x3) << 8 | v1; +} + +static inline int __reg_write(struct mma7455l_info *mma, u_int8_t reg, u_int8_t val) +{ + u_int8_t buf[2]; + + buf[0] = ((reg & 0x3f) << ADDR_SHIFT) | WRITE_BIT; + buf[1] = val; + + return spi_write(mma->spi_dev, buf, sizeof(buf)); +} + +static int reg_write(struct mma7455l_info *mma, u_int8_t reg, u_int8_t val) +{ + int ret; + + mutex_lock(&mma->lock); + ret = __reg_write(mma, reg, val); + mutex_unlock(&mma->lock); + + return ret; +} + +static s16 __reg_write_10(struct mma7455l_info *mma, u8 reg1, u8 reg2, s16 value) +{ + int ret; + u8 v1, v2; + + v1 = value & 0xFF; + if(value < 0) + v2 = ((value >> 8) & 0x3) | 0x4; + else + v2 = 0; + + ret = __reg_write(mma, reg1, v1); + ret = __reg_write(mma, reg2, v2); + return ret; +} + +static void mma7455l_work(struct work_struct *work) +{ + struct mma7455l_info *mma = + container_of(work, struct mma7455l_info, work.work); + + s8 val; + mma->working = 1; + + /* FIXME: 10 bit accuracy? */ + if (!(mma->flags & MMA7455L_STATUS_XDA)) { + val = reg_read(mma, MMA7455L_REG_XOUT8); + input_report_abs(mma->input_dev, ABS_X, val); + } + if (!(mma->flags & MMA7455L_STATUS_YDA)) { + val = reg_read(mma, MMA7455L_REG_YOUT8); + input_report_abs(mma->input_dev, ABS_Y, val); + } + if (!(mma->flags & MMA7455L_STATUS_ZDA)) { + val = reg_read(mma, MMA7455L_REG_ZOUT8); + input_report_abs(mma->input_dev, ABS_Z, val); + } + + mma->working = 0; + input_sync(mma->input_dev); + put_device(&mma->spi_dev->dev); + + /* Enable IRQ and clear out interrupt */ + reg_write(mma, MMA7455L_REG_INTRST, 0x3); + reg_write(mma, MMA7455L_REG_INTRST, 0x0); + enable_irq(mma->spi_dev->irq); +} + +static void mma7455l_schedule_work(struct mma7455l_info *mma) +{ + int status; + + get_device(&mma->spi_dev->dev); + status = schedule_delayed_work(&mma->work, HZ / 10); +} + +static irqreturn_t mma7455l_interrupt(int irq, void *_mma) +{ + struct mma7455l_info *mma = _mma; + mma7455l_schedule_work(mma); + + /* Disable any further interrupts until we have processed + * the current one */ + disable_irq(mma->spi_dev->irq); + return IRQ_HANDLED; +} + +/* sysfs */ + +static void get_mode(struct mma7455l_info *mma, u8 *mode, u8 *gSelect) +{ + u8 tmp = reg_read(mma, MMA7455L_REG_MCTL); + + *mode = tmp & MMA7455L_MODE_MASK; + *gSelect = tmp & MMA7455L_GSELECT_MASK; +} + +static void set_mode(struct mma7455l_info *mma, u8 mode, u8 gSelect) +{ + reg_write(mma, MMA7455L_REG_MCTL, mode | gSelect); +} + +static void update_mode(struct mma7455l_info *mma, u8 mode, u8 gSelect) +{ + mma->mode = mode; + mma->gSelect = gSelect; + + reg_write(mma, MMA7455L_REG_MCTL, mma->mode | mma->gSelect); +} + +static ssize_t show_measure(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct mma7455l_info *mma = dev_get_drvdata(dev); + s8 x, y, z; + u8 old_Mode, old_gSelect; + + get_mode(mma, &old_Mode, &old_gSelect); + set_mode(mma, MMA7455L_MODE_MEASUREMENT, MMA7455L_GSELECT_2); + + while (reg_read(mma, MMA7455L_REG_STATUS) == 0) { + msleep(10); + } + + x = reg_read(mma, MMA7455L_REG_XOUT8); + y = reg_read(mma, MMA7455L_REG_YOUT8); + z = reg_read(mma, MMA7455L_REG_ZOUT8); + + set_mode(mma, old_Mode, old_gSelect); + return sprintf(buf, "%d %d %d\n", x, y, z); +} + +static ssize_t show_mode(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct mma7455l_info *mma = dev_get_drvdata(dev); + + switch(mma->mode) + { + case MMA7455L_MODE_STANDBY: + return sprintf(buf, "Standby\n"); + break; + case MMA7455L_MODE_MEASUREMENT: + return sprintf(buf, "Measurement\n"); + break; + case MMA7455L_MODE_LEVELDETECTION: + return sprintf(buf, "Level Detection\n"); + break; + case MMA7455L_MODE_PULSEDETECTION: + return sprintf(buf, "Pulse Detection\n"); + break; + } + + return sprintf(buf, "Unknown mode!\n"); +} + +static ssize_t show_gSelect(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct mma7455l_info *mma = dev_get_drvdata(dev); + + switch(mma->gSelect) + { + case MMA7455L_GSELECT_8: + return sprintf(buf, "8\n"); + break; + case MMA7455L_GSELECT_4: + return sprintf(buf, "4\n"); + break; + case MMA7455L_GSELECT_2: + return sprintf(buf, "2\n"); + break; + } + + return sprintf(buf, "Unknown gSelect!\n"); +} + +static ssize_t show_level_threshold(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct mma7455l_info *mma = dev_get_drvdata(dev); + return sprintf(buf, "%u\n", reg_read(mma, MMA7455L_REG_LDTH)); +} + +static ssize_t show_calibration(struct device *dev, struct device_attribute *attr, char *buf) +{ + s16 x, y, z; + struct mma7455l_info *mma = dev_get_drvdata(dev); + + mutex_lock(&mma->lock); + x = __reg_read_10(mma, MMA7455L_REG_XOFFL, MMA7455L_REG_XOFFH); + y = __reg_read_10(mma, MMA7455L_REG_YOFFL, MMA7455L_REG_YOFFH); + z = __reg_read_10(mma, MMA7455L_REG_ZOFFL, MMA7455L_REG_ZOFFH); + mutex_unlock(&mma->lock); + + return sprintf(buf, "%d %d %d\n", x, y, z); +} + +static ssize_t write_mode(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + struct mma7455l_info *mma = dev_get_drvdata(dev); + + if (!strncmp(buf, "Standby", count)) + update_mode(mma, MMA7455L_MODE_STANDBY, mma->gSelect); + else if (!strncmp(buf, "Measurement", count)) + update_mode(mma, MMA7455L_MODE_MEASUREMENT, mma->gSelect); + else if (!strncmp(buf, "Level Detection", count)) + update_mode(mma, MMA7455L_MODE_LEVELDETECTION, mma->gSelect); + else if (!strncmp(buf, "Pulse Detection", count)) + update_mode(mma, MMA7455L_MODE_PULSEDETECTION, mma->gSelect); + + return count; +} + +static ssize_t write_gSelect(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + unsigned long v; + struct mma7455l_info *mma = dev_get_drvdata(dev); + + if(strict_strtoul(buf, 10, &v) == 0) + { + switch(v) + { + case 8: + update_mode(mma, mma->mode, MMA7455L_GSELECT_8); + break; + case 4: + update_mode(mma, mma->mode, MMA7455L_GSELECT_4); + break; + case 2: + update_mode(mma, mma->mode, MMA7455L_GSELECT_2); + break; + default: + return -EINVAL; + break; + } + return count; + } + + return -EINVAL; +} + +static ssize_t write_level_threshold(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + unsigned long v; + struct mma7455l_info *mma = dev_get_drvdata(dev); + + if(strict_strtoul(buf, 10, &v) == 0) + { + if(v <= 0xFF) { + reg_write(mma, MMA7455L_REG_LDTH, v); + return count; + } else + return -EINVAL; + } + + return -EINVAL; +} + +static ssize_t write_calibration(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + int x, y, z; + struct mma7455l_info *mma = dev_get_drvdata(dev); + + if (sscanf(buf, "%d %d %d", &x, &y, &z) == 3) + { + mutex_lock(&mma->lock); + __reg_write_10(mma, MMA7455L_REG_XOFFL, MMA7455L_REG_XOFFH, x); + __reg_write_10(mma, MMA7455L_REG_YOFFL, MMA7455L_REG_YOFFH, y); + __reg_write_10(mma, MMA7455L_REG_ZOFFL, MMA7455L_REG_ZOFFH, z); + mutex_unlock(&mma->lock); + + return count; + } + + return -EINVAL; +} + +static DEVICE_ATTR(measure, S_IRUGO, show_measure, NULL); +static DEVICE_ATTR(mode, S_IRUGO | S_IWUGO, show_mode, write_mode); +static DEVICE_ATTR(gSelect, S_IRUGO | S_IWUGO, show_gSelect, write_gSelect); +static DEVICE_ATTR(level_threshold, S_IRUGO | S_IWUGO, show_level_threshold, write_level_threshold); +static DEVICE_ATTR(calibration, S_IRUGO | S_IWUGO, show_calibration, write_calibration); + +static struct attribute *mma7455l_sysfs_entries[] = { + &dev_attr_measure.attr, + &dev_attr_mode.attr, + &dev_attr_gSelect.attr, + &dev_attr_level_threshold.attr, + &dev_attr_calibration.attr, + NULL +}; + +static struct attribute_group mma7455l_attr_group = { + .attrs = mma7455l_sysfs_entries, +}; + +/* input device handling and driver core interaction */ +static int mma7455l_input_open(struct input_dev *inp) +{ + struct mma7455l_info *mma = input_get_drvdata(inp); + if(mma->mode == MMA7455L_MODE_STANDBY) + update_mode(mma, MMA7455L_MODE_MEASUREMENT, mma->gSelect); + + return 0; +} + +static void mma7455l_input_close(struct input_dev *inp) +{ + struct mma7455l_info *mma = input_get_drvdata(inp); + update_mode(mma, MMA7455L_MODE_STANDBY, MMA7455L_GSELECT_2); +} + +static int __devinit mma7455l_probe(struct spi_device *spi) +{ + int rc; + struct mma7455l_info *mma; + struct mma7455l_platform_data *pdata = spi->dev.platform_data; + u_int8_t wai; + + mma = kzalloc(sizeof(*mma), GFP_KERNEL); + if (!mma) + return -ENOMEM; + + mutex_init(&mma->lock); + INIT_DELAYED_WORK(&mma->work, mma7455l_work); + mma->spi_dev = spi; + mma->flags = mma->working = 0; + + spi_set_drvdata(spi, mma); + + rc = spi_setup(spi); + if (rc < 0) { + printk(KERN_ERR "mma7455l error durign spi_setup of mma7455l driver\n"); + dev_set_drvdata(&spi->dev, NULL); + kfree(mma); + return rc; + } + + wai = reg_read(mma, MMA7455L_REG_WHOAMI); + if (wai != MMA7455L_WHOAMI_MAGIC) { + printk(KERN_ERR "mma7455l unknown whoami signature 0x%02x\n", wai); + dev_set_drvdata(&spi->dev, NULL); + kfree(mma); + return -ENODEV; + } + + rc = request_irq(mma->spi_dev->irq, mma7455l_interrupt, IRQF_TRIGGER_HIGH, + "mma7455l", mma); + if (rc < 0) { + dev_err(&spi->dev, "mma7455l error requesting IRQ %d\n", + mma->spi_dev->irq); + /* FIXME */ + return rc; + } + + rc = sysfs_create_group(&spi->dev.kobj, &mma7455l_attr_group); + if (rc) { + dev_err(&spi->dev, "error creating sysfs group\n"); + return rc; + } + + /* initialize input layer details */ + mma->input_dev = input_allocate_device(); + if (!mma->input_dev) { + dev_err(&spi->dev, "mma7455l Unable to allocate input device\n"); + /* FIXME */ + } + + set_bit(EV_ABS, mma->input_dev->evbit); + set_bit(ABS_X, mma->input_dev->absbit); + set_bit(ABS_Y, mma->input_dev->absbit); + set_bit(ABS_Z, mma->input_dev->absbit); + + input_set_drvdata(mma->input_dev, mma); + mma->input_dev->name = "MMA7455L"; + mma->input_dev->open = mma7455l_input_open; + mma->input_dev->close = mma7455l_input_close; + + rc = input_register_device(mma->input_dev); + if(!rc) + { + update_mode(mma, MMA7455L_MODE_STANDBY, MMA7455L_GSELECT_2); + + mutex_lock(&mma->lock); + __reg_write_10(mma, MMA7455L_REG_XOFFL, MMA7455L_REG_XOFFH, pdata->calibration_x); + __reg_write_10(mma, MMA7455L_REG_YOFFL, MMA7455L_REG_YOFFH, pdata->calibration_y); + __reg_write_10(mma, MMA7455L_REG_ZOFFL, MMA7455L_REG_ZOFFH, pdata->calibration_z); + mutex_unlock(&mma->lock); + + return 0; + } + + input_free_device(mma->input_dev); + return rc; +} + +static int __devexit mma7455l_remove(struct spi_device *spi) +{ + struct mma7455l_info *mma = dev_get_drvdata(&spi->dev); + + sysfs_remove_group(&spi->dev.kobj, &mma7455l_attr_group); + input_unregister_device(mma->input_dev); + dev_set_drvdata(&spi->dev, NULL); + kfree(mma); + + return 0; +} + +#ifdef CONFIG_PM +static int mma7455l_suspend(struct spi_device *spi, pm_message_t message) +{ + struct mma7455l_info *mma = dev_get_drvdata(&spi->dev); + get_mode(mma, &mma->mode, &mma->gSelect); + set_mode(mma, MMA7455L_MODE_STANDBY, MMA7455L_GSELECT_2); + + return 0; +} + +static int mma7455l_resume(struct spi_device *spi) +{ + struct mma7455l_info *mma = dev_get_drvdata(&spi->dev); + update_mode(mma, mma->mode, mma->gSelect); + + return 0; +} +#else +#define mma7455l_suspend NULL +#define mma7455l_resume NULL +#endif + +static struct spi_driver mma7455l_driver = { + .driver = { + .name = "mma7455l", + .owner = THIS_MODULE, + }, + + .probe = mma7455l_probe, + .remove = __devexit_p(mma7455l_remove), + .suspend = mma7455l_suspend, + .resume = mma7455l_resume, +}; + +static int __init mma7455l_init(void) +{ + return spi_register_driver(&mma7455l_driver); +} + +static void __exit mma7455l_exit(void) +{ + spi_unregister_driver(&mma7455l_driver); +} + +MODULE_AUTHOR("Gregoire Gentil "); +MODULE_LICENSE("GPL"); + +module_init(mma7455l_init); +module_exit(mma7455l_exit); diff --git a/include/linux/mma7455l.h b/include/linux/mma7455l.h new file mode 100644 index 0000000..12ab50a --- /dev/null +++ b/include/linux/mma7455l.h @@ -0,0 +1,11 @@ +#ifndef _LINUX_MMA7455L_H +#define _LINUX_MMA7455L_H + +struct mma7455l_platform_data { + /* Calibration offsets */ + s16 calibration_x; + s16 calibration_y; + s16 calibration_z; +}; + +#endif /* _LINUX_MMA7455L_H */