Commit | Line | Data |
---|---|---|
e3b3d0f5 | 1 | // SPDX-License-Identifier: GPL-2.0+ |
7fbcf3af JK |
2 | /* |
3 | * Serial Port driver for Aspeed VUART device | |
4 | * | |
5 | * Copyright (C) 2016 Jeremy Kerr <jk@ozlabs.org>, IBM Corp. | |
6 | * Copyright (C) 2006 Arnd Bergmann <arnd@arndb.de>, IBM Corp. | |
7fbcf3af JK |
7 | */ |
8 | #include <linux/device.h> | |
9 | #include <linux/module.h> | |
10 | #include <linux/of_address.h> | |
11 | #include <linux/of_irq.h> | |
12 | #include <linux/of_platform.h> | |
8d310c91 OS |
13 | #include <linux/regmap.h> |
14 | #include <linux/mfd/syscon.h> | |
5909c0bf JK |
15 | #include <linux/tty.h> |
16 | #include <linux/tty_flip.h> | |
7fbcf3af JK |
17 | #include <linux/clk.h> |
18 | ||
19 | #include "8250.h" | |
20 | ||
21 | #define ASPEED_VUART_GCRA 0x20 | |
22 | #define ASPEED_VUART_GCRA_VUART_EN BIT(0) | |
8d310c91 | 23 | #define ASPEED_VUART_GCRA_HOST_SIRQ_POLARITY BIT(1) |
7fbcf3af JK |
24 | #define ASPEED_VUART_GCRA_DISABLE_HOST_TX_DISCARD BIT(5) |
25 | #define ASPEED_VUART_GCRB 0x24 | |
26 | #define ASPEED_VUART_GCRB_HOST_SIRQ_MASK GENMASK(7, 4) | |
27 | #define ASPEED_VUART_GCRB_HOST_SIRQ_SHIFT 4 | |
28 | #define ASPEED_VUART_ADDRL 0x28 | |
29 | #define ASPEED_VUART_ADDRH 0x2c | |
30 | ||
31 | struct aspeed_vuart { | |
32 | struct device *dev; | |
33 | void __iomem *regs; | |
34 | struct clk *clk; | |
35 | int line; | |
5909c0bf JK |
36 | struct timer_list unthrottle_timer; |
37 | struct uart_8250_port *port; | |
7fbcf3af JK |
38 | }; |
39 | ||
5909c0bf JK |
40 | /* |
41 | * If we fill the tty flip buffers, we throttle the data ready interrupt | |
42 | * to prevent dropped characters. This timeout defines how long we wait | |
43 | * to (conditionally, depending on buffer state) unthrottle. | |
44 | */ | |
45 | static const int unthrottle_timeout = HZ/10; | |
46 | ||
7fbcf3af JK |
47 | /* |
48 | * The VUART is basically two UART 'front ends' connected by their FIFO | |
49 | * (no actual serial line in between). One is on the BMC side (management | |
50 | * controller) and one is on the host CPU side. | |
51 | * | |
52 | * It allows the BMC to provide to the host a "UART" that pipes into | |
53 | * the BMC itself and can then be turned by the BMC into a network console | |
54 | * of some sort for example. | |
55 | * | |
56 | * This driver is for the BMC side. The sysfs files allow the BMC | |
57 | * userspace which owns the system configuration policy, to specify | |
58 | * at what IO port and interrupt number the host side will appear | |
59 | * to the host on the Host <-> BMC LPC bus. It could be different on a | |
60 | * different system (though most of them use 3f8/4). | |
61 | */ | |
62 | ||
63 | static ssize_t lpc_address_show(struct device *dev, | |
64 | struct device_attribute *attr, char *buf) | |
65 | { | |
66 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
67 | u16 addr; | |
68 | ||
69 | addr = (readb(vuart->regs + ASPEED_VUART_ADDRH) << 8) | | |
70 | (readb(vuart->regs + ASPEED_VUART_ADDRL)); | |
71 | ||
72 | return snprintf(buf, PAGE_SIZE - 1, "0x%x\n", addr); | |
73 | } | |
74 | ||
75 | static ssize_t lpc_address_store(struct device *dev, | |
76 | struct device_attribute *attr, | |
77 | const char *buf, size_t count) | |
78 | { | |
79 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
80 | unsigned long val; | |
81 | int err; | |
82 | ||
83 | err = kstrtoul(buf, 0, &val); | |
84 | if (err) | |
85 | return err; | |
86 | ||
87 | writeb(val >> 8, vuart->regs + ASPEED_VUART_ADDRH); | |
88 | writeb(val >> 0, vuart->regs + ASPEED_VUART_ADDRL); | |
89 | ||
90 | return count; | |
91 | } | |
92 | ||
93 | static DEVICE_ATTR_RW(lpc_address); | |
94 | ||
95 | static ssize_t sirq_show(struct device *dev, | |
96 | struct device_attribute *attr, char *buf) | |
97 | { | |
98 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
99 | u8 reg; | |
100 | ||
101 | reg = readb(vuart->regs + ASPEED_VUART_GCRB); | |
102 | reg &= ASPEED_VUART_GCRB_HOST_SIRQ_MASK; | |
103 | reg >>= ASPEED_VUART_GCRB_HOST_SIRQ_SHIFT; | |
104 | ||
105 | return snprintf(buf, PAGE_SIZE - 1, "%u\n", reg); | |
106 | } | |
107 | ||
108 | static ssize_t sirq_store(struct device *dev, struct device_attribute *attr, | |
109 | const char *buf, size_t count) | |
110 | { | |
111 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
112 | unsigned long val; | |
113 | int err; | |
114 | u8 reg; | |
115 | ||
116 | err = kstrtoul(buf, 0, &val); | |
117 | if (err) | |
118 | return err; | |
119 | ||
120 | val <<= ASPEED_VUART_GCRB_HOST_SIRQ_SHIFT; | |
121 | val &= ASPEED_VUART_GCRB_HOST_SIRQ_MASK; | |
122 | ||
123 | reg = readb(vuart->regs + ASPEED_VUART_GCRB); | |
124 | reg &= ~ASPEED_VUART_GCRB_HOST_SIRQ_MASK; | |
125 | reg |= val; | |
126 | writeb(reg, vuart->regs + ASPEED_VUART_GCRB); | |
127 | ||
128 | return count; | |
129 | } | |
130 | ||
131 | static DEVICE_ATTR_RW(sirq); | |
132 | ||
8d310c91 OS |
133 | static ssize_t sirq_polarity_show(struct device *dev, |
134 | struct device_attribute *attr, char *buf) | |
135 | { | |
136 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
137 | u8 reg; | |
138 | ||
139 | reg = readb(vuart->regs + ASPEED_VUART_GCRA); | |
140 | reg &= ASPEED_VUART_GCRA_HOST_SIRQ_POLARITY; | |
141 | ||
142 | return snprintf(buf, PAGE_SIZE - 1, "%u\n", reg ? 1 : 0); | |
143 | } | |
144 | ||
145 | static void aspeed_vuart_set_sirq_polarity(struct aspeed_vuart *vuart, | |
146 | bool polarity) | |
147 | { | |
148 | u8 reg = readb(vuart->regs + ASPEED_VUART_GCRA); | |
149 | ||
150 | if (polarity) | |
151 | reg |= ASPEED_VUART_GCRA_HOST_SIRQ_POLARITY; | |
152 | else | |
153 | reg &= ~ASPEED_VUART_GCRA_HOST_SIRQ_POLARITY; | |
154 | ||
155 | writeb(reg, vuart->regs + ASPEED_VUART_GCRA); | |
156 | } | |
157 | ||
158 | static ssize_t sirq_polarity_store(struct device *dev, | |
159 | struct device_attribute *attr, | |
160 | const char *buf, size_t count) | |
161 | { | |
162 | struct aspeed_vuart *vuart = dev_get_drvdata(dev); | |
163 | unsigned long val; | |
164 | int err; | |
165 | ||
166 | err = kstrtoul(buf, 0, &val); | |
167 | if (err) | |
168 | return err; | |
169 | ||
170 | aspeed_vuart_set_sirq_polarity(vuart, val != 0); | |
171 | ||
172 | return count; | |
173 | } | |
174 | ||
175 | static DEVICE_ATTR_RW(sirq_polarity); | |
176 | ||
7fbcf3af JK |
177 | static struct attribute *aspeed_vuart_attrs[] = { |
178 | &dev_attr_sirq.attr, | |
8d310c91 | 179 | &dev_attr_sirq_polarity.attr, |
7fbcf3af JK |
180 | &dev_attr_lpc_address.attr, |
181 | NULL, | |
182 | }; | |
183 | ||
184 | static const struct attribute_group aspeed_vuart_attr_group = { | |
185 | .attrs = aspeed_vuart_attrs, | |
186 | }; | |
187 | ||
188 | static void aspeed_vuart_set_enabled(struct aspeed_vuart *vuart, bool enabled) | |
189 | { | |
190 | u8 reg = readb(vuart->regs + ASPEED_VUART_GCRA); | |
191 | ||
192 | if (enabled) | |
193 | reg |= ASPEED_VUART_GCRA_VUART_EN; | |
194 | else | |
195 | reg &= ~ASPEED_VUART_GCRA_VUART_EN; | |
196 | ||
197 | writeb(reg, vuart->regs + ASPEED_VUART_GCRA); | |
198 | } | |
199 | ||
200 | static void aspeed_vuart_set_host_tx_discard(struct aspeed_vuart *vuart, | |
201 | bool discard) | |
202 | { | |
203 | u8 reg; | |
204 | ||
205 | reg = readb(vuart->regs + ASPEED_VUART_GCRA); | |
206 | ||
207 | /* If the DISABLE_HOST_TX_DISCARD bit is set, discard is disabled */ | |
208 | if (!discard) | |
209 | reg |= ASPEED_VUART_GCRA_DISABLE_HOST_TX_DISCARD; | |
210 | else | |
211 | reg &= ~ASPEED_VUART_GCRA_DISABLE_HOST_TX_DISCARD; | |
212 | ||
213 | writeb(reg, vuart->regs + ASPEED_VUART_GCRA); | |
214 | } | |
215 | ||
216 | static int aspeed_vuart_startup(struct uart_port *uart_port) | |
217 | { | |
218 | struct uart_8250_port *uart_8250_port = up_to_u8250p(uart_port); | |
219 | struct aspeed_vuart *vuart = uart_8250_port->port.private_data; | |
220 | int rc; | |
221 | ||
222 | rc = serial8250_do_startup(uart_port); | |
223 | if (rc) | |
224 | return rc; | |
225 | ||
226 | aspeed_vuart_set_host_tx_discard(vuart, false); | |
227 | ||
228 | return 0; | |
229 | } | |
230 | ||
231 | static void aspeed_vuart_shutdown(struct uart_port *uart_port) | |
232 | { | |
233 | struct uart_8250_port *uart_8250_port = up_to_u8250p(uart_port); | |
234 | struct aspeed_vuart *vuart = uart_8250_port->port.private_data; | |
235 | ||
236 | aspeed_vuart_set_host_tx_discard(vuart, true); | |
237 | ||
238 | serial8250_do_shutdown(uart_port); | |
239 | } | |
240 | ||
5909c0bf JK |
241 | static void __aspeed_vuart_set_throttle(struct uart_8250_port *up, |
242 | bool throttle) | |
989983ea JK |
243 | { |
244 | unsigned char irqs = UART_IER_RLSI | UART_IER_RDI; | |
989983ea | 245 | |
989983ea JK |
246 | up->ier &= ~irqs; |
247 | if (!throttle) | |
248 | up->ier |= irqs; | |
249 | serial_out(up, UART_IER, up->ier); | |
5909c0bf JK |
250 | } |
251 | static void aspeed_vuart_set_throttle(struct uart_port *port, bool throttle) | |
252 | { | |
253 | struct uart_8250_port *up = up_to_u8250p(port); | |
254 | unsigned long flags; | |
255 | ||
256 | spin_lock_irqsave(&port->lock, flags); | |
257 | __aspeed_vuart_set_throttle(up, throttle); | |
989983ea JK |
258 | spin_unlock_irqrestore(&port->lock, flags); |
259 | } | |
260 | ||
261 | static void aspeed_vuart_throttle(struct uart_port *port) | |
262 | { | |
263 | aspeed_vuart_set_throttle(port, true); | |
264 | } | |
265 | ||
266 | static void aspeed_vuart_unthrottle(struct uart_port *port) | |
267 | { | |
268 | aspeed_vuart_set_throttle(port, false); | |
269 | } | |
270 | ||
5909c0bf JK |
271 | static void aspeed_vuart_unthrottle_exp(struct timer_list *timer) |
272 | { | |
273 | struct aspeed_vuart *vuart = from_timer(vuart, timer, unthrottle_timer); | |
274 | struct uart_8250_port *up = vuart->port; | |
275 | ||
276 | if (!tty_buffer_space_avail(&up->port.state->port)) { | |
a451debb DC |
277 | mod_timer(&vuart->unthrottle_timer, |
278 | jiffies + unthrottle_timeout); | |
5909c0bf JK |
279 | return; |
280 | } | |
281 | ||
282 | aspeed_vuart_unthrottle(&up->port); | |
283 | } | |
284 | ||
285 | /* | |
286 | * Custom interrupt handler to manage finer-grained flow control. Although we | |
287 | * have throttle/unthrottle callbacks, we've seen that the VUART device can | |
288 | * deliver characters faster than the ldisc has a chance to check buffer space | |
289 | * against the throttle threshold. This results in dropped characters before | |
290 | * the throttle. | |
291 | * | |
292 | * We do this by checking for flip buffer space before RX. If we have no space, | |
293 | * throttle now and schedule an unthrottle for later, once the ldisc has had | |
294 | * a chance to drain the buffers. | |
295 | */ | |
296 | static int aspeed_vuart_handle_irq(struct uart_port *port) | |
297 | { | |
298 | struct uart_8250_port *up = up_to_u8250p(port); | |
299 | unsigned int iir, lsr; | |
300 | unsigned long flags; | |
301 | int space, count; | |
302 | ||
303 | iir = serial_port_in(port, UART_IIR); | |
304 | ||
305 | if (iir & UART_IIR_NO_INT) | |
306 | return 0; | |
307 | ||
308 | spin_lock_irqsave(&port->lock, flags); | |
309 | ||
310 | lsr = serial_port_in(port, UART_LSR); | |
311 | ||
312 | if (lsr & (UART_LSR_DR | UART_LSR_BI)) { | |
313 | space = tty_buffer_space_avail(&port->state->port); | |
314 | ||
315 | if (!space) { | |
316 | /* throttle and schedule an unthrottle later */ | |
317 | struct aspeed_vuart *vuart = port->private_data; | |
318 | __aspeed_vuart_set_throttle(up, true); | |
319 | ||
320 | if (!timer_pending(&vuart->unthrottle_timer)) { | |
321 | vuart->port = up; | |
322 | mod_timer(&vuart->unthrottle_timer, | |
a451debb | 323 | jiffies + unthrottle_timeout); |
5909c0bf JK |
324 | } |
325 | ||
326 | } else { | |
327 | count = min(space, 256); | |
328 | ||
329 | do { | |
330 | serial8250_read_char(up, lsr); | |
331 | lsr = serial_in(up, UART_LSR); | |
332 | if (--count == 0) | |
333 | break; | |
334 | } while (lsr & (UART_LSR_DR | UART_LSR_BI)); | |
335 | ||
336 | tty_flip_buffer_push(&port->state->port); | |
337 | } | |
338 | } | |
339 | ||
340 | serial8250_modem_status(up); | |
341 | if (lsr & UART_LSR_THRE) | |
342 | serial8250_tx_chars(up); | |
343 | ||
596f63da | 344 | uart_unlock_and_check_sysrq(port, flags); |
5909c0bf JK |
345 | |
346 | return 1; | |
347 | } | |
348 | ||
8d310c91 OS |
349 | static void aspeed_vuart_auto_configure_sirq_polarity( |
350 | struct aspeed_vuart *vuart, struct device_node *syscon_np, | |
351 | u32 reg_offset, u32 reg_mask) | |
352 | { | |
353 | struct regmap *regmap; | |
354 | u32 value; | |
355 | ||
356 | regmap = syscon_node_to_regmap(syscon_np); | |
357 | if (IS_ERR(regmap)) { | |
358 | dev_warn(vuart->dev, | |
359 | "could not get regmap for aspeed,sirq-polarity-sense\n"); | |
360 | return; | |
361 | } | |
362 | if (regmap_read(regmap, reg_offset, &value)) { | |
363 | dev_warn(vuart->dev, "could not read hw strap table\n"); | |
364 | return; | |
365 | } | |
366 | ||
367 | aspeed_vuart_set_sirq_polarity(vuart, (value & reg_mask) == 0); | |
368 | } | |
369 | ||
7fbcf3af JK |
370 | static int aspeed_vuart_probe(struct platform_device *pdev) |
371 | { | |
8d310c91 | 372 | struct of_phandle_args sirq_polarity_sense_args; |
7fbcf3af JK |
373 | struct uart_8250_port port; |
374 | struct aspeed_vuart *vuart; | |
375 | struct device_node *np; | |
376 | struct resource *res; | |
377 | u32 clk, prop; | |
378 | int rc; | |
379 | ||
380 | np = pdev->dev.of_node; | |
381 | ||
382 | vuart = devm_kzalloc(&pdev->dev, sizeof(*vuart), GFP_KERNEL); | |
383 | if (!vuart) | |
384 | return -ENOMEM; | |
385 | ||
386 | vuart->dev = &pdev->dev; | |
5909c0bf | 387 | timer_setup(&vuart->unthrottle_timer, aspeed_vuart_unthrottle_exp, 0); |
7fbcf3af JK |
388 | |
389 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
390 | vuart->regs = devm_ioremap_resource(&pdev->dev, res); | |
391 | if (IS_ERR(vuart->regs)) | |
392 | return PTR_ERR(vuart->regs); | |
393 | ||
394 | memset(&port, 0, sizeof(port)); | |
395 | port.port.private_data = vuart; | |
396 | port.port.membase = vuart->regs; | |
397 | port.port.mapbase = res->start; | |
398 | port.port.mapsize = resource_size(res); | |
399 | port.port.startup = aspeed_vuart_startup; | |
400 | port.port.shutdown = aspeed_vuart_shutdown; | |
989983ea JK |
401 | port.port.throttle = aspeed_vuart_throttle; |
402 | port.port.unthrottle = aspeed_vuart_unthrottle; | |
403 | port.port.status = UPSTAT_SYNC_FIFO; | |
7fbcf3af | 404 | port.port.dev = &pdev->dev; |
9b614afe | 405 | port.port.has_sysrq = IS_ENABLED(CONFIG_SERIAL_8250_CONSOLE); |
7fbcf3af JK |
406 | |
407 | rc = sysfs_create_group(&vuart->dev->kobj, &aspeed_vuart_attr_group); | |
408 | if (rc < 0) | |
409 | return rc; | |
410 | ||
411 | if (of_property_read_u32(np, "clock-frequency", &clk)) { | |
412 | vuart->clk = devm_clk_get(&pdev->dev, NULL); | |
413 | if (IS_ERR(vuart->clk)) { | |
414 | dev_warn(&pdev->dev, | |
415 | "clk or clock-frequency not defined\n"); | |
cbafe9d5 AK |
416 | rc = PTR_ERR(vuart->clk); |
417 | goto err_sysfs_remove; | |
7fbcf3af JK |
418 | } |
419 | ||
420 | rc = clk_prepare_enable(vuart->clk); | |
421 | if (rc < 0) | |
cbafe9d5 | 422 | goto err_sysfs_remove; |
7fbcf3af JK |
423 | |
424 | clk = clk_get_rate(vuart->clk); | |
425 | } | |
426 | ||
427 | /* If current-speed was set, then try not to change it. */ | |
428 | if (of_property_read_u32(np, "current-speed", &prop) == 0) | |
429 | port.port.custom_divisor = clk / (16 * prop); | |
430 | ||
431 | /* Check for shifted address mapping */ | |
432 | if (of_property_read_u32(np, "reg-offset", &prop) == 0) | |
433 | port.port.mapbase += prop; | |
434 | ||
435 | /* Check for registers offset within the devices address range */ | |
436 | if (of_property_read_u32(np, "reg-shift", &prop) == 0) | |
437 | port.port.regshift = prop; | |
438 | ||
439 | /* Check for fifo size */ | |
440 | if (of_property_read_u32(np, "fifo-size", &prop) == 0) | |
441 | port.port.fifosize = prop; | |
442 | ||
443 | /* Check for a fixed line number */ | |
444 | rc = of_alias_get_id(np, "serial"); | |
445 | if (rc >= 0) | |
446 | port.port.line = rc; | |
447 | ||
448 | port.port.irq = irq_of_parse_and_map(np, 0); | |
5909c0bf | 449 | port.port.handle_irq = aspeed_vuart_handle_irq; |
7fbcf3af JK |
450 | port.port.iotype = UPIO_MEM; |
451 | port.port.type = PORT_16550A; | |
452 | port.port.uartclk = clk; | |
453 | port.port.flags = UPF_SHARE_IRQ | UPF_BOOT_AUTOCONF | |
454 | | UPF_FIXED_PORT | UPF_FIXED_TYPE | UPF_NO_THRE_TEST; | |
455 | ||
456 | if (of_property_read_bool(np, "no-loopback-test")) | |
457 | port.port.flags |= UPF_SKIP_TEST; | |
458 | ||
459 | if (port.port.fifosize) | |
460 | port.capabilities = UART_CAP_FIFO; | |
461 | ||
462 | if (of_property_read_bool(np, "auto-flow-control")) | |
463 | port.capabilities |= UART_CAP_AFE; | |
464 | ||
465 | rc = serial8250_register_8250_port(&port); | |
466 | if (rc < 0) | |
467 | goto err_clk_disable; | |
468 | ||
469 | vuart->line = rc; | |
470 | ||
8d310c91 OS |
471 | rc = of_parse_phandle_with_fixed_args( |
472 | np, "aspeed,sirq-polarity-sense", 2, 0, | |
473 | &sirq_polarity_sense_args); | |
474 | if (rc < 0) { | |
475 | dev_dbg(&pdev->dev, | |
476 | "aspeed,sirq-polarity-sense property not found\n"); | |
477 | } else { | |
478 | aspeed_vuart_auto_configure_sirq_polarity( | |
479 | vuart, sirq_polarity_sense_args.np, | |
480 | sirq_polarity_sense_args.args[0], | |
481 | BIT(sirq_polarity_sense_args.args[1])); | |
482 | of_node_put(sirq_polarity_sense_args.np); | |
483 | } | |
484 | ||
7fbcf3af JK |
485 | aspeed_vuart_set_enabled(vuart, true); |
486 | aspeed_vuart_set_host_tx_discard(vuart, true); | |
487 | platform_set_drvdata(pdev, vuart); | |
488 | ||
489 | return 0; | |
490 | ||
491 | err_clk_disable: | |
492 | clk_disable_unprepare(vuart->clk); | |
493 | irq_dispose_mapping(port.port.irq); | |
cbafe9d5 AK |
494 | err_sysfs_remove: |
495 | sysfs_remove_group(&vuart->dev->kobj, &aspeed_vuart_attr_group); | |
7fbcf3af JK |
496 | return rc; |
497 | } | |
498 | ||
499 | static int aspeed_vuart_remove(struct platform_device *pdev) | |
500 | { | |
501 | struct aspeed_vuart *vuart = platform_get_drvdata(pdev); | |
502 | ||
5909c0bf | 503 | del_timer_sync(&vuart->unthrottle_timer); |
7fbcf3af JK |
504 | aspeed_vuart_set_enabled(vuart, false); |
505 | serial8250_unregister_port(vuart->line); | |
506 | sysfs_remove_group(&vuart->dev->kobj, &aspeed_vuart_attr_group); | |
507 | clk_disable_unprepare(vuart->clk); | |
508 | ||
509 | return 0; | |
510 | } | |
511 | ||
512 | static const struct of_device_id aspeed_vuart_table[] = { | |
513 | { .compatible = "aspeed,ast2400-vuart" }, | |
514 | { .compatible = "aspeed,ast2500-vuart" }, | |
515 | { }, | |
516 | }; | |
517 | ||
518 | static struct platform_driver aspeed_vuart_driver = { | |
519 | .driver = { | |
520 | .name = "aspeed-vuart", | |
521 | .of_match_table = aspeed_vuart_table, | |
522 | }, | |
523 | .probe = aspeed_vuart_probe, | |
524 | .remove = aspeed_vuart_remove, | |
525 | }; | |
526 | ||
527 | module_platform_driver(aspeed_vuart_driver); | |
528 | ||
529 | MODULE_AUTHOR("Jeremy Kerr <jk@ozlabs.org>"); | |
530 | MODULE_LICENSE("GPL"); | |
531 | MODULE_DESCRIPTION("Driver for Aspeed VUART device"); |