drm/nv50/pm: add support for pwm fan control

Signed-off-by: Ben Skeggs <bskeggs@redhat.com>
diff --git a/drivers/gpu/drm/nouveau/nouveau_pm.c b/drivers/gpu/drm/nouveau/nouveau_pm.c
index 607e496..ee2872e 100644
--- a/drivers/gpu/drm/nouveau/nouveau_pm.c
+++ b/drivers/gpu/drm/nouveau/nouveau_pm.c
@@ -167,8 +167,11 @@
 		}
 	}
 
-	if (pm->fanspeed_get)
-		perflvl->fanspeed = pm->fanspeed_get(dev);
+	if (pm->fanspeed_get) {
+		ret = pm->fanspeed_get(dev);
+		if (ret > 0)
+			perflvl->fanspeed = ret;
+	}
 
 	return 0;
 }
diff --git a/drivers/gpu/drm/nouveau/nouveau_pm.h b/drivers/gpu/drm/nouveau/nouveau_pm.h
index f19b050..1b0bcef 100644
--- a/drivers/gpu/drm/nouveau/nouveau_pm.h
+++ b/drivers/gpu/drm/nouveau/nouveau_pm.h
@@ -66,6 +66,8 @@
 void *nv50_pm_clock_pre(struct drm_device *, struct nouveau_pm_level *,
 			u32 id, int khz);
 void nv50_pm_clock_set(struct drm_device *, void *);
+int nv50_pm_fanspeed_get(struct drm_device *);
+int nv50_pm_fanspeed_set(struct drm_device *, int percent);
 
 /* nva3_pm.c */
 int nva3_pm_clocks_get(struct drm_device *, struct nouveau_pm_level *);
diff --git a/drivers/gpu/drm/nouveau/nouveau_state.c b/drivers/gpu/drm/nouveau/nouveau_state.c
index e7ba6e7..16195e9 100644
--- a/drivers/gpu/drm/nouveau/nouveau_state.c
+++ b/drivers/gpu/drm/nouveau/nouveau_state.c
@@ -386,6 +386,8 @@
 			engine->pm.temp_get	= nv84_temp_get;
 		else
 			engine->pm.temp_get	= nv40_temp_get;
+		engine->pm.fanspeed_get		= nv50_pm_fanspeed_get;
+		engine->pm.fanspeed_set		= nv50_pm_fanspeed_set;
 		engine->vram.init		= nv50_vram_init;
 		engine->vram.takedown		= nv50_vram_fini;
 		engine->vram.get		= nv50_vram_new;
@@ -441,6 +443,8 @@
 		engine->pm.clocks_get		= nvc0_pm_clocks_get;
 		engine->pm.voltage_get		= nouveau_voltage_gpio_get;
 		engine->pm.voltage_set		= nouveau_voltage_gpio_set;
+		engine->pm.fanspeed_get		= nv50_pm_fanspeed_get;
+		engine->pm.fanspeed_set		= nv50_pm_fanspeed_set;
 		break;
 	case 0xd0:
 		engine->instmem.init		= nvc0_instmem_init;
diff --git a/drivers/gpu/drm/nouveau/nv50_pm.c b/drivers/gpu/drm/nouveau/nv50_pm.c
index 3d5a86b..713c718 100644
--- a/drivers/gpu/drm/nouveau/nv50_pm.c
+++ b/drivers/gpu/drm/nouveau/nv50_pm.c
@@ -144,3 +144,77 @@
 	kfree(state);
 }
 
+struct pwm_info {
+	int id;
+	int invert;
+	u8  tag;
+	u32 ctrl;
+	int line;
+};
+
+static int
+nv50_pm_fanspeed_pwm(struct drm_device *dev, struct pwm_info *pwm)
+{
+	struct dcb_gpio_entry *gpio;
+
+	gpio = nouveau_bios_gpio_entry(dev, 0x09);
+	if (gpio) {
+		pwm->tag = gpio->tag;
+		pwm->id = (gpio->line == 9) ? 1 : 0;
+		pwm->invert = gpio->state[0] & 1;
+		pwm->ctrl = (gpio->line < 16) ? 0xe100 : 0xe28c;
+		pwm->line = (gpio->line & 0xf);
+		return 0;
+	}
+
+	return -ENOENT;
+}
+
+int
+nv50_pm_fanspeed_get(struct drm_device *dev)
+{
+	struct drm_nouveau_private *dev_priv = dev->dev_private;
+	struct nouveau_gpio_engine *pgpio = &dev_priv->engine.gpio;
+	struct pwm_info pwm;
+	int ret;
+
+	ret = nv50_pm_fanspeed_pwm(dev, &pwm);
+	if (ret)
+		return ret;
+
+	if (nv_rd32(dev, pwm.ctrl) & (0x00000001 << pwm.line)) {
+		u32 divs = nv_rd32(dev, 0x00e114 + (pwm.id * 8));
+		u32 duty = nv_rd32(dev, 0x00e118 + (pwm.id * 8));
+		if (divs) {
+			divs = max(divs, duty);
+			if (pwm.invert)
+				duty = divs - duty;
+			return (duty * 100) / divs;
+		}
+
+		return 0;
+	}
+
+	return pgpio->get(dev, pwm.tag) * 100;
+}
+
+int
+nv50_pm_fanspeed_set(struct drm_device *dev, int percent)
+{
+	struct pwm_info pwm;
+	u32 divs, duty;
+	int ret;
+
+	ret = nv50_pm_fanspeed_pwm(dev, &pwm);
+	if (ret)
+		return ret;
+
+	divs = nv_rd32(dev, 0x00e114 + (pwm.id * 8));
+	duty = ((divs * percent) + 99) / 100;
+	if (pwm.invert)
+		duty = divs - duty;
+
+	nv_mask(dev, pwm.ctrl, 0x00010001 << pwm.line, 0x00000001 << pwm.line);
+	nv_wr32(dev, 0x00e118 + (pwm.id * 8), 0x80000000 | duty);
+	return 0;
+}