/*
 * 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