/* * Conrad Parallel Port Radio Clock driver * * Copyright (C) 1999-2003 Andreas Voegele * Copyright (C) 1997-1999 Joachim Koenig * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in the file COPYING; if not, write to the * Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA. */ /* * This driver supports the DCF77 radio clocks sold by Conrad Electronic under * order number 967602. * * If the driver is built into the kernel, you can configure it using the * kernel command-line. For example: * * pcfclock=parport0,none,parport2 (bind the first pcfclock device to the # first parallel port, disable the # second device and bind the third # device to the third parallel port) * * pcfclock=0 (disable the driver entirely) * * If the driver is loaded as a module, similar functionality is available * using module parameters. The equivalent of the above command would be: * * # insmod pcfclock.o parport=0,none,2 */ #include #include #include #include #include #include #include #include #include #include #include #include #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) #include #endif #include #include /* Major device number */ #define PCFCLOCK_MAJOR 181 /* Number of devices supported by this driver */ #define PCFCLOCK_NO 3 #define PCFCLOCK_PARPORT_UNSPEC -3 #define PCFCLOCK_PARPORT_OFF -2 #define PCFCLOCK_PARPORT_NONE -1 /* Control words */ #define PCFCLOCK_TRANSMIT_TIME ((unsigned char)0) #define PCFCLOCK_COPY_SIGNAL ((unsigned char)7) #if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0) static devfs_handle_t devfs_handle = NULL; #define iminor(inode) MINOR((inode)->i_rdev) #endif static int pcfclock_count = 0; struct pcfclock_status { unsigned int not_in_sync:8; unsigned int battery_status_low:8; }; struct pcfclock_struct { struct pardevice *dev; struct semaphore sem; }; static struct pcfclock_struct pcfclock_table[PCFCLOCK_NO]; /* * The radio clock transmits data only to the PC when requested. While the * time signal is received for the first time no communication between the PC * and the radio clock is possible. Once the radio clock has received the * time signal it may be woken up. D0, D1 and D2 are used as data lines and * Auto Feed is used as the clock line. The wake-up procedure starts with * clock set to low and D0-D2 set to 0. The clock line gets inverted every * 3 milliseconds and the data lines are counted up to 7. Afterwards the * clock is inverted again and one of the control words is transmitted for * 3 milliseconds. The wake-up procedure is completed by setting the clock * line to high. If the radio clock has recognized the call, it will begin * to transmit the requested data. * * Two binary control words are defined: * * 000: Transmit the time information to the PC. * 111: Copy the time signal to Select for about 10 minutes or up until the * next wake-up procedure. This signal may be used to implement a * quality control, for example. * * The radio clock uses Paper Out as the data line and Select as the clock * line to transmit data with a clock of 2.5 milliseconds per bit. * * The following information is always transmitted: * * Bit 00-03 synchronisation signal (always 1001) * Bit 04-06 control word (for verification) * Bit 07 1 = last radio reception failed * * The following data is transmitted in BCD format (MSB first) if the * control word is 000: * * Bit 08-11 second (low nibble) * Bit 12-15 second (high nibble) * Bit 16-19 minute (low nibble) * Bit 20-23 minute (high-nibble) * Bit 24-27 hour (low-nibble) * Bit 28-31 hour (high-nibble) * Bit 32 no information * Bit 33 1 = battery low * Bit 34 1 = daylight saving time _not_ in effect * Bit 35 1 = daylight saving time in effect * Bit 36-39 day of the week * Bit 40-43 day of the month (low nibble) * Bit 44-47 day of the month (high nibble) * Bit 48-51 month (low nibble) * Bit 52-55 month (high nibble) * Bit 56-59 year (low nibble) * Bit 60-63 year (high nibble) * Bit 64-67 number of 1/32 seconds (0-15) * Bit 68 1 = add half a second * * Bit 34 and 35 are both cleared if the last radio reception failed. * The day of the week ranges from 1 to 7; Monday is 1. * * Timing graph: * * . . . . . . . . . * 0.2.0.2 2 ms 0.2.0.2 2 ms 0.2.0.2 * . . . . . . . . . * ------------+ . . +---------------------+ * Select . . | . . | . . | * . . | . . | . . | * . . | . . | . . | * . . | . . | . . | * . . +---------------------+ . . +-- * . . . . . . . . . * . . . . . . . . . * --------------------------------------------------------- * Paper Out . . . . . . . . . * | 0 | Bit 0 | 0 | Bit 1 | 0 | * +-------------------------------------------------------- */ static void pcfclock_wake_up(struct parport *port, unsigned char control_word) { int count; for (count = 0; count < 8; count++) { parport_write_data(port, count & 7); parport_frob_control(port, PARPORT_CONTROL_AUTOFD, (count & 1) ? 0 : PARPORT_CONTROL_AUTOFD); mdelay(3); } parport_write_data(port, control_word); parport_frob_control(port, PARPORT_CONTROL_AUTOFD, PARPORT_CONTROL_AUTOFD); mdelay(3); parport_frob_control(port, PARPORT_CONTROL_AUTOFD, 0); } static int pcfclock_receive_data(struct parport *port, char *buf, int numbits) { int count, end; end = 100; for (count = 0; count < numbits; count++) { int di, index; unsigned char test; index = count / 4; *(buf + index) <<= 1; di = 0; test = (count & 1) ? 0 : PARPORT_STATUS_SELECT; while ((parport_read_status(port) & PARPORT_STATUS_SELECT) == test) { if (di++ >= end) return -EIO; udelay(100); } udelay(500); if (parport_read_status(port) & PARPORT_STATUS_PAPEROUT) *(buf + index) |= 1; end = 40; } return 0; } static int pcfclock_read_time(struct parport *port, char buf[18]) { int rc; memset(buf, 0, 18); schedule(); pcfclock_wake_up(port, PCFCLOCK_TRANSMIT_TIME); rc = pcfclock_receive_data(port, buf, 69); schedule(); if (rc == 0 && !(buf[0] == 9 && (buf[1] >> 1) == PCFCLOCK_TRANSMIT_TIME)) rc = -EIO; return rc; } static int pcfclock_is_valid_time(char buf[18]) { int wday, day, month, year; wday = buf[9] % 7; day = buf[11] * 10 + buf[10]; month = buf[13] * 10 + buf[12]; /* Has to be updated in 2099 :-) */ year = 1900 + buf[15] * 10 + buf[14]; if (year < 1999) year += 100; /* check the date and if the day of the week matches the date */ if (day >= 1 && month >= 1 && month <= 12) { int a, y, m, d; /* from Claus Tondering's Calendar FAQ */ a = (14 - month) / 12; y = year - a; m = month + 12 * a - 2; d = (day + y + y / 4 - y / 100 + y / 400 + 31 * m / 12) % 7; if (d == wday) return 1; } return 0; } static ssize_t pcfclock_read(struct file *filp, char *user_buf, size_t length, loff_t * ppos) { int rc; int try; char buf1[18], buf2[18]; unsigned int minor = iminor(filp->f_dentry->d_inode); struct pcfclock_struct *pcfclock = &pcfclock_table[minor]; struct pcfclock_status *status = (struct pcfclock_status *) &filp->private_data; if (length < 18) return -EOVERFLOW; if (filp->f_flags & O_NONBLOCK) { if (down_trylock(&pcfclock->sem)) return -EAGAIN; } else { if (down_interruptible (&pcfclock->sem)) return -ERESTARTSYS; } if ((rc = parport_claim_or_block(pcfclock->dev)) < 0) { up(&pcfclock->sem); return rc; } try = 0; while (try++ < 10) { rc = pcfclock_read_time(pcfclock->dev->port, buf1); if (rc == 0) { if (pcfclock_is_valid_time(buf1)) { rc = pcfclock_read_time(pcfclock->dev->port, buf2); if (rc == 0) { if (pcfclock_is_valid_time(buf2)) { if (memcmp(&buf1[3], &buf2[3], 13) == 0) { break; } } } } } } if (try >= 10) rc = -ENODEV; parport_release(pcfclock->dev); up(&pcfclock->sem); if (rc) { printk(KERN_WARNING "pcfclock%u: no radio clock found at %s\n", minor, pcfclock->dev->port->name); return rc; } if (buf2[1] & 1) { if (status->not_in_sync == 0) printk(KERN_WARNING "pcfclock%u: not in sync at %s\n", minor, pcfclock->dev->port->name); if (++status->not_in_sync > 240) status->not_in_sync = 0; } else { status->not_in_sync = 0; } if (buf2[8] & 4) { if (status->battery_status_low == 0) printk(KERN_WARNING "pcfclock%u: battery status low at %s\n", minor, pcfclock->dev->port->name); if (++status->battery_status_low > 240) status->battery_status_low = 0; } else { status->battery_status_low = 0; } if (copy_to_user(user_buf, buf2, 18)) return -EFAULT; return 18; } static int pcfclock_open(struct inode *inode, struct file *filp) { unsigned int minor = iminor(inode); struct pcfclock_struct *pcfclock = &pcfclock_table[minor]; struct pcfclock_status *status = (struct pcfclock_status *) &filp->private_data; if (minor >= PCFCLOCK_NO) return -ENXIO; if (!pcfclock->dev) return -ENXIO; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) if (!try_module_get(THIS_MODULE)) return -ENODEV; #else MOD_INC_USE_COUNT; #endif status->not_in_sync = 0; status->battery_status_low = 0; return 0; } static int pcfclock_release(struct inode *inode, struct file *filp) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) module_put(THIS_MODULE); #else MOD_DEC_USE_COUNT; #endif return 0; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) static struct file_operations pcfclock_fops = { .owner = THIS_MODULE, .llseek = no_llseek, .read = pcfclock_read, .open = pcfclock_open, .release = pcfclock_release, }; #else static struct file_operations pcfclock_fops = { owner: THIS_MODULE, llseek: no_llseek, read: pcfclock_read, open: pcfclock_open, release: pcfclock_release, }; #endif static int parport_nr[PCFCLOCK_NO] = {[0 ... PCFCLOCK_NO - 1] = PCFCLOCK_PARPORT_UNSPEC }; static char *parport[PCFCLOCK_NO] = { NULL, }; MODULE_PARM(parport, "1-" __MODULE_STRING(PCFCLOCK_NO) "s"); #ifndef MODULE static int __init pcfclock_setup(char *str) { static int parport_ptr; // initially zero if (str == NULL || str[0] == '\0' || strcmp(str, "0") == 0 || strcmp(str, "off") == 0) { parport_nr[0] = PCFCLOCK_PARPORT_OFF; return 1; } while (str && parport_ptr < PCFCLOCK_NO) { if (strncmp(str, "parport", 7) == 0) { int r = (int) simple_strtoul(str + 7, NULL, 10); parport_nr[parport_ptr++] = r; } else if (strncmp(str, "none", 4) == 0) { parport_nr[parport_ptr++] = PCFCLOCK_PARPORT_NONE; } if ((str = strchr(str, ','))) ++str; } return 1; } #endif static int pcfclock_register(int n, struct parport *port) { #if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0) char name[8]; #endif pcfclock_table[n].dev = parport_register_device(port, "pcfclock", NULL, NULL, NULL, 0, NULL); if (pcfclock_table[n].dev == NULL) return 1; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) devfs_mk_cdev(MKDEV(PCFCLOCK_MAJOR, n), S_IFCHR | S_IRUGO, "pcfclocks/%d", n); #else sprintf(name, "%d", n); devfs_register(devfs_handle, name, DEVFS_FL_DEFAULT, PCFCLOCK_MAJOR, n, S_IFCHR | S_IRUGO, &pcfclock_fops, NULL); #endif return 0; } static void pcfclock_attach(struct parport *port) { int n; if (pcfclock_count == PCFCLOCK_NO) { printk(KERN_INFO "pcfclock: ignoring parallel port (max. %d)\n", PCFCLOCK_NO); return; } switch (parport_nr[0]) { case PCFCLOCK_PARPORT_UNSPEC: if (pcfclock_register(pcfclock_count, port) == 0) pcfclock_count++; break; default: for (n = 0; n < PCFCLOCK_NO; n++) { if (port->number == parport_nr[n]) { if (pcfclock_register(n, port) == 0) pcfclock_count++; break; } } break; } } static void pcfclock_detach(struct parport *port) { } static struct parport_driver pcfclock_driver = { "pcfclock", pcfclock_attach, pcfclock_detach, }; int __init pcfclock_init(void) { int n; for (n = 0; n < PCFCLOCK_NO; n++) { pcfclock_table[n].dev = NULL; sema_init(&pcfclock_table[n].sem, 1); } if (parport_nr[0] == PCFCLOCK_PARPORT_OFF) return 0; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) if (register_chrdev(PCFCLOCK_MAJOR, "pcfclock", &pcfclock_fops)) { printk(KERN_ERR "pcfclock: unable to get major %d\n", PCFCLOCK_MAJOR); return -EIO; } devfs_mk_dir("pcfclocks"); #else if (devfs_register_chrdev(PCFCLOCK_MAJOR, "pcfclock", &pcfclock_fops)) { printk(KERN_ERR "pcfclock: unable to get major %d\n", PCFCLOCK_MAJOR); return -EIO; } devfs_handle = devfs_mk_dir(NULL, "pcfclocks", NULL); #endif if (parport_register_driver(&pcfclock_driver)) { printk(KERN_ERR "pcfclock: unable to register with parport\n"); return -EIO; } return 0; } static int __init pcfclock_init_module(void) { if (parport[0]) { int n; for (n = 0; n < PCFCLOCK_NO && parport[n]; n++) { if (strncmp(parport[n], "none", 4) == 0) parport_nr[n] = PCFCLOCK_PARPORT_NONE; else { char *ep; int r = (int) simple_strtoul(parport[n], &ep, 0); if (ep != parport[n]) parport_nr[n] = r; else { printk(KERN_ERR "pcfclock: bad port specifier `%s'\n", parport[n]); return -ENODEV; } } } } return pcfclock_init(); } static void __exit pcfclock_cleanup_module(void) { int n; parport_unregister_driver(&pcfclock_driver); #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) unregister_chrdev(PCFCLOCK_MAJOR, "pcfclock"); for (n = 0; n < PCFCLOCK_NO; n++) { if (pcfclock_table[n].dev != NULL) { parport_unregister_device(pcfclock_table[n].dev); devfs_remove("pcfclocks/%d", n); } } devfs_remove("pcfclocks"); #else devfs_unregister(devfs_handle); devfs_unregister_chrdev(PCFCLOCK_MAJOR, "pcfclock"); for (n = 0; n < PCFCLOCK_NO; n++) { if (pcfclock_table[n].dev != NULL) parport_unregister_device(pcfclock_table[n].dev); } #endif } __setup("pcfclock=", pcfclock_setup); module_init(pcfclock_init_module); module_exit(pcfclock_cleanup_module); MODULE_LICENSE("GPL"); #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) MODULE_ALIAS_CHARDEV_MAJOR(PCFCLOCK_MAJOR); #else EXPORT_NO_SYMBOLS; #endif /* Local Variables: */ /* c-file-style:"linux" */ /* End: */