hwmon: (pc87427) Add support for manual fan speed control
authorJean Delvare <khali@linux-fr.org>
Sat, 14 Aug 2010 19:08:58 +0000 (21:08 +0200)
committerJean Delvare <khali@linux-fr.org>
Sat, 14 Aug 2010 19:08:58 +0000 (21:08 +0200)
Add initial support for PWM outputs of the PC87427 Super-I/O chip.
Only mode change and manual fan speed control are supported. Automatic
mode configuration isn't supported, and won't be until at least one
board is known, which makes uses of the PWM outputs.

Signed-off-by: Jean Delvare <khali@linux-fr.org>
Acked-by: Guenter Roeck <guenter.roeck@ericsson.com>
Documentation/hwmon/pc87427
drivers/hwmon/Kconfig
drivers/hwmon/pc87427.c

index db5cc12..3282bf3 100644 (file)
@@ -20,8 +20,8 @@ The National Semiconductor Super I/O chip includes complete hardware
 monitoring capabilities. It can monitor up to 18 voltages, 8 fans and
 6 temperature sensors. Only the fans are supported at the moment.
 
-This chip also has fan controlling features, which are not yet supported
-by this driver either.
+This chip also has fan controlling features (up to 4 PWM outputs),
+which are partly supported by this driver.
 
 The driver assumes that no more than one chip is present, which seems
 reasonable.
@@ -36,3 +36,12 @@ signal. Speeds down to 83 RPM can be measured.
 An alarm is triggered if the rotation speed drops below a programmable
 limit. Another alarm is triggered if the speed is too low to be measured
 (including stalled or missing fan).
+
+
+Fan Speed Control
+-----------------
+
+Fan speed can be controlled by PWM outputs. There are 4 possible modes:
+always off, always on, manual and automatic. The latter isn't supported
+by the driver: you can only return to that mode if it was the original
+setting, and the configuration interface is missing.
index d50e9fa..ea3d8df 100644 (file)
@@ -711,7 +711,8 @@ config SENSORS_PC87427
          functions of the National Semiconductor PC87427 Super-I/O chip.
          The chip has two distinct logical devices, one for fan speed
          monitoring and control, and one for voltage and temperature
-         monitoring. Only fan speed monitoring is supported right now.
+         monitoring. Only fan speed monitoring and control is supported
+         right now.
 
          This driver can also be built as a module.  If so, the module
          will be called pc87427.
index 0ac55ba..8698227 100644 (file)
  *  Supports the following chips:
  *
  *  Chip        #vin    #fan    #pwm    #temp   devid
- *  PC87427     -       8       -       -       0xF2
+ *  PC87427     -       8       4       -       0xF2
  *
  *  This driver assumes that no more than one chip is present.
- *  Only fan inputs are supported so far, although the chip can do much more.
+ *  Only fans are supported so far, although the chip can do much more.
  */
 
 #include <linux/module.h>
@@ -57,10 +57,16 @@ struct pc87427_data {
        u16 fan[8];                     /* register values */
        u16 fan_min[8];                 /* register values */
        u8 fan_status[8];               /* register values */
+
+       u8 pwm_enabled;                 /* bit vector */
+       u8 pwm_auto_ok;                 /* bit vector */
+       u8 pwm_enable[4];               /* register values */
+       u8 pwm[4];                      /* register values */
 };
 
 struct pc87427_sio_data {
        u8 has_fanin;
+       u8 has_fanout;
 };
 
 /*
@@ -72,7 +78,9 @@ struct pc87427_sio_data {
 #define SIOREG_CF2     0x22    /* Configuration 2 */
 #define SIOREG_CF3     0x23    /* Configuration 3 */
 #define SIOREG_CF4     0x24    /* Configuration 4 */
+#define SIOREG_CF5     0x25    /* Configuration 5 */
 #define SIOREG_CFB     0x2B    /* Configuration B */
+#define SIOREG_CFC     0x2C    /* Configuration C */
 #define SIOREG_CFD     0x2D    /* Configuration D */
 #define SIOREG_ACT     0x30    /* Device activation */
 #define SIOREG_MAP     0x50    /* I/O or memory mapping */
@@ -188,6 +196,61 @@ static inline u16 fan_to_reg(unsigned long val)
 }
 
 /*
+ * PWM registers and conversions
+ */
+
+#define PC87427_REG_PWM_ENABLE         0x10
+#define PC87427_REG_PWM_DUTY           0x12
+
+#define PWM_ENABLE_MODE_MASK           (7 << 4)
+#define PWM_ENABLE_CTLEN               (1 << 0)
+
+#define PWM_MODE_MANUAL                        (0 << 4)
+#define PWM_MODE_AUTO                  (1 << 4)
+#define PWM_MODE_OFF                   (2 << 4)
+#define PWM_MODE_ON                    (7 << 4)
+
+/* Dedicated function to read all registers related to a given PWM output.
+   This saves us quite a few locks and bank selections.
+   Must be called with data->lock held.
+   nr is from 0 to 3 */
+static void pc87427_readall_pwm(struct pc87427_data *data, u8 nr)
+{
+       int iobase = data->address[LD_FAN];
+
+       outb(BANK_FC(nr), iobase + PC87427_REG_BANK);
+       data->pwm_enable[nr] = inb(iobase + PC87427_REG_PWM_ENABLE);
+       data->pwm[nr] = inb(iobase + PC87427_REG_PWM_DUTY);
+}
+
+static inline int pwm_enable_from_reg(u8 reg)
+{
+       switch (reg & PWM_ENABLE_MODE_MASK) {
+       case PWM_MODE_ON:
+               return 0;
+       case PWM_MODE_MANUAL:
+       case PWM_MODE_OFF:
+               return 1;
+       case PWM_MODE_AUTO:
+               return 2;
+       default:
+               return -EPROTO;
+       }
+}
+
+static inline u8 pwm_enable_to_reg(unsigned long val, u8 pwmval)
+{
+       switch (val) {
+       default:
+               return PWM_MODE_ON;
+       case 1:
+               return pwmval ? PWM_MODE_MANUAL : PWM_MODE_OFF;
+       case 2:
+               return PWM_MODE_AUTO;
+       }
+}
+
+/*
  * Data interface
  */
 
@@ -207,6 +270,14 @@ static struct pc87427_data *pc87427_update_device(struct device *dev)
                        continue;
                pc87427_readall_fan(data, i);
        }
+
+       /* PWM outputs */
+       for (i = 0; i < 4; i++) {
+               if (!(data->pwm_enabled & (1 << i)))
+                       continue;
+               pc87427_readall_pwm(data, i);
+       }
+
        data->last_updated = jiffies;
 
 done:
@@ -384,6 +455,145 @@ static const struct attribute_group pc87427_group_fan[8] = {
        { .attrs = pc87427_attributes_fan[7] },
 };
 
+/* Must be called with data->lock held and pc87427_readall_pwm() freshly
+   called */
+static void update_pwm_enable(struct pc87427_data *data, int nr, u8 mode)
+{
+       int iobase = data->address[LD_FAN];
+       data->pwm_enable[nr] &= ~PWM_ENABLE_MODE_MASK;
+       data->pwm_enable[nr] |= mode;
+       outb(data->pwm_enable[nr], iobase + PC87427_REG_PWM_ENABLE);
+}
+
+static ssize_t show_pwm_enable(struct device *dev, struct device_attribute
+                              *devattr, char *buf)
+{
+       struct pc87427_data *data = pc87427_update_device(dev);
+       int nr = to_sensor_dev_attr(devattr)->index;
+       int pwm_enable;
+
+       pwm_enable = pwm_enable_from_reg(data->pwm_enable[nr]);
+       if (pwm_enable < 0)
+               return pwm_enable;
+       return sprintf(buf, "%d\n", pwm_enable);
+}
+
+static ssize_t set_pwm_enable(struct device *dev, struct device_attribute
+                             *devattr, const char *buf, size_t count)
+{
+       struct pc87427_data *data = dev_get_drvdata(dev);
+       int nr = to_sensor_dev_attr(devattr)->index;
+       unsigned long val;
+
+       if (strict_strtoul(buf, 10, &val) < 0 || val > 2)
+               return -EINVAL;
+       /* Can't go to automatic mode if it isn't configured */
+       if (val == 2 && !(data->pwm_auto_ok & (1 << nr)))
+               return -EINVAL;
+
+       mutex_lock(&data->lock);
+       pc87427_readall_pwm(data, nr);
+       update_pwm_enable(data, nr, pwm_enable_to_reg(val, data->pwm[nr]));
+       mutex_unlock(&data->lock);
+
+       return count;
+}
+
+static ssize_t show_pwm(struct device *dev, struct device_attribute
+                       *devattr, char *buf)
+{
+       struct pc87427_data *data = pc87427_update_device(dev);
+       int nr = to_sensor_dev_attr(devattr)->index;
+
+       return sprintf(buf, "%d\n", (int)data->pwm[nr]);
+}
+
+static ssize_t set_pwm(struct device *dev, struct device_attribute
+                      *devattr, const char *buf, size_t count)
+{
+       struct pc87427_data *data = dev_get_drvdata(dev);
+       int nr = to_sensor_dev_attr(devattr)->index;
+       unsigned long val;
+       int iobase = data->address[LD_FAN];
+       u8 mode;
+
+       if (strict_strtoul(buf, 10, &val) < 0 || val > 0xff)
+               return -EINVAL;
+
+       mutex_lock(&data->lock);
+       pc87427_readall_pwm(data, nr);
+       mode = data->pwm_enable[nr] & PWM_ENABLE_MODE_MASK;
+       if (mode != PWM_MODE_MANUAL && mode != PWM_MODE_OFF) {
+               dev_notice(dev, "Can't set PWM%d duty cycle while not in "
+                          "manual mode\n", nr + 1);
+               mutex_unlock(&data->lock);
+               return -EPERM;
+       }
+
+       /* We may have to change the mode */
+       if (mode == PWM_MODE_MANUAL && val == 0) {
+               /* Transition from Manual to Off */
+               update_pwm_enable(data, nr, PWM_MODE_OFF);
+               mode = PWM_MODE_OFF;
+               dev_dbg(dev, "Switching PWM%d from %s to %s\n", nr + 1,
+                       "manual", "off");
+       } else if (mode == PWM_MODE_OFF && val != 0) {
+               /* Transition from Off to Manual */
+               update_pwm_enable(data, nr, PWM_MODE_MANUAL);
+               mode = PWM_MODE_MANUAL;
+               dev_dbg(dev, "Switching PWM%d from %s to %s\n", nr + 1,
+                       "off", "manual");
+       }
+
+       data->pwm[nr] = val;
+       if (mode == PWM_MODE_MANUAL)
+               outb(val, iobase + PC87427_REG_PWM_DUTY);
+       mutex_unlock(&data->lock);
+
+       return count;
+}
+
+static SENSOR_DEVICE_ATTR(pwm1_enable, S_IWUSR | S_IRUGO,
+                         show_pwm_enable, set_pwm_enable, 0);
+static SENSOR_DEVICE_ATTR(pwm2_enable, S_IWUSR | S_IRUGO,
+                         show_pwm_enable, set_pwm_enable, 1);
+static SENSOR_DEVICE_ATTR(pwm3_enable, S_IWUSR | S_IRUGO,
+                         show_pwm_enable, set_pwm_enable, 2);
+static SENSOR_DEVICE_ATTR(pwm4_enable, S_IWUSR | S_IRUGO,
+                         show_pwm_enable, set_pwm_enable, 3);
+
+static SENSOR_DEVICE_ATTR(pwm1, S_IWUSR | S_IRUGO, show_pwm, set_pwm, 0);
+static SENSOR_DEVICE_ATTR(pwm2, S_IWUSR | S_IRUGO, show_pwm, set_pwm, 1);
+static SENSOR_DEVICE_ATTR(pwm3, S_IWUSR | S_IRUGO, show_pwm, set_pwm, 2);
+static SENSOR_DEVICE_ATTR(pwm4, S_IWUSR | S_IRUGO, show_pwm, set_pwm, 3);
+
+static struct attribute *pc87427_attributes_pwm[4][3] = {
+       {
+               &sensor_dev_attr_pwm1_enable.dev_attr.attr,
+               &sensor_dev_attr_pwm1.dev_attr.attr,
+               NULL
+       }, {
+               &sensor_dev_attr_pwm2_enable.dev_attr.attr,
+               &sensor_dev_attr_pwm2.dev_attr.attr,
+               NULL
+       }, {
+               &sensor_dev_attr_pwm3_enable.dev_attr.attr,
+               &sensor_dev_attr_pwm3.dev_attr.attr,
+               NULL
+       }, {
+               &sensor_dev_attr_pwm4_enable.dev_attr.attr,
+               &sensor_dev_attr_pwm4.dev_attr.attr,
+               NULL
+       }
+};
+
+static const struct attribute_group pc87427_group_pwm[4] = {
+       { .attrs = pc87427_attributes_pwm[0] },
+       { .attrs = pc87427_attributes_pwm[1] },
+       { .attrs = pc87427_attributes_pwm[2] },
+       { .attrs = pc87427_attributes_pwm[3] },
+};
+
 static ssize_t show_name(struct device *dev, struct device_attribute
                         *devattr, char *buf)
 {
@@ -431,6 +641,25 @@ static void __devinit pc87427_init_device(struct device *dev)
                }
                data->fan_enabled = sio_data->has_fanin;
        }
+
+       /* Check which PWM outputs are enabled */
+       for (i = 0; i < 4; i++) {
+               if (!(sio_data->has_fanout & (1 << i))) /* Not wired */
+                       continue;
+               reg = pc87427_read8_bank(data, LD_FAN, BANK_FC(i),
+                                        PC87427_REG_PWM_ENABLE);
+               if (reg & PWM_ENABLE_CTLEN)
+                       data->pwm_enabled |= (1 << i);
+
+               /* We don't expose an interface to reconfigure the automatic
+                  fan control mode, so only allow to return to this mode if
+                  it was originally set. */
+               if ((reg & PWM_ENABLE_MODE_MASK) == PWM_MODE_AUTO) {
+                       dev_dbg(dev, "PWM%d is in automatic control mode\n",
+                               i + 1);
+                       data->pwm_auto_ok |= (1 << i);
+               }
+       }
 }
 
 static int __devinit pc87427_probe(struct platform_device *pdev)
@@ -474,6 +703,14 @@ static int __devinit pc87427_probe(struct platform_device *pdev)
                if (err)
                        goto exit_remove_files;
        }
+       for (i = 0; i < 4; i++) {
+               if (!(data->pwm_enabled & (1 << i)))
+                       continue;
+               err = sysfs_create_group(&pdev->dev.kobj,
+                                        &pc87427_group_pwm[i]);
+               if (err)
+                       goto exit_remove_files;
+       }
 
        data->hwmon_dev = hwmon_device_register(&pdev->dev);
        if (IS_ERR(data->hwmon_dev)) {
@@ -490,6 +727,11 @@ exit_remove_files:
                        continue;
                sysfs_remove_group(&pdev->dev.kobj, &pc87427_group_fan[i]);
        }
+       for (i = 0; i < 4; i++) {
+               if (!(data->pwm_enabled & (1 << i)))
+                       continue;
+               sysfs_remove_group(&pdev->dev.kobj, &pc87427_group_pwm[i]);
+       }
 exit_release_region:
        release_region(res->start, resource_size(res));
 exit_kfree:
@@ -512,6 +754,11 @@ static int __devexit pc87427_remove(struct platform_device *pdev)
                        continue;
                sysfs_remove_group(&pdev->dev.kobj, &pc87427_group_fan[i]);
        }
+       for (i = 0; i < 4; i++) {
+               if (!(data->pwm_enabled & (1 << i)))
+                       continue;
+               sysfs_remove_group(&pdev->dev.kobj, &pc87427_group_pwm[i]);
+       }
        platform_set_drvdata(pdev, NULL);
        kfree(data);
 
@@ -648,6 +895,26 @@ static int __init pc87427_find(int sioaddr, unsigned short *address,
        if ((cfg & (1 << 3)) && !(cfg_b & (1 << 5)))
                sio_data->has_fanin |= (1 << 6);        /* FANIN6 */
 
+       /* Check which fan outputs are wired */
+       sio_data->has_fanout = (1 << 0);                /* FANOUT0 */
+       if (cfg_b & (1 << 0))
+               sio_data->has_fanout |= (1 << 3);       /* FANOUT3 */
+
+       cfg = superio_inb(sioaddr, SIOREG_CFC);
+       if (!(cfg & (1 << 4))) {
+               if (cfg_b & (1 << 1))
+                       sio_data->has_fanout |= (1 << 1); /* FANOUT1 */
+               if (cfg_b & (1 << 2))
+                       sio_data->has_fanout |= (1 << 2); /* FANOUT2 */
+       }
+
+       /* FANOUT1 and FANOUT2 can each be routed to 2 different pins */
+       cfg = superio_inb(sioaddr, SIOREG_CF5);
+       if (cfg & (1 << 6))
+               sio_data->has_fanout |= (1 << 1);       /* FANOUT1 */
+       if (cfg & (1 << 5))
+               sio_data->has_fanout |= (1 << 2);       /* FANOUT2 */
+
 exit:
        superio_exit(sioaddr);
        return err;