/* Driver for parallel port LCD screen

   By James Stanley

   Run as root

   Before you compile, set up the pre-processor defines

   Connect pins like this (assuming it works like most LCD screens),
   [ground] and [+5v] can be obtained from a Molex connecter on your PSU,
   black is ground and red is +5v.

   LCD pin       Parallel port pin
   1 [VSS]       [ground]
   2 [VDD]       [+5v]
   3 [VO]        [ground]
   4 [RS]        16 (initialize)
   5 [R/W]       [ground]
   6 [E]         1 (strobe)
   7 [DB0]       2 (data 0)
   8 [DB1]       3 (data 1)
   9 [DB2]       4 (data 2)
   10 [DB3]      5 (data 3)
   11 [DB4]      6 (data 4)
   12 [DB5]      7 (data 5)
   13 [DB6]      8 (data 6)
   14 [DB7]      9 (data 7) */

#include <stdio.h>
#include <unistd.h>
#include <sys/io.h>

/* 0 for 1 line, 1 for 2 lines */
#define TWO_LINES 0

/* characters per line */
#define CHARS 20

/* character mode, for my screen 0 is 5x8 and 1 is 5x11 */
#define CHAR_MODE 0

/* 1 to show an underscore cursor */
#define SHOW_CURSOR 0

/* 1 to blink a block cursor */
#define BLINK_CURSOR 0

/* I/O port number of parallel port */
#define BASEPORT 0x378

/* shows the given character at whatever the current DDRAM address is */
void show_char(char c) {
  outb(c, BASEPORT);/* character on data bus */
  outb(4, BASEPORT + 2);/* turn RS high */
  outb(4 | 1, BASEPORT + 2);/* keeping RS high, enable */
  sleep(5);/* wait for 50us */
  outb(0, BASEPORT + 2);/* setting RS low, disable */
  sleep(5);
}

/* sends an instruction to the screen, and delays for some microseconds;
   note that you can not set the R/W bit

   usually you will want to ensure that each argument is either 0 or 1; strange
   (possibly unexpected) things will happen if this is not the case */
void send_instruction(char rs, char db7, char db6, char db5, char db4, char db3,
                      char db2, char db1, char db0, int usdelay) {
  /* set the data bus */
  outb(db7 << 7 | db6 << 6 | db5 << 5 | db4 << 4 |
       db3 << 3 | db2 << 2 | db1 << 1 | db0, BASEPORT);

  /* set the initialize pin (RS) */
  outb(rs << 2, BASEPORT + 2);

  /* set the enable pin (actually sets it to low, which is what we want for
     the LCD screen) */
  outb(rs << 2 | 1, BASEPORT + 2);

  /* and delay for the screen to do it's stuff */
  usleep(usdelay);

  /* (re)set the strobe pin (Enable) to high, as a side effect this also sets
     the initialize (RS) pin to low. If this is ever a problem for anyone, use
     'rs << 2' instead of 0 */
  outb(0, BASEPORT + 2);
}

/* initializes the screen, as per the datasheet */
void init_lcd(void) {
  /* set 8-bit mode, number of lines and character size
     NOTE: My datasheet is incorrect in its description of the initialization
     sequence. The field after TWO_LINES controls character size, _not_ whether
     or not to turn the display on; 5x8 is a 0, 5x11 is a 1 */
  send_instruction(0, 0, 0, 1, 1, TWO_LINES, CHAR_MODE, 0, 0, 50);

  /* set cursor stuff and turn display on */
  send_instruction(0, 0, 0, 0, 0, 1, 1, SHOW_CURSOR, BLINK_CURSOR, 50);

  /* clear the screen */
  send_instruction(0, 0, 0, 0, 0, 0, 0, 0, 1, 2000);

  /* increment DDRAM address after every character */
  send_instruction(0, 0, 0, 0, 0, 0, 1, 1, 0, 50);
}

int main(int argc, char **argv) {
  int n;
  int c;
  char *p;

  /* check arguments */
  if(argc != 2) {
    fprintf(stderr, "USAGE: %s string\n where string is the string to display "
            "on the LCD screen.\n", argv[0]);
  }

  /* grab the parallel port */
	if(ioperm(BASEPORT, 3, 1) != 0) {
		fprintf(stderr, "Unable to get permission for parallel port. Be root.\n");
		return 1;
	}

  /* initialize the screen */
  init_lcd();

  /* and send the text given on the command line */
  for(p = argv[1], c = 0; *p; p++, c++) {
    if(TWO_LINES && c == CHARS) {
      c = 0x40;
      /* set DDRAM address to first character of second line;
         this abuses send_instruction to fill the entire data bus with just 
         one argument (the 'c') */
      send_instruction(0, 1, 0, 0, 0, 0, 0, 0, c, 50);
    }
    show_char(*p);
  }

  ioperm(BASEPORT, 3, 0);

  return 0;
}
