
/* Listing 5.1 in "Essential Linux Device Drivers"
 * by Sreekrishnan Venkateswaran.
 * Inclusion of listings 5.2, 5.3, 5.4, and 5.5. */

/* Completed and corrected to produce a full driver
 * by Mats Erik Andersson. */

/* The original ideas of CMOS beeing verifiable by
 * CRC-methods is nonsense due to the presence of
 * hw-clock memory cells at the beginning of the
 * CMOS bank number zero. Still, this code has
 * been supplied with a corresponding library
 * call to implement the indicated functionality,
 * again by Mats Erik Andersson. */

/* Copyright GPL v2, 2008, 2009,
 * Sreekrishnan Venkateswaran, Mats Erik Andersson. */

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/pci.h>
#include <linux/crc16.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sreekrishnan Venkateswaran, Mats Erik Andersson");
MODULE_DESCRIPTION("Interface to embedded CMOS memory on PC platform.");

#define NUM_CMOS_BANKS	2       

#define CMOS_BANK_SIZE	0x100
#define DEVICE_NAME		"cmos"

#define CMOS_BANK0_ADDR_PORT	0x70
#define CMOS_BANK0_DATA_PORT	0x71
#define CMOS_BANK1_ADDR_PORT	0x72
#define CMOS_BANK1_DATA_PORT	0x73

/* ioctl values. */
#define CMOS_ADJUST_CHECKSUM	1
#define CMOS_VERIFY_CHECKSUM	2

#define CMOS_BANK1_CRC_OFFSET	0x1e

struct cmos_dev {
  u16 current_pointer;	/* Current pointer within the bank */
  u16 size;				/* Size of the bank */
  u8 bank_number;		/* CMOS bank number */
  struct cdev cdev;		/* Character dev struct */
  char name[10];		/* Name of I/O region */
  /* ... */				/* Mutexes, spinlocks, wait queues, .. */
} *cmos_devp[NUM_CMOS_BANKS];

static int cmos_open(struct inode *inodp, struct file * filp);

static int cmos_release(struct inode *inodp, struct file * filp);

static ssize_t cmos_read(struct file *filep, char __user *uaddr,
							size_t n, loff_t *offs);

static ssize_t cmos_write(struct file *filep, const char __user *uaddr,
							size_t n, loff_t *offs);

static loff_t cmos_llseek(struct file *filep, loff_t offs, int whence);

static int cmos_ioctl(struct inode *inodp, struct file *filp,
					unsigned int n, unsigned long data);

static struct file_operations cmos_fops = {
  .owner =		THIS_MODULE,  /* Owner */
  .open  =		cmos_open,    /* Open method */
  .release =	cmos_release, /* Release method */
  .read  =		cmos_read,    /* Read method */
  .write =		cmos_write,   /* Write method */
  .llseek =		cmos_llseek,  /* Seek method */
  .ioctl =		cmos_ioctl,   /* Ioctl method */
};

static dev_t cmos_dev_number;     /* Alloted device Number */
static struct class * cmos_class; /* Tie with the device model */

u8 addrports[NUM_CMOS_BANKS] =
	{ CMOS_BANK0_ADDR_PORT, CMOS_BANK1_ADDR_PORT};

u8 dataports[NUM_CMOS_BANKS] =
	{ CMOS_BANK0_DATA_PORT, CMOS_BANK1_DATA_PORT};

/*
 * Driver initialization
 */
static int __init cmos_init(void)
{
	u8 i, j;
	int ret;

	/* Request dynamic allocation of a device major number */
	if ( alloc_chrdev_region(&cmos_dev_number,
			  				0, NUM_CMOS_BANKS, DEVICE_NAME) < 0) {
		printk(KERN_ERR "%s: Cannot register device.\n", DEVICE_NAME);
		return -EIO;
	}

	/* Populate sysfs entries */  
	cmos_class = class_create(THIS_MODULE, DEVICE_NAME);

	for (i=0; i < NUM_CMOS_BANKS; i++) {
		/* Allocate memory for the per-device structure */
		cmos_devp[i] = kmalloc(sizeof (struct cmos_dev), GFP_KERNEL);

		if (!cmos_devp[i]) {
			printk(KERN_ERR "%s: Bad kmalloc call.\n", DEVICE_NAME);
			for (j=0; j<i; j++)
				kfree(cmos_devp[j]);
			ret = ENOMEM;
			goto fail1;
		}

		/* Request I/O Region */
		sprintf (cmos_devp[i]->name, "cmos%d", i);

		if (!request_region(addrports[i], 2, cmos_devp[i]->name)) {
			printk(KERN_ERR "%s: I/O port 0x%x is not free.\n",
						DEVICE_NAME, addrports[i]);
			for (j=0; j<i; j++)
				release_region(addrports[j], 2);
			ret = EIO;
			goto fail2;
		}

		/* Fill in the bank number to correlate this device 
		 * with the corresponding CMOS bank */
		cmos_devp[i]->bank_number = i;

		/* Connect the file operations with the cdev */
		cdev_init(&cmos_devp[i]->cdev, &cmos_fops);
		cmos_devp[i]->cdev.owner = THIS_MODULE;

		ret = cdev_add(&cmos_devp[i]->cdev,
				MKDEV(MAJOR(cmos_dev_number), i), 1);
		if (ret) {
			printk(KERN_ERR "%s: Bad cdev allocation.\n", DEVICE_NAME);
			for (j=0; j<i; j++)
				cdev_del(&cmos_devp[j]->cdev);
			goto fail3;
		}		

		device_create(cmos_class, NULL, MKDEV(MAJOR(cmos_dev_number), i),
					"cmos%d", i);
	}

	printk(KERN_INFO "%s: CMOS driver initialized.\n", DEVICE_NAME);

	/* Successful completion. */
	return 0;

	/* Clean up after failures. */
fail3:
	for (j=0; j < NUM_CMOS_BANKS; j++)
		release_region(addrports[j], 2);
fail2:
	for (j=0; j < NUM_CMOS_BANKS; j++)
		kfree(cmos_devp[j]);
fail1:
	class_destroy(cmos_class);
	unregister_chrdev_region(MAJOR(cmos_dev_number),
								NUM_CMOS_BANKS);
	return -ret;
}

/* Driver Exit */
static void __exit cmos_cleanup (void)
{
	u8 i;

	for (i=0; i < NUM_CMOS_BANKS; i++) {
		device_destroy(cmos_class, MKDEV(MAJOR(cmos_dev_number), i));
		cdev_del(&cmos_devp[i]->cdev);
		release_region(addrports[i], 2);
		kfree(cmos_devp[i]);
	}

	class_destroy(cmos_class);
	unregister_chrdev_region(cmos_dev_number, NUM_CMOS_BANKS);

	printk(KERN_INFO "%s: CMOS driver exited.\n", DEVICE_NAME);
	return;
}

static int cmos_open(struct inode *inodp, struct file * filp)
{
	struct cmos_dev * cmos_devp;

	/* Get super structure that contains this cdev. */
	cmos_devp = container_of(inodp->i_cdev, struct cmos_dev, cdev);

	/* Device description.*/
	filp->private_data = cmos_devp;

	/* Initialise data. */
	cmos_devp->size				= 8 * CMOS_BANK_SIZE;
	cmos_devp->current_pointer	= 0;

	return 0;
} /* cmos_open */

static int cmos_release(struct inode *inodp, struct file * filp)
{
	struct cmos_dev * cmos_devp = filp->private_data;

	cmos_devp->current_pointer = 0;

	return 0;
}

/* Physical reads */
u8 port_data_in(u8 offs, u8 bank);

/* CMOS read method */
static ssize_t cmos_read(struct file *filp, char __user *uaddr,
							size_t count, loff_t *offs)
{
	struct cmos_dev * cmos_devp = filp->private_data;
	u8 data[CMOS_BANK_SIZE + 4];
	u8 mask;
	u16 zero_out, l, i;
	size_t tx;
	u16 start_byte = cmos_devp->current_pointer / 8;
	u8 start_bit = cmos_devp->current_pointer % 8;

	if ( cmos_devp->current_pointer >= cmos_devp->size )
		return 0; /* EOF */

	/* Adjust count if it extends beyond the end of the CMOS bank. */
	if ( cmos_devp->current_pointer + count >= cmos_devp->size )
		count = cmos_devp->size - cmos_devp->current_pointer;

	/* Buffer location. */
	i = 0;
	tx = 0;

	/* Get the specified number of bits from CMOS memory. */
	while ( tx < count ) {
		data[i] = port_data_in(start_byte, cmos_devp->bank_number)
						>> start_bit;
		tx += (8 - start_bit);

		if ( start_bit && (count > tx) ) {
			data[i] |= ( port_data_in(start_byte + 1,
									cmos_devp->bank_number) << (8 - start_bit) );
			tx += start_bit;
		}

		start_byte++;
		i++;
	}

	/* Zero out excessive bit content. */
	if ( (tx > count) && (i > 0)) {
		zero_out = tx - count;
		mask = 1 << (8 - zero_out);
		for (l=0; l < zero_out; l++) {
			data[i-1] &= ~mask;
			mask <<= 1;
		}
		tx = count;
	}

	/* Performed any work? */
	if ( !tx ) 
		return -EIO;

	/* Copy to user buffer location. */
	if ( copy_to_user( uaddr, (void *) data, (tx+7)/8) != 0 )
		return -EIO;

	/* Book keeping of read position. */
	cmos_devp->current_pointer += tx;

	/* Report amount read.*/
	return tx;
}

/* Physical write method. */
void port_data_out(u8 offs, u8 data, u8 bank);

/* Logical write method. */
static ssize_t cmos_write(struct file *filp, const char __user *uaddr,
							size_t count, loff_t *offs)
{
	struct cmos_dev * cmos_devp = filp->private_data;
	u16 tx, i, n, start, end;
	u8 *kbuf, tmp_kbuf;
	u8 tmp_data = 0, mask;

	/* Calculate current byte position and remainder. */
	u16 start_byte = cmos_devp->current_pointer / 8;
	u8 start_bit = cmos_devp->current_pointer % 8;

	if ( cmos_devp->current_pointer >= cmos_devp->size )
		return 0; /* EOF */

	/* Adjust count if it extends beyond the end of the CMOS bank. */
	if ( cmos_devp->current_pointer + count >= cmos_devp->size )
		count = cmos_devp->size - cmos_devp->current_pointer;

	/* 'count' bits use '(count+7)/8' bytes of space (integer arithmetic).*/
	kbuf = kmalloc( (count+7)/8, GFP_KERNEL);
	if (kbuf == NULL)
		return -ENOMEM;

	/* Fetch bits from user buffer. */
	if ( copy_from_user(kbuf, uaddr, (count+7)/8) ) {
		kfree(kbuf);
		return -EFAULT;
	}

	/* Initialize buffer position and transmit counter. *
	 * At least one bit can be read, by the previous tests. */
	i = 0;
	tx = 0;

	/* Write the specified number of bits to the CMOS bank. */
	while (tx < count) {
		tmp_data = port_data_in(start_byte, cmos_devp->bank_number);
		mask = 1 << start_bit;
		/* First neglectable bit position. */
		end = 8;

		/* Is the presently read byte 'kbuf[i]' sufficient
		 * to extract all bits that need be written? */
		if ( (count - tx) <= (8 - start_bit) )
			/* Yes: recalculate the first neglectable bit position.
			 * By the above test, the new value is 8 or less. *
			 *
			 * Remark: After this step
			 *            (tx + (end - start_bit)) = count.
			 *         Thus all needed bits will be have been
			 *         written within one single write sequence below. */

			end = count - tx + start_bit;

		/* Erase all bits that are to be updated. */
		for ( n = start_bit; n < end; n++) {
			tmp_data &= ~mask;
			mask <<= 1;
		}

		tmp_kbuf = kbuf[i];
		mask = 1 << end;

		/* Erase indata bits that are not to be transmitted, if any. */
		for ( n = end; n < 8; n++) {
			tmp_kbuf &= ~mask;
			mask <<= 1;
		}

		/* Coalesce old data with new bit content. */
		port_data_out(start_byte, tmp_data | (tmp_kbuf << start_bit),
						cmos_devp->bank_number);

		/* The previous step wrote exactly '(end - start_bit)'
		 * bits of content. */
		tx += end - start_bit;

		/* Is there a fractional byte to be written from the
		 * present content of kbuf[i]? */
		if ( (tx < count) && start_bit ) {
			/* Yes there is! Fetch content from CMOS. */
			/* By the remarks above, '(end == 8)' allways in this case. */
			tmp_data = port_data_in(start_byte + 1, cmos_devp->bank_number);

			/* The first new content begins in 'kbuf[i]' at
			 * bit number '(end - start_bit)'. */
			start = 8 - start_bit;

			/* Present situation:
			 *    1. The lower 'start = (8 - start_bit)' bits
			 *       in 'kbuf[i]' will be discarded.
			 *    2. The upper '(8 - start) = start_bit' bits
			 *       in 'kbuf[i]' must be shifted downward and
			 *       superimposed on 'tmp_data' before write
			 *       commences to CMOS.
			 *    3. Thus, the '8 - start = start_bit' LSB bits
			 *       in 'tpm_data' ought to be erased. */

			/* Erase the lower, already used bits.
			 * The manipulation of 'mask' sets the correct number
			 * of bits, since a previous conditional made sure
			 * that 'start_bit' was positive. Thus the following
			 * computation yields 'mask >= 1'. */
			mask = (1 << start_bit) - 1;
			tmp_data &= ~mask;

			/* Superimpose old data and new data, and submit. */
			port_data_out( start_byte + 1,
							tmp_data | (kbuf[i] >> (8 - start_bit)),
							cmos_devp->bank_number);

			/* The previous write instruction, submitted exactly
			 * '(8 - start) = start_bit' new bit content.
			 * Now update the transmit counter. */
			tx += start_bit;
		}
		/* Proceed to next offset, should it be needed. */
		start_byte++;
		i++;
	}

	/* Return the allocated kernel read buffer. */
	kfree(kbuf);

	if ( !tx )
		return -EIO;

	/* Update the offset pointer. */
	cmos_devp->current_pointer += tx;

	/* Report the number of transferred bits. */
	return tx;
} /* cmos_write */

static loff_t cmos_llseek(struct file *filp, loff_t offs, int whence)
{
	struct cmos_dev * cmos_devp = filp->private_data;
	loff_t curr = (loff_t) cmos_devp->current_pointer;

	switch (whence) {
		case SEEK_SET:
			curr = offs;
			break;

		case SEEK_CUR:
			curr += offs;
			break;

		case SEEK_END:
			/* Only negative values of offs will succeed. */
			curr = cmos_devp->size + offs;
			break;

		default:
			/* Not implemented */
			return -EINVAL;
	}
	if ( (curr >= cmos_devp->size) || (curr < 0) )
		return -EINVAL;

	cmos_devp->current_pointer = (u16) curr;

	return curr;
} /* cmos_llseek */

static u16 adjust_cmos_crc(u8 bank, u16 seed);

static int cmos_ioctl(struct inode *inodp, struct file *filp,
					unsigned int cmd, unsigned long data)
{
	u16 crc;
	u8 buf;

	printk(KERN_INFO "%s: ioctl nr %d.\n", DEVICE_NAME, cmd);

	switch (cmd) {
		case CMOS_ADJUST_CHECKSUM:
			/* Calculate CRC16 of bank0, seeding with naught. */
			crc = adjust_cmos_crc(0, 0);

			/* Proceed with bank1, seeding with previous outcome. */
			crc = adjust_cmos_crc(1, crc);

			/* Store calculated CRC in the proper CMOS location. */
#if 0
			port_data_out(CMOS_BANK_CRC_OFFSET, (u8) (crc & 0xff), 1);
			port_data_out(CMOS_BANK_CRC_OFFSET + 1, (u8) (crc >> 8), 1);
#else
			printk(KERN_INFO "%s: Calculated CRC is %#04x.\n", 
							DEVICE_NAME, crc);
#endif
			return crc;
			break;

		case CMOS_VERIFY_CHECKSUM:
			/* Calculate CRC16 of bank0, seeding with naught. */
			crc = adjust_cmos_crc(0, 0);

			/* Proceed with bank1, seeding with previous outcome. */
			crc = adjust_cmos_crc(1, crc);

			printk(KERN_INFO "%s: Calculated CRC is %#04x.\n",
							DEVICE_NAME, crc);

			/* Compare calculated CRC with stored CRC. */
			buf = port_data_in(CMOS_BANK1_CRC_OFFSET, 1);
			if ( buf != (u8) (crc & 0xff) )
				return -EINVAL;

			buf = port_data_in(CMOS_BANK1_CRC_OFFSET + 1, 1);
			if ( buf != (u8) (crc >> 8) )
				return -EINVAL;
			break;
		default:
			return -EINVAL;
	}

	/* Placeholder! */
	return 0;
} /* cmos_ioctl */

u8 port_data_in(u8 addr, u8 bank) {
	u8 data;
	unsigned long flags;

	if ( unlikely(bank >= NUM_CMOS_BANKS) ) {
		printk(KERN_ERR "%s: Unknown CMOS bank number %d.\n",
							DEVICE_NAME, bank);
		return 0;
	} else {
		local_irq_save(flags);
		/* Send required address to the register. */
		outb_p(addr, addrports[bank]);
		/* Read data from bank. */
		data = inb_p(dataports[bank]);
		local_irq_restore(flags);
	}

	return data;
} /* port_data_in */

void port_data_out(u8 addr, u8 data, u8 bank) {
	unsigned long flags;

	if ( unlikely(bank >= NUM_CMOS_BANKS) ) {
		printk(KERN_ERR "%s: Unknown CMOS bank %d.\n", DEVICE_NAME, bank);
		return;
	} else {
		local_irq_save(flags);
		/* Send required address to the register. */
		outb_p(addr, addrports[bank]);
		/* Write data to bank. */
		outb_p(data, dataports[bank]);
		local_irq_restore(flags);
	}
} /* port_data_out */

static u16 adjust_cmos_crc(u8 bank, u16 seed)
{
	u16 n;
	u8 * buf, * bufp;

	if (  bank >= NUM_CMOS_BANKS )
		return 0;

	if ( (buf = kmalloc(CMOS_BANK_SIZE, GFP_KERNEL)) == NULL )
		return 0;

	bufp = buf;

	for (n = 0; n < CMOS_BANK_SIZE; n++)
		*(bufp++) = port_data_in(n, bank);

	n = crc16(seed, buf, CMOS_BANK_SIZE);

	kfree(buf);

	return n;
}

module_init(cmos_init);
module_exit(cmos_cleanup);

/* vi: set sw=4 ts=4 ai */
