summaryrefslogtreecommitdiffstats
path: root/drivers/input/serio/olpc_apsp.c
blob: 818aa466b5d2fb2378836b188d7d310dda6823f5 (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
/*
 * OLPC serio driver for multiplexed input from Marvell MMP security processor
 *
 * Copyright (C) 2011-2013 One Laptop Per Child
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/serio.h>
#include <linux/err.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/slab.h>
#include <linux/delay.h>

/*
 * The OLPC XO-1.75 and XO-4 laptops do not have a hardware PS/2 controller.
 * Instead, the OLPC firmware runs a bit-banging PS/2 implementation on an
 * otherwise-unused slow processor which is included in the Marvell MMP2/MMP3
 * SoC, known as the "Security Processor" (SP) or "Wireless Trusted Module"
 * (WTM). This firmware then reports its results via the WTM registers,
 * which we read from the Application Processor (AP, i.e. main CPU) in this
 * driver.
 *
 * On the hardware side we have a PS/2 mouse and an AT keyboard, the data
 * is multiplexed through this system. We create a serio port for each one,
 * and demultiplex the data accordingly.
 */

/* WTM register offsets */
#define SECURE_PROCESSOR_COMMAND	0x40
#define COMMAND_RETURN_STATUS		0x80
#define COMMAND_FIFO_STATUS		0xc4
#define PJ_RST_INTERRUPT		0xc8
#define PJ_INTERRUPT_MASK		0xcc

/*
 * The upper byte of SECURE_PROCESSOR_COMMAND and COMMAND_RETURN_STATUS is
 * used to identify which port (device) is being talked to. The lower byte
 * is the data being sent/received.
 */
#define PORT_MASK	0xff00
#define DATA_MASK	0x00ff
#define PORT_SHIFT	8
#define KEYBOARD_PORT	0
#define TOUCHPAD_PORT	1

/* COMMAND_FIFO_STATUS */
#define CMD_CNTR_MASK		0x7 /* Number of pending/unprocessed commands */
#define MAX_PENDING_CMDS	4   /* from device specs */

/* PJ_RST_INTERRUPT */
#define SP_COMMAND_COMPLETE_RESET	0x1

/* PJ_INTERRUPT_MASK */
#define INT_0	(1 << 0)

/* COMMAND_FIFO_STATUS */
#define CMD_STS_MASK	0x100

struct olpc_apsp {
	struct device *dev;
	struct serio *kbio;
	struct serio *padio;
	void __iomem *base;
	int open_count;
	int irq;
};

static int olpc_apsp_write(struct serio *port, unsigned char val)
{
	struct olpc_apsp *priv = port->port_data;
	unsigned int i;
	u32 which = 0;

	if (port == priv->padio)
		which = TOUCHPAD_PORT << PORT_SHIFT;
	else
		which = KEYBOARD_PORT << PORT_SHIFT;

	dev_dbg(priv->dev, "olpc_apsp_write which=%x val=%x\n", which, val);
	for (i = 0; i < 50; i++) {
		u32 sts = readl(priv->base + COMMAND_FIFO_STATUS);
		if ((sts & CMD_CNTR_MASK) < MAX_PENDING_CMDS) {
			writel(which | val,
			       priv->base + SECURE_PROCESSOR_COMMAND);
			return 0;
		}
		/* SP busy. This has not been seen in practice. */
		mdelay(1);
	}

	dev_dbg(priv->dev, "olpc_apsp_write timeout, status=%x\n",
		readl(priv->base + COMMAND_FIFO_STATUS));

	return -ETIMEDOUT;
}

static irqreturn_t olpc_apsp_rx(int irq, void *dev_id)
{
	struct olpc_apsp *priv = dev_id;
	unsigned int w, tmp;
	struct serio *serio;

	/*
	 * Write 1 to PJ_RST_INTERRUPT to acknowledge and clear the interrupt
	 * Write 0xff00 to SECURE_PROCESSOR_COMMAND.
	 */
	tmp = readl(priv->base + PJ_RST_INTERRUPT);
	if (!(tmp & SP_COMMAND_COMPLETE_RESET)) {
		dev_warn(priv->dev, "spurious interrupt?\n");
		return IRQ_NONE;
	}

	w = readl(priv->base + COMMAND_RETURN_STATUS);
	dev_dbg(priv->dev, "olpc_apsp_rx %x\n", w);

	if (w >> PORT_SHIFT == KEYBOARD_PORT)
		serio = priv->kbio;
	else
		serio = priv->padio;

	serio_interrupt(serio, w & DATA_MASK, 0);

	/* Ack and clear interrupt */
	writel(tmp | SP_COMMAND_COMPLETE_RESET, priv->base + PJ_RST_INTERRUPT);
	writel(PORT_MASK, priv->base + SECURE_PROCESSOR_COMMAND);

	pm_wakeup_event(priv->dev, 1000);
	return IRQ_HANDLED;
}

static int olpc_apsp_open(struct serio *port)
{
	struct olpc_apsp *priv = port->port_data;
	unsigned int tmp;

	if (priv->open_count++ == 0) {
		/* Enable interrupt 0 by clearing its bit */
		tmp = readl(priv->base + PJ_INTERRUPT_MASK);
		writel(tmp & ~INT_0, priv->base + PJ_INTERRUPT_MASK);
	}

	return 0;
}

static void olpc_apsp_close(struct serio *port)
{
	struct olpc_apsp *priv = port->port_data;
	unsigned int tmp;

	if (--priv->open_count == 0) {
		/* Disable interrupt 0 */
		tmp = readl(priv->base + PJ_INTERRUPT_MASK);
		writel(tmp | INT_0, priv->base + PJ_INTERRUPT_MASK);
	}
}

static int olpc_apsp_probe(struct platform_device *pdev)
{
	struct serio *kb_serio, *pad_serio;
	struct olpc_apsp *priv;
	struct resource *res;
	struct device_node *np;
	unsigned long l;
	int error;

	priv = devm_kzalloc(&pdev->dev, sizeof(struct olpc_apsp), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;

	np = pdev->dev.of_node;
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		return -ENOENT;

	priv->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(priv->base)) {
		dev_err(&pdev->dev, "Failed to map WTM registers\n");
		return PTR_ERR(priv->base);
	}

	priv->irq = platform_get_irq(pdev, 0);
	if (priv->irq < 0)
		return priv->irq;

	l = readl(priv->base + COMMAND_FIFO_STATUS);
	if (!(l & CMD_STS_MASK)) {
		dev_err(&pdev->dev, "SP cannot accept commands.\n");
		return -EIO;
	}

	/* KEYBOARD */
	kb_serio = kzalloc(sizeof(struct serio), GFP_KERNEL);
	if (!kb_serio)
		return -ENOMEM;
	kb_serio->id.type	= SERIO_8042_XL;
	kb_serio->write		= olpc_apsp_write;
	kb_serio->open		= olpc_apsp_open;
	kb_serio->close		= olpc_apsp_close;
	kb_serio->port_data	= priv;
	kb_serio->dev.parent	= &pdev->dev;
	strlcpy(kb_serio->name, "sp keyboard", sizeof(kb_serio->name));
	strlcpy(kb_serio->phys, "sp/serio0", sizeof(kb_serio->phys));
	priv->kbio		= kb_serio;
	serio_register_port(kb_serio);

	/* TOUCHPAD */
	pad_serio = kzalloc(sizeof(struct serio), GFP_KERNEL);
	if (!pad_serio) {
		error = -ENOMEM;
		goto err_pad;
	}
	pad_serio->id.type	= SERIO_8042;
	pad_serio->write	= olpc_apsp_write;
	pad_serio->open		= olpc_apsp_open;
	pad_serio->close	= olpc_apsp_close;
	pad_serio->port_data	= priv;
	pad_serio->dev.parent	= &pdev->dev;
	strlcpy(pad_serio->name, "sp touchpad", sizeof(pad_serio->name));
	strlcpy(pad_serio->phys, "sp/serio1", sizeof(pad_serio->phys));
	priv->padio		= pad_serio;
	serio_register_port(pad_serio);

	error = request_irq(priv->irq, olpc_apsp_rx, 0, "olpc-apsp", priv);
	if (error) {
		dev_err(&pdev->dev, "Failed to request IRQ\n");
		goto err_irq;
	}

	priv->dev = &pdev->dev;
	device_init_wakeup(priv->dev, 1);
	platform_set_drvdata(pdev, priv);

	dev_dbg(&pdev->dev, "probed successfully.\n");
	return 0;

err_irq:
	serio_unregister_port(pad_serio);
err_pad:
	serio_unregister_port(kb_serio);
	return error;
}

static int olpc_apsp_remove(struct platform_device *pdev)
{
	struct olpc_apsp *priv = platform_get_drvdata(pdev);

	free_irq(priv->irq, priv);

	serio_unregister_port(priv->kbio);
	serio_unregister_port(priv->padio);

	return 0;
}

static struct of_device_id olpc_apsp_dt_ids[] = {
	{ .compatible = "olpc,ap-sp", },
	{}
};
MODULE_DEVICE_TABLE(of, olpc_apsp_dt_ids);

static struct platform_driver olpc_apsp_driver = {
	.probe		= olpc_apsp_probe,
	.remove		= olpc_apsp_remove,
	.driver		= {
		.name	= "olpc-apsp",
		.owner	= THIS_MODULE,
		.of_match_table = olpc_apsp_dt_ids,
	},
};

MODULE_DESCRIPTION("OLPC AP-SP serio driver");
MODULE_LICENSE("GPL");
module_platform_driver(olpc_apsp_driver);