/* $Id: etrax100parlcd.c,v 1.1 2000/09/04 06:59:59 markusl Exp $
 * 
 * Elinux parallel LCD driver
 *
 */

#include <linux/config.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/malloc.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/ioctl.h>

#include <asm/segment.h>
#include <asm/system.h>
#include <asm/irq.h>
#include <asm/svinto.h>

#include "etrax100parlcd.h"

/*   DDRAM (Display Data RAM) addresses
 *   0                 19 
 *  64                 83
 *  20                 39
 *  84                103
 *
 */

#define LCD_HD44780_MAJOR 125

/*
 * RegisterSelect (RS) = autofd (bit 18 in R_PAR0_CTRL_DATA)
 * Read/Write (R/W_) = strb (bit 17 in R_PAR0_CTRL_DATA)
 * Enable (E) = init (bit 16 in R_PAR0_CTRL_DATA)
 */

#define LCD_E  0x00010000  /* LCD enable */
#define LCD_RW 0x00020000  /* LCD read/write */
#define LCD_RS 0x00040000  /* LCD register select */
#define PAR_OE 0x00100000  /* Output Enable on par port */

/* Flags used for the flag field in the etrax100parlcd_struct */
#define LCD_BUSY 0x00000001
#define LCD_NL   0x00000002

#define LCD_MAX_ROWS 4
#define LCD_MAX_COLS 20

typedef struct etrax100parlcd_struct {
  unsigned int flags;
  unsigned char row;
  unsigned char col;
} etrax100parlcd_struct;

static etrax100parlcd_struct lcd_status;

static int lcd_write(struct inode *inode, struct file *file,
                     const char *buf, int count);
static int lcd_ioctl(struct inode *inode, struct file *file,
                     unsigned int op, unsigned long arg);
static int lcd_open(struct inode *inode, struct file *file);
static void lcd_close(struct inode *inode, struct file *file);

static struct file_operations lcd_fops = {
  NULL,       /* lseek */
  NULL,       /* read */
  lcd_write,  /* write */
  NULL,       /* readdir */
  NULL,       /* select */
  lcd_ioctl,  /* ioctl */
  NULL,       /* mmap */
  lcd_open,   /* open */
  lcd_close   /* release */
};

static int row_col_to_addr(int row, int col)
{
  if (row == 0)
    return 0 + col;
  else if (row == 1)
    return 64 + col;
  else if (row == 2)
    return 20 + col;
  else if (row == 3)
    return 84 + col;
}

static void display_write_instr(byte instr)
{
  /* instr and LCD_E must be valid for 80ns */
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E | instr;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E | instr;
  *R_PAR0_CTRL_DATA = PAR_OE | instr;
  *R_PAR0_CTRL_DATA = 0;
}

#if 0
static void display_wait_busy(void)
{
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW | LCD_E;
  while (*R_PAR0_STATUS_DATA & 0xf0);
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RW;
  *R_PAR0_CTRL_DATA = 0;
}
#endif

static void display_write_text(byte text)
{
  /* text and LCD_E must be valid for 80ns */
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E | text;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E | text;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | text;
  *R_PAR0_CTRL_DATA = 0;
}

static void lcd_init_int(void)
{
  /* Initialization by instruction */
  display_write_instr(0x30);
  udelay(4500);
  display_write_instr(0x30);
  udelay(150);
  display_write_instr(0x30);
  udelay(150);
  display_write_instr(0x38);  /* Function set: 8bit, 2 rows and 5x8 dots */
  udelay(50);
  display_write_instr(0x04);  /* Display on/off control: all off */
  udelay(50);
  display_write_instr(0x01);  /* Clear display */
  udelay(1700);
  display_write_instr(0x06);  /* Entry mode set: inc cursor */
  udelay(50);
  display_write_instr(0x0e);  /* Display on/off control: disp on, cursor on */
  udelay(50);    
}

static int lcd_write(struct inode *inode, struct file *file,
              const char *buf, int count)
{
  unsigned int i;
  int retval;

  retval = verify_area(VERIFY_READ, (void *)buf, count);
  if (retval != 0) {
    printk("LCD_HD44780: VERIFY_READ error: %i\n", retval);
    return retval;
  }
  
  for (i = 0; i < count; i++) {
    
    if (lcd_status.flags & LCD_NL) {
      if (buf[i] == '\n') {
        if (++lcd_status.row < LCD_MAX_ROWS) {
          lcd_status.col = 0;
        }
        else {
          lcd_status.row = 0;
          lcd_status.col = 0;
          lcd_ioctl(0, 0, LCD_HD44780_CLEAR, 0);
        }
        continue;
      }
    }
    
    if (lcd_status.col < LCD_MAX_COLS) {

      display_write_instr(0x80 | (unsigned char)
                          row_col_to_addr(lcd_status.row, lcd_status.col));
      udelay(50);
//      display_wait_busy();
      
      lcd_status.col++;
    }
    else {
      if (lcd_status.row < LCD_MAX_ROWS) {
        lcd_status.row++;
        lcd_status.col = 0;
      }
      else {
        lcd_status.row = 0;
        lcd_status.col = 0;
        lcd_ioctl(0, 0, LCD_HD44780_CLEAR, 0);
      }
      display_write_instr(0x80 | (unsigned char)
                          row_col_to_addr(lcd_status.row, lcd_status.col++));
      udelay(50);
//      display_wait_busy();
      
    }
    display_write_text(buf[i]);
    udelay(50);
//    display_wait_busy();
    
  }

  return count;
}

static int lcd_ioctl(struct inode *inode, struct file *file,
              unsigned int op, unsigned long arg)
{
  switch (op) {
   case LCD_HD44780_CAH:  /* Cursor at home */
    display_write_instr(0x02);
    lcd_status.row = 0;
    lcd_status.col = 0;
    udelay(1700);
    break;
    
   case LCD_HD44780_CLEAR:  /* Clear display */
    display_write_instr(0x01);
    lcd_status.row = 0;
    lcd_status.col = 0;
    udelay(1700);
    break;
    
   case LCD_HD44780_SET_DDRAM:  /* Set DDRAM address */
    display_write_instr(0x80 | (unsigned char)arg);
    udelay(50);
    break;
    
   case LCD_HD44780_DISP:  /* Display on/off control */
    display_write_instr(0x08 | (unsigned char)arg);
    udelay(50);
    break;

   case LCD_HD44780_LCD_SHIFT:  /* Cursor/display shift */
    display_write_instr(0x10 | (unsigned char)arg);
    udelay(50);
    break;

   case LCD_HD44780_NEWLINE:  /* Set new line mode */
    if (arg == 1)
      lcd_status.flags |= LCD_NL;
    else if (arg == 0)
      lcd_status.flags &= ~LCD_NL;
    break;

   case LCD_HD44780_GOTO_XY:  /* Move cursor to x, y */
    lcd_status.row = (arg & 0x000000ff);
    lcd_status.col = (arg & 0x0000ff00) >> 8;
    break;

   default:
    return -EINVAL;
    break;
  }  

  return 0;
}

static int lcd_open(struct inode *inode, struct file *file)
{
  if (lcd_status.flags & LCD_BUSY) {
    return -EBUSY;
  }
  
  lcd_status.flags |= LCD_BUSY;
  lcd_status.flags |= LCD_NL;
  
  lcd_status.row = 0;
  lcd_status.col = 0;

  *R_PAR0_CONFIG = 0x00000060;  /* Manual-mode, enable port,
                                   immeditate mode change */
  
  lcd_init_int();
  
  return 0;
}

static void lcd_close(struct inode *inode, struct file *file)
{
  *R_PAR0_CONFIG = 0x00000000;  /* Reset port */

  lcd_status.flags = 0;
}

void etrax_par_lcd_hd44780_init()
{
  printk("Etrax/100 parallel LCD HD44780 driver v0.1 (c) 2000 Axis Communications AB\r\n");
  
  if (register_chrdev(LCD_HD44780_MAJOR, "LCD_HD44780", &lcd_fops)) {
    printk("unable to get major %d for LCD_HD44780\n", LCD_HD44780_MAJOR);
  }
}
