All of lore.kernel.org
 help / color / mirror / Atom feed
From: Simon Guinot <simon@sequanux.org>
To: linux-arm-kernel@lists.infradead.org
Subject: [lm-sensors] [PATCH v3] hwmon: add generic GPIO fan driver
Date: Thu, 21 Oct 2010 22:44:19 +0000	[thread overview]
Message-ID: <1287701059-2226-1-git-send-email-simon@sequanux.org> (raw)
In-Reply-To: <20101021223555.GT29120@kw.sim.vm.gnt>

From: Simon Guinot <sguinot@lacie.com>

This patch adds hwmon support for the GPIO connected fan.

Platform specific informations as GPIO pinout and speed conversion array
(rpm from/to GPIO value) are passed to the driver via platform_data.

Signed-off-by: Simon Guinot <sguinot@lacie.com>
---
 drivers/hwmon/Kconfig    |    9 +
 drivers/hwmon/Makefile   |    1 +
 drivers/hwmon/gpio-fan.c |  558 ++++++++++++++++++++++++++++++++++++++++++++++
 include/linux/gpio-fan.h |   36 +++
 4 files changed, 604 insertions(+), 0 deletions(-)
 create mode 100644 drivers/hwmon/gpio-fan.c
 create mode 100644 include/linux/gpio-fan.h

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 4d4d09b..10ce9da 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -399,6 +399,15 @@ config SENSORS_GL520SM
 	  This driver can also be built as a module.  If so, the module
 	  will be called gl520sm.
 
+config SENSORS_GPIO_FAN
+	tristate "GPIO fan"
+	depends on GENERIC_GPIO
+	help
+	  If you say yes here you get support for the GPIO connected fans.
+
+	  This driver can also be built as a module.  If so, the module
+	  will be called gpio-fan.
+
 config SENSORS_CORETEMP
 	tristate "Intel Core/Core2/Atom temperature sensor"
 	depends on X86 && PCI && EXPERIMENTAL
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index e3c2484..a793e28 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -51,6 +51,7 @@ obj-$(CONFIG_SENSORS_FSCHMD)	+= fschmd.o
 obj-$(CONFIG_SENSORS_G760A)	+= g760a.o
 obj-$(CONFIG_SENSORS_GL518SM)	+= gl518sm.o
 obj-$(CONFIG_SENSORS_GL520SM)	+= gl520sm.o
+obj-$(CONFIG_SENSORS_GPIO_FAN)	+= gpio-fan.o
 obj-$(CONFIG_SENSORS_ULTRA45)	+= ultra45_env.o
 obj-$(CONFIG_SENSORS_HDAPS)	+= hdaps.o
 obj-$(CONFIG_SENSORS_I5K_AMB)	+= i5k_amb.o
diff --git a/drivers/hwmon/gpio-fan.c b/drivers/hwmon/gpio-fan.c
new file mode 100644
index 0000000..0e4d575
--- /dev/null
+++ b/drivers/hwmon/gpio-fan.c
@@ -0,0 +1,558 @@
+/*
+ * gpio-fan.c - Hwmon driver for GPIO connected fan.
+ *
+ * Copyright (C) 2010 LaCie
+ *
+ * Author: Simon Guinot <sguinot@lacie.com>
+ *
+ * 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
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/platform_device.h>
+#include <linux/err.h>
+#include <linux/mutex.h>
+#include <linux/hwmon.h>
+#include <linux/gpio.h>
+#include <linux/gpio-fan.h>
+
+struct gpio_fan_data {
+	struct platform_device	*pdev;
+	struct device		*hwmon_dev;
+	struct mutex		lock; /* lock GPIOs operations. */
+	int			num_ctrl;
+	unsigned		*ctrl;
+	int			num_speed;
+	struct gpio_fan_speed	*speed;
+	int			speed_index;
+#ifdef CONFIG_PM
+	int			resume_speed;
+#endif
+	bool			pwm_enable;
+	struct gpio_fan_alarm	*alarm;
+	struct work_struct	alarm_work;
+};
+
+/*
+ * Alarm GPIO.
+ */
+
+static void fan_alarm_notify(struct work_struct *ws)
+{
+	struct gpio_fan_data *fan_data +		container_of(ws, struct gpio_fan_data, alarm_work);
+
+	sysfs_notify(&fan_data->pdev->dev.kobj, NULL, "fan1_alarm");
+	kobject_uevent(&fan_data->pdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static irqreturn_t fan_alarm_irq_handler(int irq, void *dev_id)
+{
+	struct gpio_fan_data *fan_data = dev_id;
+
+	schedule_work(&fan_data->alarm_work);
+
+	return IRQ_NONE;
+}
+
+static ssize_t show_fan_alarm(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	struct gpio_fan_alarm *alarm = fan_data->alarm;
+	int value = gpio_get_value(alarm->gpio);
+
+	if (alarm->active_low)
+		value = !value;
+
+	return sprintf(buf, "%d\n", value);
+}
+
+static DEVICE_ATTR(fan1_alarm, S_IRUGO, show_fan_alarm, NULL);
+
+static int fan_alarm_init(struct gpio_fan_data *fan_data,
+			  struct gpio_fan_alarm *alarm)
+{
+	int err;
+	int alarm_irq;
+	struct platform_device *pdev = fan_data->pdev;
+
+	fan_data->alarm = alarm;
+
+	err = gpio_request(alarm->gpio, "GPIO fan alarm");
+	if (err)
+		return err;
+
+	err = gpio_direction_input(alarm->gpio);
+	if (err)
+		goto err_free_gpio;
+
+	err = device_create_file(&pdev->dev, &dev_attr_fan1_alarm);
+	if (err)
+		goto err_free_gpio;
+
+	/*
+	 * If the alarm GPIO don't support interrupts, just leave
+	 * without initializing the fail notification support.
+	 */
+	alarm_irq = gpio_to_irq(alarm->gpio);
+	if (alarm_irq < 0)
+		return 0;
+
+	INIT_WORK(&fan_data->alarm_work, fan_alarm_notify);
+	set_irq_type(alarm_irq, IRQ_TYPE_EDGE_BOTH);
+	err = request_irq(alarm_irq, fan_alarm_irq_handler, IRQF_SHARED,
+			  "GPIO fan alarm", fan_data);
+	if (err)
+		goto err_free_sysfs;
+
+	return 0;
+
+err_free_sysfs:
+	device_remove_file(&pdev->dev, &dev_attr_fan1_alarm);
+err_free_gpio:
+	gpio_free(alarm->gpio);
+
+	return err;
+}
+
+static void fan_alarm_free(struct gpio_fan_data *fan_data)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int alarm_irq = gpio_to_irq(fan_data->alarm->gpio);
+
+	if (alarm_irq >= 0)
+		free_irq(alarm_irq, fan_data);
+	device_remove_file(&pdev->dev, &dev_attr_fan1_alarm);
+	gpio_free(fan_data->alarm->gpio);
+}
+
+/*
+ * Control GPIOs.
+ */
+
+/* Must be called with fan_data->lock held, except during initialization. */
+static void __set_fan_ctrl(struct gpio_fan_data *fan_data, int ctrl_val)
+{
+	int i;
+
+	for (i = 0; i < fan_data->num_ctrl; i++)
+		gpio_set_value(fan_data->ctrl[i], (ctrl_val >> i) & 1);
+}
+
+static int __get_fan_ctrl(struct gpio_fan_data *fan_data)
+{
+	int i;
+	int ctrl_val = 0;
+
+	for (i = 0; i < fan_data->num_ctrl; i++) {
+		int value;
+
+		value = gpio_get_value(fan_data->ctrl[i]);
+		ctrl_val |= (value << i);
+	}
+	return ctrl_val;
+}
+
+/* Must be called with fan_data->lock held, except during initialization. */
+static void set_fan_speed(struct gpio_fan_data *fan_data, int speed_index)
+{
+	if (fan_data->speed_index = speed_index)
+		return;
+
+	__set_fan_ctrl(fan_data, fan_data->speed[speed_index].ctrl_val);
+	fan_data->speed_index = speed_index;
+}
+
+static int get_fan_speed_index(struct gpio_fan_data *fan_data)
+{
+	int ctrl_val = __get_fan_ctrl(fan_data);
+	int i;
+
+	for (i = 0; i < fan_data->num_speed; i++)
+		if (fan_data->speed[i].ctrl_val = ctrl_val)
+			return i;
+
+	dev_warn(&fan_data->pdev->dev,
+		 "missing speed array entry for GPIO value 0x%x\n", ctrl_val);
+
+	return -EINVAL;
+}
+
+static int rpm_to_speed_index(struct gpio_fan_data *fan_data, int rpm)
+{
+	struct gpio_fan_speed *speed = fan_data->speed;
+	int i;
+
+	for (i = 0; i < fan_data->num_speed; i++)
+		if (speed[i].rpm >= rpm)
+			return i;
+
+	return fan_data->num_speed - 1;
+}
+
+static ssize_t show_pwm(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	u8 pwm = fan_data->speed_index * 255 / (fan_data->num_speed - 1);
+
+	return sprintf(buf, "%d\n", pwm);
+}
+
+static ssize_t set_pwm(struct device *dev, struct device_attribute *attr,
+		       const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long pwm;
+	int speed_index;
+	int ret = count;
+
+	if (strict_strtoul(buf, 10, &pwm) || pwm > 255)
+		return -EINVAL;
+
+	mutex_lock(&fan_data->lock);
+
+	if (!fan_data->pwm_enable) {
+		ret = -EPERM;
+		goto exit_unlock;
+	}
+
+	speed_index = DIV_ROUND_UP(pwm * (fan_data->num_speed - 1), 255);
+	set_fan_speed(fan_data, speed_index);
+
+exit_unlock:
+	mutex_unlock(&fan_data->lock);
+
+	return ret;
+}
+
+static ssize_t show_pwm_enable(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->pwm_enable);
+}
+
+static ssize_t set_pwm_enable(struct device *dev, struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long val;
+
+	if (strict_strtoul(buf, 10, &val) || val > 1)
+		return -EINVAL;
+
+	if (fan_data->pwm_enable = val)
+		return count;
+
+	mutex_lock(&fan_data->lock);
+
+	fan_data->pwm_enable = val;
+
+	/* Disable manual control mode: set fan at full speed. */
+	if (val = 0)
+		set_fan_speed(fan_data, fan_data->num_speed - 1);
+
+	mutex_unlock(&fan_data->lock);
+
+	return count;
+}
+
+static ssize_t show_pwm_mode(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	return sprintf(buf, "0\n");
+}
+
+static ssize_t show_rpm_min(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->speed[0].rpm);
+}
+
+static ssize_t show_rpm_max(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n",
+		       fan_data->speed[fan_data->num_speed - 1].rpm);
+}
+
+static ssize_t show_rpm(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->speed[fan_data->speed_index].rpm);
+}
+
+static ssize_t set_rpm(struct device *dev, struct device_attribute *attr,
+		       const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long rpm;
+	int ret = count;
+
+	if (strict_strtoul(buf, 10, &rpm))
+		return -EINVAL;
+
+	mutex_lock(&fan_data->lock);
+
+	if (!fan_data->pwm_enable) {
+		ret = -EPERM;
+		goto exit_unlock;
+	}
+
+	set_fan_speed(fan_data, rpm_to_speed_index(fan_data, rpm));
+
+exit_unlock:
+	mutex_unlock(&fan_data->lock);
+
+	return ret;
+}
+
+static DEVICE_ATTR(pwm1, S_IRUGO | S_IWUSR, show_pwm, set_pwm);
+static DEVICE_ATTR(pwm1_enable, S_IRUGO | S_IWUSR,
+		   show_pwm_enable, set_pwm_enable);
+static DEVICE_ATTR(pwm1_mode, S_IRUGO, show_pwm_mode, NULL);
+static DEVICE_ATTR(fan1_min, S_IRUGO, show_rpm_min, NULL);
+static DEVICE_ATTR(fan1_max, S_IRUGO, show_rpm_max, NULL);
+static DEVICE_ATTR(fan1_input, S_IRUGO, show_rpm, NULL);
+static DEVICE_ATTR(fan1_target, S_IRUGO | S_IWUSR, show_rpm, set_rpm);
+
+static struct attribute *gpio_fan_ctrl_attributes[] = {
+	&dev_attr_pwm1.attr,
+	&dev_attr_pwm1_enable.attr,
+	&dev_attr_pwm1_mode.attr,
+	&dev_attr_fan1_input.attr,
+	&dev_attr_fan1_target.attr,
+	&dev_attr_fan1_min.attr,
+	&dev_attr_fan1_max.attr,
+	NULL
+};
+
+static const struct attribute_group gpio_fan_ctrl_group = {
+	.attrs = gpio_fan_ctrl_attributes,
+};
+
+static int fan_ctrl_init(struct gpio_fan_data *fan_data,
+			 struct gpio_fan_platform_data *pdata)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int num_ctrl = pdata->num_ctrl;
+	unsigned *ctrl = pdata->ctrl;
+	int i, err;
+
+	for (i = 0; i < num_ctrl; i++) {
+		err = gpio_request(ctrl[i], "GPIO fan control");
+		if (err)
+			goto err_free_gpio;
+
+		err = gpio_direction_output(ctrl[i], gpio_get_value(ctrl[i]));
+		if (err) {
+			gpio_free(ctrl[i]);
+			goto err_free_gpio;
+		}
+	}
+
+	err = sysfs_create_group(&pdev->dev.kobj, &gpio_fan_ctrl_group);
+	if (err)
+		goto err_free_gpio;
+
+	fan_data->num_ctrl = num_ctrl;
+	fan_data->ctrl = ctrl;
+	fan_data->num_speed = pdata->num_speed;
+	fan_data->speed = pdata->speed;
+	fan_data->pwm_enable = true; /* Enable manual fan speed control. */
+	fan_data->speed_index = get_fan_speed_index(fan_data);
+	if (fan_data->speed_index < 0) {
+		err = -ENODEV;
+		goto err_free_gpio;
+	}
+
+	return 0;
+
+err_free_gpio:
+	for (i = i - 1; i >= 0; i--)
+		gpio_free(ctrl[i]);
+
+	return err;
+}
+
+static void fan_ctrl_free(struct gpio_fan_data *fan_data)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int i;
+
+	sysfs_remove_group(&pdev->dev.kobj, &gpio_fan_ctrl_group);
+	for (i = 0; i < fan_data->num_ctrl; i++)
+		gpio_free(fan_data->ctrl[i]);
+}
+
+/*
+ * Platform driver.
+ */
+
+static ssize_t show_name(struct device *dev,
+			 struct device_attribute *attr, char *buf)
+{
+	return sprintf(buf, "gpio-fan\n");
+}
+
+static DEVICE_ATTR(name, S_IRUGO, show_name, NULL);
+
+static int __devinit gpio_fan_probe(struct platform_device *pdev)
+{
+	int err = 0;
+	struct gpio_fan_data *fan_data;
+	struct gpio_fan_platform_data *pdata = pdev->dev.platform_data;
+
+	if (!pdata)
+		return -EINVAL;
+
+	fan_data = kzalloc(sizeof(struct gpio_fan_data), GFP_KERNEL);
+	if (!fan_data)
+		return -ENOMEM;
+
+	fan_data->pdev = pdev;
+	platform_set_drvdata(pdev, fan_data);
+	mutex_init(&fan_data->lock);
+
+	/* Configure alarm GPIO if available. */
+	if (pdata->alarm) {
+		err = fan_alarm_init(fan_data, pdata->alarm);
+		if (err)
+			goto err_free_data;
+	}
+
+	/* Configure control GPIOs if available. */
+	if (pdata->ctrl && pdata->num_ctrl > 0) {
+		if (!pdata->speed || pdata->num_speed <= 1) {
+			err = -EINVAL;
+			goto err_free_alarm;
+		}
+		err = fan_ctrl_init(fan_data, pdata);
+		if (err)
+			goto err_free_alarm;
+	}
+
+	err = device_create_file(&pdev->dev, &dev_attr_name);
+	if (err)
+		goto err_free_ctrl;
+
+	/* Make this driver part of hwmon class. */
+	fan_data->hwmon_dev = hwmon_device_register(&pdev->dev);
+	if (IS_ERR(fan_data->hwmon_dev)) {
+		err = PTR_ERR(fan_data->hwmon_dev);
+		goto err_remove_name;
+	}
+
+	dev_info(&pdev->dev, "GPIO fan initialized\n");
+
+	return 0;
+
+err_remove_name:
+	device_remove_file(&pdev->dev, &dev_attr_name);
+err_free_ctrl:
+	if (fan_data->ctrl)
+		fan_ctrl_free(fan_data);
+err_free_alarm:
+	if (fan_data->alarm)
+		fan_alarm_free(fan_data);
+err_free_data:
+	platform_set_drvdata(pdev, NULL);
+	kfree(fan_data);
+
+	return err;
+}
+
+static int __devexit gpio_fan_remove(struct platform_device *pdev)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	hwmon_device_unregister(fan_data->hwmon_dev);
+	device_remove_file(&pdev->dev, &dev_attr_name);
+	if (fan_data->alarm)
+		fan_alarm_free(fan_data);
+	if (fan_data->ctrl)
+		fan_ctrl_free(fan_data);
+	kfree(fan_data);
+
+	return 0;
+}
+
+#ifdef CONFIG_PM
+static int gpio_fan_suspend(struct platform_device *pdev, pm_message_t state)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	if (fan_data->ctrl) {
+		fan_data->resume_speed = fan_data->speed_index;
+		set_fan_speed(fan_data, 0);
+	}
+
+	return 0;
+}
+
+static int gpio_fan_resume(struct platform_device *pdev)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	if (fan_data->ctrl)
+		set_fan_speed(fan_data, fan_data->resume_speed);
+
+	return 0;
+}
+#else
+#define gpio_fan_suspend NULL
+#define gpio_fan_resume NULL
+#endif
+
+static struct platform_driver gpio_fan_driver = {
+	.probe		= gpio_fan_probe,
+	.remove		= __devexit_p(gpio_fan_remove),
+	.suspend	= gpio_fan_suspend,
+	.resume		= gpio_fan_resume,
+	.driver	= {
+		.name	= "gpio-fan",
+	},
+};
+
+static int __init gpio_fan_init(void)
+{
+	return platform_driver_register(&gpio_fan_driver);
+}
+
+static void __exit gpio_fan_exit(void)
+{
+	platform_driver_unregister(&gpio_fan_driver);
+}
+
+module_init(gpio_fan_init);
+module_exit(gpio_fan_exit);
+
+MODULE_AUTHOR("Simon Guinot <sguinot@lacie.com>");
+MODULE_DESCRIPTION("GPIO FAN driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:gpio-fan");
diff --git a/include/linux/gpio-fan.h b/include/linux/gpio-fan.h
new file mode 100644
index 0000000..0966591
--- /dev/null
+++ b/include/linux/gpio-fan.h
@@ -0,0 +1,36 @@
+/*
+ * include/linux/gpio-fan.h
+ *
+ * Platform data structure for GPIO fan driver
+ *
+ * This file is licensed under the terms of the GNU General Public
+ * License version 2.  This program is licensed "as is" without any
+ * warranty of any kind, whether express or implied.
+ */
+
+#ifndef __LINUX_GPIO_FAN_H
+#define __LINUX_GPIO_FAN_H
+
+struct gpio_fan_alarm {
+	unsigned	gpio;
+	unsigned	active_low;
+};
+
+struct gpio_fan_speed {
+	int rpm;
+	int ctrl_val;
+};
+
+struct gpio_fan_platform_data {
+	int			num_ctrl;
+	unsigned		*ctrl;	/* fan control GPIOs. */
+	struct gpio_fan_alarm	*alarm;	/* fan alarm GPIO. */
+	/*
+	 * Speed conversion array: rpm from/to GPIO bit field.
+	 * This array _must_ be sorted in ascending rpm order.
+	 */
+	int			num_speed;
+	struct gpio_fan_speed	*speed;
+};
+
+#endif /* __LINUX_GPIO_FAN_H */
-- 
1.6.3.1


_______________________________________________
lm-sensors mailing list
lm-sensors@lm-sensors.org
http://lists.lm-sensors.org/mailman/listinfo/lm-sensors

WARNING: multiple messages have this Message-ID (diff)
From: simon@sequanux.org (Simon Guinot)
To: linux-arm-kernel@lists.infradead.org
Subject: [PATCH v3] hwmon: add generic GPIO fan driver
Date: Fri, 22 Oct 2010 00:44:19 +0200	[thread overview]
Message-ID: <1287701059-2226-1-git-send-email-simon@sequanux.org> (raw)
In-Reply-To: <20101021223555.GT29120@kw.sim.vm.gnt>

From: Simon Guinot <sguinot@lacie.com>

This patch adds hwmon support for the GPIO connected fan.

Platform specific informations as GPIO pinout and speed conversion array
(rpm from/to GPIO value) are passed to the driver via platform_data.

Signed-off-by: Simon Guinot <sguinot@lacie.com>
---
 drivers/hwmon/Kconfig    |    9 +
 drivers/hwmon/Makefile   |    1 +
 drivers/hwmon/gpio-fan.c |  558 ++++++++++++++++++++++++++++++++++++++++++++++
 include/linux/gpio-fan.h |   36 +++
 4 files changed, 604 insertions(+), 0 deletions(-)
 create mode 100644 drivers/hwmon/gpio-fan.c
 create mode 100644 include/linux/gpio-fan.h

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 4d4d09b..10ce9da 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -399,6 +399,15 @@ config SENSORS_GL520SM
 	  This driver can also be built as a module.  If so, the module
 	  will be called gl520sm.
 
+config SENSORS_GPIO_FAN
+	tristate "GPIO fan"
+	depends on GENERIC_GPIO
+	help
+	  If you say yes here you get support for the GPIO connected fans.
+
+	  This driver can also be built as a module.  If so, the module
+	  will be called gpio-fan.
+
 config SENSORS_CORETEMP
 	tristate "Intel Core/Core2/Atom temperature sensor"
 	depends on X86 && PCI && EXPERIMENTAL
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index e3c2484..a793e28 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -51,6 +51,7 @@ obj-$(CONFIG_SENSORS_FSCHMD)	+= fschmd.o
 obj-$(CONFIG_SENSORS_G760A)	+= g760a.o
 obj-$(CONFIG_SENSORS_GL518SM)	+= gl518sm.o
 obj-$(CONFIG_SENSORS_GL520SM)	+= gl520sm.o
+obj-$(CONFIG_SENSORS_GPIO_FAN)	+= gpio-fan.o
 obj-$(CONFIG_SENSORS_ULTRA45)	+= ultra45_env.o
 obj-$(CONFIG_SENSORS_HDAPS)	+= hdaps.o
 obj-$(CONFIG_SENSORS_I5K_AMB)	+= i5k_amb.o
diff --git a/drivers/hwmon/gpio-fan.c b/drivers/hwmon/gpio-fan.c
new file mode 100644
index 0000000..0e4d575
--- /dev/null
+++ b/drivers/hwmon/gpio-fan.c
@@ -0,0 +1,558 @@
+/*
+ * gpio-fan.c - Hwmon driver for GPIO connected fan.
+ *
+ * Copyright (C) 2010 LaCie
+ *
+ * Author: Simon Guinot <sguinot@lacie.com>
+ *
+ * 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
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/platform_device.h>
+#include <linux/err.h>
+#include <linux/mutex.h>
+#include <linux/hwmon.h>
+#include <linux/gpio.h>
+#include <linux/gpio-fan.h>
+
+struct gpio_fan_data {
+	struct platform_device	*pdev;
+	struct device		*hwmon_dev;
+	struct mutex		lock; /* lock GPIOs operations. */
+	int			num_ctrl;
+	unsigned		*ctrl;
+	int			num_speed;
+	struct gpio_fan_speed	*speed;
+	int			speed_index;
+#ifdef CONFIG_PM
+	int			resume_speed;
+#endif
+	bool			pwm_enable;
+	struct gpio_fan_alarm	*alarm;
+	struct work_struct	alarm_work;
+};
+
+/*
+ * Alarm GPIO.
+ */
+
+static void fan_alarm_notify(struct work_struct *ws)
+{
+	struct gpio_fan_data *fan_data =
+		container_of(ws, struct gpio_fan_data, alarm_work);
+
+	sysfs_notify(&fan_data->pdev->dev.kobj, NULL, "fan1_alarm");
+	kobject_uevent(&fan_data->pdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static irqreturn_t fan_alarm_irq_handler(int irq, void *dev_id)
+{
+	struct gpio_fan_data *fan_data = dev_id;
+
+	schedule_work(&fan_data->alarm_work);
+
+	return IRQ_NONE;
+}
+
+static ssize_t show_fan_alarm(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	struct gpio_fan_alarm *alarm = fan_data->alarm;
+	int value = gpio_get_value(alarm->gpio);
+
+	if (alarm->active_low)
+		value = !value;
+
+	return sprintf(buf, "%d\n", value);
+}
+
+static DEVICE_ATTR(fan1_alarm, S_IRUGO, show_fan_alarm, NULL);
+
+static int fan_alarm_init(struct gpio_fan_data *fan_data,
+			  struct gpio_fan_alarm *alarm)
+{
+	int err;
+	int alarm_irq;
+	struct platform_device *pdev = fan_data->pdev;
+
+	fan_data->alarm = alarm;
+
+	err = gpio_request(alarm->gpio, "GPIO fan alarm");
+	if (err)
+		return err;
+
+	err = gpio_direction_input(alarm->gpio);
+	if (err)
+		goto err_free_gpio;
+
+	err = device_create_file(&pdev->dev, &dev_attr_fan1_alarm);
+	if (err)
+		goto err_free_gpio;
+
+	/*
+	 * If the alarm GPIO don't support interrupts, just leave
+	 * without initializing the fail notification support.
+	 */
+	alarm_irq = gpio_to_irq(alarm->gpio);
+	if (alarm_irq < 0)
+		return 0;
+
+	INIT_WORK(&fan_data->alarm_work, fan_alarm_notify);
+	set_irq_type(alarm_irq, IRQ_TYPE_EDGE_BOTH);
+	err = request_irq(alarm_irq, fan_alarm_irq_handler, IRQF_SHARED,
+			  "GPIO fan alarm", fan_data);
+	if (err)
+		goto err_free_sysfs;
+
+	return 0;
+
+err_free_sysfs:
+	device_remove_file(&pdev->dev, &dev_attr_fan1_alarm);
+err_free_gpio:
+	gpio_free(alarm->gpio);
+
+	return err;
+}
+
+static void fan_alarm_free(struct gpio_fan_data *fan_data)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int alarm_irq = gpio_to_irq(fan_data->alarm->gpio);
+
+	if (alarm_irq >= 0)
+		free_irq(alarm_irq, fan_data);
+	device_remove_file(&pdev->dev, &dev_attr_fan1_alarm);
+	gpio_free(fan_data->alarm->gpio);
+}
+
+/*
+ * Control GPIOs.
+ */
+
+/* Must be called with fan_data->lock held, except during initialization. */
+static void __set_fan_ctrl(struct gpio_fan_data *fan_data, int ctrl_val)
+{
+	int i;
+
+	for (i = 0; i < fan_data->num_ctrl; i++)
+		gpio_set_value(fan_data->ctrl[i], (ctrl_val >> i) & 1);
+}
+
+static int __get_fan_ctrl(struct gpio_fan_data *fan_data)
+{
+	int i;
+	int ctrl_val = 0;
+
+	for (i = 0; i < fan_data->num_ctrl; i++) {
+		int value;
+
+		value = gpio_get_value(fan_data->ctrl[i]);
+		ctrl_val |= (value << i);
+	}
+	return ctrl_val;
+}
+
+/* Must be called with fan_data->lock held, except during initialization. */
+static void set_fan_speed(struct gpio_fan_data *fan_data, int speed_index)
+{
+	if (fan_data->speed_index == speed_index)
+		return;
+
+	__set_fan_ctrl(fan_data, fan_data->speed[speed_index].ctrl_val);
+	fan_data->speed_index = speed_index;
+}
+
+static int get_fan_speed_index(struct gpio_fan_data *fan_data)
+{
+	int ctrl_val = __get_fan_ctrl(fan_data);
+	int i;
+
+	for (i = 0; i < fan_data->num_speed; i++)
+		if (fan_data->speed[i].ctrl_val == ctrl_val)
+			return i;
+
+	dev_warn(&fan_data->pdev->dev,
+		 "missing speed array entry for GPIO value 0x%x\n", ctrl_val);
+
+	return -EINVAL;
+}
+
+static int rpm_to_speed_index(struct gpio_fan_data *fan_data, int rpm)
+{
+	struct gpio_fan_speed *speed = fan_data->speed;
+	int i;
+
+	for (i = 0; i < fan_data->num_speed; i++)
+		if (speed[i].rpm >= rpm)
+			return i;
+
+	return fan_data->num_speed - 1;
+}
+
+static ssize_t show_pwm(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	u8 pwm = fan_data->speed_index * 255 / (fan_data->num_speed - 1);
+
+	return sprintf(buf, "%d\n", pwm);
+}
+
+static ssize_t set_pwm(struct device *dev, struct device_attribute *attr,
+		       const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long pwm;
+	int speed_index;
+	int ret = count;
+
+	if (strict_strtoul(buf, 10, &pwm) || pwm > 255)
+		return -EINVAL;
+
+	mutex_lock(&fan_data->lock);
+
+	if (!fan_data->pwm_enable) {
+		ret = -EPERM;
+		goto exit_unlock;
+	}
+
+	speed_index = DIV_ROUND_UP(pwm * (fan_data->num_speed - 1), 255);
+	set_fan_speed(fan_data, speed_index);
+
+exit_unlock:
+	mutex_unlock(&fan_data->lock);
+
+	return ret;
+}
+
+static ssize_t show_pwm_enable(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->pwm_enable);
+}
+
+static ssize_t set_pwm_enable(struct device *dev, struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long val;
+
+	if (strict_strtoul(buf, 10, &val) || val > 1)
+		return -EINVAL;
+
+	if (fan_data->pwm_enable == val)
+		return count;
+
+	mutex_lock(&fan_data->lock);
+
+	fan_data->pwm_enable = val;
+
+	/* Disable manual control mode: set fan at full speed. */
+	if (val == 0)
+		set_fan_speed(fan_data, fan_data->num_speed - 1);
+
+	mutex_unlock(&fan_data->lock);
+
+	return count;
+}
+
+static ssize_t show_pwm_mode(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	return sprintf(buf, "0\n");
+}
+
+static ssize_t show_rpm_min(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->speed[0].rpm);
+}
+
+static ssize_t show_rpm_max(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n",
+		       fan_data->speed[fan_data->num_speed - 1].rpm);
+}
+
+static ssize_t show_rpm(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", fan_data->speed[fan_data->speed_index].rpm);
+}
+
+static ssize_t set_rpm(struct device *dev, struct device_attribute *attr,
+		       const char *buf, size_t count)
+{
+	struct gpio_fan_data *fan_data = dev_get_drvdata(dev);
+	unsigned long rpm;
+	int ret = count;
+
+	if (strict_strtoul(buf, 10, &rpm))
+		return -EINVAL;
+
+	mutex_lock(&fan_data->lock);
+
+	if (!fan_data->pwm_enable) {
+		ret = -EPERM;
+		goto exit_unlock;
+	}
+
+	set_fan_speed(fan_data, rpm_to_speed_index(fan_data, rpm));
+
+exit_unlock:
+	mutex_unlock(&fan_data->lock);
+
+	return ret;
+}
+
+static DEVICE_ATTR(pwm1, S_IRUGO | S_IWUSR, show_pwm, set_pwm);
+static DEVICE_ATTR(pwm1_enable, S_IRUGO | S_IWUSR,
+		   show_pwm_enable, set_pwm_enable);
+static DEVICE_ATTR(pwm1_mode, S_IRUGO, show_pwm_mode, NULL);
+static DEVICE_ATTR(fan1_min, S_IRUGO, show_rpm_min, NULL);
+static DEVICE_ATTR(fan1_max, S_IRUGO, show_rpm_max, NULL);
+static DEVICE_ATTR(fan1_input, S_IRUGO, show_rpm, NULL);
+static DEVICE_ATTR(fan1_target, S_IRUGO | S_IWUSR, show_rpm, set_rpm);
+
+static struct attribute *gpio_fan_ctrl_attributes[] = {
+	&dev_attr_pwm1.attr,
+	&dev_attr_pwm1_enable.attr,
+	&dev_attr_pwm1_mode.attr,
+	&dev_attr_fan1_input.attr,
+	&dev_attr_fan1_target.attr,
+	&dev_attr_fan1_min.attr,
+	&dev_attr_fan1_max.attr,
+	NULL
+};
+
+static const struct attribute_group gpio_fan_ctrl_group = {
+	.attrs = gpio_fan_ctrl_attributes,
+};
+
+static int fan_ctrl_init(struct gpio_fan_data *fan_data,
+			 struct gpio_fan_platform_data *pdata)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int num_ctrl = pdata->num_ctrl;
+	unsigned *ctrl = pdata->ctrl;
+	int i, err;
+
+	for (i = 0; i < num_ctrl; i++) {
+		err = gpio_request(ctrl[i], "GPIO fan control");
+		if (err)
+			goto err_free_gpio;
+
+		err = gpio_direction_output(ctrl[i], gpio_get_value(ctrl[i]));
+		if (err) {
+			gpio_free(ctrl[i]);
+			goto err_free_gpio;
+		}
+	}
+
+	err = sysfs_create_group(&pdev->dev.kobj, &gpio_fan_ctrl_group);
+	if (err)
+		goto err_free_gpio;
+
+	fan_data->num_ctrl = num_ctrl;
+	fan_data->ctrl = ctrl;
+	fan_data->num_speed = pdata->num_speed;
+	fan_data->speed = pdata->speed;
+	fan_data->pwm_enable = true; /* Enable manual fan speed control. */
+	fan_data->speed_index = get_fan_speed_index(fan_data);
+	if (fan_data->speed_index < 0) {
+		err = -ENODEV;
+		goto err_free_gpio;
+	}
+
+	return 0;
+
+err_free_gpio:
+	for (i = i - 1; i >= 0; i--)
+		gpio_free(ctrl[i]);
+
+	return err;
+}
+
+static void fan_ctrl_free(struct gpio_fan_data *fan_data)
+{
+	struct platform_device *pdev = fan_data->pdev;
+	int i;
+
+	sysfs_remove_group(&pdev->dev.kobj, &gpio_fan_ctrl_group);
+	for (i = 0; i < fan_data->num_ctrl; i++)
+		gpio_free(fan_data->ctrl[i]);
+}
+
+/*
+ * Platform driver.
+ */
+
+static ssize_t show_name(struct device *dev,
+			 struct device_attribute *attr, char *buf)
+{
+	return sprintf(buf, "gpio-fan\n");
+}
+
+static DEVICE_ATTR(name, S_IRUGO, show_name, NULL);
+
+static int __devinit gpio_fan_probe(struct platform_device *pdev)
+{
+	int err = 0;
+	struct gpio_fan_data *fan_data;
+	struct gpio_fan_platform_data *pdata = pdev->dev.platform_data;
+
+	if (!pdata)
+		return -EINVAL;
+
+	fan_data = kzalloc(sizeof(struct gpio_fan_data), GFP_KERNEL);
+	if (!fan_data)
+		return -ENOMEM;
+
+	fan_data->pdev = pdev;
+	platform_set_drvdata(pdev, fan_data);
+	mutex_init(&fan_data->lock);
+
+	/* Configure alarm GPIO if available. */
+	if (pdata->alarm) {
+		err = fan_alarm_init(fan_data, pdata->alarm);
+		if (err)
+			goto err_free_data;
+	}
+
+	/* Configure control GPIOs if available. */
+	if (pdata->ctrl && pdata->num_ctrl > 0) {
+		if (!pdata->speed || pdata->num_speed <= 1) {
+			err = -EINVAL;
+			goto err_free_alarm;
+		}
+		err = fan_ctrl_init(fan_data, pdata);
+		if (err)
+			goto err_free_alarm;
+	}
+
+	err = device_create_file(&pdev->dev, &dev_attr_name);
+	if (err)
+		goto err_free_ctrl;
+
+	/* Make this driver part of hwmon class. */
+	fan_data->hwmon_dev = hwmon_device_register(&pdev->dev);
+	if (IS_ERR(fan_data->hwmon_dev)) {
+		err = PTR_ERR(fan_data->hwmon_dev);
+		goto err_remove_name;
+	}
+
+	dev_info(&pdev->dev, "GPIO fan initialized\n");
+
+	return 0;
+
+err_remove_name:
+	device_remove_file(&pdev->dev, &dev_attr_name);
+err_free_ctrl:
+	if (fan_data->ctrl)
+		fan_ctrl_free(fan_data);
+err_free_alarm:
+	if (fan_data->alarm)
+		fan_alarm_free(fan_data);
+err_free_data:
+	platform_set_drvdata(pdev, NULL);
+	kfree(fan_data);
+
+	return err;
+}
+
+static int __devexit gpio_fan_remove(struct platform_device *pdev)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	hwmon_device_unregister(fan_data->hwmon_dev);
+	device_remove_file(&pdev->dev, &dev_attr_name);
+	if (fan_data->alarm)
+		fan_alarm_free(fan_data);
+	if (fan_data->ctrl)
+		fan_ctrl_free(fan_data);
+	kfree(fan_data);
+
+	return 0;
+}
+
+#ifdef CONFIG_PM
+static int gpio_fan_suspend(struct platform_device *pdev, pm_message_t state)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	if (fan_data->ctrl) {
+		fan_data->resume_speed = fan_data->speed_index;
+		set_fan_speed(fan_data, 0);
+	}
+
+	return 0;
+}
+
+static int gpio_fan_resume(struct platform_device *pdev)
+{
+	struct gpio_fan_data *fan_data = platform_get_drvdata(pdev);
+
+	if (fan_data->ctrl)
+		set_fan_speed(fan_data, fan_data->resume_speed);
+
+	return 0;
+}
+#else
+#define gpio_fan_suspend NULL
+#define gpio_fan_resume NULL
+#endif
+
+static struct platform_driver gpio_fan_driver = {
+	.probe		= gpio_fan_probe,
+	.remove		= __devexit_p(gpio_fan_remove),
+	.suspend	= gpio_fan_suspend,
+	.resume		= gpio_fan_resume,
+	.driver	= {
+		.name	= "gpio-fan",
+	},
+};
+
+static int __init gpio_fan_init(void)
+{
+	return platform_driver_register(&gpio_fan_driver);
+}
+
+static void __exit gpio_fan_exit(void)
+{
+	platform_driver_unregister(&gpio_fan_driver);
+}
+
+module_init(gpio_fan_init);
+module_exit(gpio_fan_exit);
+
+MODULE_AUTHOR("Simon Guinot <sguinot@lacie.com>");
+MODULE_DESCRIPTION("GPIO FAN driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:gpio-fan");
diff --git a/include/linux/gpio-fan.h b/include/linux/gpio-fan.h
new file mode 100644
index 0000000..0966591
--- /dev/null
+++ b/include/linux/gpio-fan.h
@@ -0,0 +1,36 @@
+/*
+ * include/linux/gpio-fan.h
+ *
+ * Platform data structure for GPIO fan driver
+ *
+ * This file is licensed under the terms of the GNU General Public
+ * License version 2.  This program is licensed "as is" without any
+ * warranty of any kind, whether express or implied.
+ */
+
+#ifndef __LINUX_GPIO_FAN_H
+#define __LINUX_GPIO_FAN_H
+
+struct gpio_fan_alarm {
+	unsigned	gpio;
+	unsigned	active_low;
+};
+
+struct gpio_fan_speed {
+	int rpm;
+	int ctrl_val;
+};
+
+struct gpio_fan_platform_data {
+	int			num_ctrl;
+	unsigned		*ctrl;	/* fan control GPIOs. */
+	struct gpio_fan_alarm	*alarm;	/* fan alarm GPIO. */
+	/*
+	 * Speed conversion array: rpm from/to GPIO bit field.
+	 * This array _must_ be sorted in ascending rpm order.
+	 */
+	int			num_speed;
+	struct gpio_fan_speed	*speed;
+};
+
+#endif /* __LINUX_GPIO_FAN_H */
-- 
1.6.3.1

  reply	other threads:[~2010-10-21 22:44 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2010-10-20 16:37 [lm-sensors] [PATCH v2] hwmon: add generic GPIO fan driver Simon Guinot
2010-10-20 16:37 ` Simon Guinot
2010-10-20 16:37 ` [lm-sensors] " Simon Guinot
2010-10-20 16:37   ` Simon Guinot
2010-10-20 16:59   ` [lm-sensors] " Guenter Roeck
2010-10-20 16:59     ` Guenter Roeck
2010-10-20 19:10     ` [lm-sensors] " Simon Guinot
2010-10-20 19:10       ` Simon Guinot
2010-10-20 20:42       ` [lm-sensors] " Guenter Roeck
2010-10-20 20:42         ` Guenter Roeck
2010-10-20 19:18     ` [lm-sensors] " Russell King - ARM Linux
2010-10-20 19:18       ` Russell King - ARM Linux
2010-10-20 20:06       ` [lm-sensors] " Guenter Roeck
2010-10-20 20:06         ` Guenter Roeck
2010-10-21  6:41   ` [lm-sensors] " Guenter Roeck
2010-10-21  6:41     ` Guenter Roeck
2010-10-21 14:06     ` [lm-sensors] " Simon Guinot
2010-10-21 14:06       ` Simon Guinot
2010-10-21 14:43       ` [lm-sensors] " Guenter Roeck
2010-10-21 14:43         ` Guenter Roeck
2010-10-21 21:59         ` [lm-sensors] " Simon Guinot
2010-10-21 21:59           ` Simon Guinot
2010-10-21 22:16           ` Guenter Roeck
2010-10-21 22:16             ` Guenter Roeck
2010-10-21 22:35             ` Simon Guinot
2010-10-21 22:35               ` Simon Guinot
2010-10-21 22:44               ` Simon Guinot [this message]
2010-10-21 22:44                 ` [PATCH v3] " Simon Guinot
2010-10-22  1:51                 ` [lm-sensors] " Guenter Roeck
2010-10-22  1:51                   ` Guenter Roeck
2010-10-22 12:25                   ` [lm-sensors] " Simon Guinot
2010-10-22 12:25                     ` Simon Guinot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1287701059-2226-1-git-send-email-simon@sequanux.org \
    --to=simon@sequanux.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.