aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/gpio/gpio-w2sg0004.c
blob: 7f6517c70564a7a7abdf1b70a6f5385365b5c943 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
 * gpio-w2sg0004
 * Virtual GPIO of controlling the w2sg0004 GPS receiver.
 *
 * This receiver has an ON/OFF pin which must be toggled to
 * turn the device 'on' of 'off'.  A high->low->high toggle
 * will switch the device on if it is off, and off if it is on.
 * It is not possible to directly detect the state of the device.
 * However when it is on it will send characters on a UART line
 * regularly.
 * On the OMAP3, the UART line can also be programmed as a GPIO
 * on which we can receive interrupts.
 * So when we want the device to be 'off' we can reprogram
 * the line, toggle the ON/OFF pin and hope that it is off.
 * However if an interrupt arrives we know that it is really on
 * and can toggle again.
 *
 * To enable receiving on/off requests we create a gpio_chip
 * with a single 'output' GPIO.  When it is low, the
 * GPS is turned off.  When it is high, it is turned on.
 * This can be configured as the DTR GPIO on the UART which
 * connects the GPS.  Then whenever the tty is open, the GPS
 * will be switched on, and whenever it is closed, the GPS will
 * be switched off.
 *
 */

#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/gpio-w2sg0004.h>
#include <linux/workqueue.h>

/*
 * There seems to restrictions on how quickly we can toggle the
 * on/off line.  data sheets says "two rtc ticks", whatever that means.
 * If we do it too soon it doesn't work.
 * So we have a state machine which uses the common work queue to ensure
 * clean transitions.
 * When a change is requested we record that request and only act on it
 * once the previous change has completed.
 * A change involves a 10ms low pulse, and a 990ms raised level, so only
 * one change per second.
 */

struct gpio_w2sg {
	int		is_on;
	unsigned long	last_toggle;
	unsigned long	backoff;	/* time to wait since last_toggle */
	int		on_off_gpio;
	int		rx_gpio;
	int		rx_irq;

	u16		on_state;  /* Mux state when GPS is on */
	u16		off_state; /* Mux state when GPS is off */

	enum {idle, down, up} state;
	int		requested;
	int		suspended;
	int		rx_redirected;
	spinlock_t	lock;
	struct gpio_chip gpio;
	struct delayed_work work;
};

void omap_mux_set_gpio(u16 val, int gpio);
static void toggle_work(struct work_struct *work)
{
	struct gpio_w2sg *gw2sg = container_of(
		work, struct gpio_w2sg, work.work);
	switch (gw2sg->state) {
	case up:
		gw2sg->state = idle;
		printk("GPS idle\n");
	case idle:
		spin_lock_irq(&gw2sg->lock);
		if (gw2sg->requested == gw2sg->is_on) {
			if (!gw2sg->is_on && !gw2sg->rx_redirected) {
				gw2sg->rx_redirected = 1;
				omap_mux_set_gpio(gw2sg->off_state,
						  gw2sg->rx_gpio);
				enable_irq(gw2sg->rx_irq);
			}
			spin_unlock_irq(&gw2sg->lock);
			return;
		}
		spin_unlock_irq(&gw2sg->lock);
		gpio_set_value_cansleep(gw2sg->on_off_gpio, 0);
		printk("GPS down\n");
		gw2sg->state = down;
		schedule_delayed_work(&gw2sg->work,
				      msecs_to_jiffies(10));
		break;
	case down:
		gpio_set_value_cansleep(gw2sg->on_off_gpio, 1);
		gw2sg->state = up;
		gw2sg->last_toggle = jiffies;
		printk("GPS up\n");
		gw2sg->is_on = !gw2sg->is_on;
		schedule_delayed_work(&gw2sg->work,
				      msecs_to_jiffies(10));
		break;
	}
}

static irqreturn_t gpio_w2sg_isr(int irq, void *dev_id)
{
	struct gpio_w2sg *gw2sg = dev_id;
	unsigned long flags;
	printk("!");
	if (!gw2sg->requested &&
	    !gw2sg->is_on &&
	    gw2sg->state == idle &&
	    time_after(jiffies,
		       gw2sg->last_toggle + gw2sg->backoff)) {
		/* Should be off by now, time to toggle again */
		gw2sg->is_on = 1;
		gw2sg->backoff *= 2;
		spin_lock_irqsave(&gw2sg->lock, flags);
		if (!gw2sg->suspended)
			schedule_delayed_work(&gw2sg->work, 0);
		spin_unlock_irqrestore(&gw2sg->lock, flags);
	}
	return IRQ_HANDLED;
}

static void gpio_w2sg_set_value(struct gpio_chip *gc,
				unsigned offset, int val)
{
	unsigned long flags;
	struct gpio_w2sg *gw2sg = container_of(gc, struct gpio_w2sg,
					       gpio);
	printk("GPS SET to %d\n", val);
	spin_lock_irqsave(&gw2sg->lock, flags);
	if (val && !gw2sg->requested) {
		if (gw2sg->rx_redirected) {
			gw2sg->rx_redirected = 0;
			disable_irq(gw2sg->rx_irq);
			omap_mux_set_gpio(gw2sg->on_state, gw2sg->rx_gpio);
		}
		gw2sg->requested = 1;
	} else if (!val && gw2sg->requested) {
		gw2sg->backoff = HZ;
		gw2sg->requested = 0;
	} else
		goto unlock;
	if (!gw2sg->suspended)
		schedule_delayed_work(&gw2sg->work, 0);
unlock:
	spin_unlock_irqrestore(&gw2sg->lock, flags);
}

static int gpio_w2sg_direction_output(struct gpio_chip *gc,
				     unsigned offset, int val)
{
	gpio_w2sg_set_value(gc, offset, val);
	return 0;
}

static int gpio_w2sg_probe(struct platform_device *pdev)
{
	struct gpio_w2sg_data *pdata = pdev->dev.platform_data;
	struct gpio_w2sg *gw2sg;
	int err;

	gw2sg = kzalloc(sizeof(*gw2sg), GFP_KERNEL);
	if (gw2sg == NULL)
		return -ENOMEM;
	gw2sg->on_off_gpio = pdata->on_off_gpio;
	gw2sg->rx_gpio = pdata->rx_gpio;
	gw2sg->on_state = pdata->on_state;
	gw2sg->off_state = pdata->off_state;

	gw2sg->is_on = 0;
	gw2sg->requested = 1;
	gw2sg->state = idle;
	gw2sg->last_toggle = jiffies;
	gw2sg->backoff = HZ;

	gw2sg->gpio.label = "gpio-w2sg0004";
	gw2sg->gpio.ngpio = 1;
	gw2sg->gpio.base = pdata->ctrl_gpio;
	gw2sg->gpio.owner = THIS_MODULE;
	gw2sg->gpio.direction_output = gpio_w2sg_direction_output;
	gw2sg->gpio.set = gpio_w2sg_set_value;
	gw2sg->gpio.can_sleep = 0;
	INIT_DELAYED_WORK(&gw2sg->work, toggle_work);
	spin_lock_init(&gw2sg->lock);

	err = gpio_request(gw2sg->on_off_gpio, "gpio-w2sg0004-on-off");
	if (err < 0)
		goto out;
	gpio_direction_output(gw2sg->on_off_gpio, false);

	err = gpio_request(gw2sg->rx_gpio, "gpio-w2sg0004-rx");
	if (err < 0)
		goto out1;
	gpio_direction_input(gw2sg->rx_gpio);

	gw2sg->rx_irq = gpio_to_irq(gw2sg->rx_gpio);
	if (gw2sg->rx_irq < 0)
		goto out2;

	err = request_threaded_irq(gw2sg->rx_irq, NULL, gpio_w2sg_isr,
				   IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
				   "gpio-w2sg0004-rx",
				   gw2sg);
	if (err < 0) {
		dev_err(&pdev->dev, "Unable to claim irq %d; error %d\n",
			gw2sg->rx_irq, err);
		goto out2;
	}
	disable_irq(gw2sg->rx_irq);
	err = gpiochip_add(&gw2sg->gpio);
	if (err)
		goto out3;
	platform_set_drvdata(pdev, gw2sg);
	return 0;

out3:
	free_irq(gw2sg->rx_irq, gw2sg);
out2:
	gpio_free(gw2sg->rx_gpio);
out1:
	gpio_free(gw2sg->on_off_gpio);
out:
	kfree(gw2sg);
	return err;
}

static int gpio_w2sg_remove(struct platform_device *pdev)
{
	struct gpio_w2sg *gw2sg = platform_get_drvdata(pdev);
	int ret;

	cancel_delayed_work_sync(&gw2sg->work);
	ret = gpiochip_remove(&gw2sg->gpio);
	if (ret)
		return ret;
	free_irq(gw2sg->rx_irq, gw2sg);
	gpio_free(gw2sg->rx_gpio);
	gpio_free(gw2sg->on_off_gpio);
	kfree(gw2sg);
	return 0;
}

static int gpio_w2sg_suspend(struct device *dev)
{
	/* Ignore the GPIO and just turn device off.
	 * we cannot really wait for a separate thread to
	 * do things, so we disable that and do it all
	 * here
	 */
	struct gpio_w2sg *gw2sg = dev_get_drvdata(dev);

	spin_lock_irq(&gw2sg->lock);
	gw2sg->suspended = 1;
	spin_unlock_irq(&gw2sg->lock);

	cancel_delayed_work_sync(&gw2sg->work);
	if (gw2sg->state == down) {
		msleep(10);
		gpio_set_value_cansleep(gw2sg->on_off_gpio, 1);
		gw2sg->last_toggle = jiffies;
		gw2sg->is_on = !gw2sg->is_on;
		gw2sg->state = up;
	}
	if (gw2sg->state == up) {
		msleep(10);
		gw2sg->state = idle;
	}
	if (gw2sg->is_on) {
		printk("GPS off for suspend %d %d\n", gw2sg->requested, gw2sg->is_on);
		gpio_set_value_cansleep(gw2sg->on_off_gpio, 0);
		msleep(10);
		gpio_set_value_cansleep(gw2sg->on_off_gpio, 1);
		gw2sg->is_on = 0;
	}
	return 0;
}

static int gpio_w2sg_resume(struct device *dev)
{
	struct gpio_w2sg *gw2sg = dev_get_drvdata(dev);

	spin_lock_irq(&gw2sg->lock);
	gw2sg->suspended = 0;
	spin_unlock_irq(&gw2sg->lock);
	printk("GPS resuming %d %d\n", gw2sg->requested, gw2sg->is_on);
	schedule_delayed_work(&gw2sg->work, 0);
	return 0;
}

SIMPLE_DEV_PM_OPS(w2sg_pm_ops, gpio_w2sg_suspend, gpio_w2sg_resume);

static struct platform_driver gpio_w2sg_driver = {
	.driver.name	= "w2s-gpio",
	.driver.owner	= THIS_MODULE,
	.driver.pm	= &w2sg_pm_ops,
	.probe		= gpio_w2sg_probe,
	.remove		= gpio_w2sg_remove,
};

static int __init gpio_w2sg_init(void)
{
	return platform_driver_register(&gpio_w2sg_driver);
}
module_init(gpio_w2sg_init);

static void __exit gpio_w2sg_exit(void)
{
	platform_driver_unregister(&gpio_w2sg_driver);
}
module_exit(gpio_w2sg_exit);

MODULE_AUTHOR("NeilBrown <neilb@suse.de>");
MODULE_DESCRIPTION("w2sg0004 GPS virtual GPIO driver");
MODULE_LICENSE("GPL v2");