blob: ccd5c189226ee7b0a87466a090ab0e8fa83e146d [file] [log] [blame]
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -07001/* arch/arm/mach-msm/smd_tty.c
2 *
3 * Copyright (C) 2007 Google, Inc.
4 * Copyright (c) 2009-2011, Code Aurora Forum. All rights reserved.
5 * Author: Brian Swetland <swetland@google.com>
6 *
7 * This software is licensed under the terms of the GNU General Public
8 * License version 2, as published by the Free Software Foundation, and
9 * may be copied, distributed, and modified under those terms.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 */
17
18#include <linux/module.h>
19#include <linux/fs.h>
20#include <linux/cdev.h>
21#include <linux/device.h>
22#include <linux/interrupt.h>
23#include <linux/delay.h>
24#include <linux/wakelock.h>
25#include <linux/platform_device.h>
26#include <linux/sched.h>
27
28#include <linux/tty.h>
29#include <linux/tty_driver.h>
30#include <linux/tty_flip.h>
31
32#include <mach/msm_smd.h>
33#include <mach/peripheral-loader.h>
Jeff Hugo4c0ba6c2011-07-15 11:47:13 -060034#include <mach/socinfo.h>
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -070035
36#include "smd_private.h"
37
38#define MAX_SMD_TTYS 37
39#define MAX_TTY_BUF_SIZE 2048
40
41static DEFINE_MUTEX(smd_tty_lock);
42
43static uint smd_tty_modem_wait;
44module_param_named(modem_wait, smd_tty_modem_wait,
45 uint, S_IRUGO | S_IWUSR | S_IWGRP);
46
47struct smd_tty_info {
48 smd_channel_t *ch;
49 struct tty_struct *tty;
50 struct wake_lock wake_lock;
51 int open_count;
52 struct tasklet_struct tty_tsklt;
53 struct timer_list buf_req_timer;
54 struct completion ch_allocated;
55 struct platform_driver driver;
56 void *pil;
57 int in_reset;
58 int in_reset_updated;
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -060059 int is_open;
60 wait_queue_head_t ch_opened_wait_queue;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -070061 spinlock_t reset_lock;
Eric Holmberg513ad582011-12-14 16:27:13 -070062 struct smd_config *smd;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -070063};
64
Eric Holmberg513ad582011-12-14 16:27:13 -070065/**
66 * SMD port configuration.
67 *
68 * @tty_dev_index Index into smd_tty[]
69 * @port_name Name of the SMD port
70 * @dev_name Name of the TTY Device (if NULL, @port_name is used)
71 * @edge SMD edge
72 */
73struct smd_config {
74 uint32_t tty_dev_index;
75 const char *port_name;
76 const char *dev_name;
77 uint32_t edge;
78};
79
80static struct smd_config smd_configs[] = {
81 {0, "DS", NULL, SMD_APPS_MODEM},
82 {1, "APPS_FM", NULL, SMD_APPS_WCNSS},
83 {2, "APPS_RIVA_BT_ACL", NULL, SMD_APPS_WCNSS},
84 {3, "APPS_RIVA_BT_CMD", NULL, SMD_APPS_WCNSS},
85 {4, "MBALBRIDGE", NULL, SMD_APPS_MODEM},
86 {7, "DATA1", NULL, SMD_APPS_MODEM},
87 {21, "DATA21", NULL, SMD_APPS_MODEM},
88 {27, "GPSNMEA", NULL, SMD_APPS_MODEM},
89 {36, "LOOPBACK", "LOOPBACK_TTY", SMD_APPS_MODEM},
90};
91#define DS_IDX 0
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -070092#define LOOPBACK_IDX 36
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -070093
94static struct delayed_work loopback_work;
95static struct smd_tty_info smd_tty[MAX_SMD_TTYS];
96
97static int is_in_reset(struct smd_tty_info *info)
98{
99 return info->in_reset;
100}
101
102static void buf_req_retry(unsigned long param)
103{
104 struct smd_tty_info *info = (struct smd_tty_info *)param;
Karthikeyan Ramasubramanianf8ad4132011-11-22 09:11:18 -0700105 unsigned long flags;
106
107 spin_lock_irqsave(&info->reset_lock, flags);
108 if (info->is_open) {
109 spin_unlock_irqrestore(&info->reset_lock, flags);
110 tasklet_hi_schedule(&info->tty_tsklt);
111 return;
112 }
113 spin_unlock_irqrestore(&info->reset_lock, flags);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700114}
115
116static void smd_tty_read(unsigned long param)
117{
118 unsigned char *ptr;
119 int avail;
120 struct smd_tty_info *info = (struct smd_tty_info *)param;
121 struct tty_struct *tty = info->tty;
122
123 if (!tty)
124 return;
125
126 for (;;) {
127 if (is_in_reset(info)) {
128 /* signal TTY clients using TTY_BREAK */
129 tty_insert_flip_char(tty, 0x00, TTY_BREAK);
130 tty_flip_buffer_push(tty);
131 break;
132 }
133
134 if (test_bit(TTY_THROTTLED, &tty->flags)) break;
135 avail = smd_read_avail(info->ch);
136 if (avail == 0)
137 break;
138
139 if (avail > MAX_TTY_BUF_SIZE)
140 avail = MAX_TTY_BUF_SIZE;
141
142 avail = tty_prepare_flip_string(tty, &ptr, avail);
143 if (avail <= 0) {
144 if (!timer_pending(&info->buf_req_timer)) {
145 init_timer(&info->buf_req_timer);
146 info->buf_req_timer.expires = jiffies +
147 ((30 * HZ)/1000);
148 info->buf_req_timer.function = buf_req_retry;
149 info->buf_req_timer.data = param;
150 add_timer(&info->buf_req_timer);
151 }
152 return;
153 }
154
155 if (smd_read(info->ch, ptr, avail) != avail) {
156 /* shouldn't be possible since we're in interrupt
157 ** context here and nobody else could 'steal' our
158 ** characters.
159 */
160 printk(KERN_ERR "OOPS - smd_tty_buffer mismatch?!");
161 }
162
163 wake_lock_timeout(&info->wake_lock, HZ / 2);
164 tty_flip_buffer_push(tty);
165 }
166
167 /* XXX only when writable and necessary */
168 tty_wakeup(tty);
169}
170
171static void smd_tty_notify(void *priv, unsigned event)
172{
173 struct smd_tty_info *info = priv;
174 unsigned long flags;
175
176 switch (event) {
177 case SMD_EVENT_DATA:
Karthikeyan Ramasubramanianf8ad4132011-11-22 09:11:18 -0700178 spin_lock_irqsave(&info->reset_lock, flags);
179 if (!info->is_open) {
180 spin_unlock_irqrestore(&info->reset_lock, flags);
181 break;
182 }
183 spin_unlock_irqrestore(&info->reset_lock, flags);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700184 /* There may be clients (tty framework) that are blocked
185 * waiting for space to write data, so if a possible read
186 * interrupt came in wake anyone waiting and disable the
187 * interrupts
188 */
189 if (smd_write_avail(info->ch)) {
190 smd_disable_read_intr(info->ch);
191 if (info->tty)
192 wake_up_interruptible(&info->tty->write_wait);
193 }
194 tasklet_hi_schedule(&info->tty_tsklt);
195 break;
196
197 case SMD_EVENT_OPEN:
198 spin_lock_irqsave(&info->reset_lock, flags);
199 info->in_reset = 0;
200 info->in_reset_updated = 1;
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -0600201 info->is_open = 1;
202 wake_up_interruptible(&info->ch_opened_wait_queue);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700203 spin_unlock_irqrestore(&info->reset_lock, flags);
204 break;
205
206 case SMD_EVENT_CLOSE:
207 spin_lock_irqsave(&info->reset_lock, flags);
208 info->in_reset = 1;
209 info->in_reset_updated = 1;
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -0600210 info->is_open = 0;
211 wake_up_interruptible(&info->ch_opened_wait_queue);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700212 spin_unlock_irqrestore(&info->reset_lock, flags);
213 /* schedule task to send TTY_BREAK */
214 tasklet_hi_schedule(&info->tty_tsklt);
215
216 if (info->tty->index == LOOPBACK_IDX)
217 schedule_delayed_work(&loopback_work,
218 msecs_to_jiffies(1000));
219 break;
220 }
221}
222
223static uint32_t is_modem_smsm_inited(void)
224{
225 uint32_t modem_state;
226 uint32_t ready_state = (SMSM_INIT | SMSM_SMDINIT);
227
228 modem_state = smsm_get_state(SMSM_MODEM_STATE);
229 return (modem_state & ready_state) == ready_state;
230}
231
232static int smd_tty_open(struct tty_struct *tty, struct file *f)
233{
234 int res = 0;
Eric Holmberg513ad582011-12-14 16:27:13 -0700235 unsigned int n = tty->index;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700236 struct smd_tty_info *info;
237 char *peripheral = NULL;
238
239
Eric Holmberg513ad582011-12-14 16:27:13 -0700240 if (n >= MAX_SMD_TTYS || !smd_tty[n].smd)
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700241 return -ENODEV;
242
243 info = smd_tty + n;
244
245 mutex_lock(&smd_tty_lock);
246 tty->driver_data = info;
247
248 if (info->open_count++ == 0) {
Eric Holmberg513ad582011-12-14 16:27:13 -0700249 if (smd_tty[n].smd->edge == SMD_APPS_MODEM)
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700250 peripheral = "modem";
251
252 if (peripheral) {
Eric Holmberg513ad582011-12-14 16:27:13 -0700253 info->pil = pil_get(peripheral);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700254 if (IS_ERR(info->pil)) {
255 res = PTR_ERR(info->pil);
256 goto out;
257 }
258
259 /* Wait for the modem SMSM to be inited for the SMD
260 * Loopback channel to be allocated at the modem. Since
261 * the wait need to be done atmost once, using msleep
262 * doesn't degrade the performance.
263 */
Eric Holmberg513ad582011-12-14 16:27:13 -0700264 if (n == LOOPBACK_IDX) {
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700265 if (!is_modem_smsm_inited())
266 msleep(5000);
267 smsm_change_state(SMSM_APPS_STATE,
268 0, SMSM_SMD_LOOPBACK);
269 msleep(100);
270 }
271
272
273 /*
274 * Wait for a channel to be allocated so we know
275 * the modem is ready enough.
276 */
277 if (smd_tty_modem_wait) {
278 res = wait_for_completion_interruptible_timeout(
279 &info->ch_allocated,
280 msecs_to_jiffies(smd_tty_modem_wait *
281 1000));
282
283 if (res == 0) {
284 pr_err("Timed out waiting for SMD"
285 " channel\n");
286 res = -ETIMEDOUT;
287 goto release_pil;
288 } else if (res < 0) {
289 pr_err("Error waiting for SMD channel:"
290 " %d\n",
291 res);
292 goto release_pil;
293 }
294
295 res = 0;
296 }
297 }
298
299
300 info->tty = tty;
301 tasklet_init(&info->tty_tsklt, smd_tty_read,
302 (unsigned long)info);
303 wake_lock_init(&info->wake_lock, WAKE_LOCK_SUSPEND,
Eric Holmberg513ad582011-12-14 16:27:13 -0700304 smd_tty[n].smd->port_name);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700305 if (!info->ch) {
Eric Holmberg513ad582011-12-14 16:27:13 -0700306 res = smd_named_open_on_edge(smd_tty[n].smd->port_name,
307 smd_tty[n].smd->edge,
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700308 &info->ch, info,
309 smd_tty_notify);
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -0600310 if (res < 0) {
311 pr_err("%s: %s open failed %d\n", __func__,
Eric Holmberg513ad582011-12-14 16:27:13 -0700312 smd_tty[n].smd->port_name, res);
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -0600313 goto release_pil;
314 }
315
316 res = wait_event_interruptible_timeout(
317 info->ch_opened_wait_queue,
318 info->is_open, (2 * HZ));
319 if (res == 0)
320 res = -ETIMEDOUT;
321 if (res < 0) {
322 pr_err("%s: wait for %s smd_open failed %d\n",
Eric Holmberg513ad582011-12-14 16:27:13 -0700323 __func__, smd_tty[n].smd->port_name,
324 res);
Karthikeyan Ramasubramaniand5e63af2011-07-29 11:30:59 -0600325 goto release_pil;
326 }
327 res = 0;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700328 }
329 }
330
331release_pil:
332 if (res < 0)
333 pil_put(info->pil);
334 else
335 smd_disable_read_intr(info->ch);
336out:
337 mutex_unlock(&smd_tty_lock);
338
339 return res;
340}
341
342static void smd_tty_close(struct tty_struct *tty, struct file *f)
343{
344 struct smd_tty_info *info = tty->driver_data;
Karthikeyan Ramasubramaniand563de52011-11-22 08:38:17 -0700345 unsigned long flags;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700346
347 if (info == 0)
348 return;
349
350 mutex_lock(&smd_tty_lock);
351 if (--info->open_count == 0) {
Karthikeyan Ramasubramaniand563de52011-11-22 08:38:17 -0700352 spin_lock_irqsave(&info->reset_lock, flags);
353 info->is_open = 0;
354 spin_unlock_irqrestore(&info->reset_lock, flags);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700355 if (info->tty) {
356 tasklet_kill(&info->tty_tsklt);
357 wake_lock_destroy(&info->wake_lock);
358 info->tty = 0;
359 }
360 tty->driver_data = 0;
361 del_timer(&info->buf_req_timer);
362 if (info->ch) {
363 smd_close(info->ch);
364 info->ch = 0;
365 pil_put(info->pil);
366 }
367 }
368 mutex_unlock(&smd_tty_lock);
369}
370
371static int smd_tty_write(struct tty_struct *tty, const unsigned char *buf, int len)
372{
373 struct smd_tty_info *info = tty->driver_data;
374 int avail;
375
376 /* if we're writing to a packet channel we will
377 ** never be able to write more data than there
378 ** is currently space for
379 */
380 if (is_in_reset(info))
381 return -ENETRESET;
382
383 avail = smd_write_avail(info->ch);
384 /* if no space, we'll have to setup a notification later to wake up the
385 * tty framework when space becomes avaliable
386 */
387 if (!avail) {
388 smd_enable_read_intr(info->ch);
389 return 0;
390 }
391 if (len > avail)
392 len = avail;
393
394 return smd_write(info->ch, buf, len);
395}
396
397static int smd_tty_write_room(struct tty_struct *tty)
398{
399 struct smd_tty_info *info = tty->driver_data;
400 return smd_write_avail(info->ch);
401}
402
403static int smd_tty_chars_in_buffer(struct tty_struct *tty)
404{
405 struct smd_tty_info *info = tty->driver_data;
406 return smd_read_avail(info->ch);
407}
408
409static void smd_tty_unthrottle(struct tty_struct *tty)
410{
411 struct smd_tty_info *info = tty->driver_data;
Karthikeyan Ramasubramanianf8ad4132011-11-22 09:11:18 -0700412 unsigned long flags;
413
414 spin_lock_irqsave(&info->reset_lock, flags);
415 if (info->is_open) {
416 spin_unlock_irqrestore(&info->reset_lock, flags);
417 tasklet_hi_schedule(&info->tty_tsklt);
418 return;
419 }
420 spin_unlock_irqrestore(&info->reset_lock, flags);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700421}
422
423/*
424 * Returns the current TIOCM status bits including:
425 * SMD Signals (DTR/DSR, CTS/RTS, CD, RI)
426 * TIOCM_OUT1 - reset state (1=in reset)
427 * TIOCM_OUT2 - reset state updated (1=updated)
428 */
429static int smd_tty_tiocmget(struct tty_struct *tty)
430{
431 struct smd_tty_info *info = tty->driver_data;
432 unsigned long flags;
433 int tiocm;
434
435 tiocm = smd_tiocmget(info->ch);
436
437 spin_lock_irqsave(&info->reset_lock, flags);
438 tiocm |= (info->in_reset ? TIOCM_OUT1 : 0);
439 if (info->in_reset_updated) {
440 tiocm |= TIOCM_OUT2;
441 info->in_reset_updated = 0;
442 }
443 spin_unlock_irqrestore(&info->reset_lock, flags);
444
445 return tiocm;
446}
447
448static int smd_tty_tiocmset(struct tty_struct *tty,
449 unsigned int set, unsigned int clear)
450{
451 struct smd_tty_info *info = tty->driver_data;
452
453 if (info->in_reset)
454 return -ENETRESET;
455
456 return smd_tiocmset(info->ch, set, clear);
457}
458
459static void loopback_probe_worker(struct work_struct *work)
460{
461 /* wait for modem to restart before requesting loopback server */
462 if (!is_modem_smsm_inited())
463 schedule_delayed_work(&loopback_work, msecs_to_jiffies(1000));
464 else
465 smsm_change_state(SMSM_APPS_STATE,
466 0, SMSM_SMD_LOOPBACK);
467}
468
469static struct tty_operations smd_tty_ops = {
470 .open = smd_tty_open,
471 .close = smd_tty_close,
472 .write = smd_tty_write,
473 .write_room = smd_tty_write_room,
474 .chars_in_buffer = smd_tty_chars_in_buffer,
475 .unthrottle = smd_tty_unthrottle,
476 .tiocmget = smd_tty_tiocmget,
477 .tiocmset = smd_tty_tiocmset,
478};
479
480static int smd_tty_dummy_probe(struct platform_device *pdev)
481{
Eric Holmberg513ad582011-12-14 16:27:13 -0700482 int n;
483 int idx;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700484
Eric Holmberg513ad582011-12-14 16:27:13 -0700485 for (n = 0; n < ARRAY_SIZE(smd_configs); ++n) {
486 idx = smd_configs[n].tty_dev_index;
487
488 if (!smd_configs[n].dev_name)
489 continue;
490
491 if (!strncmp(pdev->name, smd_configs[n].dev_name,
492 SMD_MAX_CH_NAME_LEN)) {
493 complete_all(&smd_tty[idx].ch_allocated);
494 return 0;
495 }
496 }
497 pr_err("%s: unknown device '%s'\n", __func__, pdev->name);
498
499 return -ENODEV;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700500}
501
502static struct tty_driver *smd_tty_driver;
503
504static int __init smd_tty_init(void)
505{
506 int ret;
Eric Holmberg513ad582011-12-14 16:27:13 -0700507 int n;
508 int idx;
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700509
510 smd_tty_driver = alloc_tty_driver(MAX_SMD_TTYS);
511 if (smd_tty_driver == 0)
512 return -ENOMEM;
513
514 smd_tty_driver->owner = THIS_MODULE;
515 smd_tty_driver->driver_name = "smd_tty_driver";
516 smd_tty_driver->name = "smd";
517 smd_tty_driver->major = 0;
518 smd_tty_driver->minor_start = 0;
519 smd_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
520 smd_tty_driver->subtype = SERIAL_TYPE_NORMAL;
521 smd_tty_driver->init_termios = tty_std_termios;
522 smd_tty_driver->init_termios.c_iflag = 0;
523 smd_tty_driver->init_termios.c_oflag = 0;
524 smd_tty_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
525 smd_tty_driver->init_termios.c_lflag = 0;
526 smd_tty_driver->flags = TTY_DRIVER_RESET_TERMIOS |
527 TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
528 tty_set_operations(smd_tty_driver, &smd_tty_ops);
529
530 ret = tty_register_driver(smd_tty_driver);
Eric Holmberg513ad582011-12-14 16:27:13 -0700531 if (ret) {
532 put_tty_driver(smd_tty_driver);
533 pr_err("%s: driver registration failed %d\n", __func__, ret);
534 return ret;
Jeff Hugo4c0ba6c2011-07-15 11:47:13 -0600535 }
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700536
Eric Holmberg513ad582011-12-14 16:27:13 -0700537 for (n = 0; n < ARRAY_SIZE(smd_configs); ++n) {
538 idx = smd_configs[n].tty_dev_index;
539
540 if (smd_configs[n].dev_name == NULL)
541 smd_configs[n].dev_name = smd_configs[n].port_name;
542
543 if (idx == DS_IDX) {
544 /*
545 * DS port uses the kernel API starting with
546 * 8660 Fusion. Only register the userspace
547 * platform device for older targets.
548 */
549 int legacy_ds = 0;
550
551 legacy_ds |= cpu_is_msm7x01() || cpu_is_msm7x25();
552 legacy_ds |= cpu_is_msm7x27() || cpu_is_msm7x30();
553 legacy_ds |= cpu_is_qsd8x50() || cpu_is_msm8x55();
554 legacy_ds |= cpu_is_msm8x60() &&
555 (socinfo_get_platform_subtype() == 0x1);
556
557 if (!legacy_ds)
558 continue;
559 }
560
561 tty_register_device(smd_tty_driver, idx, 0);
562 init_completion(&smd_tty[idx].ch_allocated);
563
564 /* register platform device */
565 smd_tty[idx].driver.probe = smd_tty_dummy_probe;
566 smd_tty[idx].driver.driver.name = smd_configs[n].dev_name;
567 smd_tty[idx].driver.driver.owner = THIS_MODULE;
568 spin_lock_init(&smd_tty[idx].reset_lock);
569 smd_tty[idx].is_open = 0;
570 init_waitqueue_head(&smd_tty[idx].ch_opened_wait_queue);
571 ret = platform_driver_register(&smd_tty[idx].driver);
572
573 if (ret) {
574 pr_err("%s: init failed %d (%d)\n", __func__, idx, ret);
575 smd_tty[idx].driver.probe = NULL;
576 goto out;
577 }
578 smd_tty[idx].smd = &smd_configs[n];
579 }
580 INIT_DELAYED_WORK(&loopback_work, loopback_probe_worker);
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700581 return 0;
582
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700583out:
Eric Holmberg513ad582011-12-14 16:27:13 -0700584 /* unregister platform devices */
585 for (n = 0; n < ARRAY_SIZE(smd_configs); ++n) {
586 idx = smd_configs[n].tty_dev_index;
587
588 if (smd_tty[idx].driver.probe) {
589 platform_driver_unregister(&smd_tty[idx].driver);
590 tty_unregister_device(smd_tty_driver, idx);
591 }
592 }
593
Bryan Huntsman3f2bc4d2011-08-16 17:27:22 -0700594 tty_unregister_driver(smd_tty_driver);
595 put_tty_driver(smd_tty_driver);
596 return ret;
597}
598
599module_init(smd_tty_init);