/*  lcd_module.c
 *  Copyright 2002 Bjorn Bringert <bjorn@bringert.net>
 */

/*
 * Some parts of this driver are based on the lcd driver available at
 * http://lcd.sourceforge.net/, which is
 * copyright 1998-2001 Nils Faerber <nils@kernelconcepts.de>
 */

/*  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; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


#include <linux/config.h>

#ifdef CONFIG_MODVERSIONS
#include <linux/modversions.h>
#endif

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <asm/uaccess.h>
#include <linux/delay.h>
#include <linux/parport.h>

#include "sysdep.h"
#include "c_table.h"
#include "lcd_module.h"


#define LCD_DRIVER_NAME "lcd"
#define LCD LCD_DRIVER_NAME ": "

/* time to delay when writing */
#define DELAY_TIME 50

/* LCD control bits */
#define LCD_RW		0x01
#define LCD_RS		0x02

/* LCD functions */
#define LCD_CLEAR	0x01
#define LCD_HOME	0x02

#define LCD_SET_MODE	0x04
#define LCD_INC		0x02 	/* with LCD_MODESET*/

#define LCD_SET_ONOFF	0x08
#define LCD_ON		0x04 	/* with LCD_SET_ONOFF */
#define LCD_CURS_ON 	0x02 	/* with LCD_SET_ONOFF */
#define LCD_CURS_BLINK 	0x01 	/* with LCD_SET_ONOFF */

#define LCD_SET_SHIFT	0x10
#define LCD_DISP_SHIFT	0x08	/* with LCD_SET_SHIFT */
#define LCD_SHIFT_RIGHT	0x04	/* with LCD_SET_SHIFT */

#define LCD_SET_FUNC	0x20
#define LCD_8BIT	0x10 	/* with LCD_SET_FUNC */
#define LCD_2LINES	0x08 	/* with LCD_SET_FUNC */
#define LCD_LARGE_FONT	0x04 	/* with LCD_SET_FUNC */

#define LCD_SET_CGRAM	0x40
#define LCD_SET_DDRAM	0x80

#define FILE_DISP(filp) ((struct display *)(filp)->private_data)

#define DEFAULT_CURSOR LCD_CURS_ON

#define CGRAM_SIZE 8
#define CHAR_HEIGHT 8

#define MAX_ESC_PARAMS (CHAR_HEIGHT+1)

EXPORT_NO_SYMBOLS;

struct display {
	int cols;
	int rows;
	int x;
	int y;
	int cursor_state;
	int esc_seen;			/* 0 = no ESC, 1 = ESC seen */
	int esc_first;			/* first character after ESC */
	int esc_curr_param;		/* index into esc_params */
	int esc_params[MAX_ESC_PARAMS];
	char esc_op;
	int start_line;
	int in_use;			/* access protected by sem */
	char * buf;
	struct semaphore sem;

	struct parport * port;
	struct pardevice * dev;
};

/*
 * Static variables
 */

static struct display * displays[MAX_DISPLAYS] = { [0 ... MAX_DISPLAYS-1] = NULL };

static int major = DEFAULT_LCD_MAJOR;
static char * port[MAX_DISPLAYS] = { LCD_DEFAULT_PORT, [1 ... MAX_DISPLAYS-1] = "none" };
static int cols[MAX_DISPLAYS] = { [0 ... MAX_DISPLAYS-1] = DEFAULT_COLS };
static int rows[MAX_DISPLAYS] = { [0 ... MAX_DISPLAYS-1] = DEFAULT_ROWS };


/*
 * Module info and parameters
 */

MODULE_LICENSE ("GPL");
MODULE_DESCRIPTION ("LCD display driver");
MODULE_AUTHOR ("Bjorn Bringert <bjorn@bringert.net>");

MODULE_PARM (major, "i");
MODULE_PARM_DESC (major, "Major number for the driver");
MODULE_PARM (port, "1-" __MODULE_STRING(MAX_DISPLAYS) "l");
MODULE_PARM_DESC (port, "port=none|parportN[,none|parportM]..., ports to use");
MODULE_PARM (cols, "1-" __MODULE_STRING(MAX_DISPLAYS) "i");
MODULE_PARM_DESC (cols, "Number of columns for each display");
MODULE_PARM (rows, "1-" __MODULE_STRING(MAX_DISPLAYS) "i");
MODULE_PARM_DESC (rows, "Number of rows for each display");


/*
 * Prototypes
 */

static struct display * create_display (struct parport *, int, int);
static void destroy_display (struct display *);


/*
 * Hardware stuff
 */

/*
 * Sleep for 'n' uS
 * If n is less that the time between ticks, call udelay else sleep
 */
void sleep_delay (unsigned long n)
{
	int d = n / (1000000 / HZ);

	if (!d) {
		udelay(n);
	} else {
		/* Yield CPU time */
		unsigned long x = jiffies;
		while((jiffies-x) <= d)
			schedule();
	}
}

static void write_command (struct display * d, char command)
{
	DPRINTK(KERN_DEBUG LCD "write_command(%d)\n", command);

	parport_write_control(d->port, LCD_RS);
	parport_write_data(d->port, command);
	parport_write_control(d->port, LCD_RS | LCD_RW);/* start write */
	sleep_delay(DELAY_TIME);
	parport_write_control(d->port, LCD_RS);		/* end write */
	sleep_delay(DELAY_TIME);
	if (command == LCD_CLEAR || command == LCD_HOME) sleep_delay(2000);

}

static void write_data (struct display * d, char data)
{
	DPRINTK(KERN_DEBUG LCD "write_data(%d)\n", data);

	parport_write_control(d->port, 0x00);
	parport_write_data(d->port, c_table[(unsigned char)data]);
	parport_write_control(d->port, LCD_RW);	/* start write */
	sleep_delay(DELAY_TIME);
	parport_write_control(d->port, 0x00);	/* end write */
	sleep_delay(DELAY_TIME);
}

static inline int claim_port (struct display * d)
{
	DPRINTK(KERN_DEBUG LCD "claiming %s\n", d->port->name);
	return parport_claim(d->dev);
}

static inline void release_port (struct display * d)
{
	DPRINTK(KERN_DEBUG LCD "releasing %s\n", d->port->name);
	parport_release(d->dev);
}

static int lcd_init_hardware (struct display * d)
{
	int result;

	d->dev = parport_register_device(d->port, LCD_DRIVER_NAME, NULL, NULL, NULL, 0, d);
	if (!d->dev) {
		printk(KERN_ERR LCD "parport_register_device(%s, ...) failed\n", d->port->name);
		return -EIO;
	}

	result = claim_port(d);
	if (result < 0) {
		printk(KERN_ERR LCD "could not claim %s\n", d->port->name);
		return result;
	}

	sleep_delay(15000);
	write_command(d, LCD_SET_FUNC | LCD_8BIT);
	sleep_delay(4200);
	write_command(d, LCD_SET_FUNC | LCD_8BIT);
	sleep_delay(200);
	write_command(d, LCD_SET_FUNC | LCD_8BIT);
	sleep_delay(4200);
	write_command(d, LCD_SET_FUNC | LCD_8BIT | LCD_2LINES);
	sleep_delay(240);
	write_command(d, LCD_SET_ONOFF);
	sleep_delay(240);
	write_command(d, LCD_SET_ONOFF | LCD_ON | DEFAULT_CURSOR);
	sleep_delay(1740);
	write_command(d, LCD_SET_MODE | LCD_INC);
	sleep_delay(240);

	release_port(d);

	return 0;
}

static void lcd_release_hardware (struct display * d)
{
	parport_unregister_device(d->dev);
}

static void setup_cursor(struct display * d, int cursor_state)
{
	write_command(d, LCD_SET_ONOFF | LCD_ON | cursor_state);
}

static void pos_cursor (struct display * d, int col, int line)
{
	unsigned char offset;

	/* weird line offsets */
	offset = 20 * (line / 2) + 64 * (line % 2);

	write_command(d, LCD_SET_DDRAM | (offset + col));
}

static void lcd_reset (struct display * d) {
	write_command(d, LCD_CLEAR);
}

/* lines should be an array of length CHAR_HEIGHT. */
static void define_char (struct display * d, unsigned char c, unsigned char lines[])
{
	int i;

	if (c >= CGRAM_SIZE) return;
	write_command(d, LCD_SET_CGRAM | CHAR_HEIGHT * c);
	for (i = 0; i < CHAR_HEIGHT; i++) {
		write_data(d, lines[i]);
	}
	pos_cursor(d, d->x, d->y);
}


/*
 * Hardware independent stuff
 */

static int inline xy_to_index (struct display * d, int x, int y)
{
	return d->cols*((y + d->start_line) % d->rows) + x;
}

static void update_display_settings(struct display * d)
{
	setup_cursor(d, d->cursor_state);
}

static void update_disp (struct display * d)
{
	int offset, x, y;

	for (y = 0; y < d->rows; y++) {
		pos_cursor(d, 0, y);
		offset = d->cols * ((y + d->start_line) % d->rows);
		for (x = 0; x < d->cols; x++) {
			write_data(d, d->buf[offset + x]);
		}
	}

	pos_cursor(d, d->x, d->y);
}

static void clear_esc (struct display * d)
{
	d->esc_seen = 0;
	d->esc_first = '\0';
	d->esc_curr_param = 0;
	memset(d->esc_params, 0, MAX_ESC_PARAMS * sizeof(int));
	d->esc_op = '\0';
}

static void lcd_init (struct display * d)
{
	memset(d->buf, ' ', d->cols*d->rows*sizeof(char));
	d->start_line = 0;
	d->x = 0;
	d->y = 0;
	d->cursor_state = DEFAULT_CURSOR;
	clear_esc(d);
	lcd_reset(d);
}

static void lcd_gotoxy (struct display * d, int x, int y)
{
	DPRINTK(KERN_DEBUG LCD "lcd_gotoxy(%d, %d)\n", x, y);
	d->x = x;
	d->y = y;
	pos_cursor(d, d->x, d->y);
}

static void lcd_scroll (struct display * d, int dy)
{
	DPRINTK(KERN_DEBUG LCD "lcd_scroll(%d)\n", dy);

	for (; dy > 0; dy--) {
		memset(&d->buf[d->start_line * d->cols], ' ', d->cols); 	/* clear row 0 */
		d->start_line = (d->start_line + 1) % d->rows;			/* scroll 1 line */
	}

	update_disp(d);
}

static void lcd_putchar (struct display * d, char c)
{
	d->buf[xy_to_index(d, d->x, d->y)] = c;
	write_data(d, c);
}

static void lcd_fill (struct display * d, char c, int length)
{
	int i, start, size;

	start = xy_to_index(d, d->x, d->y);
	size = d->cols * d->rows;
	for (i = 0; i < length; i++) {
		d->buf[(start + i) % size] = c;
	}
	update_disp(d);
}

static void lcd_insert_chars (struct display * d, char c, int length)
{
	int here, left, size, i;

	here = xy_to_index(d, d->x, d->y);
	left = d->cols * (d->rows - d->y) - d->x;
	size = d->cols * d->rows;

	for (i = left - (length + 1); i >= 0; i--) {
		d->buf[(length + here + i) % size] = d->buf[(here + i) % size];
	}

	for (i = min(length, left) - 1; i >= 0; i--) {
		d->buf[(here + i) % size] =  c;
	}

	update_disp(d);
}

static void lcd_delete_chars (struct display * d, char c, int length)
{
	int here, copy, size, i;

	here = xy_to_index(d, d->x, d->y);
	copy = d->cols * (d->rows - d->y) - d->x - length;
	size = d->cols * d->rows;

	for (i = 0; i < copy; i++) {
		d->buf[(here + i) % size] = d->buf[(length + here + i) % size];
	}

	for (i = 0; i < length; i++) {
		d->buf[(copy + here + i) % size] = c;
	}

	update_disp(d);
}

static void setpos (struct display * d, int x, int y)
{
	int n;

	DPRINTK(KERN_DEBUG LCD "setpos(%d, %d)\n", x, y);

	if (y < 0) {
		lcd_scroll(d, y);
		y = 0;
	} else if (y >= d->rows) {
		lcd_scroll(d, y - (d->rows - 1));
		y = d->rows - 1;
	}
	if (x < 0) {
		n = 1 - x / d->cols;
		x += n * d->cols;
		y -= n;
	} else if (x >= d->cols) {
		n = x / d->cols;
		x -= n * d->cols;
		y += n;
	}
	lcd_gotoxy(d, x, y);
}

static void putchar (struct display * d, char c)
{
	lcd_putchar(d, c);

	d->x++;
	if (d->x >= d->cols) {
		d->x = 0;
		d->y++;
		if (d->y >= d->rows) {
			d->y--;
			lcd_scroll(d, 1);
		}
		lcd_gotoxy(d, d->x, d->y);
	}
}


/*
 * Escape sequence handling
 * For a reference on VT100 escape sequences see:
 * http://vt100.net/docs/vt100-ug/chapter3.html#S3.3.3
 */

static void do_esc(struct display * d)
{
	int n, m, old_x, old_y;

	n = d->esc_params[0];
	m = d->esc_params[1];

	old_x = d->x;
	old_y = d->y;

	switch (d->esc_first) {
		case '\0':
			switch (d->esc_op) {
				case 'c':	/* reset display */
					lcd_init(d);
					return;
				case 'D':	/* index */
					setpos(d, d->x, d->y + 1);
					return;
				case 'E':	/* next line */
					setpos(d, 0, d->y + 1);
					return;
				case 'M':	/* reverse index */
					setpos(d, d->x, d->y - 1);
					return;
				default:	/* unknown operation, do nothing */
					return;
			}
		case '?':
			switch (d->esc_op) {
				case 'h':	/* set mode */
					switch (n) {
						case 25:	/* cursor on */
							d->cursor_state |= LCD_CURS_ON;
							update_display_settings(d);
							return;
						default:	/* unknown setting, do nothing */
							return;
					}
				case 'l':	/* reset mode */
					switch (n) {		/* cursor off */
						case 25:
							d->cursor_state &= ~LCD_CURS_ON;
							update_display_settings(d);
							return;
						default:	/* unknown setting, do nothing */
							return;
					}
				default:	/* unknown operation, do nothing */
					return;
			}
		case '[':
			switch (d->esc_op) {
				case 'A':	/* move up n lines  */
					n = max(n, 1);
					setpos(d, d->x, d->y - n);
					return;
				case 'B':	/* move down n lines */
					n = max(n, 1);
					setpos(d, d->x, d->y + n);
					return;
				case 'C':	/* move right n characters */
					n = max(n, 1);
					setpos(d, d->x + n, d->y);
					return;
				case 'D':	/* move left n characters */
					n = max(n, 1);
					setpos(d, d->x - n, d->y);
					return;
				case 'f':	/* move to pos n m */
				case 'H':	/* H and f are equivalent */
					n = max(n, 0);
					m = max(m, 0);
					setpos(d, n, m);
					return;
				case 'J':	/* clear screen */
					n = max(n, 0);
					switch (n) {
						case 0:		/* (default) from cursor */
							lcd_fill(d, ' ', d->cols * (d->rows - d->y) - d->x);
							break;
						case 1:		/* to cursor */
							lcd_gotoxy(d, 0, 0);
							lcd_fill(d, ' ', d->cols * old_y + old_x);
							break;
						case 2:		/* entire screen, and home cursor */
							lcd_gotoxy(d, 0, 0);
							lcd_fill(d, ' ', d->cols * d->rows);
							old_x = old_y = 0;
							break;
						default: 	/* unknown, do nothing */
							return;
					}
					lcd_gotoxy(d, old_x, old_y);
					return;
				case 'K':	/* clear line */
					n = max(n, 0);
					switch (n) {
						case 0:		/* (default) from cursor */
							lcd_fill(d, ' ', d->cols - d->x);
							break;
						case 1:		/* to cursor */
							lcd_gotoxy(d, 0, d->y);
							lcd_fill(d, ' ', old_x);
							break;
						case 2:		/* entire line */
							lcd_gotoxy(d, 0, d->y);
							lcd_fill(d, ' ', d->cols);
							break;
						default: 	/* unknown, do nothing */
							return;
					}
					lcd_gotoxy(d, old_x, old_y);
					return;
				case 'L':	/* insert n lines */
					n = max(n, 1);
					lcd_insert_chars(d, ' ', n * d->cols);
					return;
				case 'M':	/* delete n lines */
					n = max(n, 1);
					lcd_delete_chars(d, ' ', n * d->cols);
					return;
				case 'P':	/* delete n chars */
					n = max(n, 1);
					lcd_delete_chars(d, ' ', n);
					return;

				case 'R':	/* define character */
					{
						int i;
						unsigned char lines[CHAR_HEIGHT];
						for (i = 0; i < CHAR_HEIGHT; i++) {
							lines[i] = (unsigned char)d->esc_params[i+1];
						}
						define_char(d, (unsigned char)n, lines);
					}
					return;
				case '@':	/* insert n chars */
					n = max(n, 1);
					lcd_insert_chars(d, ' ', n);
					return;
				default: 	/* unknown operation, ignore */
					return;
			}

		default:	/* unknown initial character, do nothing */
			return;
	}
}

static void parse_esc (struct display * d, char c)
{
	if (c >= '0' && c <= '9') {
		d->esc_params[d->esc_curr_param] = 10 * d->esc_params[d->esc_curr_param] + (c - '0');
	} else if (c == ';') {
		if (d->esc_curr_param < MAX_ESC_PARAMS-1) { /* ignore extra parameters if too many */
			d->esc_curr_param++;
			d->esc_params[d->esc_curr_param] = 0;
		}
	} else {
		d->esc_op = c;
		do_esc(d);
		clear_esc(d);
	}
}

static void display_write_char (struct display * d, char c)
{
	if (!d->esc_seen) {
		switch (c) {
			case '\010':		/* BS - backspace */
				setpos(d, d->x-1, d->y);
				putchar(d, ' ');
				setpos(d, d->x-1, d->y);
				return;
			case '\011':		/* HT - horizontal tab */
				setpos(d, d->x + TAB_SIZE - (d->x % TAB_SIZE), d->y);
				return;
			case '\012':		/* LF - newline */
			case '\013':		/* VT - vertical tab */
			case '\014':		/* FF - form feed */
				setpos(d, 0, d->y+1);
				return;
			case '\015':		/* CR - to beginning of line */
				setpos(d, 0, d->y);
				return;
			case '\033':		/* ESC */
				d->esc_seen = 1;
				return;
			default:
				putchar(d, c);
				return;
		}
	} else {
		switch (c) {
			case '?':
			case '[':
				d->esc_first = c;
				return;
			default:
				parse_esc(d, c);
				return;
		}
	}
}

/*
 * Device setup and cleanup
 */

/* allocates a display structure, registers and initializes the hardware */
static struct display * create_display (struct parport * p, int cols, int rows)
{
	char * buf;
	struct display * d;
	int buf_size, result;

	buf_size = cols*rows*sizeof(char);

	buf = (char *)kmalloc(buf_size, GFP_KERNEL);
	if (!buf) {
		printk(KERN_ERR LCD "Could not allocate buffer for display on %s\n", p->name);
		return NULL;
	}

	d = (struct display *)kmalloc(sizeof(struct display), GFP_KERNEL);
	if (!d) {
		printk(KERN_ERR LCD "Could not allocate structure for display on %s\n", p->name);
		kfree(buf);
		return NULL;
	}

	d->cols = cols;
	d->rows = rows;
	d->in_use = 0;
	d->buf = buf;
	sema_init(&d->sem, 1);
	d->port = p;

	result = lcd_init_hardware(d);
	if (result < 0) {
		kfree(buf);
		kfree(d);
		return NULL;
	}

	result = claim_port(d);
	if (result < 0) {
		printk(KERN_ERR LCD "Could not claim %s\n", d->port->name);
		kfree(buf);
		kfree(d);
		return NULL;
	}

	lcd_init(d);

	release_port(d);

	return d;
}

/* frees a display structure and unregisters the hardware */
static void destroy_display (struct display * d)
{
	printk(KERN_INFO LCD "releasing display connected to %s\n", d->port->name);
	lcd_release_hardware(d);
	kfree(d->buf);
	kfree(d);
}

static int init_displays (void)
{
	struct parport * p;
	int i, displays_set_up = 0;

	for (p = parport_enumerate(); p != NULL; p = p->next) {
		for (i = 0; i < MAX_DISPLAYS; i++) {
			if (strcmp(port[i], "none") == 0) {
				/* do nothing */
			} else if (strcmp(port[i], p->name) == 0) {
				displays[i] = create_display(p, cols[i], rows[i]);
				if (displays[i] == NULL) {
					printk(KERN_ERR LCD "Could not initialize display %i on %s\n", i, p->name);
					return -EIO;
				}
				printk(KERN_INFO LCD "display %d (%d x %d) set up on %s\n", i, displays[i]->cols, displays[i]->rows, p->name);
				displays_set_up++;
			}
		}
	}

	return displays_set_up;
}

static void release_displays (void)
{
	int i;
	for (i = 0; i < MAX_DISPLAYS; i++) {
		if (displays[i] != NULL) {
			destroy_display(displays[i]);
			displays[i] = NULL;
		}
	}
}


/*
 *	Character device stuff
 */

static int lcd_disp_open (struct inode * inode, struct file * filp)
{
	struct display * d;
	int num;

	num = MINOR(inode->i_rdev);
	DPRINTK(KERN_DEBUG LCD "open minor: %d\n", num);

	if (num >= MAX_DISPLAYS) {
		printk(KERN_ERR LCD "only %d displays supported, tried to open display %d\n", MAX_DISPLAYS, num);
		return -ENODEV;
	}

	d = displays[num];
	if (d == NULL) {
		printk(KERN_ERR LCD "display %d not set up\n", num);
		return -ENODEV;
	}

	filp->private_data = d;

	/* do this before maybe sleeping on the semaphore */
	MOD_INC_USE_COUNT;

	if (down_interruptible(&d->sem)) {
		MOD_DEC_USE_COUNT;
		return -ERESTARTSYS;
	}

	if (d->in_use) {
		up(&d->sem);
		MOD_DEC_USE_COUNT;
		printk(KERN_INFO LCD "display already in use\n");
		return -EBUSY;
	}
	d->in_use = 1;

	up(&d->sem);

	DPRINTK(KERN_DEBUG LCD "display opened\n");

	return 0;
}

static int lcd_disp_release (struct inode * inode, struct file * filp)
{
	struct display * d = FILE_DISP(filp);

	if (down_interruptible(&d->sem))
		return -ERESTARTSYS;

	d->in_use = 0;

	up(&d->sem);

	MOD_DEC_USE_COUNT;

	DPRINTK(KERN_DEBUG LCD "display closed\n");

	return 0;
}

/* write() for display devices */
static ssize_t lcd_disp_write (struct file * filp, const char * buf, size_t length, loff_t *offp)
{
	struct display * d = FILE_DISP(filp);
	char c;
	int i, result;

	result = claim_port(d);
	if (result < 0) {
		printk(KERN_ERR LCD "Could not claim %s\n", d->port->name);
		return result;
	}

	for (i = 0; i < length; i++) {
		get_user(c, buf + i);
		display_write_char(d, c);
	}

	release_port(d);

	return length;
}

struct file_operations lcd_disp_fops = {
	open: lcd_disp_open,
	release: lcd_disp_release,
	write: lcd_disp_write,
};

/*
 * Module init and cleanup
 */

#define ENFORCE_ARR_VALUE(arr,i,op,val)					\
	if ((arr)[(i)] op (val)) {					\
		printk(KERN_INFO LCD #arr "[%d] = %d " #op 		\
		" %d, set to %d\n", (i), (arr)[(i)], (val), (val));	\
		(arr)[(i)] = (val);					\
	}

void validate_parameters(void)
{
	int i;

	for (i = 0; i < MAX_DISPLAYS; i++) {
		ENFORCE_ARR_VALUE(cols, i, <, MIN_COLS);
		ENFORCE_ARR_VALUE(cols, i, >, MAX_COLS);
		ENFORCE_ARR_VALUE(rows, i, <, MIN_ROWS);
		ENFORCE_ARR_VALUE(rows, i, >, MAX_ROWS);
	}
}

int init_module(void)
{
	int result;

	printk(KERN_INFO LCD "driver loading...\n");

	SET_MODULE_OWNER(&lcd_disp_fops);

	validate_parameters();

	result = register_chrdev(major, LCD_DRIVER_NAME, &lcd_disp_fops);
	if (result < 0) {
		printk(KERN_ERR LCD "could not register major number %d\n", major);
		return result;
	}
	if (major == 0) {
		major = result; /* dynamic major */
	}

	result = init_displays();
	if (result < 0) {
		printk(KERN_ERR LCD "could not initialize displays\n");
		unregister_chrdev(major, LCD_DRIVER_NAME);
		return result;
	} else if (result == 0) {
		printk(KERN_ERR LCD "no displays found\n");
		return -ENODEV;
	}

	printk(KERN_INFO LCD "driver loaded with major number %d\n", major);

	return 0;
}

void cleanup_module(void)
{
	int result;

	release_displays();

	result = unregister_chrdev(major, LCD_DRIVER_NAME);
	if (result < 0) {
		printk(KERN_ERR LCD "could not unregister major number %d\n", major);
	}

	printk(KERN_INFO LCD "driver unloaded\n");
}