/*
* 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 <linux/version.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/devfs_fs_kernel.h>
#include <linux/string.h>
#include <linux/delay.h>
#include <linux/parport.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
#include <linux/device.h>
#endif
#include <asm/uaccess.h>
#include <asm/semaphore.h>
/* 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: */
syntax highlighted by Code2HTML, v. 0.9.1