altera_avalon_lcd_16207.c
/*
* This file provides the implementation of the functions used to drive a
* LCD panel.
*
* Characters written to the device will appear on the LCD panel as though
* it is a very small terminal. If the lines written to the terminal are
* longer than the number of characters on the terminal then it will scroll
* the lines of text automatically to display them all.
*
* If more lines are written than will fit on the terminal then it will scroll
* when characters are written to the line "below" the last displayed one -
* the cursor is allowed to sit below the visible area of the screen providing
* that this line is entirely blank.
*
* The following control sequences may be used to move around and do useful
* stuff:
* CR Moves back to the start of the current line
* LF Moves down a line and back to the start
* BS Moves back a character without erasing
* ESC Starts a VT100 style escape sequence
*
* The following escape sequences are recognised:
* ESC [ <row> ; <col> H Move to row and column specified (positions are
* counted from the top left which is 1;1)
* ESC [ K Clear from current position to end of line
* ESC [ 2 J Clear screen and go to top left
*
*/
/* ===================================================================== */
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include "sys/alt_dev.h"
#include "sys/alt_alarm.h"
#include "altera_avalon_lcd_16207.h"
#include "altera_avalon_lcd_16207_regs.h"
/* --------------------------------------------------------------------- */
/* Commands which can be written to the COMMAND register */
enum /* Write to character RAM */
{
LCD_CMD_WRITE_DATA = 0x80
/* Bits 6:0 hold character RAM address */
};
enum /* Write to character generator RAM */
{
LCD_CMD_WRITE_CGR = 0x40
/* Bits 5:0 hold character generator RAM address */
};
enum /* Function Set command */
{
LCD_CMD_FUNCTION_SET = 0x20,
LCD_CMD_8BIT = 0x10,
LCD_CMD_TWO_LINE = 0x08,
LCD_CMD_BIGFONT = 0x04
};
enum /* Shift command */
{
LCD_CMD_SHIFT = 0x10,
LCD_CMD_SHIFT_DISPLAY = 0x08,
LCD_CMD_SHIFT_RIGHT = 0x04
};
enum /* On/Off command */
{
LCD_CMD_ONOFF = 0x08,
LCD_CMD_ENABLE_DISP = 0x04,
LCD_CMD_ENABLE_CURSOR = 0x02,
LCD_CMD_ENABLE_BLINK = 0x01
};
enum /* Entry Mode command */
{
LCD_CMD_MODES = 0x04,
LCD_CMD_MODE_INC = 0x02,
LCD_CMD_MODE_SHIFT = 0x01
};
enum /* Home command */
{
LCD_CMD_HOME = 0x02
};
enum /* Clear command */
{
LCD_CMD_CLEAR = 0x01
};
/* Where in LCD character space do the rows start */
static char colstart[4] = { 0x00, 0x40, 0x20, 0x60 };
/* --------------------------------------------------------------------- */
static void lcd_write_command(alt_LCD_16207_dev * dev, unsigned char command)
{
unsigned int base = dev->base;
/* We impose a timeout on the driver in case the LCD panel isn't connected.
* The first time we call this function the timeout is approx 25ms
* (assuming 5 cycles per loop and a 200MHz clock). Obviously systems
* with slower clocks, or debug builds, or slower memory will take longer.
*/
int i = 1000000;
/* Don't bother if the LCD panel didn't work before */
if (dev->broken)
return;
/* Wait until LCD isn't busy. */
while (IORD_ALTERA_AVALON_LCD_16207_STATUS(base) & ALTERA_AVALON_LCD_16207_STATUS_BUSY_MSK)
if (--i == 0)
{
dev->broken = 1;
return;
}
/* Despite what it says in the datasheet, the LCD isn't ready to accept
* a write immediately after it returns BUSY=0. Wait for 100us more.
*/
usleep(100);
IOWR_ALTERA_AVALON_LCD_16207_COMMAND(base, command);
}
/* --------------------------------------------------------------------- */
static void lcd_write_data(alt_LCD_16207_dev * dev, unsigned char data)
{
unsigned int base = dev->base;
/* We impose a timeout on the driver in case the LCD panel isn't connected.
* The first time we call this function the timeout is approx 25ms
* (assuming 5 cycles per loop and a 200MHz clock). Obviously systems
* with slower clocks, or debug builds, or slower memory will take longer.
*/
int i = 1000000;
/* Don't bother if the LCD panel didn't work before */
if (dev->broken)
return;
/* Wait until LCD isn't busy. */
while (IORD_ALTERA_AVALON_LCD_16207_STATUS(base) & ALTERA_AVALON_LCD_16207_STATUS_BUSY_MSK)
if (--i == 0)
{
dev->broken = 1;
return;
}
/* Despite what it says in the datasheet, the LCD isn't ready to accept
* a write immediately after it returns BUSY=0. Wait for 100us more.
*/
usleep(100);
IOWR_ALTERA_AVALON_LCD_16207_DATA(base, data);
dev->address++;
}
/* --------------------------------------------------------------------- */
static void lcd_clear_screen(alt_LCD_16207_dev * dev)
{
int y;
lcd_write_command(dev, LCD_CMD_CLEAR);
dev->x = 0;
dev->y = 0;
dev->address = 0;
for (y = 0 ; y < ALT_LCD_HEIGHT ; y++)
{
memset(dev->line[y].data, ' ', sizeof(dev->line[0].data));
memset(dev->line[y].visible, ' ', sizeof(dev->line[0].visible));
dev->line[y].width = 0;
}
}
/* --------------------------------------------------------------------- */
static void lcd_repaint_screen(alt_LCD_16207_dev * dev)
{
int y, x;
/* scrollpos controls how much the lines have scrolled round. The speed
* each line scrolls at is controlled by its speed variable - while
* scrolline lines will wrap at the position set by width
*/
int scrollpos = dev->scrollpos;
for (y = 0 ; y < ALT_LCD_HEIGHT ; y++)
{
int width = dev->line[y].width;
int offset = (scrollpos * dev->line[y].speed) >> 8;
if (offset >= width)
offset = 0;
for (x = 0 ; x < ALT_LCD_WIDTH ; x++)
{
char c = dev->line[y].data[(x + offset) % width];
/* Writing data takes 40us, so don't do it unless required */
if (dev->line[y].visible[x] != c)
{
unsigned char address = x + colstart[y];
if (address != dev->address)
{
lcd_write_command(dev, LCD_CMD_WRITE_DATA | address);
dev->address = address;
}
lcd_write_data(dev, c);
dev->line[y].visible[x] = c;
}
}
}
}
/* --------------------------------------------------------------------- */
static void lcd_scroll_up(alt_LCD_16207_dev * dev)
{
int y;
for (y = 0 ; y < ALT_LCD_HEIGHT ; y++)
{
if (y < ALT_LCD_HEIGHT-1)
memcpy(dev->line[y].data, dev->line[y+1].data, ALT_LCD_VIRTUAL_WIDTH);
else
memset(dev->line[y].data, ' ', ALT_LCD_VIRTUAL_WIDTH);
}
dev->y--;
}
/* --------------------------------------------------------------------- */
static void lcd_handle_escape(alt_LCD_16207_dev * dev, char c)
{
int parm1 = 0, parm2 = 0;
if (dev->escape[0] == '[')
{
char * ptr = dev->escape+1;
while (isdigit(*ptr))
parm1 = (parm1 * 10) + (*ptr++ - '0');
if (*ptr == ';')
{
ptr++;
while (isdigit(*ptr))
parm2 = (parm2 * 10) + (*ptr++ - '0');
}
}
else
parm1 = -1;
switch (c)
{
case 'H': /* ESC '[' <y> ';' <x> 'H' : Move cursor to location */
case 'f': /* Same as above */
if (parm2 > 0)
dev->x = parm2 - 1;
if (parm1 > 0)
{
dev->y = parm1 - 1;
if (dev->y > ALT_LCD_HEIGHT * 2)
dev->y = ALT_LCD_HEIGHT * 2;
while (dev->y > ALT_LCD_HEIGHT)
lcd_scroll_up(dev);
}
break;
case 'J':
/* ESC J is clear to beginning of line [unimplemented]
* ESC [ 0 J is clear to bottom of screen [unimplemented]
* ESC [ 1 J is clear to beginning of screen [unimplemented]
* ESC [ 2 J is clear screen
*/
if (parm1 == 2)
lcd_clear_screen(dev);
break;
case 'K':
/* ESC K is clear to end of line
* ESC [ 0 K is clear to end of line
* ESC [ 1 K is clear to beginning of line [unimplemented]
* ESC [ 2 K is clear line [unimplemented]
*/
if (parm1 < 1)
{
if (dev->x < ALT_LCD_VIRTUAL_WIDTH)
memset(dev->line[dev->y].data + dev->x, ' ', ALT_LCD_VIRTUAL_WIDTH - dev->x);
}
break;
}
}
/* --------------------------------------------------------------------- */
int alt_lcd_16207_write(alt_fd* fd, const char* ptr, int len)
{
alt_LCD_16207_dev * dev = (alt_LCD_16207_dev*) fd->dev;
const char * end = ptr + len;
int y;
int widthmax;
/* When running in a multi threaded environment, obtain the "write_lock"
* semaphore. This ensures that writing to the device is thread-safe.
*/
ALT_SEM_PEND (dev->write_lock, 0);
/* Tell the routine which is called off the timer interrupt that the
* foreground routines are active so it must not repaint the display. */
dev->active = 1;
for ( ; ptr < end ; ptr++)
{
char c = *ptr;
if (dev->esccount >= 0)
{
unsigned int esccount = dev->esccount;
/* Single character escape sequences can end with any character
* Multi character escape sequences start with '[' and contain
* digits and semicolons before terminating
*/
if ((esccount == 0 && c != '[') ||
(esccount > 0 && !isdigit(c) && c != ';'))
{
dev->escape[esccount] = 0;
lcd_handle_escape(dev, c);
dev->esccount = -1;
}
else if (dev->esccount < sizeof(dev->escape)-1)
{
dev->escape[esccount] = c;
dev->esccount++;
}
}
else if (c == 27) /* ESC */
{
dev->esccount = 0;
}
else if (c == '\r')
{
dev->x = 0;
}
else if (c == '\n')
{
dev->x = 0;
dev->y++;
/* Let the cursor sit at X=0, Y=HEIGHT without scrolling so the user
* can print two lines of data without losing one.
*/
if (dev->y > ALT_LCD_HEIGHT)
lcd_scroll_up(dev);
}
else if (c == '\b')
{
if (dev->x > 0)
dev->x--;
}
else if (isprint(c))
{
/* If we didn't scroll on the last linefeed then we might need to do
* it now. */
if (dev->y >= ALT_LCD_HEIGHT)
lcd_scroll_up(dev);
if (dev->x < ALT_LCD_VIRTUAL_WIDTH)
dev->line[dev->y].data[dev->x] = c;
dev->x++;
}
}
/* Recalculate the scrolling parameters */
widthmax = ALT_LCD_WIDTH;
for (y = 0 ; y < ALT_LCD_HEIGHT ; y++)
{
int width;
for (width = ALT_LCD_VIRTUAL_WIDTH ; width > 0 ; width--)
if (dev->line[y].data[width-1] != ' ')
break;
/* The minimum width is the size of the LCD panel. If the real width
* is long enough to require scrolling then add an extra space so the
* end of the message doesn't run into the beginning of it.
*/
if (width <= ALT_LCD_WIDTH)
width = ALT_LCD_WIDTH;
else
width++;
dev->line[y].width = width;
if (widthmax < width)
widthmax = width;
dev->line[y].speed = 0; /* By default lines don't scroll */
}
if (widthmax <= ALT_LCD_WIDTH)
dev->scrollmax = 0;
else
{
widthmax *= 2;
dev->scrollmax = widthmax;
/* Now calculate how fast each of the other lines should go */
for (y = 0 ; y < ALT_LCD_HEIGHT ; y++)
if (dev->line[y].width > ALT_LCD_WIDTH)
{
/* You have three options for how to make the display scroll, chosen
* using the preprocessor directives below
*/
#if 1
/* This option makes all the lines scroll round at different speeds
* which are chosen so that all the scrolls finish at the same time.
*/
dev->line[y].speed = 256 * dev->line[y].width / widthmax;
#elif 1
/* This option pads the shorter lines with spaces so that they all
* scroll together.
*/
dev->line[y].width = widthmax / 2;
dev->line[y].speed = 256/2;
#else
/* This option makes the shorter lines stop after they have rotated
* and waits for the longer lines to catch up
*/
dev->line[y].speed = 256/2;
#endif
}
}
/* Repaint once, then check whether there has been a missed repaint
* (because active was set when the timer interrupt occurred). If there
* has been a missed repaint then paint again. And again. etc.
*/
for ( ; ; )
{
int old_scrollpos = dev->scrollpos;
lcd_repaint_screen(dev);
/* Let the timer routines repaint the display again */
dev->active = 0;
/* Have the timer routines tried to scroll while we were painting?
* If not then we can exit */
if (dev->scrollpos == old_scrollpos)
break;
/* We need to repaint again since the display scrolled while we were
* painting last time */
dev->active = 1;
}
/* Now that access to the display is complete, release the write
* semaphore so that other threads can access the buffer.
*/
ALT_SEM_POST (dev->write_lock);
return len;
}
/* --------------------------------------------------------------------- */
/* This should be in a top level header file really */
#define container_of(ptr, type, member) ((type *)((char *)ptr - offsetof(type, member)))
/*
* Timeout routine is called every second
*/
alt_u32 alt_lcd_16207_timeout(void * context)
{
alt_LCD_16207_dev * dev = (alt_LCD_16207_dev *) context;
/* Update the scrolling position */
if (dev->scrollpos + 1 >= dev->scrollmax)
dev->scrollpos = 0;
else
dev->scrollpos = dev->scrollpos + 1;
/* Repaint the panel unless the foreground will do it again soon */
if (dev->scrollmax > 0 && !dev->active)
lcd_repaint_screen(dev);
return dev->period;
}
/* --------------------------------------------------------------------- */
/*
* lcd_16207_init is called at boot time to initialise the LCD driver
*/
void alt_lcd_16207_init(alt_LCD_16207_dev * dev)
{
unsigned int base = dev->base;
/* Mark the device as functional */
dev->broken = 0;
ALT_SEM_CREATE (&dev->write_lock, 1);
/* TODO: check that usleep can be called in an initialisation routine */
/* The initialisation sequence below is copied from the datasheet for
* the 16207 LCD display. The first commands need to be timed because
* the BUSY bit in the status register doesn't work until the display
* has been reset three times.
*/
/* Wait for 15 ms then reset */
usleep(15000);
IOWR_ALTERA_AVALON_LCD_16207_COMMAND(base, LCD_CMD_FUNCTION_SET | LCD_CMD_8BIT);
/* Wait for another 4.1ms and reset again */
usleep(4100);
IOWR_ALTERA_AVALON_LCD_16207_COMMAND(base, LCD_CMD_FUNCTION_SET | LCD_CMD_8BIT);
/* Wait a further 1 ms and reset a third time */
usleep(1000);
IOWR_ALTERA_AVALON_LCD_16207_COMMAND(base, LCD_CMD_FUNCTION_SET | LCD_CMD_8BIT);
/* Setup interface parameters: 8 bit bus, 2 rows, 5x7 font */
lcd_write_command(dev, LCD_CMD_FUNCTION_SET | LCD_CMD_8BIT | LCD_CMD_TWO_LINE);
/* Turn display off */
lcd_write_command(dev, LCD_CMD_ONOFF);
/* Clear display */
lcd_clear_screen(dev);
/* Set mode: increment after writing, don't shift display */
lcd_write_command(dev, LCD_CMD_MODES | LCD_CMD_MODE_INC);
/* Turn display on */
lcd_write_command(dev, LCD_CMD_ONOFF | LCD_CMD_ENABLE_DISP);
dev->esccount = -1;
memset(dev->escape, 0, sizeof(dev->escape));
dev->scrollpos = 0;
dev->scrollmax = 0;
dev->active = 0;
dev->period = alt_ticks_per_second() / 10; /* Call every 100ms */
alt_alarm_start(&dev->alarm, dev->period, &alt_lcd_16207_timeout, dev);
/* make the device available to the system */
alt_dev_reg(&dev->dev);
}
/* --------------------------------------------------------------------- */
Maintained by John Loomis, updated Thu Apr 12 10:02:53 2007