Commit | Line | Data |
---|---|---|
1da177e4 LT |
1 | /* zoltrix radio plus driver for Linux radio support |
2 | * (c) 1998 C. van Schaik <carl@leg.uct.ac.za> | |
3 | * | |
4286c6f6 | 4 | * BUGS |
1da177e4 LT |
5 | * Due to the inconsistency in reading from the signal flags |
6 | * it is difficult to get an accurate tuned signal. | |
7 | * | |
8 | * It seems that the card is not linear to 0 volume. It cuts off | |
9 | * at a low volume, and it is not possible (at least I have not found) | |
10 | * to get fine volume control over the low volume range. | |
11 | * | |
12 | * Some code derived from code by Romolo Manfredini | |
13 | * romolo@bicnet.it | |
14 | * | |
15 | * 1999-05-06 - (C. van Schaik) | |
16 | * - Make signal strength and stereo scans | |
4286c6f6 | 17 | * kinder to cpu while in delay |
1da177e4 LT |
18 | * 1999-01-05 - (C. van Schaik) |
19 | * - Changed tuning to 1/160Mhz accuracy | |
20 | * - Added stereo support | |
21 | * (card defaults to stereo) | |
22 | * (can explicitly force mono on the card) | |
23 | * (can detect if station is in stereo) | |
24 | * - Added unmute function | |
25 | * - Reworked ioctl functions | |
26 | * 2002-07-15 - Fix Stereo typo | |
2ab65299 MCC |
27 | * |
28 | * 2006-07-24 - Converted to V4L2 API | |
29 | * by Mauro Carvalho Chehab <mchehab@infradead.org> | |
1da177e4 LT |
30 | */ |
31 | ||
32 | #include <linux/module.h> /* Modules */ | |
33 | #include <linux/init.h> /* Initdata */ | |
fb911ee8 | 34 | #include <linux/ioport.h> /* request_region */ |
1da177e4 | 35 | #include <linux/delay.h> /* udelay, msleep */ |
2ab65299 | 36 | #include <linux/videodev2.h> /* kernel radio structs */ |
ec632c8a HV |
37 | #include <linux/mutex.h> |
38 | #include <linux/version.h> /* for KERNEL_VERSION MACRO */ | |
39 | #include <linux/io.h> /* outb, outb_p */ | |
ec632c8a | 40 | #include <media/v4l2-device.h> |
35ea11ff | 41 | #include <media/v4l2-ioctl.h> |
1da177e4 | 42 | |
ec632c8a HV |
43 | MODULE_AUTHOR("C.van Schaik"); |
44 | MODULE_DESCRIPTION("A driver for the Zoltrix Radio Plus."); | |
45 | MODULE_LICENSE("GPL"); | |
2ab65299 | 46 | |
1da177e4 LT |
47 | #ifndef CONFIG_RADIO_ZOLTRIX_PORT |
48 | #define CONFIG_RADIO_ZOLTRIX_PORT -1 | |
49 | #endif | |
50 | ||
51 | static int io = CONFIG_RADIO_ZOLTRIX_PORT; | |
52 | static int radio_nr = -1; | |
53 | ||
ec632c8a HV |
54 | module_param(io, int, 0); |
55 | MODULE_PARM_DESC(io, "I/O address of the Zoltrix Radio Plus (0x20c or 0x30c)"); | |
56 | module_param(radio_nr, int, 0); | |
57 | ||
58 | #define RADIO_VERSION KERNEL_VERSION(0, 0, 2) | |
59 | ||
60 | struct zoltrix { | |
61 | struct v4l2_device v4l2_dev; | |
62 | struct video_device vdev; | |
63 | int io; | |
1da177e4 LT |
64 | int curvol; |
65 | unsigned long curfreq; | |
66 | int muted; | |
67 | unsigned int stereo; | |
3593cab5 | 68 | struct mutex lock; |
1da177e4 LT |
69 | }; |
70 | ||
ec632c8a HV |
71 | static struct zoltrix zoltrix_card; |
72 | ||
73 | static int zol_setvol(struct zoltrix *zol, int vol) | |
1da177e4 | 74 | { |
ec632c8a HV |
75 | zol->curvol = vol; |
76 | if (zol->muted) | |
1da177e4 LT |
77 | return 0; |
78 | ||
ec632c8a | 79 | mutex_lock(&zol->lock); |
1da177e4 | 80 | if (vol == 0) { |
ec632c8a HV |
81 | outb(0, zol->io); |
82 | outb(0, zol->io); | |
83 | inb(zol->io + 3); /* Zoltrix needs to be read to confirm */ | |
84 | mutex_unlock(&zol->lock); | |
1da177e4 LT |
85 | return 0; |
86 | } | |
87 | ||
ec632c8a | 88 | outb(zol->curvol-1, zol->io); |
1da177e4 | 89 | msleep(10); |
ec632c8a HV |
90 | inb(zol->io + 2); |
91 | mutex_unlock(&zol->lock); | |
1da177e4 LT |
92 | return 0; |
93 | } | |
94 | ||
ec632c8a | 95 | static void zol_mute(struct zoltrix *zol) |
1da177e4 | 96 | { |
ec632c8a HV |
97 | zol->muted = 1; |
98 | mutex_lock(&zol->lock); | |
99 | outb(0, zol->io); | |
100 | outb(0, zol->io); | |
101 | inb(zol->io + 3); /* Zoltrix needs to be read to confirm */ | |
102 | mutex_unlock(&zol->lock); | |
1da177e4 LT |
103 | } |
104 | ||
ec632c8a | 105 | static void zol_unmute(struct zoltrix *zol) |
1da177e4 | 106 | { |
ec632c8a HV |
107 | zol->muted = 0; |
108 | zol_setvol(zol, zol->curvol); | |
1da177e4 LT |
109 | } |
110 | ||
ec632c8a | 111 | static int zol_setfreq(struct zoltrix *zol, unsigned long freq) |
1da177e4 LT |
112 | { |
113 | /* tunes the radio to the desired frequency */ | |
ec632c8a | 114 | struct v4l2_device *v4l2_dev = &zol->v4l2_dev; |
1da177e4 | 115 | unsigned long long bitmask, f, m; |
ec632c8a | 116 | unsigned int stereo = zol->stereo; |
1da177e4 LT |
117 | int i; |
118 | ||
b4be2048 | 119 | if (freq == 0) { |
ec632c8a | 120 | v4l2_warn(v4l2_dev, "cannot set a frequency of 0.\n"); |
b4be2048 AK |
121 | return -EINVAL; |
122 | } | |
123 | ||
1da177e4 | 124 | m = (freq / 160 - 8800) * 2; |
ec632c8a | 125 | f = (unsigned long long)m + 0x4d1c; |
1da177e4 LT |
126 | |
127 | bitmask = 0xc480402c10080000ull; | |
128 | i = 45; | |
129 | ||
ec632c8a | 130 | mutex_lock(&zol->lock); |
4286c6f6 | 131 | |
ec632c8a | 132 | zol->curfreq = freq; |
1da177e4 | 133 | |
ec632c8a HV |
134 | outb(0, zol->io); |
135 | outb(0, zol->io); | |
136 | inb(zol->io + 3); /* Zoltrix needs to be read to confirm */ | |
1da177e4 | 137 | |
ec632c8a HV |
138 | outb(0x40, zol->io); |
139 | outb(0xc0, zol->io); | |
140 | ||
141 | bitmask = (bitmask ^ ((f & 0xff) << 47) ^ ((f & 0xff00) << 30) ^ (stereo << 31)); | |
1da177e4 LT |
142 | while (i--) { |
143 | if ((bitmask & 0x8000000000000000ull) != 0) { | |
ec632c8a | 144 | outb(0x80, zol->io); |
1da177e4 | 145 | udelay(50); |
ec632c8a | 146 | outb(0x00, zol->io); |
1da177e4 | 147 | udelay(50); |
ec632c8a | 148 | outb(0x80, zol->io); |
1da177e4 LT |
149 | udelay(50); |
150 | } else { | |
ec632c8a | 151 | outb(0xc0, zol->io); |
1da177e4 | 152 | udelay(50); |
ec632c8a | 153 | outb(0x40, zol->io); |
1da177e4 | 154 | udelay(50); |
ec632c8a | 155 | outb(0xc0, zol->io); |
1da177e4 LT |
156 | udelay(50); |
157 | } | |
158 | bitmask *= 2; | |
159 | } | |
160 | /* termination sequence */ | |
ec632c8a HV |
161 | outb(0x80, zol->io); |
162 | outb(0xc0, zol->io); | |
163 | outb(0x40, zol->io); | |
1da177e4 | 164 | udelay(1000); |
ec632c8a | 165 | inb(zol->io + 2); |
1da177e4 | 166 | |
4286c6f6 MCC |
167 | udelay(1000); |
168 | ||
ec632c8a HV |
169 | if (zol->muted) { |
170 | outb(0, zol->io); | |
171 | outb(0, zol->io); | |
172 | inb(zol->io + 3); | |
1da177e4 LT |
173 | udelay(1000); |
174 | } | |
4286c6f6 | 175 | |
ec632c8a | 176 | mutex_unlock(&zol->lock); |
4286c6f6 | 177 | |
ec632c8a HV |
178 | if (!zol->muted) |
179 | zol_setvol(zol, zol->curvol); | |
1da177e4 LT |
180 | return 0; |
181 | } | |
182 | ||
183 | /* Get signal strength */ | |
ec632c8a | 184 | static int zol_getsigstr(struct zoltrix *zol) |
1da177e4 LT |
185 | { |
186 | int a, b; | |
187 | ||
ec632c8a HV |
188 | mutex_lock(&zol->lock); |
189 | outb(0x00, zol->io); /* This stuff I found to do nothing */ | |
190 | outb(zol->curvol, zol->io); | |
1da177e4 LT |
191 | msleep(20); |
192 | ||
ec632c8a | 193 | a = inb(zol->io); |
1da177e4 | 194 | msleep(10); |
ec632c8a | 195 | b = inb(zol->io); |
1da177e4 | 196 | |
ec632c8a | 197 | mutex_unlock(&zol->lock); |
4286c6f6 | 198 | |
1da177e4 | 199 | if (a != b) |
ec632c8a | 200 | return 0; |
1da177e4 | 201 | |
ec632c8a HV |
202 | /* I found this out by playing with a binary scanner on the card io */ |
203 | return a == 0xcf || a == 0xdf || a == 0xef; | |
1da177e4 LT |
204 | } |
205 | ||
ec632c8a | 206 | static int zol_is_stereo(struct zoltrix *zol) |
1da177e4 LT |
207 | { |
208 | int x1, x2; | |
209 | ||
ec632c8a | 210 | mutex_lock(&zol->lock); |
4286c6f6 | 211 | |
ec632c8a HV |
212 | outb(0x00, zol->io); |
213 | outb(zol->curvol, zol->io); | |
1da177e4 LT |
214 | msleep(20); |
215 | ||
ec632c8a | 216 | x1 = inb(zol->io); |
1da177e4 | 217 | msleep(10); |
ec632c8a | 218 | x2 = inb(zol->io); |
1da177e4 | 219 | |
ec632c8a | 220 | mutex_unlock(&zol->lock); |
4286c6f6 | 221 | |
ec632c8a | 222 | return x1 == x2 && x1 == 0xcf; |
1da177e4 LT |
223 | } |
224 | ||
a1314b1a DL |
225 | static int vidioc_querycap(struct file *file, void *priv, |
226 | struct v4l2_capability *v) | |
227 | { | |
228 | strlcpy(v->driver, "radio-zoltrix", sizeof(v->driver)); | |
229 | strlcpy(v->card, "Zoltrix Radio", sizeof(v->card)); | |
ec632c8a | 230 | strlcpy(v->bus_info, "ISA", sizeof(v->bus_info)); |
a1314b1a | 231 | v->version = RADIO_VERSION; |
ec632c8a | 232 | v->capabilities = V4L2_CAP_TUNER | V4L2_CAP_RADIO; |
a1314b1a DL |
233 | return 0; |
234 | } | |
235 | ||
236 | static int vidioc_g_tuner(struct file *file, void *priv, | |
237 | struct v4l2_tuner *v) | |
1da177e4 | 238 | { |
ec632c8a | 239 | struct zoltrix *zol = video_drvdata(file); |
1da177e4 | 240 | |
a1314b1a DL |
241 | if (v->index > 0) |
242 | return -EINVAL; | |
2ab65299 | 243 | |
ec632c8a | 244 | strlcpy(v->name, "FM", sizeof(v->name)); |
a1314b1a | 245 | v->type = V4L2_TUNER_RADIO; |
ec632c8a HV |
246 | v->rangelow = 88 * 16000; |
247 | v->rangehigh = 108 * 16000; | |
248 | v->rxsubchans = V4L2_TUNER_SUB_MONO | V4L2_TUNER_SUB_STEREO; | |
a1314b1a DL |
249 | v->capability = V4L2_TUNER_CAP_LOW; |
250 | if (zol_is_stereo(zol)) | |
251 | v->audmode = V4L2_TUNER_MODE_STEREO; | |
252 | else | |
253 | v->audmode = V4L2_TUNER_MODE_MONO; | |
ec632c8a | 254 | v->signal = 0xFFFF * zol_getsigstr(zol); |
a1314b1a DL |
255 | return 0; |
256 | } | |
2ab65299 | 257 | |
a1314b1a DL |
258 | static int vidioc_s_tuner(struct file *file, void *priv, |
259 | struct v4l2_tuner *v) | |
260 | { | |
ec632c8a | 261 | return v->index ? -EINVAL : 0; |
a1314b1a | 262 | } |
2ab65299 | 263 | |
a1314b1a DL |
264 | static int vidioc_s_frequency(struct file *file, void *priv, |
265 | struct v4l2_frequency *f) | |
266 | { | |
ec632c8a | 267 | struct zoltrix *zol = video_drvdata(file); |
2ab65299 | 268 | |
ec632c8a | 269 | if (zol_setfreq(zol, f->frequency) != 0) |
b4be2048 | 270 | return -EINVAL; |
a1314b1a DL |
271 | return 0; |
272 | } | |
2ab65299 | 273 | |
a1314b1a DL |
274 | static int vidioc_g_frequency(struct file *file, void *priv, |
275 | struct v4l2_frequency *f) | |
276 | { | |
ec632c8a | 277 | struct zoltrix *zol = video_drvdata(file); |
a1314b1a DL |
278 | |
279 | f->type = V4L2_TUNER_RADIO; | |
280 | f->frequency = zol->curfreq; | |
281 | return 0; | |
282 | } | |
1da177e4 | 283 | |
a1314b1a DL |
284 | static int vidioc_queryctrl(struct file *file, void *priv, |
285 | struct v4l2_queryctrl *qc) | |
286 | { | |
ec632c8a HV |
287 | switch (qc->id) { |
288 | case V4L2_CID_AUDIO_MUTE: | |
289 | return v4l2_ctrl_query_fill(qc, 0, 1, 1, 1); | |
290 | case V4L2_CID_AUDIO_VOLUME: | |
291 | return v4l2_ctrl_query_fill(qc, 0, 65535, 4096, 65535); | |
a1314b1a DL |
292 | } |
293 | return -EINVAL; | |
294 | } | |
295 | ||
296 | static int vidioc_g_ctrl(struct file *file, void *priv, | |
297 | struct v4l2_control *ctrl) | |
298 | { | |
ec632c8a | 299 | struct zoltrix *zol = video_drvdata(file); |
a1314b1a DL |
300 | |
301 | switch (ctrl->id) { | |
302 | case V4L2_CID_AUDIO_MUTE: | |
303 | ctrl->value = zol->muted; | |
304 | return 0; | |
305 | case V4L2_CID_AUDIO_VOLUME: | |
306 | ctrl->value = zol->curvol * 4096; | |
307 | return 0; | |
308 | } | |
309 | return -EINVAL; | |
310 | } | |
311 | ||
312 | static int vidioc_s_ctrl(struct file *file, void *priv, | |
313 | struct v4l2_control *ctrl) | |
314 | { | |
ec632c8a | 315 | struct zoltrix *zol = video_drvdata(file); |
a1314b1a DL |
316 | |
317 | switch (ctrl->id) { | |
318 | case V4L2_CID_AUDIO_MUTE: | |
319 | if (ctrl->value) | |
320 | zol_mute(zol); | |
321 | else { | |
322 | zol_unmute(zol); | |
ec632c8a | 323 | zol_setvol(zol, zol->curvol); |
2ab65299 | 324 | } |
a1314b1a DL |
325 | return 0; |
326 | case V4L2_CID_AUDIO_VOLUME: | |
ec632c8a | 327 | zol_setvol(zol, ctrl->value / 4096); |
a1314b1a DL |
328 | return 0; |
329 | } | |
330 | zol->stereo = 1; | |
ec632c8a | 331 | if (zol_setfreq(zol, zol->curfreq) != 0) |
b4be2048 | 332 | return -EINVAL; |
2ab65299 MCC |
333 | #if 0 |
334 | /* FIXME: Implement stereo/mono switch on V4L2 */ | |
ec632c8a HV |
335 | if (v->mode & VIDEO_SOUND_STEREO) { |
336 | zol->stereo = 1; | |
337 | zol_setfreq(zol, zol->curfreq); | |
338 | } | |
339 | if (v->mode & VIDEO_SOUND_MONO) { | |
340 | zol->stereo = 0; | |
341 | zol_setfreq(zol, zol->curfreq); | |
342 | } | |
2ab65299 | 343 | #endif |
a1314b1a DL |
344 | return -EINVAL; |
345 | } | |
2ab65299 | 346 | |
a1314b1a DL |
347 | static int vidioc_g_input(struct file *filp, void *priv, unsigned int *i) |
348 | { | |
349 | *i = 0; | |
350 | return 0; | |
1da177e4 LT |
351 | } |
352 | ||
a1314b1a | 353 | static int vidioc_s_input(struct file *filp, void *priv, unsigned int i) |
1da177e4 | 354 | { |
ec632c8a | 355 | return i ? -EINVAL : 0; |
a1314b1a DL |
356 | } |
357 | ||
ec632c8a | 358 | static int vidioc_g_audio(struct file *file, void *priv, |
a1314b1a DL |
359 | struct v4l2_audio *a) |
360 | { | |
ec632c8a HV |
361 | a->index = 0; |
362 | strlcpy(a->name, "Radio", sizeof(a->name)); | |
363 | a->capability = V4L2_AUDCAP_STEREO; | |
a1314b1a | 364 | return 0; |
1da177e4 LT |
365 | } |
366 | ||
ec632c8a HV |
367 | static int vidioc_s_audio(struct file *file, void *priv, |
368 | struct v4l2_audio *a) | |
369 | { | |
370 | return a->index ? -EINVAL : 0; | |
371 | } | |
1da177e4 | 372 | |
bec43661 | 373 | static const struct v4l2_file_operations zoltrix_fops = |
1da177e4 LT |
374 | { |
375 | .owner = THIS_MODULE, | |
a1314b1a | 376 | .ioctl = video_ioctl2, |
1da177e4 LT |
377 | }; |
378 | ||
a399810c | 379 | static const struct v4l2_ioctl_ops zoltrix_ioctl_ops = { |
a1314b1a DL |
380 | .vidioc_querycap = vidioc_querycap, |
381 | .vidioc_g_tuner = vidioc_g_tuner, | |
382 | .vidioc_s_tuner = vidioc_s_tuner, | |
383 | .vidioc_g_audio = vidioc_g_audio, | |
384 | .vidioc_s_audio = vidioc_s_audio, | |
385 | .vidioc_g_input = vidioc_g_input, | |
386 | .vidioc_s_input = vidioc_s_input, | |
387 | .vidioc_g_frequency = vidioc_g_frequency, | |
388 | .vidioc_s_frequency = vidioc_s_frequency, | |
389 | .vidioc_queryctrl = vidioc_queryctrl, | |
390 | .vidioc_g_ctrl = vidioc_g_ctrl, | |
391 | .vidioc_s_ctrl = vidioc_s_ctrl, | |
1da177e4 LT |
392 | }; |
393 | ||
394 | static int __init zoltrix_init(void) | |
395 | { | |
ec632c8a HV |
396 | struct zoltrix *zol = &zoltrix_card; |
397 | struct v4l2_device *v4l2_dev = &zol->v4l2_dev; | |
398 | int res; | |
399 | ||
400 | strlcpy(v4l2_dev->name, "zoltrix", sizeof(v4l2_dev->name)); | |
401 | zol->io = io; | |
402 | if (zol->io == -1) { | |
b24c20cc | 403 | v4l2_err(v4l2_dev, "You must set an I/O address with io=0x20c or 0x30c\n"); |
1da177e4 LT |
404 | return -EINVAL; |
405 | } | |
ec632c8a HV |
406 | if (zol->io != 0x20c && zol->io != 0x30c) { |
407 | v4l2_err(v4l2_dev, "invalid port, try 0x20c or 0x30c\n"); | |
1da177e4 LT |
408 | return -ENXIO; |
409 | } | |
410 | ||
ec632c8a HV |
411 | if (!request_region(zol->io, 2, "zoltrix")) { |
412 | v4l2_err(v4l2_dev, "port 0x%x already in use\n", zol->io); | |
1da177e4 LT |
413 | return -EBUSY; |
414 | } | |
415 | ||
ec632c8a HV |
416 | res = v4l2_device_register(NULL, v4l2_dev); |
417 | if (res < 0) { | |
418 | release_region(zol->io, 2); | |
419 | v4l2_err(v4l2_dev, "Could not register v4l2_device\n"); | |
420 | return res; | |
421 | } | |
422 | ||
423 | strlcpy(zol->vdev.name, v4l2_dev->name, sizeof(zol->vdev.name)); | |
424 | zol->vdev.v4l2_dev = v4l2_dev; | |
425 | zol->vdev.fops = &zoltrix_fops; | |
426 | zol->vdev.ioctl_ops = &zoltrix_ioctl_ops; | |
427 | zol->vdev.release = video_device_release_empty; | |
428 | video_set_drvdata(&zol->vdev, zol); | |
429 | ||
430 | if (video_register_device(&zol->vdev, VFL_TYPE_RADIO, radio_nr) < 0) { | |
431 | v4l2_device_unregister(v4l2_dev); | |
432 | release_region(zol->io, 2); | |
1da177e4 LT |
433 | return -EINVAL; |
434 | } | |
ec632c8a | 435 | v4l2_info(v4l2_dev, "Zoltrix Radio Plus card driver.\n"); |
1da177e4 | 436 | |
ec632c8a | 437 | mutex_init(&zol->lock); |
4286c6f6 | 438 | |
1da177e4 LT |
439 | /* mute card - prevents noisy bootups */ |
440 | ||
441 | /* this ensures that the volume is all the way down */ | |
442 | ||
ec632c8a HV |
443 | outb(0, zol->io); |
444 | outb(0, zol->io); | |
1da177e4 | 445 | msleep(20); |
ec632c8a | 446 | inb(zol->io + 3); |
1da177e4 | 447 | |
ec632c8a HV |
448 | zol->curvol = 0; |
449 | zol->stereo = 1; | |
1da177e4 LT |
450 | |
451 | return 0; | |
452 | } | |
453 | ||
ec632c8a | 454 | static void __exit zoltrix_exit(void) |
1da177e4 | 455 | { |
ec632c8a HV |
456 | struct zoltrix *zol = &zoltrix_card; |
457 | ||
458 | video_unregister_device(&zol->vdev); | |
459 | v4l2_device_unregister(&zol->v4l2_dev); | |
460 | release_region(zol->io, 2); | |
1da177e4 LT |
461 | } |
462 | ||
463 | module_init(zoltrix_init); | |
ec632c8a | 464 | module_exit(zoltrix_exit); |
1da177e4 | 465 |