1/* $NetBSD: fcu.c,v 1.5 2022/04/08 10:17:53 andvar Exp $ */
2
3/*-
4 * Copyright (c) 2018 Michael Lorenz
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 *    notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 *    notice, this list of conditions and the following disclaimer in the
14 *    documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
17 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
18 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include <sys/cdefs.h>
30__KERNEL_RCSID(0, "$NetBSD: fcu.c,v 1.5 2022/04/08 10:17:53 andvar Exp $");
31
32#include <sys/param.h>
33#include <sys/systm.h>
34#include <sys/device.h>
35#include <sys/conf.h>
36#include <sys/bus.h>
37#include <sys/kthread.h>
38#include <sys/sysctl.h>
39
40#include <dev/i2c/i2cvar.h>
41
42#include <dev/sysmon/sysmonvar.h>
43
44#include <dev/ofw/openfirm.h>
45
46#include <macppc/dev/fancontrolvar.h>
47
48//#define FCU_DEBUG
49#ifdef FCU_DEBUG
50#define DPRINTF printf
51#else
52#define DPRINTF if (0) printf
53#endif
54
55/* FCU registers, from OpenBSD's fcu.c */
56#define FCU_FAN_FAIL	0x0b		/* fans states in bits 0<1-6>7 */
57#define FCU_FAN_ACTIVE	0x0d
58#define FCU_FANREAD(x)	0x11 + (x)*2
59#define FCU_FANSET(x)	0x10 + (x)*2
60#define FCU_PWM_FAIL	0x2b
61#define FCU_PWM_ACTIVE	0x2d
62#define FCU_PWMREAD(x)	0x30 + (x)*2
63
64
65typedef struct _fcu_fan {
66	int target;
67	int reg;
68	int base_rpm, max_rpm;
69	int step;
70	int duty;	/* for pwm fans */
71} fcu_fan_t;
72
73#define FCU_ZONE_CPU		0
74#define FCU_ZONE_CASE		1
75#define FCU_ZONE_DRIVEBAY	2
76#define FCU_ZONE_COUNT		3
77
78struct fcu_softc {
79	device_t	sc_dev;
80	i2c_tag_t	sc_i2c;
81	i2c_addr_t	sc_addr;
82	struct sysctlnode 	*sc_sysctl_me;
83	struct sysmon_envsys	*sc_sme;
84	envsys_data_t		sc_sensors[32];
85	int			sc_nsensors;
86	fancontrol_zone_t	sc_zones[FCU_ZONE_COUNT];
87	fcu_fan_t		sc_fans[FANCONTROL_MAX_FANS];
88	int			sc_nfans;
89	lwp_t			*sc_thread;
90	bool			sc_dying, sc_pwm;
91	uint8_t			sc_eeprom0[160];
92	uint8_t			sc_eeprom1[160];
93};
94
95static int	fcu_match(device_t, cfdata_t, void *);
96static void	fcu_attach(device_t, device_t, void *);
97
98static void	fcu_sensors_refresh(struct sysmon_envsys *, envsys_data_t *);
99
100static bool is_cpu(const envsys_data_t *);
101static bool is_case(const envsys_data_t *);
102static bool is_drive(const envsys_data_t *);
103
104static int fcu_set_rpm(void *, int, int);
105static int fcu_get_rpm(void *, int);
106static void fcu_adjust(void *);
107
108CFATTACH_DECL_NEW(fcu, sizeof(struct fcu_softc),
109    fcu_match, fcu_attach, NULL, NULL);
110
111static const struct device_compatible_entry compat_data[] = {
112	{ .compat = "fcu" },
113	DEVICE_COMPAT_EOL
114};
115
116static int
117fcu_match(device_t parent, cfdata_t match, void *aux)
118{
119	struct i2c_attach_args *ia = aux;
120	int match_result;
121
122	if (iic_use_direct_match(ia, match, compat_data, &match_result))
123		return match_result;
124
125	if (ia->ia_addr == 0x2f)
126		return I2C_MATCH_ADDRESS_ONLY;
127
128	return 0;
129}
130
131static void
132fcu_attach(device_t parent, device_t self, void *aux)
133{
134	struct fcu_softc *sc = device_private(self);
135	struct i2c_attach_args *ia = aux;
136	int have_eeprom1 = 1, i;
137
138	sc->sc_dev = self;
139	sc->sc_i2c = ia->ia_tag;
140	sc->sc_addr = ia->ia_addr;
141
142	aprint_naive("\n");
143	aprint_normal(": Fan Control Unit\n");
144
145	sysctl_createv(NULL, 0, NULL, (void *) &sc->sc_sysctl_me,
146	    CTLFLAG_READWRITE,
147	    CTLTYPE_NODE, device_xname(sc->sc_dev), NULL,
148	    NULL, 0, NULL, 0,
149	    CTL_MACHDEP, CTL_CREATE, CTL_EOL);
150
151	if (get_cpuid(0, sc->sc_eeprom0) < 160) {
152		/*
153		 * XXX this should never happen, we depend on the EEPROM for
154		 * calibration data to make sense of temperature and voltage
155		 * sensors elsewhere, and fan parameters here.
156		 */
157		aprint_error_dev(self, "no EEPROM data for CPU 0\n");
158		return;
159	}
160	if (get_cpuid(1, sc->sc_eeprom1) < 160)
161		have_eeprom1 = 0;
162
163	/* init zones */
164	sc->sc_zones[FCU_ZONE_CPU].name = "CPUs";
165	sc->sc_zones[FCU_ZONE_CPU].filter = is_cpu;
166	sc->sc_zones[FCU_ZONE_CPU].cookie = sc;
167	sc->sc_zones[FCU_ZONE_CPU].get_rpm = fcu_get_rpm;
168	sc->sc_zones[FCU_ZONE_CPU].set_rpm = fcu_set_rpm;
169	sc->sc_zones[FCU_ZONE_CPU].Tmin = 50;
170	sc->sc_zones[FCU_ZONE_CPU].Tmax = 85;
171	sc->sc_zones[FCU_ZONE_CPU].nfans = 0;
172	sc->sc_zones[FCU_ZONE_CASE].name = "Slots";
173	sc->sc_zones[FCU_ZONE_CASE].filter = is_case;
174	sc->sc_zones[FCU_ZONE_CASE].cookie = sc;
175	sc->sc_zones[FCU_ZONE_CASE].Tmin = 50;
176	sc->sc_zones[FCU_ZONE_CASE].Tmax = 75;
177	sc->sc_zones[FCU_ZONE_CASE].nfans = 0;
178	sc->sc_zones[FCU_ZONE_CASE].get_rpm = fcu_get_rpm;
179	sc->sc_zones[FCU_ZONE_CASE].set_rpm = fcu_set_rpm;
180	sc->sc_zones[FCU_ZONE_DRIVEBAY].name = "Drivebays";
181	sc->sc_zones[FCU_ZONE_DRIVEBAY].filter = is_drive;
182	sc->sc_zones[FCU_ZONE_DRIVEBAY].cookie = sc;
183	sc->sc_zones[FCU_ZONE_DRIVEBAY].get_rpm = fcu_get_rpm;
184	sc->sc_zones[FCU_ZONE_DRIVEBAY].set_rpm = fcu_set_rpm;
185	sc->sc_zones[FCU_ZONE_DRIVEBAY].Tmin = 30;
186	sc->sc_zones[FCU_ZONE_DRIVEBAY].Tmax = 50;
187	sc->sc_zones[FCU_ZONE_DRIVEBAY].nfans = 0;
188
189	sc->sc_sme = sysmon_envsys_create();
190	sc->sc_sme->sme_name = device_xname(self);
191	sc->sc_sme->sme_cookie = sc;
192	sc->sc_sme->sme_refresh = fcu_sensors_refresh;
193
194	sc->sc_sensors[0].units = ENVSYS_SFANRPM;
195	sc->sc_sensors[1].state = ENVSYS_SINVALID;
196	sc->sc_nfans = 0;
197
198	/* round up sensors */
199	int ch;
200
201	sc->sc_nsensors = 0;
202	ch = OF_child(ia->ia_cookie);
203	while (ch != 0) {
204		char type[32], descr[32];
205		uint32_t reg;
206
207		envsys_data_t *s = &sc->sc_sensors[sc->sc_nsensors];
208
209		s->state = ENVSYS_SINVALID;
210
211		if (OF_getprop(ch, "device_type", type, 32) <= 0)
212			goto next;
213
214		if (strcmp(type, "fan-rpm-control") == 0) {
215			s->units = ENVSYS_SFANRPM;
216		} else if (strcmp(type, "fan-pwm-control") == 0) {
217			/* XXX we get the type from the register number */
218			s->units = ENVSYS_SFANRPM;
219/* skip those for now since we don't really know how to interpret them */
220#if 0
221		} else if (strcmp(type, "power-sensor") == 0) {
222			s->units = ENVSYS_SVOLTS_DC;
223#endif
224		} else if (strcmp(type, "gpi-sensor") == 0) {
225			s->units = ENVSYS_INDICATOR;
226		} else {
227			/* ignore other types for now */
228			goto next;
229		}
230
231		if (OF_getprop(ch, "reg", &reg, sizeof(reg)) <= 0)
232			goto next;
233		s->private = reg;
234
235		if (OF_getprop(ch, "location", descr, 32) <= 0)
236			goto next;
237		strcpy(s->desc, descr);
238
239		if (s->units == ENVSYS_SFANRPM) {
240			fcu_fan_t *fan = &sc->sc_fans[sc->sc_nfans];
241			uint8_t *eeprom = NULL;
242			uint16_t rmin, rmax;
243
244			if (strstr(descr, "CPU A") != NULL)
245				eeprom = sc->sc_eeprom0;
246			if (strstr(descr, "CPU B") != NULL) {
247				/*
248				 * XXX
249				 * this should never happen
250				 */
251				if (have_eeprom1 == 0) {
252					eeprom = sc->sc_eeprom0;
253				} else
254					eeprom = sc->sc_eeprom1;
255			}
256
257			fan->reg = reg;
258			fan->target = 0;
259			fan->duty = 0x80;
260
261			/* speed settings from EEPROM */
262			if (strstr(descr, "PUMP") != NULL) {
263				KASSERT(eeprom != NULL);
264				memcpy(&rmin, &eeprom[0x54], 2);
265				memcpy(&rmax, &eeprom[0x56], 2);
266				fan->base_rpm = rmin;
267				fan->max_rpm = rmax;
268				fan->step = (rmax - rmin) / 30;
269			} else if (strstr(descr, "INTAKE") != NULL) {
270				KASSERT(eeprom != NULL);
271				memcpy(&rmin, &eeprom[0x4c], 2);
272				memcpy(&rmax, &eeprom[0x4e], 2);
273				fan->base_rpm = rmin;
274				fan->max_rpm = rmax;
275				fan->step = (rmax - rmin) / 30;
276			} else if (strstr(descr, "EXHAUST") != NULL) {
277				KASSERT(eeprom != NULL);
278				memcpy(&rmin, &eeprom[0x50], 2);
279				memcpy(&rmax, &eeprom[0x52], 2);
280				fan->base_rpm = rmin;
281				fan->max_rpm = rmax;
282				fan->step = (rmax - rmin) / 30;
283			} else if (strstr(descr, "DRIVE") != NULL ) {
284				fan->base_rpm = 1000;
285				fan->max_rpm = 3000;
286				fan->step = 100;
287			} else {
288				fan->base_rpm = 1000;
289				fan->max_rpm = 3000;
290				fan->step = 100;
291			}
292			DPRINTF("fan %s: %d - %d rpm, step %d\n",
293			   descr, fan->base_rpm, fan->max_rpm, fan->step);
294
295			/* now stuff them into zones */
296			if (strstr(descr, "CPU") != NULL) {
297				fancontrol_zone_t *z = &sc->sc_zones[FCU_ZONE_CPU];
298				z->fans[z->nfans].num = sc->sc_nfans;
299				z->fans[z->nfans].min_rpm = fan->base_rpm;
300				z->fans[z->nfans].max_rpm = fan->max_rpm;
301				z->fans[z->nfans].name = s->desc;
302				z->nfans++;
303			} else if ((strstr(descr, "BACKSIDE") != NULL) ||
304				   (strstr(descr, "SLOT") != NULL))  {
305				fancontrol_zone_t *z = &sc->sc_zones[FCU_ZONE_CASE];
306				z->fans[z->nfans].num = sc->sc_nfans;
307				z->fans[z->nfans].min_rpm = fan->base_rpm;
308				z->fans[z->nfans].max_rpm = fan->max_rpm;
309				z->fans[z->nfans].name = s->desc;
310				z->nfans++;
311			} else if (strstr(descr, "DRIVE") != NULL) {
312				fancontrol_zone_t *z = &sc->sc_zones[FCU_ZONE_DRIVEBAY];
313				z->fans[z->nfans].num = sc->sc_nfans;
314				z->fans[z->nfans].min_rpm = fan->base_rpm;
315				z->fans[z->nfans].max_rpm = fan->max_rpm;
316				z->fans[z->nfans].name = s->desc;
317				z->nfans++;
318			}
319			sc->sc_nfans++;
320		}
321		sysmon_envsys_sensor_attach(sc->sc_sme, s);
322		sc->sc_nsensors++;
323next:
324		ch = OF_peer(ch);
325	}
326	sysmon_envsys_register(sc->sc_sme);
327
328	/* setup sysctls for our zones etc. */
329	for (i = 0; i < FCU_ZONE_COUNT; i++) {
330		fancontrol_init_zone(&sc->sc_zones[i], sc->sc_sysctl_me);
331	}
332
333	sc->sc_dying = FALSE;
334	kthread_create(PRI_NONE, 0, curcpu(), fcu_adjust, sc, &sc->sc_thread,
335	    "fan control");
336}
337
338static void
339fcu_sensors_refresh(struct sysmon_envsys *sme, envsys_data_t *edata)
340{
341	struct fcu_softc *sc = sme->sme_cookie;
342	uint8_t cmd;
343	uint16_t data = -1;
344	int error;
345
346	if (edata->units == ENVSYS_SFANRPM) {
347	    	cmd = edata->private + 1;
348	} else
349		cmd = edata->private;
350
351	/* fcu is a macppc only thing so we can safely assume big endian */
352	iic_acquire_bus(sc->sc_i2c, 0);
353	error = iic_exec(sc->sc_i2c, I2C_OP_READ_WITH_STOP,
354	    sc->sc_addr, &cmd, 1, &data, 2, 0);
355	iic_release_bus(sc->sc_i2c, 0);
356
357	if (error) {
358		edata->state = ENVSYS_SINVALID;
359		return;
360	}
361
362	edata->state = ENVSYS_SVALID;
363
364	switch (edata->units) {
365		case ENVSYS_SFANRPM:
366			edata->value_cur = data >> 3;
367			break;
368		case ENVSYS_SVOLTS_DC:
369			/* XXX this reads bogus */
370			edata->value_cur = data * 1000;
371			break;
372		case ENVSYS_INDICATOR:
373			/* guesswork for now */
374			edata->value_cur = data >> 8;
375			break;
376		default:
377			edata->state = ENVSYS_SINVALID;
378	}
379}
380
381static bool
382is_cpu(const envsys_data_t *edata)
383{
384	if (edata->units != ENVSYS_STEMP)
385		return false;
386	if (strstr(edata->desc, "CPU") != NULL)
387		return TRUE;
388	return false;
389}
390
391static bool
392is_case(const envsys_data_t *edata)
393{
394	if (edata->units != ENVSYS_STEMP)
395		return false;
396	if ((strstr(edata->desc, "MLB") != NULL) ||
397	    (strstr(edata->desc, "BACKSIDE") != NULL) ||
398	    (strstr(edata->desc, "U3") != NULL))
399		return TRUE;
400	return false;
401}
402
403static bool
404is_drive(const envsys_data_t *edata)
405{
406	if (edata->units != ENVSYS_STEMP)
407		return false;
408	if (strstr(edata->desc, "DRIVE") != NULL)
409		return TRUE;
410	return false;
411}
412
413static int
414fcu_get_rpm(void *cookie, int which)
415{
416	struct fcu_softc *sc = cookie;
417	fcu_fan_t *f = &sc->sc_fans[which];
418	int error;
419	uint16_t data;
420	uint8_t cmd;
421
422	iic_acquire_bus(sc->sc_i2c, 0);
423	cmd = f->reg + 1;
424	error = iic_exec(sc->sc_i2c, I2C_OP_READ_WITH_STOP,
425	    sc->sc_addr, &cmd, 1, &data, 2, 0);
426	iic_release_bus(sc->sc_i2c, 0);
427	if (error != 0) return -1;
428	data = data >> 3;
429	return data;
430}
431
432static int
433fcu_set_rpm(void *cookie, int which, int speed)
434{
435	struct fcu_softc *sc = cookie;
436	fcu_fan_t *f = &sc->sc_fans[which];
437	int error = 0;
438	uint8_t cmd;
439
440	if (speed > f->max_rpm) speed = f->max_rpm;
441	if (speed < f->base_rpm) speed = f->base_rpm;
442
443	if (f->reg < 0x30) {
444		uint16_t data;
445		/* simple rpm fan, just poke the register */
446
447		if (f->target == speed) return 0;
448		iic_acquire_bus(sc->sc_i2c, 0);
449		cmd = f->reg;
450		data = (speed << 3);
451		error = iic_exec(sc->sc_i2c, I2C_OP_WRITE_WITH_STOP,
452		    sc->sc_addr, &cmd, 1, &data, 2, 0);
453		iic_release_bus(sc->sc_i2c, 0);
454	} else {
455		int diff;
456		int nduty = f->duty;
457		int current_speed;
458		/* pwm fan, measure speed, then adjust duty cycle */
459		DPRINTF("pwm fan ");
460		current_speed = fcu_get_rpm(sc, which);
461		diff = current_speed - speed;
462		DPRINTF("d %d s %d t %d diff %d ", f->duty, current_speed, speed, diff);
463		if (diff > 100) {
464			nduty = uimax(20, nduty - 1);
465		}
466		if (diff < -100) {
467			nduty = uimin(0xd0, nduty + 1);
468		}
469		cmd = f->reg;
470		DPRINTF("%s nduty %d", __func__, nduty);
471		if (nduty != f->duty) {
472			uint8_t arg = nduty;
473			iic_acquire_bus(sc->sc_i2c, 0);
474			error = iic_exec(sc->sc_i2c, I2C_OP_WRITE_WITH_STOP,
475			    sc->sc_addr, &cmd, 1, &arg, 1, 0);
476			iic_release_bus(sc->sc_i2c, 0);
477			f->duty = nduty;
478			sc->sc_pwm = TRUE;
479
480		}
481		DPRINTF("ok\n");
482	}
483	if (error) printf("boo\n");
484	f->target = speed;
485	return 0;
486}
487
488static void
489fcu_adjust(void *cookie)
490{
491	struct fcu_softc *sc = cookie;
492	int i;
493	uint8_t cmd, data;
494
495	while (!sc->sc_dying) {
496		/* poke the FCU so we don't go 747 */
497		iic_acquire_bus(sc->sc_i2c, 0);
498		cmd = FCU_FAN_ACTIVE;
499		iic_exec(sc->sc_i2c, I2C_OP_READ_WITH_STOP,
500		    sc->sc_addr, &cmd, 1, &data, 1, 0);
501		iic_release_bus(sc->sc_i2c, 0);
502		sc->sc_pwm = FALSE;
503		for (i = 0; i < FCU_ZONE_COUNT; i++)
504			fancontrol_adjust_zone(&sc->sc_zones[i]);
505		/*
506		 * take a shorter nap if we're in the process of adjusting a
507		 * PWM fan, which relies on measuring speed and then changing
508		 * its duty cycle until we're reasonable close to the target
509		 * speed
510		 */
511		kpause("fanctrl", true, mstohz(sc->sc_pwm ? 1000 : 2000), NULL);
512	}
513	kthread_exit(0);
514}
515