|
@@ -15,10 +15,10 @@
|
|
|
* 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 */
|
|
@@ -187,6 +195,61 @@ static inline u16 fan_to_reg(unsigned long val)
|
|
|
return ((1350000UL + val / 2) / val) << 2;
|
|
|
}
|
|
|
|
|
|
+/*
|
|
|
+ * 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;
|