| /* | 
 |  * interface to user space for the gigaset driver | 
 |  * | 
 |  * Copyright (c) 2004 by Hansjoerg Lipp <hjlipp@web.de> | 
 |  * | 
 |  * ===================================================================== | 
 |  *    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. | 
 |  * ===================================================================== | 
 |  */ | 
 |  | 
 | #include "gigaset.h" | 
 | #include <linux/gigaset_dev.h> | 
 | #include <linux/tty_flip.h> | 
 | #include <linux/module.h> | 
 |  | 
 | /*** our ioctls ***/ | 
 |  | 
 | static int if_lock(struct cardstate *cs, int *arg) | 
 | { | 
 | 	int cmd = *arg; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: if_lock (%d)", cs->minor_index, cmd); | 
 |  | 
 | 	if (cmd > 1) | 
 | 		return -EINVAL; | 
 |  | 
 | 	if (cmd < 0) { | 
 | 		*arg = cs->mstate == MS_LOCKED; | 
 | 		return 0; | 
 | 	} | 
 |  | 
 | 	if (!cmd && cs->mstate == MS_LOCKED && cs->connected) { | 
 | 		cs->ops->set_modem_ctrl(cs, 0, TIOCM_DTR | TIOCM_RTS); | 
 | 		cs->ops->baud_rate(cs, B115200); | 
 | 		cs->ops->set_line_ctrl(cs, CS8); | 
 | 		cs->control_state = TIOCM_DTR | TIOCM_RTS; | 
 | 	} | 
 |  | 
 | 	cs->waiting = 1; | 
 | 	if (!gigaset_add_event(cs, &cs->at_state, EV_IF_LOCK, | 
 | 			       NULL, cmd, NULL)) { | 
 | 		cs->waiting = 0; | 
 | 		return -ENOMEM; | 
 | 	} | 
 | 	gigaset_schedule_event(cs); | 
 |  | 
 | 	wait_event(cs->waitqueue, !cs->waiting); | 
 |  | 
 | 	if (cs->cmd_result >= 0) { | 
 | 		*arg = cs->cmd_result; | 
 | 		return 0; | 
 | 	} | 
 |  | 
 | 	return cs->cmd_result; | 
 | } | 
 |  | 
 | static int if_version(struct cardstate *cs, unsigned arg[4]) | 
 | { | 
 | 	static const unsigned version[4] = GIG_VERSION; | 
 | 	static const unsigned compat[4] = GIG_COMPAT; | 
 | 	unsigned cmd = arg[0]; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: if_version (%d)", cs->minor_index, cmd); | 
 |  | 
 | 	switch (cmd) { | 
 | 	case GIGVER_DRIVER: | 
 | 		memcpy(arg, version, sizeof version); | 
 | 		return 0; | 
 | 	case GIGVER_COMPAT: | 
 | 		memcpy(arg, compat, sizeof compat); | 
 | 		return 0; | 
 | 	case GIGVER_FWBASE: | 
 | 		cs->waiting = 1; | 
 | 		if (!gigaset_add_event(cs, &cs->at_state, EV_IF_VER, | 
 | 				       NULL, 0, arg)) { | 
 | 			cs->waiting = 0; | 
 | 			return -ENOMEM; | 
 | 		} | 
 | 		gigaset_schedule_event(cs); | 
 |  | 
 | 		wait_event(cs->waitqueue, !cs->waiting); | 
 |  | 
 | 		if (cs->cmd_result >= 0) | 
 | 			return 0; | 
 |  | 
 | 		return cs->cmd_result; | 
 | 	default: | 
 | 		return -EINVAL; | 
 | 	} | 
 | } | 
 |  | 
 | static int if_config(struct cardstate *cs, int *arg) | 
 | { | 
 | 	gig_dbg(DEBUG_IF, "%u: if_config (%d)", cs->minor_index, *arg); | 
 |  | 
 | 	if (*arg != 1) | 
 | 		return -EINVAL; | 
 |  | 
 | 	if (cs->mstate != MS_LOCKED) | 
 | 		return -EBUSY; | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		pr_err("%s: not connected\n", __func__); | 
 | 		return -ENODEV; | 
 | 	} | 
 |  | 
 | 	*arg = 0; | 
 | 	return gigaset_enterconfigmode(cs); | 
 | } | 
 |  | 
 | /*** the terminal driver ***/ | 
 |  | 
 | static int if_open(struct tty_struct *tty, struct file *filp) | 
 | { | 
 | 	struct cardstate *cs; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%d+%d: %s()", | 
 | 		tty->driver->minor_start, tty->index, __func__); | 
 |  | 
 | 	cs = gigaset_get_cs_by_tty(tty); | 
 | 	if (!cs || !try_module_get(cs->driver->owner)) | 
 | 		return -ENODEV; | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) { | 
 | 		module_put(cs->driver->owner); | 
 | 		return -ERESTARTSYS; | 
 | 	} | 
 | 	tty->driver_data = cs; | 
 |  | 
 | 	++cs->port.count; | 
 |  | 
 | 	if (cs->port.count == 1) { | 
 | 		tty_port_tty_set(&cs->port, tty); | 
 | 		cs->port.low_latency = 1; | 
 | 	} | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void if_close(struct tty_struct *tty, struct file *filp) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 |  | 
 | 	if (!cs) { /* happens if we didn't find cs in open */ | 
 | 		gig_dbg(DEBUG_IF, "%s: no cardstate", __func__); | 
 | 		return; | 
 | 	} | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 |  | 
 | 	if (!cs->connected) | 
 | 		gig_dbg(DEBUG_IF, "not connected");	/* nothing to do */ | 
 | 	else if (!cs->port.count) | 
 | 		dev_warn(cs->dev, "%s: device not opened\n", __func__); | 
 | 	else if (!--cs->port.count) | 
 | 		tty_port_tty_set(&cs->port, NULL); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	module_put(cs->driver->owner); | 
 | } | 
 |  | 
 | static int if_ioctl(struct tty_struct *tty, | 
 | 		    unsigned int cmd, unsigned long arg) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	int retval = -ENODEV; | 
 | 	int int_arg; | 
 | 	unsigned char buf[6]; | 
 | 	unsigned version[4]; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s(0x%x)", cs->minor_index, __func__, cmd); | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) | 
 | 		return -ERESTARTSYS; | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 		retval = -ENODEV; | 
 | 	} else { | 
 | 		retval = 0; | 
 | 		switch (cmd) { | 
 | 		case GIGASET_REDIR: | 
 | 			retval = get_user(int_arg, (int __user *) arg); | 
 | 			if (retval >= 0) | 
 | 				retval = if_lock(cs, &int_arg); | 
 | 			if (retval >= 0) | 
 | 				retval = put_user(int_arg, (int __user *) arg); | 
 | 			break; | 
 | 		case GIGASET_CONFIG: | 
 | 			retval = get_user(int_arg, (int __user *) arg); | 
 | 			if (retval >= 0) | 
 | 				retval = if_config(cs, &int_arg); | 
 | 			if (retval >= 0) | 
 | 				retval = put_user(int_arg, (int __user *) arg); | 
 | 			break; | 
 | 		case GIGASET_BRKCHARS: | 
 | 			retval = copy_from_user(&buf, | 
 | 						(const unsigned char __user *) arg, 6) | 
 | 				? -EFAULT : 0; | 
 | 			if (retval >= 0) { | 
 | 				gigaset_dbg_buffer(DEBUG_IF, "GIGASET_BRKCHARS", | 
 | 						   6, (const unsigned char *) arg); | 
 | 				retval = cs->ops->brkchars(cs, buf); | 
 | 			} | 
 | 			break; | 
 | 		case GIGASET_VERSION: | 
 | 			retval = copy_from_user(version, | 
 | 						(unsigned __user *) arg, sizeof version) | 
 | 				? -EFAULT : 0; | 
 | 			if (retval >= 0) | 
 | 				retval = if_version(cs, version); | 
 | 			if (retval >= 0) | 
 | 				retval = copy_to_user((unsigned __user *) arg, | 
 | 						      version, sizeof version) | 
 | 					? -EFAULT : 0; | 
 | 			break; | 
 | 		default: | 
 | 			gig_dbg(DEBUG_IF, "%s: arg not supported - 0x%04x", | 
 | 				__func__, cmd); | 
 | 			retval = -ENOIOCTLCMD; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	return retval; | 
 | } | 
 |  | 
 | static int if_tiocmget(struct tty_struct *tty) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	int retval; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) | 
 | 		return -ERESTARTSYS; | 
 |  | 
 | 	retval = cs->control_state & (TIOCM_RTS | TIOCM_DTR); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	return retval; | 
 | } | 
 |  | 
 | static int if_tiocmset(struct tty_struct *tty, | 
 | 		       unsigned int set, unsigned int clear) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	int retval; | 
 | 	unsigned mc; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s(0x%x, 0x%x)", | 
 | 		cs->minor_index, __func__, set, clear); | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) | 
 | 		return -ERESTARTSYS; | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 		retval = -ENODEV; | 
 | 	} else { | 
 | 		mc = (cs->control_state | set) & ~clear & (TIOCM_RTS | TIOCM_DTR); | 
 | 		retval = cs->ops->set_modem_ctrl(cs, cs->control_state, mc); | 
 | 		cs->control_state = mc; | 
 | 	} | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	return retval; | 
 | } | 
 |  | 
 | static int if_write(struct tty_struct *tty, const unsigned char *buf, int count) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	struct cmdbuf_t *cb; | 
 | 	int retval; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) | 
 | 		return -ERESTARTSYS; | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 		retval = -ENODEV; | 
 | 		goto done; | 
 | 	} | 
 | 	if (cs->mstate != MS_LOCKED) { | 
 | 		dev_warn(cs->dev, "can't write to unlocked device\n"); | 
 | 		retval = -EBUSY; | 
 | 		goto done; | 
 | 	} | 
 | 	if (count <= 0) { | 
 | 		/* nothing to do */ | 
 | 		retval = 0; | 
 | 		goto done; | 
 | 	} | 
 |  | 
 | 	cb = kmalloc(sizeof(struct cmdbuf_t) + count, GFP_KERNEL); | 
 | 	if (!cb) { | 
 | 		dev_err(cs->dev, "%s: out of memory\n", __func__); | 
 | 		retval = -ENOMEM; | 
 | 		goto done; | 
 | 	} | 
 |  | 
 | 	memcpy(cb->buf, buf, count); | 
 | 	cb->len = count; | 
 | 	cb->offset = 0; | 
 | 	cb->next = NULL; | 
 | 	cb->wake_tasklet = &cs->if_wake_tasklet; | 
 | 	retval = cs->ops->write_cmd(cs, cb); | 
 | done: | 
 | 	mutex_unlock(&cs->mutex); | 
 | 	return retval; | 
 | } | 
 |  | 
 | static int if_write_room(struct tty_struct *tty) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	int retval; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	if (mutex_lock_interruptible(&cs->mutex)) | 
 | 		return -ERESTARTSYS; | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 		retval = -ENODEV; | 
 | 	} else if (cs->mstate != MS_LOCKED) { | 
 | 		dev_warn(cs->dev, "can't write to unlocked device\n"); | 
 | 		retval = -EBUSY; | 
 | 	} else | 
 | 		retval = cs->ops->write_room(cs); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	return retval; | 
 | } | 
 |  | 
 | static int if_chars_in_buffer(struct tty_struct *tty) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	int retval = 0; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 |  | 
 | 	if (!cs->connected) | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 	else if (cs->mstate != MS_LOCKED) | 
 | 		dev_warn(cs->dev, "can't write to unlocked device\n"); | 
 | 	else | 
 | 		retval = cs->ops->chars_in_buffer(cs); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 |  | 
 | 	return retval; | 
 | } | 
 |  | 
 | static void if_throttle(struct tty_struct *tty) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 |  | 
 | 	if (!cs->connected) | 
 | 		gig_dbg(DEBUG_IF, "not connected");	/* nothing to do */ | 
 | 	else | 
 | 		gig_dbg(DEBUG_IF, "%s: not implemented\n", __func__); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 | } | 
 |  | 
 | static void if_unthrottle(struct tty_struct *tty) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 |  | 
 | 	if (!cs->connected) | 
 | 		gig_dbg(DEBUG_IF, "not connected");	/* nothing to do */ | 
 | 	else | 
 | 		gig_dbg(DEBUG_IF, "%s: not implemented\n", __func__); | 
 |  | 
 | 	mutex_unlock(&cs->mutex); | 
 | } | 
 |  | 
 | static void if_set_termios(struct tty_struct *tty, struct ktermios *old) | 
 | { | 
 | 	struct cardstate *cs = tty->driver_data; | 
 | 	unsigned int iflag; | 
 | 	unsigned int cflag; | 
 | 	unsigned int old_cflag; | 
 | 	unsigned int control_state, new_state; | 
 |  | 
 | 	gig_dbg(DEBUG_IF, "%u: %s()", cs->minor_index, __func__); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 |  | 
 | 	if (!cs->connected) { | 
 | 		gig_dbg(DEBUG_IF, "not connected"); | 
 | 		goto out; | 
 | 	} | 
 |  | 
 | 	iflag = tty->termios.c_iflag; | 
 | 	cflag = tty->termios.c_cflag; | 
 | 	old_cflag = old ? old->c_cflag : cflag; | 
 | 	gig_dbg(DEBUG_IF, "%u: iflag %x cflag %x old %x", | 
 | 		cs->minor_index, iflag, cflag, old_cflag); | 
 |  | 
 | 	/* get a local copy of the current port settings */ | 
 | 	control_state = cs->control_state; | 
 |  | 
 | 	/* | 
 | 	 * Update baud rate. | 
 | 	 * Do not attempt to cache old rates and skip settings, | 
 | 	 * disconnects screw such tricks up completely. | 
 | 	 * Premature optimization is the root of all evil. | 
 | 	 */ | 
 |  | 
 | 	/* reassert DTR and (maybe) RTS on transition from B0 */ | 
 | 	if ((old_cflag & CBAUD) == B0) { | 
 | 		new_state = control_state | TIOCM_DTR; | 
 | 		/* don't set RTS if using hardware flow control */ | 
 | 		if (!(old_cflag & CRTSCTS)) | 
 | 			new_state |= TIOCM_RTS; | 
 | 		gig_dbg(DEBUG_IF, "%u: from B0 - set DTR%s", | 
 | 			cs->minor_index, | 
 | 			(new_state & TIOCM_RTS) ? " only" : "/RTS"); | 
 | 		cs->ops->set_modem_ctrl(cs, control_state, new_state); | 
 | 		control_state = new_state; | 
 | 	} | 
 |  | 
 | 	cs->ops->baud_rate(cs, cflag & CBAUD); | 
 |  | 
 | 	if ((cflag & CBAUD) == B0) { | 
 | 		/* Drop RTS and DTR */ | 
 | 		gig_dbg(DEBUG_IF, "%u: to B0 - drop DTR/RTS", cs->minor_index); | 
 | 		new_state = control_state & ~(TIOCM_DTR | TIOCM_RTS); | 
 | 		cs->ops->set_modem_ctrl(cs, control_state, new_state); | 
 | 		control_state = new_state; | 
 | 	} | 
 |  | 
 | 	/* | 
 | 	 * Update line control register (LCR) | 
 | 	 */ | 
 |  | 
 | 	cs->ops->set_line_ctrl(cs, cflag); | 
 |  | 
 | 	/* save off the modified port settings */ | 
 | 	cs->control_state = control_state; | 
 |  | 
 | out: | 
 | 	mutex_unlock(&cs->mutex); | 
 | } | 
 |  | 
 | static const struct tty_operations if_ops = { | 
 | 	.open =			if_open, | 
 | 	.close =		if_close, | 
 | 	.ioctl =		if_ioctl, | 
 | 	.write =		if_write, | 
 | 	.write_room =		if_write_room, | 
 | 	.chars_in_buffer =	if_chars_in_buffer, | 
 | 	.set_termios =		if_set_termios, | 
 | 	.throttle =		if_throttle, | 
 | 	.unthrottle =		if_unthrottle, | 
 | 	.tiocmget =		if_tiocmget, | 
 | 	.tiocmset =		if_tiocmset, | 
 | }; | 
 |  | 
 |  | 
 | /* wakeup tasklet for the write operation */ | 
 | static void if_wake(unsigned long data) | 
 | { | 
 | 	struct cardstate *cs = (struct cardstate *)data; | 
 |  | 
 | 	tty_port_tty_wakeup(&cs->port); | 
 | } | 
 |  | 
 | /*** interface to common ***/ | 
 |  | 
 | void gigaset_if_init(struct cardstate *cs) | 
 | { | 
 | 	struct gigaset_driver *drv; | 
 |  | 
 | 	drv = cs->driver; | 
 | 	if (!drv->have_tty) | 
 | 		return; | 
 |  | 
 | 	tasklet_init(&cs->if_wake_tasklet, if_wake, (unsigned long) cs); | 
 |  | 
 | 	mutex_lock(&cs->mutex); | 
 | 	cs->tty_dev = tty_port_register_device(&cs->port, drv->tty, | 
 | 			cs->minor_index, NULL); | 
 |  | 
 | 	if (!IS_ERR(cs->tty_dev)) | 
 | 		dev_set_drvdata(cs->tty_dev, cs); | 
 | 	else { | 
 | 		pr_warning("could not register device to the tty subsystem\n"); | 
 | 		cs->tty_dev = NULL; | 
 | 	} | 
 | 	mutex_unlock(&cs->mutex); | 
 | } | 
 |  | 
 | void gigaset_if_free(struct cardstate *cs) | 
 | { | 
 | 	struct gigaset_driver *drv; | 
 |  | 
 | 	drv = cs->driver; | 
 | 	if (!drv->have_tty) | 
 | 		return; | 
 |  | 
 | 	tasklet_disable(&cs->if_wake_tasklet); | 
 | 	tasklet_kill(&cs->if_wake_tasklet); | 
 | 	cs->tty_dev = NULL; | 
 | 	tty_unregister_device(drv->tty, cs->minor_index); | 
 | } | 
 |  | 
 | /** | 
 |  * gigaset_if_receive() - pass a received block of data to the tty device | 
 |  * @cs:		device descriptor structure. | 
 |  * @buffer:	received data. | 
 |  * @len:	number of bytes received. | 
 |  * | 
 |  * Called by asyncdata/isocdata if a block of data received from the | 
 |  * device must be sent to userspace through the ttyG* device. | 
 |  */ | 
 | void gigaset_if_receive(struct cardstate *cs, | 
 | 			unsigned char *buffer, size_t len) | 
 | { | 
 | 	tty_insert_flip_string(&cs->port, buffer, len); | 
 | 	tty_flip_buffer_push(&cs->port); | 
 | } | 
 | EXPORT_SYMBOL_GPL(gigaset_if_receive); | 
 |  | 
 | /* gigaset_if_initdriver | 
 |  * Initialize tty interface. | 
 |  * parameters: | 
 |  *	drv		Driver | 
 |  *	procname	Name of the driver (e.g. for /proc/tty/drivers) | 
 |  *	devname		Name of the device files (prefix without minor number) | 
 |  */ | 
 | void gigaset_if_initdriver(struct gigaset_driver *drv, const char *procname, | 
 | 			   const char *devname) | 
 | { | 
 | 	int ret; | 
 | 	struct tty_driver *tty; | 
 |  | 
 | 	drv->have_tty = 0; | 
 |  | 
 | 	drv->tty = tty = alloc_tty_driver(drv->minors); | 
 | 	if (tty == NULL) | 
 | 		goto enomem; | 
 |  | 
 | 	tty->type =		TTY_DRIVER_TYPE_SERIAL; | 
 | 	tty->subtype =		SERIAL_TYPE_NORMAL; | 
 | 	tty->flags =		TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; | 
 |  | 
 | 	tty->driver_name =	procname; | 
 | 	tty->name =		devname; | 
 | 	tty->minor_start =	drv->minor; | 
 |  | 
 | 	tty->init_termios          = tty_std_termios; | 
 | 	tty->init_termios.c_cflag  = B9600 | CS8 | CREAD | HUPCL | CLOCAL; | 
 | 	tty_set_operations(tty, &if_ops); | 
 |  | 
 | 	ret = tty_register_driver(tty); | 
 | 	if (ret < 0) { | 
 | 		pr_err("error %d registering tty driver\n", ret); | 
 | 		goto error; | 
 | 	} | 
 | 	gig_dbg(DEBUG_IF, "tty driver initialized"); | 
 | 	drv->have_tty = 1; | 
 | 	return; | 
 |  | 
 | enomem: | 
 | 	pr_err("out of memory\n"); | 
 | error: | 
 | 	if (drv->tty) | 
 | 		put_tty_driver(drv->tty); | 
 | } | 
 |  | 
 | void gigaset_if_freedriver(struct gigaset_driver *drv) | 
 | { | 
 | 	if (!drv->have_tty) | 
 | 		return; | 
 |  | 
 | 	drv->have_tty = 0; | 
 | 	tty_unregister_driver(drv->tty); | 
 | 	put_tty_driver(drv->tty); | 
 | } |