summaryrefslogtreecommitdiff
path: root/drivers/input/serio/sun4i-ps2.c
blob: a681a2c04e399f07566df355558605316e5e3ec2 (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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
// SPDX-License-Identifier: GPL-2.0-only
/*
 *	Driver for Allwinner A10 PS2 host controller
 *
 *	Author: Vishnu Patekar <vishnupatekar0510@gmail.com>
 *		Aaron.maoye <leafy.myeh@newbietech.com>
 */

#include <linux/module.h>
#include <linux/serio.h>
#include <linux/interrupt.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/mod_devicetable.h>
#include <linux/platform_device.h>

#define DRIVER_NAME		"sun4i-ps2"

/* register offset definitions */
#define PS2_REG_GCTL		0x00	/* PS2 Module Global Control Reg */
#define PS2_REG_DATA		0x04	/* PS2 Module Data Reg		*/
#define PS2_REG_LCTL		0x08	/* PS2 Module Line Control Reg */
#define PS2_REG_LSTS		0x0C	/* PS2 Module Line Status Reg	*/
#define PS2_REG_FCTL		0x10	/* PS2 Module FIFO Control Reg */
#define PS2_REG_FSTS		0x14	/* PS2 Module FIFO Status Reg	*/
#define PS2_REG_CLKDR		0x18	/* PS2 Module Clock Divider Reg*/

/*  PS2 GLOBAL CONTROL REGISTER PS2_GCTL */
#define PS2_GCTL_INTFLAG	BIT(4)
#define PS2_GCTL_INTEN		BIT(3)
#define PS2_GCTL_RESET		BIT(2)
#define PS2_GCTL_MASTER		BIT(1)
#define PS2_GCTL_BUSEN		BIT(0)

/* PS2 LINE CONTROL REGISTER */
#define PS2_LCTL_NOACK		BIT(18)
#define PS2_LCTL_TXDTOEN	BIT(8)
#define PS2_LCTL_STOPERREN	BIT(3)
#define PS2_LCTL_ACKERREN	BIT(2)
#define PS2_LCTL_PARERREN	BIT(1)
#define PS2_LCTL_RXDTOEN	BIT(0)

/* PS2 LINE STATUS REGISTER */
#define PS2_LSTS_TXTDO		BIT(8)
#define PS2_LSTS_STOPERR	BIT(3)
#define PS2_LSTS_ACKERR		BIT(2)
#define PS2_LSTS_PARERR		BIT(1)
#define PS2_LSTS_RXTDO		BIT(0)

#define PS2_LINE_ERROR_BIT \
	(PS2_LSTS_TXTDO | PS2_LSTS_STOPERR | PS2_LSTS_ACKERR | \
	PS2_LSTS_PARERR | PS2_LSTS_RXTDO)

/* PS2 FIFO CONTROL REGISTER */
#define PS2_FCTL_TXRST		BIT(17)
#define PS2_FCTL_RXRST		BIT(16)
#define PS2_FCTL_TXUFIEN	BIT(10)
#define PS2_FCTL_TXOFIEN	BIT(9)
#define PS2_FCTL_TXRDYIEN	BIT(8)
#define PS2_FCTL_RXUFIEN	BIT(2)
#define PS2_FCTL_RXOFIEN	BIT(1)
#define PS2_FCTL_RXRDYIEN	BIT(0)

/* PS2 FIFO STATUS REGISTER */
#define PS2_FSTS_TXUF		BIT(10)
#define PS2_FSTS_TXOF		BIT(9)
#define PS2_FSTS_TXRDY		BIT(8)
#define PS2_FSTS_RXUF		BIT(2)
#define PS2_FSTS_RXOF		BIT(1)
#define PS2_FSTS_RXRDY		BIT(0)

#define PS2_FIFO_ERROR_BIT \
	(PS2_FSTS_TXUF | PS2_FSTS_TXOF | PS2_FSTS_RXUF | PS2_FSTS_RXOF)

#define PS2_SAMPLE_CLK		1000000
#define PS2_SCLK		125000

struct sun4i_ps2data {
	struct serio *serio;
	struct device *dev;

	/* IO mapping base */
	void __iomem	*reg_base;

	/* clock management */
	struct clk	*clk;

	/* irq */
	spinlock_t	lock;
	int		irq;
};

static irqreturn_t sun4i_ps2_interrupt(int irq, void *dev_id)
{
	struct sun4i_ps2data *drvdata = dev_id;
	u32 intr_status;
	u32 fifo_status;
	unsigned char byte;
	unsigned int rxflags = 0;
	u32 rval;

	spin_lock(&drvdata->lock);

	/* Get the PS/2 interrupts and clear them */
	intr_status  = readl(drvdata->reg_base + PS2_REG_LSTS);
	fifo_status  = readl(drvdata->reg_base + PS2_REG_FSTS);

	/* Check line status register */
	if (intr_status & PS2_LINE_ERROR_BIT) {
		rxflags = (intr_status & PS2_LINE_ERROR_BIT) ? SERIO_FRAME : 0;
		rxflags |= (intr_status & PS2_LSTS_PARERR) ? SERIO_PARITY : 0;
		rxflags |= (intr_status & PS2_LSTS_PARERR) ? SERIO_TIMEOUT : 0;

		rval = PS2_LSTS_TXTDO | PS2_LSTS_STOPERR | PS2_LSTS_ACKERR |
			PS2_LSTS_PARERR | PS2_LSTS_RXTDO;
		writel(rval, drvdata->reg_base + PS2_REG_LSTS);
	}

	/* Check FIFO status register */
	if (fifo_status & PS2_FIFO_ERROR_BIT) {
		rval = PS2_FSTS_TXUF | PS2_FSTS_TXOF | PS2_FSTS_TXRDY |
			PS2_FSTS_RXUF | PS2_FSTS_RXOF | PS2_FSTS_RXRDY;
		writel(rval, drvdata->reg_base + PS2_REG_FSTS);
	}

	rval = (fifo_status >> 16) & 0x3;
	while (rval--) {
		byte = readl(drvdata->reg_base + PS2_REG_DATA) & 0xff;
		serio_interrupt(drvdata->serio, byte, rxflags);
	}

	writel(intr_status, drvdata->reg_base + PS2_REG_LSTS);
	writel(fifo_status, drvdata->reg_base + PS2_REG_FSTS);

	spin_unlock(&drvdata->lock);

	return IRQ_HANDLED;
}

static int sun4i_ps2_open(struct serio *serio)
{
	struct sun4i_ps2data *drvdata = serio->port_data;
	u32 src_clk = 0;
	u32 clk_scdf;
	u32 clk_pcdf;
	u32 rval;
	unsigned long flags;

	/* Set line control and enable interrupt */
	rval = PS2_LCTL_STOPERREN | PS2_LCTL_ACKERREN
		| PS2_LCTL_PARERREN | PS2_LCTL_RXDTOEN;
	writel(rval, drvdata->reg_base + PS2_REG_LCTL);

	/* Reset FIFO */
	rval = PS2_FCTL_TXRST | PS2_FCTL_RXRST | PS2_FCTL_TXUFIEN
		| PS2_FCTL_TXOFIEN | PS2_FCTL_RXUFIEN
		| PS2_FCTL_RXOFIEN | PS2_FCTL_RXRDYIEN;

	writel(rval, drvdata->reg_base + PS2_REG_FCTL);

	src_clk = clk_get_rate(drvdata->clk);
	/* Set clock divider register */
	clk_scdf = src_clk / PS2_SAMPLE_CLK - 1;
	clk_pcdf = PS2_SAMPLE_CLK / PS2_SCLK - 1;
	rval = (clk_scdf << 8) | clk_pcdf;
	writel(rval, drvdata->reg_base + PS2_REG_CLKDR);

	/* Set global control register */
	rval = PS2_GCTL_RESET | PS2_GCTL_INTEN | PS2_GCTL_MASTER
		| PS2_GCTL_BUSEN;

	spin_lock_irqsave(&drvdata->lock, flags);
	writel(rval, drvdata->reg_base + PS2_REG_GCTL);
	spin_unlock_irqrestore(&drvdata->lock, flags);

	return 0;
}

static void sun4i_ps2_close(struct serio *serio)
{
	struct sun4i_ps2data *drvdata = serio->port_data;
	u32 rval;

	/* Shut off the interrupt */
	rval = readl(drvdata->reg_base + PS2_REG_GCTL);
	writel(rval & ~(PS2_GCTL_INTEN), drvdata->reg_base + PS2_REG_GCTL);

	synchronize_irq(drvdata->irq);
}

static int sun4i_ps2_write(struct serio *serio, unsigned char val)
{
	unsigned long expire = jiffies + msecs_to_jiffies(10000);
	struct sun4i_ps2data *drvdata = serio->port_data;

	do {
		if (readl(drvdata->reg_base + PS2_REG_FSTS) & PS2_FSTS_TXRDY) {
			writel(val, drvdata->reg_base + PS2_REG_DATA);
			return 0;
		}
	} while (time_before(jiffies, expire));

	return SERIO_TIMEOUT;
}

static int sun4i_ps2_probe(struct platform_device *pdev)
{
	struct resource *res; /* IO mem resources */
	struct sun4i_ps2data *drvdata;
	struct serio *serio;
	struct device *dev = &pdev->dev;
	unsigned int irq;
	int error;

	drvdata = kzalloc(sizeof(struct sun4i_ps2data), GFP_KERNEL);
	serio = kzalloc(sizeof(struct serio), GFP_KERNEL);
	if (!drvdata || !serio) {
		error = -ENOMEM;
		goto err_free_mem;
	}

	spin_lock_init(&drvdata->lock);

	/* IO */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		dev_err(dev, "failed to locate registers\n");
		error = -ENXIO;
		goto err_free_mem;
	}

	drvdata->reg_base = ioremap(res->start, resource_size(res));
	if (!drvdata->reg_base) {
		dev_err(dev, "failed to map registers\n");
		error = -ENOMEM;
		goto err_free_mem;
	}

	drvdata->clk = clk_get(dev, NULL);
	if (IS_ERR(drvdata->clk)) {
		error = PTR_ERR(drvdata->clk);
		dev_err(dev, "couldn't get clock %d\n", error);
		goto err_ioremap;
	}

	error = clk_prepare_enable(drvdata->clk);
	if (error) {
		dev_err(dev, "failed to enable clock %d\n", error);
		goto err_clk;
	}

	serio->id.type = SERIO_8042;
	serio->write = sun4i_ps2_write;
	serio->open = sun4i_ps2_open;
	serio->close = sun4i_ps2_close;
	serio->port_data = drvdata;
	serio->dev.parent = dev;
	strlcpy(serio->name, dev_name(dev), sizeof(serio->name));
	strlcpy(serio->phys, dev_name(dev), sizeof(serio->phys));

	/* shutoff interrupt */
	writel(0, drvdata->reg_base + PS2_REG_GCTL);

	/* Get IRQ for the device */
	irq = platform_get_irq(pdev, 0);
	if (!irq) {
		dev_err(dev, "no IRQ found\n");
		error = -ENXIO;
		goto err_disable_clk;
	}

	drvdata->irq = irq;
	drvdata->serio = serio;
	drvdata->dev = dev;

	error = request_irq(drvdata->irq, sun4i_ps2_interrupt, 0,
			    DRIVER_NAME, drvdata);
	if (error) {
		dev_err(drvdata->dev, "failed to allocate interrupt %d: %d\n",
			drvdata->irq, error);
		goto err_disable_clk;
	}

	serio_register_port(serio);
	platform_set_drvdata(pdev, drvdata);

	return 0;	/* success */

err_disable_clk:
	clk_disable_unprepare(drvdata->clk);
err_clk:
	clk_put(drvdata->clk);
err_ioremap:
	iounmap(drvdata->reg_base);
err_free_mem:
	kfree(serio);
	kfree(drvdata);
	return error;
}

static int sun4i_ps2_remove(struct platform_device *pdev)
{
	struct sun4i_ps2data *drvdata = platform_get_drvdata(pdev);

	serio_unregister_port(drvdata->serio);

	free_irq(drvdata->irq, drvdata);

	clk_disable_unprepare(drvdata->clk);
	clk_put(drvdata->clk);

	iounmap(drvdata->reg_base);

	kfree(drvdata);

	return 0;
}

static const struct of_device_id sun4i_ps2_match[] = {
	{ .compatible = "allwinner,sun4i-a10-ps2", },
	{ },
};

MODULE_DEVICE_TABLE(of, sun4i_ps2_match);

static struct platform_driver sun4i_ps2_driver = {
	.probe		= sun4i_ps2_probe,
	.remove		= sun4i_ps2_remove,
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = sun4i_ps2_match,
	},
};
module_platform_driver(sun4i_ps2_driver);

MODULE_AUTHOR("Vishnu Patekar <vishnupatekar0510@gmail.com>");
MODULE_AUTHOR("Aaron.maoye <leafy.myeh@newbietech.com>");
MODULE_DESCRIPTION("Allwinner A10/Sun4i PS/2 driver");
MODULE_LICENSE("GPL v2");