diff mbox

sh: sh7723: Add A/D converter driver support

Message ID 95F51F4B902CAC40AF459205F6322F0171E8D49856@BMK019S01.emtrion.local (mailing list archive)
State Changes Requested
Headers show

Commit Message

Pietrek, Markus Jan. 29, 2010, 9:40 a.m. UTC
None
diff mbox

Patch

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 46c3c56..5d35645 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1066,6 +1066,15 @@  config SENSORS_MC13783_ADC
         help
           Support for the A/D converter on MC13783 PMIC.

+config SENSORS_SH_ADC
+        tristate "SH ADC"
+        depends on CPU_SUBTYPE_SH7723
+        help
+          Support for the A/D converter on the SH7723. Only single mode is supported, sampling only
+         one channel at a time. The voltage reference can be set in
+          /sys/devices/platform/sh_adc.0/vref as millivolt. Sampling can be done by reading from the
+          channels an0, an1, an2 or an3 (values are in millivolt).
+
 if ACPI

 comment "ACPI drivers"
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 450c8e8..9b352b3 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -98,6 +98,7 @@  obj-$(CONFIG_SENSORS_W83L785TS)       += w83l785ts.o
 obj-$(CONFIG_SENSORS_W83L786NG)        += w83l786ng.o
 obj-$(CONFIG_SENSORS_WM831X)   += wm831x-hwmon.o
 obj-$(CONFIG_SENSORS_WM8350)   += wm8350-hwmon.o
+obj-$(CONFIG_SENSORS_SH_ADC)   += sh_adc.o

 ifeq ($(CONFIG_HWMON_DEBUG_CHIP),y)
 EXTRA_CFLAGS += -DDEBUG
diff --git a/drivers/hwmon/sh_adc.c b/drivers/hwmon/sh_adc.c
new file mode 100644
index 0000000..7cf01d6
--- /dev/null
+++ b/drivers/hwmon/sh_adc.c
@@ -0,0 +1,366 @@ 
+/*
+ * sh_adc.c
+ *
+ * Copyright (c) 2010 by emtrion GmbH
+ *
+ * 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.
+ *
+ * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * 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.
+ *
+ * Author:      Markus Pietrek
+ * Description: Driver for the ADC of the SH (SH7723). Only single
+ *              mode is supported
+ *
+ **/
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/clk.h>
+#include <linux/interrupt.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/wait.h>
+#include <asm/io.h>
+
+#include "sh_adc.h"
+
+/* A/D Converter registers */
+#define ADDR(ch)       (0x00+((ch)&0x7)*2)
+#define ADCSR          0x08
+
+#define ADCSR_ADF      (1 << 15)
+#define ADCSR_ADIE     (1 << 14)
+#define ADCSR_ADST     (1 << 13)
+#define ADCSR_CKS_545  (2 <<  6)
+#define ADCSR_MULTI_SINGLE (0 << 4)
+#define ADCSR_CH(ch)    ((ch)&0x7)
+
+#define MAX_ANALOG_CHANNELS    4
+
+struct sh_adc_priv {
+       struct device          *hwmon_dev;
+       struct platform_device *pdev;
+       wait_queue_head_t       waitq;
+        void __iomem  *base;
+        struct clk    *clk;
+        struct mutex  mutex;
+       spinlock_t    lock;
+       int           irq;
+       int           voltage_reference;
+
+       struct {
+               struct attribute      *attrs[MAX_ANALOG_CHANNELS+1+1]; /* analog channels+vref+NULL */
+               struct attribute_group group;
+       } sysfs;
+
+       volatile char sample_available;
+};
+
+/********** low-level I/O **********/
+
+static inline void sh_adc_write(const struct sh_adc_priv *priv, u16 val, int reg)
+{
+        iowrite16(val, priv->base + reg);
+}
+
+static inline u16 sh_adc_read(const struct sh_adc_priv *priv, int reg)
+{
+        return ioread16(priv->base + reg);
+}
+
+static irqreturn_t sh_adc_isr(int irq, void *dev_id)
+{
+        struct sh_adc_priv *priv = dev_id;
+       u16 status;
+
+       spin_lock(&priv->lock);
+       status = sh_adc_read(priv, ADCSR);
+
+       if (status & ADCSR_ADF) {
+               /* clear end-of-conversion flag */
+               sh_adc_write(priv, status & ~ADCSR_ADF, ADCSR);
+
+               priv->sample_available = 1;
+               wake_up_interruptible(&priv->waitq);
+       }
+       spin_unlock(&priv->lock);
+
+       return IRQ_HANDLED;
+}
+
+static int sh_adc_get_sample(struct sh_adc_priv *priv, int ch, u16 *sample)
+{
+       int res;
+       unsigned long flags;
+
+       enable_irq(priv->irq);
+       clk_enable(priv->clk);
+
+       spin_lock_irqsave(&priv->lock, flags);
+       priv->sample_available = 0;
+       /* set to maximum conversion time */
+       sh_adc_write(priv,
+                    ADCSR_ADIE    |
+                    ADCSR_ADST    |
+                    ADCSR_CKS_545 |
+                    ADCSR_MULTI_SINGLE |
+                    ADCSR_CH(ch),
+                    ADCSR);
+       spin_unlock_irqrestore(&priv->lock, flags);
+
+       res = wait_event_interruptible(priv->waitq, (priv->sample_available == 1));
+
+       spin_lock_irqsave(&priv->lock, flags);
+       *sample = sh_adc_read(priv, ADDR(ch));
+       sh_adc_write(priv, 0, ADCSR);
+       spin_unlock_irqrestore(&priv->lock, flags);
+
+       clk_disable(priv->clk);
+       disable_irq(priv->irq);
+
+       return 0;
+}
+
+/*********** platform sysfs **********/
+
+static ssize_t sh_adc_sysfs_show_vref(struct device *dev, struct device_attribute *da,
+                                     char *buf)
+{
+       struct platform_device *pdev = to_platform_device(dev);
+       struct sh_adc_priv *priv     = platform_get_drvdata(pdev);
+       int vref;
+
+       mutex_lock(&priv->mutex);
+       vref = priv->voltage_reference;
+       mutex_unlock(&priv->mutex);
+
+       return sprintf(buf, "%i mV\n", vref);
+}
+
+static ssize_t sh_adc_sysfs_set_vref(struct device *dev, struct device_attribute *devattr,
+                                    const char *buf, size_t count)
+{
+       struct platform_device *pdev = to_platform_device(dev);
+       struct sh_adc_priv *priv     = platform_get_drvdata(pdev);
+       int vref = simple_strtol(buf, NULL, 10);
+
+       mutex_lock(&priv->mutex);
+       priv->voltage_reference = vref;
+       mutex_unlock(&priv->mutex);
+
+       return count;
+}
+
+static ssize_t sh_adc_sysfs_show_anx(struct device *dev, struct device_attribute *da,
+                                    char *buf)
+{
+       struct platform_device *pdev = to_platform_device(dev);
+       struct sh_adc_priv *priv     = platform_get_drvdata(pdev);
+       struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
+       int res;
+       u16 sample;
+       u16 vin;
+
+       /* we support only single mode. No other channel can be read at the same
+        * time */
+       mutex_lock(&priv->mutex);
+       res = sh_adc_get_sample(priv, attr->index, &sample);
+       /* convert the 10bit digital value to mV */
+       vin = (sample*priv->voltage_reference)/65536;
+       mutex_unlock(&priv->mutex);
+
+       return res ? res : sprintf(buf, "%u mV\n", vin);
+}
+
+#define MK(index) SENSOR_ATTR(an##index, S_IRUGO, sh_adc_sysfs_show_anx, NULL, index)
+static struct sensor_device_attribute sh_adc_dev_attr[MAX_ANALOG_CHANNELS] = {
+       MK(0),
+       MK(1),
+       MK(2),
+       MK(3),
+};
+#undef MK
+SENSOR_DEVICE_ATTR(vref, S_IRUGO | S_IWUSR, sh_adc_sysfs_show_vref, sh_adc_sysfs_set_vref, 0);
+
+/*********** platform probe **********/
+
+static int __devinit sh_adc_probe(struct platform_device *pdev)
+{
+        struct sh_adc_priv *priv;
+        struct sh_adc_platform_data *pdata = dev_get_platdata(&pdev->dev);
+       struct resource *res;
+       struct attribute **attr;
+        int error = 0;
+       int i;
+       unsigned long flags;
+
+        if (!pdata) {
+                dev_err(&pdev->dev, "Need platform data\n");
+                error = -EINVAL;
+                goto error;
+        }
+
+        priv = kzalloc(sizeof(*priv), GFP_KERNEL);
+        if (!priv) {
+                dev_err(&pdev->dev, "cannot get memory\n");
+                error = -ENOMEM;
+                goto error;
+        }
+        priv->pdev = pdev;
+        platform_set_drvdata(pdev, priv);
+
+        mutex_init(&priv->mutex);
+       spin_lock_init(&priv->lock);
+       init_waitqueue_head(&priv->waitq);
+
+       priv->voltage_reference = pdata->voltage_reference;
+
+       /* register resources */
+       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+        if (!res) {
+                dev_err(&pdev->dev, "cannot get platform mem resources\n");
+                error = -EINVAL;
+                goto error_mem;
+        }
+
+        priv->base = ioremap(res->start, (res->end-res->start)+1);
+        if (!priv->base) {
+                dev_err(&pdev->dev, "cannot get io memory\n");
+                error = -ENOMEM;
+                goto error_mem;
+        }
+
+       res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
+        if (!res) {
+                dev_err(&pdev->dev, "cannot get platform irq resources\n");
+                error = -EINVAL;
+                goto error_irq;
+        }
+       priv->irq = res->start;
+        error = request_irq(priv->irq, sh_adc_isr, IRQF_DISABLED, dev_name(&priv->pdev->dev), priv);
+        if (error) {
+                dev_err(&pdev->dev, "cannot get irq\n");
+                goto error_irq;
+        }
+
+        priv->clk = clk_get(&pdev->dev, "adc0");
+        if (IS_ERR(priv->clk)) {
+                dev_err(&pdev->dev, "cannot get clock");
+                goto error_clock;
+        }
+
+       /* register sysfs hooks */
+       attr = &priv->sysfs.attrs[0];
+       *attr = &sensor_dev_attr_vref.dev_attr.attr;
+       attr++;
+       /* add SH_ADC_PORT_A/B/C/D when present on board */
+       for (i=0; i < ARRAY_SIZE(sh_adc_dev_attr); i++) {
+               if (pdata->ports & (1 << i)) {
+                       *attr = &sh_adc_dev_attr[i].dev_attr.attr;
+                       attr++;
+               }
+       }
+       *attr = NULL;
+       priv->sysfs.group.attrs = priv->sysfs.attrs;
+
+       error = sysfs_create_group(&pdev->dev.kobj, &priv->sysfs.group);
+       if (error) {
+                dev_err(&pdev->dev, "sysfs failed");
+               goto error_sysfs;
+       }
+
+       /* register hwmon hooks */
+       priv->hwmon_dev = hwmon_device_register(&pdev->dev);
+       if (IS_ERR(priv->hwmon_dev)) {
+                dev_err(&pdev->dev, "hwmon_device_register failed\n");
+               error = PTR_ERR(priv->hwmon_dev);
+               goto error_hwmon;
+       }
+
+       /* reset A/D */
+       clk_enable(priv->clk);
+       disable_irq(priv->irq);
+       spin_lock_irqsave(&priv->lock, flags);
+       sh_adc_write(priv, 0, ADCSR);
+       if (sh_adc_read(priv, ADCSR))
+               /* clear end of A/D conversion */
+               sh_adc_write(priv, ADCSR_ADF, ADCSR);
+       spin_unlock_irqrestore(&priv->lock, flags);
+       clk_disable(priv->clk);
+
+        dev_info(&pdev->dev, "initialized\n");
+        return 0;
+
+error_hwmon:
+       sysfs_remove_group(&pdev->dev.kobj, &priv->sysfs.group);
+
+error_sysfs:
+        clk_put(priv->clk);
+
+error_clock:
+        free_irq(priv->irq, priv);
+
+error_irq:
+        iounmap(priv->base);
+
+error_mem:
+        kfree(priv);
+
+error:
+        return error;
+}
+
+static int __devexit sh_adc_remove(struct platform_device *pdev)
+{
+       struct sh_adc_priv *priv = platform_get_drvdata(pdev);
+
+       hwmon_device_unregister(priv->hwmon_dev);
+       sysfs_remove_group(&pdev->dev.kobj, &priv->sysfs.group);
+        clk_put(priv->clk);
+        free_irq(priv->irq, priv);
+        iounmap(priv->base);
+        kfree(priv);
+
+        platform_set_drvdata(pdev, NULL);
+
+        return 0;
+}
+
+
+static struct platform_driver sh_adc_driver = {
+       .probe          = sh_adc_probe,
+       .remove         = __devexit_p(sh_adc_remove),
+       .driver         = {
+               .name   = "sh_adc",
+               .owner  = THIS_MODULE,
+       },
+};
+
+static int __init sh_adc_init(void)
+{
+       return platform_driver_register(&sh_adc_driver);
+}
+
+static void __exit sh_adc_exit(void)
+{
+       platform_driver_unregister(&sh_adc_driver);
+}
+
+module_init(sh_adc_init);
+module_exit(sh_adc_exit);
+
+MODULE_AUTHOR("Markus Pietrek");
+MODULE_DESCRIPTION("SH7723 A/D Converter driver");
+MODULE_LICENSE("GPL");
+
diff --git a/drivers/hwmon/sh_adc.h b/drivers/hwmon/sh_adc.h
new file mode 100644
index 0000000..964a3a2
--- /dev/null
+++ b/drivers/hwmon/sh_adc.h
@@ -0,0 +1,33 @@ 
+/*
+ * sh_adc.h
+ *
+ * Copyright (c) 2010 by emtrion GmbH
+ *
+ * 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.
+ *
+ * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * 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.
+ *
+ **/
+
+#ifndef __DRIVERS_HWMON_SH_ADC_H
+#define __DRIVERS_HWMON_SH_ADC_H
+
+struct sh_adc_platform_data {
+       int     voltage_reference; /* in mV */
+       u8      ports;
+#define SH_ADC_PORT_A  (1<<0)
+#define SH_ADC_PORT_B  (1<<1)
+#define SH_ADC_PORT_C  (1<<2)
+#define SH_ADC_PORT_D  (1<<3)
+};
+
+#endif /* __DRIVERS_HWMON_SH_ADC_H */