blob: f5a5470c03b33974f2d2442855e49fc904dc9ff2 [file] [edit]
/*
* TTY on a LCD connected to I2C
* Supports Newhaven NHD-0216K3Z-NSW-BBW
*
* Copyright (C) 2013 Altera Corporation. All rights reserved.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#define DRV_NAME "lcd-comm"
#define DEV_NAME "ttyLCD"
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/tty.h>
#include <linux/delay.h>
#define LCD_COMMAND 0xfe
#define LCD_DISPLAY_ON 0x41
#define LCD_DISPLAY_OFF 0x42
#define LCD_SET_CURSOR 0x45
#define LCD_BACKSPACE 0x4e
#define LCD_CLEAR_SCREEN 0x51
#define LCD_BRIGHTNESS 0x53
#define LCD_CUSTOM_CHAR 0x54
#define LCD_BYTES_PER_FONT 8
#define LCD_BYTES_PER_FONT_CMD (LCD_BYTES_PER_FONT + 3)
#define LCD_BRIGHTNESS_MIN 1
#define LCD_BRIGHTNESS_MAX 8
#define ASCII_BS 0x08
#define ASCII_LF 0x0a
#define ASCII_CR 0x0d
#define ASCII_ESC 0x1b
#define ASCII_SPACE 0x20
#define ASCII_BACKSLASH 0x5c
#define ASCII_TILDE 0x7e
/* The NewHaven display has 8 custom characters that are user-loadable init
its cg ram. */
#define CUSTOM_BACKSLASH 0x00
#define CUSTOM_TILDE 0x01
struct custom_font {
const char font_cmd[LCD_BYTES_PER_FONT_CMD];
};
/* Array of commands to send to set up custom fonts. */
static struct custom_font custom_fonts[] = {
{ { LCD_COMMAND, LCD_CUSTOM_CHAR, CUSTOM_BACKSLASH, 0x00, 0x10, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00, }, },
{ { LCD_COMMAND, LCD_CUSTOM_CHAR, CUSTOM_TILDE, 0x00, 0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00, }, },
};
struct lcd {
struct device *dev;
struct i2c_client *client;
struct tty_driver *lcd_tty_driver;
struct tty_port port;
unsigned int width;
unsigned int height;
unsigned int brightness;
char *buffer;
unsigned int top_line;
unsigned int cursor_line;
unsigned int cursor_col;
};
#define MAX_LCDS 1
static struct lcd lcd_data_static[MAX_LCDS];
static int lcd_cmd_no_params(struct lcd *lcd_data, u8 cmd)
{
int count;
u8 buf[2] = {LCD_COMMAND, cmd};
count = i2c_master_send(lcd_data->client, buf, sizeof(buf));
if (count != sizeof(buf)) {
pr_err("%s: i2c_master_send returns %d\n", __func__, count);
return -1;
}
msleep(1);
return 0;
}
static int lcd_cmd_one_param(struct lcd *lcd_data, u8 cmd, u8 param)
{
int count;
u8 buf[3] = {LCD_COMMAND, cmd, param};
count = i2c_master_send(lcd_data->client, buf, sizeof(buf));
if (count != sizeof(buf)) {
pr_err("%s: i2c_master_send returns %d\n", __func__, count);
return -1;
}
msleep(1);
return 0;
}
static int lcd_cmd_backlight_brightness(struct lcd *lcd_data, u8 brightness)
{
return lcd_cmd_one_param(lcd_data, LCD_BRIGHTNESS, brightness);
}
static int lcd_cmd_display_on(struct lcd *lcd_data)
{
return lcd_cmd_no_params(lcd_data, LCD_DISPLAY_ON);
}
static int lcd_cmd_display_off(struct lcd *lcd_data)
{
return lcd_cmd_no_params(lcd_data, LCD_DISPLAY_OFF);
}
static int lcd_cmd_clear_screen(struct lcd *lcd_data)
{
return lcd_cmd_no_params(lcd_data, LCD_CLEAR_SCREEN);
}
static int lcd_cmd_backspace(struct lcd *lcd_data)
{
return lcd_cmd_no_params(lcd_data, LCD_BACKSPACE);
}
/* Note that this has to happen early on or the LCD module will not
process the command */
static int lcd_load_custom_fonts(struct lcd *lcd_data)
{
int count, i;
for (i = 0; i < ARRAY_SIZE(custom_fonts); i++) {
count = i2c_master_send(lcd_data->client,
(const char *)&custom_fonts[i].font_cmd,
LCD_BYTES_PER_FONT_CMD);
if (count != LCD_BYTES_PER_FONT_CMD) {
pr_err("%s: i2c_master_send returns %d\n", __func__, count);
return -1;
}
}
return 0;
}
static char lcd_translate_printable_char(char val)
{
if (val == ASCII_BACKSLASH)
return CUSTOM_BACKSLASH;
else if (val == ASCII_TILDE)
return CUSTOM_TILDE;
return val;
}
/* From NHD-0216K3Z-NSW-BBY Display Module datasheet. */
#define LCD_CURSOR_LINE_MULTIPLIER 0x40
static int lcd_cmd_set_cursor(struct lcd *lcd_data, u8 line, u8 col)
{
u8 cursor;
BUG_ON((line >= lcd_data->height) || (col >= lcd_data->width));
cursor = col + (LCD_CURSOR_LINE_MULTIPLIER * line);
return lcd_cmd_one_param(lcd_data, LCD_SET_CURSOR, cursor);
}
/*
* Map a line on the lcd display to a line on the buffer.
* Note that the top line on the display (line 0) may not be line 0 on the
* buffer due to scrolling.
*/
static unsigned int lcd_line_to_buf_line(struct lcd *lcd_data,
unsigned int line)
{
unsigned int buf_line;
buf_line = line + lcd_data->top_line;
if (buf_line >= lcd_data->height)
buf_line -= lcd_data->height;
return buf_line;
}
/* Returns a pointer to the line, column position in the lcd buffer */
static char *lcd_buf_pointer(struct lcd *lcd_data, unsigned int line,
unsigned int col)
{
unsigned int buf_line;
char *buf;
if ((lcd_data->cursor_line >= lcd_data->height) ||
(lcd_data->cursor_col >= lcd_data->width))
return lcd_data->buffer;
buf_line = lcd_line_to_buf_line(lcd_data, line);
buf = lcd_data->buffer + (buf_line * lcd_data->width) + col;
return buf;
}
static void lcd_clear_buffer_line(struct lcd *lcd_data, int line)
{
char *buf;
BUG_ON(line >= lcd_data->height);
buf = lcd_buf_pointer(lcd_data, line, 0);
memset(buf, ASCII_SPACE, lcd_data->width);
}
static void lcd_clear_buffer(struct lcd *lcd_data)
{
memset(lcd_data->buffer, ASCII_SPACE,
lcd_data->width * lcd_data->height);
lcd_data->cursor_line = 0;
lcd_data->cursor_col = 0;
lcd_data->top_line = 0;
}
static void lcd_reprint_one_line(struct lcd *lcd_data, u8 line)
{
char *buf = lcd_buf_pointer(lcd_data, line, 0);
lcd_cmd_set_cursor(lcd_data, line, 0);
i2c_master_send(lcd_data->client, buf, lcd_data->width);
}
static void lcd_print_top_n_lines(struct lcd *lcd_data, u8 lines)
{
unsigned int disp_line = 0;
while (disp_line < lines)
lcd_reprint_one_line(lcd_data, disp_line++);
}
static void lcd_add_char_at_cursor(struct lcd *lcd_data, char val)
{
char *buf;
buf = lcd_buf_pointer(lcd_data, lcd_data->cursor_line,
lcd_data->cursor_col);
*buf = val;
if (lcd_data->cursor_col < (lcd_data->width - 1))
lcd_data->cursor_col++;
}
static void lcd_crlf(struct lcd *lcd_data)
{
if (lcd_data->cursor_line < (lcd_data->height - 1)) {
/* Next line is blank, carriage return to beginning of line. */
lcd_data->cursor_line++;
if (lcd_data->cursor_line >= lcd_data->height)
lcd_data->cursor_line = 0;
} else {
/* Display is full. Scroll up one line. */
lcd_data->top_line++;
if (lcd_data->top_line >= lcd_data->height)
lcd_data->top_line = 0;
lcd_cmd_clear_screen(lcd_data);
lcd_clear_buffer_line(lcd_data, lcd_data->cursor_line);
lcd_print_top_n_lines(lcd_data, lcd_data->height);
}
lcd_cmd_set_cursor(lcd_data, lcd_data->height - 1, 0);
lcd_data->cursor_col = 0;
}
static void lcd_backspace(struct lcd *lcd_data)
{
if (lcd_data->cursor_col > 0) {
lcd_cmd_backspace(lcd_data);
lcd_data->cursor_col--;
}
}
static int lcd_write(struct tty_struct *tty, const unsigned char *buf,
unsigned int count)
{
struct lcd *lcd_data = tty->driver_data;
int buf_i = 0, left;
char val;
#ifdef DEBUG
char *dbgbuf = kzalloc(count + 1, GFP_KERNEL);
strncpy(dbgbuf, buf, count);
pr_debug("\n%s: count=%d buf[0]=%02x --->%s<---\n", __func__, count,
buf[0], dbgbuf);
#endif /* DEBUG */
if (count == 0) {
#ifdef DEBUG
kfree(dbgbuf);
#endif /* DEBUG */
return 0;
}
while (buf_i < count) {
left = count - buf_i;
/* process displayable chars */
if ((0x20 <= buf[buf_i]) && (buf[buf_i] <= 0x7f)) {
while ((buf_i < count) &&
((0x20 <= buf[buf_i]) && (buf[buf_i] <= 0x7f))) {
val = lcd_translate_printable_char(buf[buf_i]);
lcd_add_char_at_cursor(lcd_data, val);
buf_i++;
}
/* flush the line out to the display when we get to eol */
lcd_reprint_one_line(lcd_data, lcd_data->cursor_line);
/*
* ECMA-48 CSI sequences (from console_codes man page)
*
* ESC [ 2 J : erase whole display.
* ESC [ 2 K : erase whole line.
*/
} else if (buf[buf_i] == ASCII_ESC) {
if ((left >= 4) &&
(buf[buf_i + 1] == '[') &&
(buf[buf_i + 2] == '2') &&
(buf[buf_i + 3] == 'J')) {
pr_debug("ESC [2J = clear screan\n");
lcd_clear_buffer(lcd_data);
lcd_cmd_clear_screen(lcd_data);
buf_i += 4;
} else if ((left >= 4) &&
(buf[buf_i + 1] == '[') &&
(buf[buf_i + 2] == '2') &&
(buf[buf_i + 3] == 'K')) {
pr_debug("ESC [2K = clear line\n");
lcd_clear_buffer_line(lcd_data, lcd_data->cursor_line);
lcd_reprint_one_line(lcd_data, lcd_data->cursor_line);
lcd_cmd_set_cursor(lcd_data, lcd_data->cursor_line, 0);
lcd_data->cursor_col = 0;
buf_i += 4;
} else {
pr_debug("Unsupported escape sequence\n");
buf_i++;
}
} else if ((left >= 2) &&
(buf[buf_i] == ASCII_CR) && (buf[buf_i + 1] == ASCII_LF)) {
pr_debug("ASCII_CR/LF\n");
lcd_crlf(lcd_data);
buf_i += 2;
} else if ((left >= 1) && (buf[buf_i] == ASCII_CR)) {
pr_debug("ASCII_CR\n");
lcd_crlf(lcd_data);
buf_i++;
} else if ((left >= 1) && (buf[buf_i] == ASCII_LF)) {
pr_debug("ASCII_LF\n");
lcd_crlf(lcd_data);
buf_i++;
} else if ((left >= 1) && (buf[buf_i] == ASCII_BS)) {
pr_debug("ASCII_BS\n");
lcd_backspace(lcd_data);
buf_i++;
} else {
pr_debug("%s - Unsupported command 0x%02x\n", __func__, buf[buf_i]);
buf_i++;
}
}
#ifdef DEBUG
kfree(dbgbuf);
#endif /* DEBUG */
return count;
}
static ssize_t brightness_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct lcd *lcd_data = dev_get_drvdata(dev);
return scnprintf(buf, 2, "%d\n", lcd_data->brightness);
}
static ssize_t brightness_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct lcd *lcd_data = dev_get_drvdata(dev);
int ret, brightness;
ret = sscanf(buf, "%d", &brightness);
if (ret != 1)
return -EINVAL;
if ((brightness < LCD_BRIGHTNESS_MIN) ||
(brightness > LCD_BRIGHTNESS_MAX)) {
dev_err(lcd_data->dev, "out of range (%d to %d)\n",
LCD_BRIGHTNESS_MIN, LCD_BRIGHTNESS_MAX);
return -EINVAL;
}
lcd_data->brightness = brightness;
lcd_cmd_backlight_brightness(lcd_data, brightness);
return count;
}
static DEVICE_ATTR(brightness, S_IRUGO | S_IWUSR, brightness_show, brightness_store);
static struct attribute *lcd_attrs[] = {
&dev_attr_brightness.attr,
NULL,
};
static struct attribute_group lcd_attr_group = {
.attrs = lcd_attrs,
};
static int lcd_install(struct tty_driver *driver, struct tty_struct *tty)
{
struct lcd *lcd_data;
lcd_data = &lcd_data_static[tty->index];
if (lcd_data == NULL)
return -ENODEV;
tty->driver_data = lcd_data;
return tty_port_install(&lcd_data->port, driver, tty);
}
static int lcd_open(struct tty_struct *tty, struct file *filp)
{
struct lcd *lcd_data = tty->driver_data;
unsigned long flags;
tty->driver_data = lcd_data;
spin_lock_irqsave(&lcd_data->port.lock, flags);
lcd_data->port.count++;
spin_unlock_irqrestore(&lcd_data->port.lock, flags);
tty_port_tty_set(&lcd_data->port, tty);
return 0;
}
static void lcd_close(struct tty_struct *tty, struct file *filp)
{
struct lcd *lcd_data = tty->driver_data;
unsigned long flags;
bool last;
spin_lock_irqsave(&lcd_data->port.lock, flags);
--lcd_data->port.count;
last = (lcd_data->port.count == 0);
spin_unlock_irqrestore(&lcd_data->port.lock, flags);
if (last)
tty_port_tty_set(&lcd_data->port, NULL);
}
static unsigned int lcd_write_room(struct tty_struct *tty)
{
struct lcd *lcd_data = tty->driver_data;
return lcd_data->height * lcd_data->width;
}
static const struct tty_operations lcd_ops = {
.install = lcd_install,
.open = lcd_open,
.close = lcd_close,
.write = lcd_write,
.write_room = lcd_write_room,
};
static int lcd_probe(struct i2c_client *client)
{
struct device_node *np = client->dev.of_node;
struct lcd *lcd_data;
struct tty_driver *lcd_tty_driver;
unsigned int width = 0, height = 0, i, brightness = 0;
char *buffer;
int ret = -ENOMEM;
of_property_read_u32(np, "height", &height);
of_property_read_u32(np, "width", &width);
if ((width == 0) || (height == 0)) {
dev_err(&client->dev,
"Need to specify lcd width/height in device tree\n");
ret = -EINVAL;
goto err_devtree;
}
of_property_read_u32(np, "brightness", &brightness);
if ((brightness < LCD_BRIGHTNESS_MIN) ||
(brightness > LCD_BRIGHTNESS_MAX)) {
dev_info(&client->dev,
"lcd brighness not set or out of range, defaulting to maximum\n");
brightness = LCD_BRIGHTNESS_MAX;
}
for (i = 0 ; i < MAX_LCDS ; i++)
if (lcd_data_static[i].client == NULL)
break;
if (i >= MAX_LCDS) {
ret = -ENODEV;
dev_warn(&client->dev,
"More than %d I2C LCD displays found. Giving up.\n",
MAX_LCDS);
goto err_devtree;
}
lcd_data = &lcd_data_static[i];
buffer = kzalloc(height * width, GFP_KERNEL);
if (!buffer)
goto err_devtree;
i2c_set_clientdata(client, lcd_data);
lcd_data->client = client;
lcd_data->dev = &client->dev;
lcd_data->height = height;
lcd_data->width = width;
lcd_data->buffer = buffer;
lcd_data->brightness = brightness;
dev_set_drvdata(&client->dev, lcd_data);
tty_port_init(&lcd_data->port);
lcd_tty_driver = tty_alloc_driver(MAX_LCDS, 0);
if (!lcd_tty_driver)
goto err_driver;
lcd_tty_driver->driver_name = DRV_NAME;
lcd_tty_driver->name = DEV_NAME;
lcd_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
lcd_tty_driver->subtype = SERIAL_TYPE_NORMAL;
lcd_tty_driver->init_termios = tty_std_termios;
tty_set_operations(lcd_tty_driver, &lcd_ops);
ret = tty_register_driver(lcd_tty_driver);
if (ret)
goto err_register;
lcd_data->lcd_tty_driver = lcd_tty_driver;
lcd_clear_buffer(lcd_data);
lcd_load_custom_fonts(lcd_data);
lcd_cmd_display_on(lcd_data);
lcd_cmd_backlight_brightness(lcd_data, brightness);
lcd_cmd_clear_screen(lcd_data);
ret = sysfs_create_group(&lcd_data->dev->kobj, &lcd_attr_group);
if (ret) {
dev_err(lcd_data->dev, "Can't create sysfs attrs for lcd\n");
return ret;
}
dev_info(&client->dev, "LCD driver initialized\n");
return 0;
err_register:
tty_driver_kref_put(lcd_data->lcd_tty_driver);
err_driver:
kfree(buffer);
err_devtree:
return ret;
}
static void __exit lcd_remove(struct i2c_client *client)
{
struct lcd *lcd_data = i2c_get_clientdata(client);
lcd_cmd_display_off(lcd_data);
sysfs_remove_group(&lcd_data->dev->kobj, &lcd_attr_group);
tty_unregister_driver(lcd_data->lcd_tty_driver);
tty_driver_kref_put(lcd_data->lcd_tty_driver);
kfree(lcd_data->buffer);
}
static const struct of_device_id lcd_of_match[] = {
{ .compatible = "newhaven,nhd-0216k3z-nsw-bbw", },
{},
};
static const struct i2c_device_id lcd_id[] = {
{ DRV_NAME, 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, lcd_id);
static struct i2c_driver lcd_i2c_driver = {
.driver = {
.name = DRV_NAME,
.owner = THIS_MODULE,
.of_match_table = lcd_of_match,
},
.probe = lcd_probe,
.remove = lcd_remove,
.id_table = lcd_id,
};
static int __init lcd_init(void)
{
return i2c_add_driver(&lcd_i2c_driver);
}
subsys_initcall(lcd_init);
static void __exit lcd_exit(void)
{
i2c_del_driver(&lcd_i2c_driver);
}
module_exit(lcd_exit);
MODULE_DESCRIPTION("LCD 2x16");
MODULE_LICENSE("GPL");