Add LCD register abstraction for MIPI DCS like controllers. This hides LCD controller interface implementation details. When built with debugfs, the register is available to userspace. Signed-off-by: Noralf Trønnes <noralf@xxxxxxxxxxx> --- drivers/staging/fbtft/Kconfig | 2 + drivers/staging/fbtft/Makefile | 6 +- drivers/staging/fbtft/lcdreg/Kconfig | 4 + drivers/staging/fbtft/lcdreg/Makefile | 3 + drivers/staging/fbtft/lcdreg/internal.h | 9 + drivers/staging/fbtft/lcdreg/lcdreg-core.c | 187 ++++++++++++++++++ drivers/staging/fbtft/lcdreg/lcdreg-debugfs.c | 272 ++++++++++++++++++++++++++ drivers/staging/fbtft/lcdreg/lcdreg.h | 151 ++++++++++++++ 8 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 drivers/staging/fbtft/lcdreg/Kconfig create mode 100644 drivers/staging/fbtft/lcdreg/Makefile create mode 100644 drivers/staging/fbtft/lcdreg/internal.h create mode 100644 drivers/staging/fbtft/lcdreg/lcdreg-core.c create mode 100644 drivers/staging/fbtft/lcdreg/lcdreg-debugfs.c create mode 100644 drivers/staging/fbtft/lcdreg/lcdreg.h diff --git a/drivers/staging/fbtft/Kconfig b/drivers/staging/fbtft/Kconfig index 995a910..2d1490f 100644 --- a/drivers/staging/fbtft/Kconfig +++ b/drivers/staging/fbtft/Kconfig @@ -167,3 +167,5 @@ config FB_FLEX config FB_TFT_FBTFT_DEVICE tristate "Module to for adding FBTFT devices" depends on FB_TFT + +source "drivers/staging/fbtft/lcdreg/Kconfig" diff --git a/drivers/staging/fbtft/Makefile b/drivers/staging/fbtft/Makefile index e773f0f..2769421 100644 --- a/drivers/staging/fbtft/Makefile +++ b/drivers/staging/fbtft/Makefile @@ -1,8 +1,10 @@ -# Core module +# Core modules obj-$(CONFIG_FB_TFT) += fbtft.o fbtft-y += fbtft-core.o fbtft-sysfs.o fbtft-bus.o fbtft-io.o -# drivers +obj-$(CONFIG_LCDREG) += lcdreg/ + +# Controller drivers obj-$(CONFIG_FB_TFT_AGM1264K_FL) += fb_agm1264k-fl.o obj-$(CONFIG_FB_TFT_BD663474) += fb_bd663474.o obj-$(CONFIG_FB_TFT_HX8340BN) += fb_hx8340bn.o diff --git a/drivers/staging/fbtft/lcdreg/Kconfig b/drivers/staging/fbtft/lcdreg/Kconfig new file mode 100644 index 0000000..ec0c097 --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/Kconfig @@ -0,0 +1,4 @@ +config LCDREG + tristate + depends on GPIOLIB + default n diff --git a/drivers/staging/fbtft/lcdreg/Makefile b/drivers/staging/fbtft/lcdreg/Makefile new file mode 100644 index 0000000..c9ea774 --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_LCDREG) += lcdreg.o +lcdreg-y += lcdreg-core.o +lcdreg-$(CONFIG_DEBUG_FS) += lcdreg-debugfs.o diff --git a/drivers/staging/fbtft/lcdreg/internal.h b/drivers/staging/fbtft/lcdreg/internal.h new file mode 100644 index 0000000..04035b9 --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/internal.h @@ -0,0 +1,9 @@ +#include "lcdreg.h" + +#ifdef CONFIG_DEBUG_FS +extern void lcdreg_debugfs_init(struct lcdreg *reg); +extern void lcdreg_debugfs_exit(struct lcdreg *reg); +#else +static inline void lcdreg_debugfs_init(struct lcdreg *reg) { } +static inline void lcdreg_debugfs_exit(struct lcdreg *reg) { } +#endif diff --git a/drivers/staging/fbtft/lcdreg/lcdreg-core.c b/drivers/staging/fbtft/lcdreg/lcdreg-core.c new file mode 100644 index 0000000..4a1131c --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/lcdreg-core.c @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2015 Noralf Trønnes + * + * 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. + */ + +#include <linux/device.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/slab.h> + +#include "internal.h" +#include "lcdreg.h" + +/** + * Write to LCD register + * + * @reg: LCD register + * @regnr: Register number + * @transfer: Transfer to write + */ +int lcdreg_write(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer) +{ + if (!transfer) + return -EINVAL; + + if (!transfer->width) + transfer->width = reg->def_width; + + dev_dbg(reg->dev, + "lcdreg_write: regnr=0x%02x, index=%u, count=%u, width=%u\n", + regnr, transfer->index, transfer->count, transfer->width); + lcdreg_dbg_transfer_buf(transfer); + + return reg->write(reg, regnr, transfer); +} +EXPORT_SYMBOL(lcdreg_write); + +/** + * Write 32-bit wide buffer to LCD register + * @reg: lcdreg + * @regnr: Register number + * @buf: Buffer to write + * @count: Number of words to write + */ +int lcdreg_write_buf32(struct lcdreg *reg, unsigned regnr, const u32 *buf, + unsigned count) +{ + struct lcdreg_transfer tr = { + .index = 1, + .width = reg->def_width, + .count = count, + }; + int i, ret; + + if (!buf) + return -EINVAL; + + tr.buf = kmalloc_array(count, sizeof(*buf), GFP_KERNEL); + if (!tr.buf) + return -ENOMEM; + + if (reg->def_width <= 8) + for (i = 0; i < tr.count; i++) + ((u8 *)tr.buf)[i] = buf[i]; + else + for (i = 0; i < tr.count; i++) + ((u16 *)tr.buf)[i] = buf[i]; + ret = lcdreg_write(reg, regnr, &tr); + kfree(tr.buf); + + return ret; +} +EXPORT_SYMBOL(lcdreg_write_buf32); + +/** + * Read from LCD register + * + * @reg: LCD register + * @regnr: Register number + * @transfer: Transfer to read into + */ +int lcdreg_read(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer) +{ + int ret; + + if (!transfer) + return -EINVAL; + + if (!transfer->width) + transfer->width = reg->def_width; + + dev_dbg(reg->dev, + "lcdreg_read: regnr=0x%02x, index=%u, count=%u, width=%u\n", + regnr, transfer->index, transfer->count, transfer->width); + + ret = reg->read(reg, regnr, transfer); + + lcdreg_dbg_transfer_buf(transfer); + + return ret; +} +EXPORT_SYMBOL(lcdreg_read); + +/** + * Read from LCD register into 32-bit wide buffer + * @reg: LCD register + * @regnr: Register number + * @buf: Buffer to read into + * @count: Number of words to read + */ +int lcdreg_readreg_buf32(struct lcdreg *reg, unsigned regnr, u32 *buf, + unsigned count) +{ + struct lcdreg_transfer tr = { + .index = (reg->quirks & LCDREG_INDEX0_ON_READ) ? 0 : 1, + .count = count, + }; + int i, ret; + + if (!buf || !count) + return -EINVAL; + + tr.buf = kmalloc_array(count, sizeof(*buf), GFP_KERNEL); + if (!tr.buf) + return -ENOMEM; + + ret = lcdreg_read(reg, regnr, &tr); + if (ret) { + kfree(tr.buf); + return ret; + } + + if (reg->def_width <= 8) + for (i = 0; i < count; i++) + buf[i] = ((u8 *)tr.buf)[i]; + else + for (i = 0; i < count; i++) + buf[i] = ((u16 *)tr.buf)[i]; + kfree(tr.buf); + + return ret; +} +EXPORT_SYMBOL(lcdreg_readreg_buf32); + +static void devm_lcdreg_release(struct device *dev, void *res) +{ + struct lcdreg *reg = *(struct lcdreg **)res; + + lcdreg_debugfs_exit(reg); + mutex_destroy(®->lock); +} + +/** + * Device managed lcdreg initialization + * + * @dev: Device backing the LCD register + * @reg: LCD register + */ +struct lcdreg *devm_lcdreg_init(struct device *dev, struct lcdreg *reg) +{ + + struct lcdreg **ptr; + + if (!dev || !reg) + return ERR_PTR(-EINVAL); + + ptr = devres_alloc(devm_lcdreg_release, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + *ptr = reg; + devres_add(dev, ptr); + reg->dev = dev; + mutex_init(®->lock); + lcdreg_debugfs_init(reg); + + return reg; +} +EXPORT_SYMBOL(devm_lcdreg_init); + +MODULE_LICENSE("GPL"); diff --git a/drivers/staging/fbtft/lcdreg/lcdreg-debugfs.c b/drivers/staging/fbtft/lcdreg/lcdreg-debugfs.c new file mode 100644 index 0000000..1cba4c2 --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/lcdreg-debugfs.c @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2015 Noralf Trønnes + * + * 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. + */ + +#include <linux/debugfs.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/uaccess.h> + +#include "lcdreg.h" + +static struct dentry *lcdreg_debugfs_root; + +static int lcdreg_userbuf_to_u32(const char __user *user_buf, size_t count, + u32 *dest, size_t dest_size) +{ + char *buf, *start; + unsigned long value; + int ret = 0; + int i; + + buf = kmalloc(count, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + if (copy_from_user(buf, user_buf, count)) { + kfree(buf); + return -EFAULT; + } + buf[count] = 0; + for (i = 0; i < count; i++) + if (buf[i] == ' ' || buf[i] == '\n') + buf[i] = 0; + i = 0; + start = buf; + while (start < buf + count) { + if (*start == 0) { + start++; + continue; + } + if (i == dest_size) { + ret = -EFBIG; + break; + } + if (kstrtoul(start, 16, &value)) { + ret = -EINVAL; + break; + } + dest[i++] = value; + while (*start != 0) + start++; + }; + kfree(buf); + if (ret) + return ret; + + return i ? i : -EINVAL; +} + +static ssize_t lcdreg_debugfs_write_file(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct lcdreg *reg = file->private_data; + int ret; + u32 txbuf[128]; + + ret = lcdreg_userbuf_to_u32(user_buf, count, txbuf, ARRAY_SIZE(txbuf)); + if (ret <= 0) + return -EINVAL; + + add_taint(TAINT_USER, LOCKDEP_STILL_OK); + + lcdreg_lock(reg); + ret = lcdreg_write_buf32(reg, txbuf[0], txbuf + 1, ret - 1); + lcdreg_unlock(reg); + + return ret ? ret : count; +} + +static const struct file_operations lcdreg_debugfs_write_fops = { + .open = simple_open, + .write = lcdreg_debugfs_write_file, + .llseek = default_llseek, +}; + +static ssize_t lcdreg_debugfs_read_wr(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct lcdreg *reg = file->private_data; + struct lcdreg_transfer tr = { + .index = (reg->quirks & LCDREG_INDEX0_ON_READ) ? 0 : 1, + .width = reg->debugfs_read_width, + .count = 1, + }; + u32 txbuf[1]; + char *buf = NULL; + int ret; + + ret = lcdreg_userbuf_to_u32(user_buf, count, txbuf, ARRAY_SIZE(txbuf)); + if (ret != 1) + return -EINVAL; + + tr.buf = kmalloc(lcdreg_bytes_per_word(tr.width), GFP_KERNEL); + if (!tr.buf) + return -ENOMEM; + + add_taint(TAINT_USER, LOCKDEP_STILL_OK); + + lcdreg_lock(reg); + ret = lcdreg_read(reg, txbuf[0], &tr); + lcdreg_unlock(reg); + if (ret) + goto error_out; + + if (!reg->debugfs_read_result) { + reg->debugfs_read_result = kmalloc(16, GFP_KERNEL); + if (!reg->debugfs_read_result) { + ret = -ENOMEM; + goto error_out; + } + } + + buf = reg->debugfs_read_result; + + switch (tr.width) { + case 8: + snprintf(buf, 16, "%02x\n", *(u8 *)tr.buf); + break; + case 16: + snprintf(buf, 16, "%04x\n", *(u16 *)tr.buf); + break; + case 24: + case 32: + snprintf(buf, 16, "%08x\n", *(u32 *)tr.buf); + break; + default: + ret = -EINVAL; + goto error_out; + } + +error_out: + kfree(tr.buf); + if (ret) { + kfree(reg->debugfs_read_result); + reg->debugfs_read_result = NULL; + return ret; + } + + return count; +} + +static ssize_t lcdreg_debugfs_read_rd(struct file *file, + char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct lcdreg *reg = file->private_data; + + if (*ppos < 0 || !count) + return -EINVAL; + + if (!reg->debugfs_read_result) + return -ENODATA; + + return simple_read_from_buffer(user_buf, count, ppos, + reg->debugfs_read_result, + strlen(reg->debugfs_read_result)); +} + +static const struct file_operations lcdreg_debugfs_read_fops = { + .open = simple_open, + .read = lcdreg_debugfs_read_rd, + .write = lcdreg_debugfs_read_wr, + .llseek = default_llseek, +}; + +static ssize_t lcdreg_debugfs_reset_wr(struct file *file, + const char __user *user_buf, + size_t count, loff_t *ppos) +{ + struct lcdreg *reg = file->private_data; + + add_taint(TAINT_USER, LOCKDEP_STILL_OK); + lcdreg_reset(reg); + + return count; +} + +static const struct file_operations lcdreg_debugfs_reset_fops = { + .open = simple_open, + .write = lcdreg_debugfs_reset_wr, + .llseek = default_llseek, +}; + +static int lcdreg_debugfs_readwidth_set(void *data, u64 val) +{ + struct lcdreg *reg = data; + + reg->debugfs_read_width = val; + + return 0; +} +static int lcdreg_debugfs_readwidth_get(void *data, u64 *val) +{ + struct lcdreg *reg = data; + + /* + * def_width is not set when lcdreg_debugfs_init() is run, it's + * set later by the controller init code. Hence the need for this + * late assignment. + */ + if (!reg->debugfs_read_width) + reg->debugfs_read_width = reg->def_width; + + *val = reg->debugfs_read_width; + + return 0; +} + +DEFINE_SIMPLE_ATTRIBUTE(lcdreg_debugfs_readwidth_fops, + lcdreg_debugfs_readwidth_get, + lcdreg_debugfs_readwidth_set, "%llu\n"); + +void lcdreg_debugfs_init(struct lcdreg *reg) +{ + if (IS_ERR_OR_NULL(lcdreg_debugfs_root)) + return; + + reg->debugfs = debugfs_create_dir(dev_name(reg->dev), + lcdreg_debugfs_root); + if (!reg->debugfs) { + dev_warn(reg->dev, "Failed to create debugfs directory\n"); + return; + } + + debugfs_create_file("read_width", 0440, reg->debugfs, reg, + &lcdreg_debugfs_readwidth_fops); + debugfs_create_file("read", 0660, reg->debugfs, reg, + &lcdreg_debugfs_read_fops); + debugfs_create_file("write", 0220, reg->debugfs, reg, + &lcdreg_debugfs_write_fops); + debugfs_create_file("reset", 0220, reg->debugfs, reg, + &lcdreg_debugfs_reset_fops); +} + +void lcdreg_debugfs_exit(struct lcdreg *reg) +{ + debugfs_remove_recursive(reg->debugfs); +} + +static int lcdreg_debugfs_module_init(void) +{ + lcdreg_debugfs_root = debugfs_create_dir("lcdreg", NULL); + if (!lcdreg_debugfs_root) + pr_warn("lcdreg: Failed to create debugfs root\n"); + return 0; +} +module_init(lcdreg_debugfs_module_init); + +static void lcdreg_debugfs_module_exit(void) +{ + debugfs_remove_recursive(lcdreg_debugfs_root); +} +module_exit(lcdreg_debugfs_module_exit); + +MODULE_LICENSE("GPL"); diff --git a/drivers/staging/fbtft/lcdreg/lcdreg.h b/drivers/staging/fbtft/lcdreg/lcdreg.h new file mode 100644 index 0000000..0ddf40e --- /dev/null +++ b/drivers/staging/fbtft/lcdreg/lcdreg.h @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2015 Noralf Trønnes + * + * 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. + */ + +#ifndef __LINUX_LCDREG_H +#define __LINUX_LCDREG_H + +#include <linux/device.h> +#include <linux/mutex.h> + +/** + * struct lcdreg_transfer - LCD register transfer + * + * @index: register index (address) + * Known under the following names: + * D/C (command=0, data=1) + * RS (register selection: index=0, data=1) + * D/I (data/index: index=0, data=1) + * @buf: data array to transfer + * @count: number of items in array + * @width: override default regwidth + */ +struct lcdreg_transfer { + unsigned index; + void *buf; + unsigned count; + unsigned width; +}; + +/** + * struct lcdreg - interface to LCD register + * + * @dev: device interface + * @lock: mutex for register access locking + * @def_width: default register width + * @readable: LCD register is readable + * @write: write to LCD register + * @read: read from LCD register + * @reset: reset LCD controller + * @quirks: Deviations from the MIPI DBI standard + */ +struct lcdreg { + struct device *dev; + struct mutex lock; + unsigned def_width; + bool readable; + + int (*write)(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer); + int (*read)(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer); + void (*reset)(struct lcdreg *reg); + + u64 quirks; +/* slowdown command (index=0) */ +#define LCDREG_SLOW_INDEX0_WRITE BIT(0) +/* + * The MIPI DBI spec states that D/C should be HIGH during register reading. + * However, not all SPI master drivers support cs_change on last transfer and + * there are LCD controllers that ignore D/C on read. + */ +#define LCDREG_INDEX0_ON_READ BIT(1) + +#ifdef CONFIG_DEBUG_FS + struct dentry *debugfs; + u32 debugfs_read_width; + char *debugfs_read_result; +#endif +}; + +struct lcdreg *devm_lcdreg_init(struct device *dev, + struct lcdreg *reg); +extern int lcdreg_write(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer); +extern int lcdreg_write_buf32(struct lcdreg *reg, unsigned regnr, + const u32 *data, unsigned count); + +#define lcdreg_writereg(lcdreg, regnr, seq...) \ +({\ + u32 d[] = { seq };\ + lcdreg_write_buf32(lcdreg, regnr, d, ARRAY_SIZE(d));\ +}) + +extern int lcdreg_read(struct lcdreg *reg, unsigned regnr, + struct lcdreg_transfer *transfer); +extern int lcdreg_readreg_buf32(struct lcdreg *reg, unsigned regnr, u32 *buf, + unsigned count); + +static inline void lcdreg_reset(struct lcdreg *reg) +{ + reg->reset(reg); +} + +static inline void lcdreg_lock(struct lcdreg *reg) +{ + mutex_lock(®->lock); +} + +static inline void lcdreg_unlock(struct lcdreg *reg) +{ + mutex_unlock(®->lock); +} + +static inline bool lcdreg_is_readable(struct lcdreg *reg) +{ + return reg->readable; +} + +static inline unsigned lcdreg_bytes_per_word(unsigned bits_per_word) +{ + if (bits_per_word <= 8) + return 1; + else if (bits_per_word <= 16) + return 2; + else /* bits_per_word <= 32 */ + return 4; +} + +#if defined(CONFIG_DYNAMIC_DEBUG) + +#define lcdreg_dbg_transfer_buf(transfer) \ +do { \ + int groupsize = lcdreg_bytes_per_word(transfer->width); \ + size_t len = min_t(size_t, 32, transfer->count * groupsize); \ + \ + print_hex_dump_debug(" buf=", DUMP_PREFIX_NONE, 32, \ + groupsize, transfer->buf, len, false); \ +} while (0) + +#elif defined(DEBUG) + +#define lcdreg_dbg_transfer_buf(transfer) \ +do { \ + int groupsize = lcdreg_bytes_per_word(transfer->width); \ + size_t len = min_t(size_t, 32, transfer->count * groupsize); \ + \ + print_hex_dump(KERN_DEBUG, " buf=", DUMP_PREFIX_NONE, 32, \ + groupsize, transfer->buf, len, false); \ +} while (0) +#else + +#define lcdreg_dbg_transfer_buf(transfer) + +#endif /* DEBUG || CONFIG_DYNAMIC_DEBUG */ + +#endif /* __LINUX_LCDREG_H */ -- 2.2.2 _______________________________________________ devel mailing list devel@xxxxxxxxxxxxxxxxxxxxxx http://driverdev.linuxdriverproject.org/mailman/listinfo/driverdev-devel