| // SPDX-License-Identifier: GPL-2.0-only | 
 | /* | 
 |  * Miro PCM20 radio driver for Linux radio support | 
 |  * (c) 1998 Ruurd Reitsma <R.A.Reitsma@wbmt.tudelft.nl> | 
 |  * Thanks to Norberto Pellici for the ACI device interface specification | 
 |  * The API part is based on the radiotrack driver by M. Kirkwood | 
 |  * This driver relies on the aci mixer provided by the snd-miro | 
 |  * ALSA driver. | 
 |  * Look there for further info... | 
 |  * | 
 |  * From the original miro RDS sources: | 
 |  * | 
 |  *  (c) 2001 Robert Siemer <Robert.Siemer@gmx.de> | 
 |  * | 
 |  *  Many thanks to Fred Seidel <seidel@metabox.de>, the | 
 |  *  designer of the RDS decoder hardware. With his help | 
 |  *  I was able to code this driver. | 
 |  *  Thanks also to Norberto Pellicci, Dominic Mounteney | 
 |  *  <DMounteney@pinnaclesys.com> and www.teleauskunft.de | 
 |  *  for good hints on finding Fred. It was somewhat hard | 
 |  *  to locate him here in Germany... [: | 
 |  * | 
 |  * This code has been reintroduced and converted to use | 
 |  * the new V4L2 RDS API by: | 
 |  * | 
 |  * Hans Verkuil <hansverk@cisco.com> | 
 |  */ | 
 |  | 
 | #include <linux/module.h> | 
 | #include <linux/init.h> | 
 | #include <linux/io.h> | 
 | #include <linux/delay.h> | 
 | #include <linux/videodev2.h> | 
 | #include <linux/kthread.h> | 
 | #include <media/v4l2-device.h> | 
 | #include <media/v4l2-ioctl.h> | 
 | #include <media/v4l2-ctrls.h> | 
 | #include <media/v4l2-fh.h> | 
 | #include <media/v4l2-event.h> | 
 | #include <sound/aci.h> | 
 |  | 
 | #define RDS_DATASHIFT          2   /* Bit 2 */ | 
 | #define RDS_DATAMASK        (1 << RDS_DATASHIFT) | 
 | #define RDS_BUSYMASK        0x10   /* Bit 4 */ | 
 | #define RDS_CLOCKMASK       0x08   /* Bit 3 */ | 
 | #define RDS_DATA(x)         (((x) >> RDS_DATASHIFT) & 1) | 
 |  | 
 | #define RDS_STATUS      0x01 | 
 | #define RDS_STATIONNAME 0x02 | 
 | #define RDS_TEXT        0x03 | 
 | #define RDS_ALTFREQ     0x04 | 
 | #define RDS_TIMEDATE    0x05 | 
 | #define RDS_PI_CODE     0x06 | 
 | #define RDS_PTYTATP     0x07 | 
 | #define RDS_RESET       0x08 | 
 | #define RDS_RXVALUE     0x09 | 
 |  | 
 | static int radio_nr = -1; | 
 | module_param(radio_nr, int, 0); | 
 | MODULE_PARM_DESC(radio_nr, "Set radio device number (/dev/radioX).  Default: -1 (autodetect)"); | 
 |  | 
 | struct pcm20 { | 
 | 	struct v4l2_device v4l2_dev; | 
 | 	struct video_device vdev; | 
 | 	struct v4l2_ctrl_handler ctrl_handler; | 
 | 	struct v4l2_ctrl *rds_pty; | 
 | 	struct v4l2_ctrl *rds_ps_name; | 
 | 	struct v4l2_ctrl *rds_radio_test; | 
 | 	struct v4l2_ctrl *rds_ta; | 
 | 	struct v4l2_ctrl *rds_tp; | 
 | 	struct v4l2_ctrl *rds_ms; | 
 | 	/* thread for periodic RDS status checking */ | 
 | 	struct task_struct *kthread; | 
 | 	unsigned long freq; | 
 | 	u32 audmode; | 
 | 	struct snd_miro_aci *aci; | 
 | 	struct mutex lock; | 
 | }; | 
 |  | 
 | static struct pcm20 pcm20_card = { | 
 | 	.freq = 87 * 16000, | 
 | 	.audmode = V4L2_TUNER_MODE_STEREO, | 
 | }; | 
 |  | 
 |  | 
 | static int rds_waitread(struct snd_miro_aci *aci) | 
 | { | 
 | 	u8 byte; | 
 | 	int i = 2000; | 
 |  | 
 | 	do { | 
 | 		byte = inb(aci->aci_port + ACI_REG_RDS); | 
 | 		i--; | 
 | 	} while ((byte & RDS_BUSYMASK) && i); | 
 |  | 
 | 	/* | 
 | 	 * It's magic, but without this the data that you read later on | 
 | 	 * is unreliable and full of bit errors. With this 1 usec delay | 
 | 	 * everything is fine. | 
 | 	 */ | 
 | 	udelay(1); | 
 | 	return i ? byte : -1; | 
 | } | 
 |  | 
 | static int rds_rawwrite(struct snd_miro_aci *aci, u8 byte) | 
 | { | 
 | 	if (rds_waitread(aci) >= 0) { | 
 | 		outb(byte, aci->aci_port + ACI_REG_RDS); | 
 | 		return 0; | 
 | 	} | 
 | 	return -1; | 
 | } | 
 |  | 
 | static int rds_write(struct snd_miro_aci *aci, u8 byte) | 
 | { | 
 | 	u8 sendbuffer[8]; | 
 | 	int i; | 
 |  | 
 | 	for (i = 7; i >= 0; i--) | 
 | 		sendbuffer[7 - i] = (byte & (1 << i)) ? RDS_DATAMASK : 0; | 
 | 	sendbuffer[0] |= RDS_CLOCKMASK; | 
 |  | 
 | 	for (i = 0; i < 8; i++) | 
 | 		rds_rawwrite(aci, sendbuffer[i]); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int rds_readcycle_nowait(struct snd_miro_aci *aci) | 
 | { | 
 | 	outb(0, aci->aci_port + ACI_REG_RDS); | 
 | 	return rds_waitread(aci); | 
 | } | 
 |  | 
 | static int rds_readcycle(struct snd_miro_aci *aci) | 
 | { | 
 | 	if (rds_rawwrite(aci, 0) < 0) | 
 | 		return -1; | 
 | 	return rds_waitread(aci); | 
 | } | 
 |  | 
 | static int rds_ack(struct snd_miro_aci *aci) | 
 | { | 
 | 	int i = rds_readcycle(aci); | 
 |  | 
 | 	if (i < 0) | 
 | 		return -1; | 
 | 	if (i & RDS_DATAMASK) | 
 | 		return 0;  /* ACK  */ | 
 | 	return 1;  /* NACK */ | 
 | } | 
 |  | 
 | static int rds_cmd(struct snd_miro_aci *aci, u8 cmd, u8 databuffer[], u8 datasize) | 
 | { | 
 | 	int i, j; | 
 |  | 
 | 	rds_write(aci, cmd); | 
 |  | 
 | 	/* RDS_RESET doesn't need further processing */ | 
 | 	if (cmd == RDS_RESET) | 
 | 		return 0; | 
 | 	if (rds_ack(aci)) | 
 | 		return -EIO; | 
 | 	if (datasize == 0) | 
 | 		return 0; | 
 |  | 
 | 	/* to be able to use rds_readcycle_nowait() | 
 | 	   I have to waitread() here */ | 
 | 	if (rds_waitread(aci) < 0) | 
 | 		return -1; | 
 |  | 
 | 	memset(databuffer, 0, datasize); | 
 |  | 
 | 	for (i = 0; i < 8 * datasize; i++) { | 
 | 		j = rds_readcycle_nowait(aci); | 
 | 		if (j < 0) | 
 | 			return -EIO; | 
 | 		databuffer[i / 8] |= RDS_DATA(j) << (7 - (i % 8)); | 
 | 	} | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int pcm20_setfreq(struct pcm20 *dev, unsigned long freq) | 
 | { | 
 | 	unsigned char freql; | 
 | 	unsigned char freqh; | 
 | 	struct snd_miro_aci *aci = dev->aci; | 
 |  | 
 | 	freq /= 160; | 
 | 	if (!(aci->aci_version == 0x07 || aci->aci_version >= 0xb0)) | 
 | 		freq /= 10;  /* I don't know exactly which version | 
 | 			      * needs this hack */ | 
 | 	freql = freq & 0xff; | 
 | 	freqh = freq >> 8; | 
 |  | 
 | 	rds_cmd(aci, RDS_RESET, NULL, 0); | 
 | 	return snd_aci_cmd(aci, ACI_WRITE_TUNE, freql, freqh); | 
 | } | 
 |  | 
 | static int vidioc_querycap(struct file *file, void *priv, | 
 | 				struct v4l2_capability *v) | 
 | { | 
 | 	strscpy(v->driver, "Miro PCM20", sizeof(v->driver)); | 
 | 	strscpy(v->card, "Miro PCM20", sizeof(v->card)); | 
 | 	strscpy(v->bus_info, "ISA:radio-miropcm20", sizeof(v->bus_info)); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static bool sanitize(char *p, int size) | 
 | { | 
 | 	int i; | 
 | 	bool ret = true; | 
 |  | 
 | 	for (i = 0; i < size; i++) { | 
 | 		if (p[i] < 32) { | 
 | 			p[i] = ' '; | 
 | 			ret = false; | 
 | 		} | 
 | 	} | 
 | 	return ret; | 
 | } | 
 |  | 
 | static int vidioc_g_tuner(struct file *file, void *priv, | 
 | 				struct v4l2_tuner *v) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 | 	int res; | 
 | 	u8 buf; | 
 |  | 
 | 	if (v->index) | 
 | 		return -EINVAL; | 
 | 	strscpy(v->name, "FM", sizeof(v->name)); | 
 | 	v->type = V4L2_TUNER_RADIO; | 
 | 	v->rangelow = 87*16000; | 
 | 	v->rangehigh = 108*16000; | 
 | 	res = snd_aci_cmd(dev->aci, ACI_READ_TUNERSTATION, -1, -1); | 
 | 	v->signal = (res & 0x80) ? 0 : 0xffff; | 
 | 	/* Note: stereo detection does not work if the audio is muted, | 
 | 	   it will default to mono in that case. */ | 
 | 	res = snd_aci_cmd(dev->aci, ACI_READ_TUNERSTEREO, -1, -1); | 
 | 	v->rxsubchans = (res & 0x40) ? V4L2_TUNER_SUB_MONO : | 
 | 					V4L2_TUNER_SUB_STEREO; | 
 | 	v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | | 
 | 			V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_CONTROLS; | 
 | 	v->audmode = dev->audmode; | 
 | 	res = rds_cmd(dev->aci, RDS_RXVALUE, &buf, 1); | 
 | 	if (res >= 0 && buf) | 
 | 		v->rxsubchans |= V4L2_TUNER_SUB_RDS; | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int vidioc_s_tuner(struct file *file, void *priv, | 
 | 				const struct v4l2_tuner *v) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 |  | 
 | 	if (v->index) | 
 | 		return -EINVAL; | 
 | 	if (v->audmode > V4L2_TUNER_MODE_STEREO) | 
 | 		dev->audmode = V4L2_TUNER_MODE_STEREO; | 
 | 	else | 
 | 		dev->audmode = v->audmode; | 
 | 	snd_aci_cmd(dev->aci, ACI_SET_TUNERMONO, | 
 | 			dev->audmode == V4L2_TUNER_MODE_MONO, -1); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int vidioc_g_frequency(struct file *file, void *priv, | 
 | 				struct v4l2_frequency *f) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 |  | 
 | 	if (f->tuner != 0) | 
 | 		return -EINVAL; | 
 |  | 
 | 	f->type = V4L2_TUNER_RADIO; | 
 | 	f->frequency = dev->freq; | 
 | 	return 0; | 
 | } | 
 |  | 
 |  | 
 | static int vidioc_s_frequency(struct file *file, void *priv, | 
 | 				const struct v4l2_frequency *f) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 |  | 
 | 	if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO) | 
 | 		return -EINVAL; | 
 |  | 
 | 	dev->freq = clamp_t(u32, f->frequency, 87 * 16000U, 108 * 16000U); | 
 | 	pcm20_setfreq(dev, dev->freq); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int pcm20_s_ctrl(struct v4l2_ctrl *ctrl) | 
 | { | 
 | 	struct pcm20 *dev = container_of(ctrl->handler, struct pcm20, ctrl_handler); | 
 |  | 
 | 	switch (ctrl->id) { | 
 | 	case V4L2_CID_AUDIO_MUTE: | 
 | 		snd_aci_cmd(dev->aci, ACI_SET_TUNERMUTE, ctrl->val, -1); | 
 | 		return 0; | 
 | 	} | 
 | 	return -EINVAL; | 
 | } | 
 |  | 
 | static int pcm20_thread(void *data) | 
 | { | 
 | 	struct pcm20 *dev = data; | 
 | 	const unsigned no_rds_start_counter = 5; | 
 | 	const unsigned sleep_msecs = 2000; | 
 | 	unsigned no_rds_counter = no_rds_start_counter; | 
 |  | 
 | 	for (;;) { | 
 | 		char text_buffer[66]; | 
 | 		u8 buf; | 
 | 		int res; | 
 |  | 
 | 		msleep_interruptible(sleep_msecs); | 
 |  | 
 | 		if (kthread_should_stop()) | 
 | 			break; | 
 |  | 
 | 		res = rds_cmd(dev->aci, RDS_RXVALUE, &buf, 1); | 
 | 		if (res) | 
 | 			continue; | 
 | 		if (buf == 0) { | 
 | 			if (no_rds_counter == 0) | 
 | 				continue; | 
 | 			no_rds_counter--; | 
 | 			if (no_rds_counter) | 
 | 				continue; | 
 |  | 
 | 			/* | 
 | 			 * No RDS seen for no_rds_start_counter * sleep_msecs | 
 | 			 * milliseconds, clear all RDS controls to their | 
 | 			 * default values. | 
 | 			 */ | 
 | 			v4l2_ctrl_s_ctrl_string(dev->rds_ps_name, ""); | 
 | 			v4l2_ctrl_s_ctrl(dev->rds_ms, 1); | 
 | 			v4l2_ctrl_s_ctrl(dev->rds_ta, 0); | 
 | 			v4l2_ctrl_s_ctrl(dev->rds_tp, 0); | 
 | 			v4l2_ctrl_s_ctrl(dev->rds_pty, 0); | 
 | 			v4l2_ctrl_s_ctrl_string(dev->rds_radio_test, ""); | 
 | 			continue; | 
 | 		} | 
 | 		no_rds_counter = no_rds_start_counter; | 
 |  | 
 | 		res = rds_cmd(dev->aci, RDS_STATUS, &buf, 1); | 
 | 		if (res) | 
 | 			continue; | 
 | 		if ((buf >> 3) & 1) { | 
 | 			res = rds_cmd(dev->aci, RDS_STATIONNAME, text_buffer, 8); | 
 | 			text_buffer[8] = 0; | 
 | 			if (!res && sanitize(text_buffer, 8)) | 
 | 				v4l2_ctrl_s_ctrl_string(dev->rds_ps_name, text_buffer); | 
 | 		} | 
 | 		if ((buf >> 6) & 1) { | 
 | 			u8 pty; | 
 |  | 
 | 			res = rds_cmd(dev->aci, RDS_PTYTATP, &pty, 1); | 
 | 			if (!res) { | 
 | 				v4l2_ctrl_s_ctrl(dev->rds_ms, !!(pty & 0x01)); | 
 | 				v4l2_ctrl_s_ctrl(dev->rds_ta, !!(pty & 0x02)); | 
 | 				v4l2_ctrl_s_ctrl(dev->rds_tp, !!(pty & 0x80)); | 
 | 				v4l2_ctrl_s_ctrl(dev->rds_pty, (pty >> 2) & 0x1f); | 
 | 			} | 
 | 		} | 
 | 		if ((buf >> 4) & 1) { | 
 | 			res = rds_cmd(dev->aci, RDS_TEXT, text_buffer, 65); | 
 | 			text_buffer[65] = 0; | 
 | 			if (!res && sanitize(text_buffer + 1, 64)) | 
 | 				v4l2_ctrl_s_ctrl_string(dev->rds_radio_test, text_buffer + 1); | 
 | 		} | 
 | 	} | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int pcm20_open(struct file *file) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 | 	int res = v4l2_fh_open(file); | 
 |  | 
 | 	if (!res && v4l2_fh_is_singular_file(file) && | 
 | 	    IS_ERR_OR_NULL(dev->kthread)) { | 
 | 		dev->kthread = kthread_run(pcm20_thread, dev, "%s", | 
 | 					   dev->v4l2_dev.name); | 
 | 		if (IS_ERR(dev->kthread)) { | 
 | 			v4l2_err(&dev->v4l2_dev, "kernel_thread() failed\n"); | 
 | 			v4l2_fh_release(file); | 
 | 			return PTR_ERR(dev->kthread); | 
 | 		} | 
 | 	} | 
 | 	return res; | 
 | } | 
 |  | 
 | static int pcm20_release(struct file *file) | 
 | { | 
 | 	struct pcm20 *dev = video_drvdata(file); | 
 |  | 
 | 	if (v4l2_fh_is_singular_file(file) && !IS_ERR_OR_NULL(dev->kthread)) { | 
 | 		kthread_stop(dev->kthread); | 
 | 		dev->kthread = NULL; | 
 | 	} | 
 | 	return v4l2_fh_release(file); | 
 | } | 
 |  | 
 | static const struct v4l2_file_operations pcm20_fops = { | 
 | 	.owner		= THIS_MODULE, | 
 | 	.open		= pcm20_open, | 
 | 	.poll		= v4l2_ctrl_poll, | 
 | 	.release	= pcm20_release, | 
 | 	.unlocked_ioctl	= video_ioctl2, | 
 | }; | 
 |  | 
 | static const struct v4l2_ioctl_ops pcm20_ioctl_ops = { | 
 | 	.vidioc_querycap    = vidioc_querycap, | 
 | 	.vidioc_g_tuner     = vidioc_g_tuner, | 
 | 	.vidioc_s_tuner     = vidioc_s_tuner, | 
 | 	.vidioc_g_frequency = vidioc_g_frequency, | 
 | 	.vidioc_s_frequency = vidioc_s_frequency, | 
 | 	.vidioc_log_status  = v4l2_ctrl_log_status, | 
 | 	.vidioc_subscribe_event = v4l2_ctrl_subscribe_event, | 
 | 	.vidioc_unsubscribe_event = v4l2_event_unsubscribe, | 
 | }; | 
 |  | 
 | static const struct v4l2_ctrl_ops pcm20_ctrl_ops = { | 
 | 	.s_ctrl = pcm20_s_ctrl, | 
 | }; | 
 |  | 
 | static int __init pcm20_init(void) | 
 | { | 
 | 	struct pcm20 *dev = &pcm20_card; | 
 | 	struct v4l2_device *v4l2_dev = &dev->v4l2_dev; | 
 | 	struct v4l2_ctrl_handler *hdl; | 
 | 	int res; | 
 |  | 
 | 	dev->aci = snd_aci_get_aci(); | 
 | 	if (dev->aci == NULL) { | 
 | 		v4l2_err(v4l2_dev, | 
 | 			 "you must load the snd-miro driver first!\n"); | 
 | 		return -ENODEV; | 
 | 	} | 
 | 	strscpy(v4l2_dev->name, "radio-miropcm20", sizeof(v4l2_dev->name)); | 
 | 	mutex_init(&dev->lock); | 
 |  | 
 | 	res = v4l2_device_register(NULL, v4l2_dev); | 
 | 	if (res < 0) { | 
 | 		v4l2_err(v4l2_dev, "could not register v4l2_device\n"); | 
 | 		return -EINVAL; | 
 | 	} | 
 |  | 
 | 	hdl = &dev->ctrl_handler; | 
 | 	v4l2_ctrl_handler_init(hdl, 7); | 
 | 	v4l2_ctrl_new_std(hdl, &pcm20_ctrl_ops, | 
 | 			V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1); | 
 | 	dev->rds_pty = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_PTY, 0, 0x1f, 1, 0); | 
 | 	dev->rds_ps_name = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_PS_NAME, 0, 8, 8, 0); | 
 | 	dev->rds_radio_test = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_RADIO_TEXT, 0, 64, 64, 0); | 
 | 	dev->rds_ta = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_TRAFFIC_ANNOUNCEMENT, 0, 1, 1, 0); | 
 | 	dev->rds_tp = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_TRAFFIC_PROGRAM, 0, 1, 1, 0); | 
 | 	dev->rds_ms = v4l2_ctrl_new_std(hdl, NULL, | 
 | 			V4L2_CID_RDS_RX_MUSIC_SPEECH, 0, 1, 1, 1); | 
 | 	v4l2_dev->ctrl_handler = hdl; | 
 | 	if (hdl->error) { | 
 | 		res = hdl->error; | 
 | 		v4l2_err(v4l2_dev, "Could not register control\n"); | 
 | 		goto err_hdl; | 
 | 	} | 
 | 	strscpy(dev->vdev.name, v4l2_dev->name, sizeof(dev->vdev.name)); | 
 | 	dev->vdev.v4l2_dev = v4l2_dev; | 
 | 	dev->vdev.fops = &pcm20_fops; | 
 | 	dev->vdev.ioctl_ops = &pcm20_ioctl_ops; | 
 | 	dev->vdev.release = video_device_release_empty; | 
 | 	dev->vdev.lock = &dev->lock; | 
 | 	dev->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO | | 
 | 				V4L2_CAP_RDS_CAPTURE; | 
 | 	video_set_drvdata(&dev->vdev, dev); | 
 | 	snd_aci_cmd(dev->aci, ACI_SET_TUNERMONO, | 
 | 			dev->audmode == V4L2_TUNER_MODE_MONO, -1); | 
 | 	pcm20_setfreq(dev, dev->freq); | 
 |  | 
 | 	if (video_register_device(&dev->vdev, VFL_TYPE_RADIO, radio_nr) < 0) | 
 | 		goto err_hdl; | 
 |  | 
 | 	v4l2_info(v4l2_dev, "Mirosound PCM20 Radio tuner\n"); | 
 | 	return 0; | 
 | err_hdl: | 
 | 	v4l2_ctrl_handler_free(hdl); | 
 | 	v4l2_device_unregister(v4l2_dev); | 
 | 	return -EINVAL; | 
 | } | 
 |  | 
 | MODULE_AUTHOR("Ruurd Reitsma, Krzysztof Helt"); | 
 | MODULE_DESCRIPTION("A driver for the Miro PCM20 radio card."); | 
 | MODULE_LICENSE("GPL"); | 
 |  | 
 | static void __exit pcm20_cleanup(void) | 
 | { | 
 | 	struct pcm20 *dev = &pcm20_card; | 
 |  | 
 | 	video_unregister_device(&dev->vdev); | 
 | 	snd_aci_cmd(dev->aci, ACI_SET_TUNERMUTE, 1, -1); | 
 | 	v4l2_ctrl_handler_free(&dev->ctrl_handler); | 
 | 	v4l2_device_unregister(&dev->v4l2_dev); | 
 | } | 
 |  | 
 | module_init(pcm20_init); | 
 | module_exit(pcm20_cleanup); |