xref: /netbsd-src/sys/dev/i2c/pcf8574.c (revision a8c74629f602faa0ccf8a463757d7baf858bbf3a)
1 /*-
2  * Copyright (c) 2020 The NetBSD Foundation, Inc.
3  * All rights reserved.
4  *
5  * This code is derived from software contributed to The NetBSD Foundation
6  * by Julian Coleman.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in the
15  *    documentation and/or other materials provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
18  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
21  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27  * POSSIBILITY OF SUCH DAMAGE.
28  */
29 
30 /*
31  * A driver for Philips Semiconductor (NXP) PCF8574/PCF857A GPIO's.
32  * Uses device properties to connect pins to the appropriate subsystem.
33  */
34 
35 #include <sys/cdefs.h>
36 __KERNEL_RCSID(0, "$NetBSD: pcf8574.c,v 1.2 2020/10/31 14:39:31 jdc Exp $");
37 
38 #include <sys/param.h>
39 #include <sys/systm.h>
40 #include <sys/device.h>
41 #include <sys/kernel.h>
42 
43 #include <dev/sysmon/sysmonvar.h>
44 
45 #include <dev/i2c/i2cvar.h>
46 #include <dev/led.h>
47 
48 #ifdef PCF8574_DEBUG
49 #define DPRINTF printf
50 #else
51 #define DPRINTF if (0) printf
52 #endif
53 
54 struct pcf8574_led {
55 	void *cookie;
56 	struct led_device *led;
57 	uint8_t mask, v_on, v_off;
58 };
59 
60 #define PCF8574_NPINS	8
61 struct pcf8574_softc {
62 	device_t	sc_dev;
63 	i2c_tag_t	sc_tag;
64 	i2c_addr_t	sc_addr;
65 	uint8_t		sc_state;
66 	uint8_t		sc_mask;
67 
68 	int		sc_nleds;
69 	struct pcf8574_led sc_leds[PCF8574_NPINS];
70 
71 	struct sysmon_envsys *sc_sme;
72 	envsys_data_t	sc_sensor[PCF8574_NPINS];
73 	int		sc_pin_sensor[PCF8574_NPINS];
74 	int		sc_pin_active[PCF8574_NPINS];
75 
76 #ifdef PCF8574_DEBUG
77 	callout_t	sc_timer;
78 #endif
79 };
80 
81 static int	pcf8574_match(device_t, cfdata_t, void *);
82 static void	pcf8574_attach(device_t, device_t, void *);
83 static int	pcf8574_detach(device_t, int);
84 
85 static int	pcf8574_read(struct pcf8574_softc *sc, uint8_t *val);
86 static int	pcf8574_write(struct pcf8574_softc *sc, uint8_t val);
87 static void	pcf8574_attach_led(
88 			struct pcf8574_softc *, char *, int, int, int);
89 void		pcf8574_refresh(struct sysmon_envsys *, envsys_data_t *);
90 int		pcf8574_get_led(void *);
91 void		pcf8574_set_led(void *, int);
92 
93 #ifdef PCF8574_DEBUG
94 static void pcf8574_timeout(void *);
95 #endif
96 
97 CFATTACH_DECL_NEW(pcf8574io, sizeof(struct pcf8574_softc),
98 	pcf8574_match, pcf8574_attach, pcf8574_detach, NULL);
99 
100 static const struct device_compatible_entry compat_data[] = {
101 	{ "i2c-pcf8574",		0 },
102 	{ NULL,				0 }
103 };
104 
105 static int
106 pcf8574_match(device_t parent, cfdata_t cf, void *aux)
107 {
108 	struct i2c_attach_args *ia = aux;
109 	int match_result;
110 
111 	if (iic_use_direct_match(ia, cf, compat_data, &match_result))
112 		return match_result;
113 
114 	/* We don't support indirect matches */
115 	return 0;
116 }
117 
118 static void
119 pcf8574_attach(device_t parent, device_t self, void *aux)
120 {
121 	struct pcf8574_softc *sc = device_private(self);
122 	struct i2c_attach_args *ia = aux;
123 	prop_dictionary_t dict = device_properties(self);
124 	prop_array_t pins;
125 	prop_dictionary_t pin;
126 	int i, num, def, envc = 0;
127 	char name[32];
128 	const char *nptr = NULL, *spptr;
129 	bool ok = TRUE, act, sysmon = FALSE;
130 
131 	sc->sc_tag = ia->ia_tag;
132 	sc->sc_addr = ia->ia_addr;
133 	sc->sc_dev = self;
134 
135 	/*
136 	 * The PCF8574 requires input pins to be written with the value 1,
137 	 * and then read.  Assume that all pins are input initially.
138 	 * We'll use the mask when we write, because we have to write a
139 	 * value for every pin, and we don't want to change input pins.
140 	 */
141 	sc->sc_mask = 0xff;
142 
143 	pcf8574_read(sc, &sc->sc_state);
144 
145 #ifdef PCF8574_DEBUG
146 	aprint_normal(": GPIO: state = 0x%02x\n", sc->sc_state);
147 
148 	callout_init(&sc->sc_timer, CALLOUT_MPSAFE);
149 	callout_reset(&sc->sc_timer, hz*30, pcf8574_timeout, sc);
150 #else
151 	aprint_normal(": GPIO\n");
152 #endif
153 
154 	pins = prop_dictionary_get(dict, "pins");
155 	if (pins == NULL)
156 		return;
157 
158 	for (i = 0; i < prop_array_count(pins); i++) {
159 		pin = prop_array_get(pins, i);
160 		ok &= prop_dictionary_get_cstring_nocopy(pin, "name", &nptr);
161 		ok &= prop_dictionary_get_uint32(pin, "pin", &num);
162 		ok &= prop_dictionary_get_bool(pin, "active_high", &act);
163 		/* optional default state */
164 		def = -1;
165 		prop_dictionary_get_int32(pin, "default_state", &def);
166 		if (!ok)
167 			continue;
168 		/* Extract pin type from the name */
169 		spptr = strstr(nptr, " ");
170 		if (spptr == NULL)
171 			continue;
172 		spptr += 1;
173 		strncpy(name, spptr, 31);
174 		sc->sc_pin_active[i] = act;
175 		if (!strncmp(nptr, "LED ", 4)) {
176 			sc->sc_mask &= ~(1 << num);
177 			pcf8574_attach_led(sc, name, num, act, def);
178 		}
179 		if (!strncmp(nptr, "INDICATOR ", 4)) {
180 			if (!sysmon) {
181 				sc->sc_sme = sysmon_envsys_create();
182 				sysmon = TRUE;
183 			}
184 			/* envsys sensor # to pin # mapping */
185 			sc->sc_pin_sensor[envc] = num;
186 			sc->sc_sensor[i].state = ENVSYS_SINVALID;
187 			sc->sc_sensor[i].units = ENVSYS_INDICATOR;
188 			strlcpy(sc->sc_sensor[i].desc, name,
189 			    sizeof(sc->sc_sensor[i].desc));
190 			if (sysmon_envsys_sensor_attach(sc->sc_sme,
191 			    &sc->sc_sensor[i])) {
192 				sysmon_envsys_destroy(sc->sc_sme);
193 				aprint_error_dev(self,
194 				    "unable to attach pin %d at sysmon\n", i);
195 				return;
196 			}
197 			DPRINTF("%s: added indicator: pin %d sensor %d (%s)\n",
198 			    device_xname(sc->sc_dev), num, envc, name);
199 			envc++;
200 		}
201 	}
202 
203 	if (sysmon) {
204 		sc->sc_sme->sme_name = device_xname(self);
205 		sc->sc_sme->sme_cookie = sc;
206 		sc->sc_sme->sme_refresh = pcf8574_refresh;
207 		if (sysmon_envsys_register(sc->sc_sme)) {
208 			aprint_error_dev(self,
209 			    "unable to register with sysmon\n");
210 			sysmon_envsys_destroy(sc->sc_sme);
211 			return;
212 		}
213 	}
214 }
215 
216 static int
217 pcf8574_detach(device_t self, int flags)
218 {
219 	struct pcf8574_softc *sc = device_private(self);
220 	int i;
221 
222 	if (sc->sc_sme != NULL)
223 		sysmon_envsys_unregister(sc->sc_sme);
224 
225 	for (i = 0; i < sc->sc_nleds; i++)
226 		led_detach(sc->sc_leds[i].led);
227 
228 #ifdef PCF8574_DEBUG
229 	callout_halt(&sc->sc_timer, NULL);
230 	callout_destroy(&sc->sc_timer);
231 #endif
232 	return 0;
233 }
234 
235 static int
236 pcf8574_read(struct pcf8574_softc *sc, uint8_t *val)
237 {
238 	int err = 0;
239 
240 	if ((err = iic_acquire_bus(sc->sc_tag, 0)) != 0)
241 		return err;
242 	err = iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP,
243 	    sc->sc_addr, NULL, 0, val, 1, 0);
244 	iic_release_bus(sc->sc_tag, 0);
245 	return err;
246 }
247 
248 static int
249 pcf8574_write(struct pcf8574_softc *sc, uint8_t val)
250 {
251 	int err = 0;
252 
253 	if ((err = iic_acquire_bus(sc->sc_tag, 0)) != 0)
254 		return err;
255 	err = iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr,
256 	    &val, 1, NULL, 0, 0);
257 	iic_release_bus(sc->sc_tag, 0);
258 	return err;
259 }
260 
261 static void
262 pcf8574_attach_led(struct pcf8574_softc *sc, char *n, int pin, int act, int def)
263 {
264 	struct pcf8574_led *l;
265 
266 	l = &sc->sc_leds[sc->sc_nleds];
267 	l->cookie = sc;
268 	l->mask = 1 << pin;
269 	l->v_on = act ? l->mask : 0;
270 	l->v_off = act ? 0 : l->mask;
271 	led_attach(n, l, pcf8574_get_led, pcf8574_set_led);
272 	if (def != -1)
273 		pcf8574_set_led(l, def);
274 	DPRINTF("%s: attached LED: %02x %02x %02x def %d\n",
275 	    device_xname(sc->sc_dev), l->mask, l->v_on, l->v_off, def);
276 	sc->sc_nleds++;
277 }
278 
279 void
280 pcf8574_refresh(struct sysmon_envsys *sme, envsys_data_t *edata)
281 {
282 	struct pcf8574_softc *sc = sme->sme_cookie;
283 	int pin = sc->sc_pin_sensor[edata->sensor];
284 	int act = sc->sc_pin_active[pin];
285 
286 	pcf8574_read(sc, &sc->sc_state);
287 	if (act)
288 		edata->value_cur = sc->sc_state & 1 << pin ? TRUE : FALSE;
289 	else
290 		edata->value_cur = sc->sc_state & 1 << pin ? FALSE : TRUE;
291 	edata->state = ENVSYS_SVALID;
292 }
293 
294 int
295 pcf8574_get_led(void *cookie)
296 {
297 	struct pcf8574_led *l = cookie;
298 	struct pcf8574_softc *sc = l->cookie;
299 
300 	return ((sc->sc_state & l->mask) == l->v_on);
301 }
302 
303 void
304 pcf8574_set_led(void *cookie, int val)
305 {
306 	struct pcf8574_led *l = cookie;
307 	struct pcf8574_softc *sc = l->cookie;
308 	uint32_t newstate;
309 
310 	newstate = sc->sc_state & ~l->mask;
311 	newstate |= val ? l->v_on : l->v_off;
312 	DPRINTF("%s: set LED: %02x -> %02x, %02x %02x %02x\n",
313 	    device_xname(sc->sc_dev), sc->sc_state, newstate, l->mask, l->v_on, l->v_off);
314 	if (newstate != sc->sc_state) {
315 		pcf8574_write(sc, newstate | sc->sc_mask);
316 		pcf8574_read(sc, &sc->sc_state);
317 	}
318 }
319 
320 #ifdef PCF8574_DEBUG
321 static void
322 pcf8574_timeout(void *v)
323 {
324 	struct pcf8574_softc *sc = v;
325 	uint8_t val;
326 
327 	pcf8574_read(sc, &val);
328 	if (val != sc->sc_state)
329 		aprint_normal_dev(sc->sc_dev,
330 		    "status change: 0x%02x > 0x%02x\n", sc->sc_state, val);
331 	sc->sc_state = val;
332 
333 	callout_reset(&sc->sc_timer, hz*60, pcf8574_timeout, sc);
334 }
335 #endif
336