diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index e3dba6f..94a846d 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -83,3 +83,4 @@ obj-$(CONFIG_DRM_FSL_DCU) += fsl-dcu/
 obj-$(CONFIG_DRM_ETNAVIV) += etnaviv/
 obj-$(CONFIG_DRM_ARCPGU)+= arc/
 obj-y			+= hisilicon/
+obj-y += drm-text/
diff --git a/drivers/gpu/drm/drm-text/Makefile b/drivers/gpu/drm/drm-text/Makefile
new file mode 100644
index 0000000..48f55bc
--- /dev/null
+++ b/drivers/gpu/drm/drm-text/Makefile
@@ -0,0 +1,4 @@
+drm-text-y := drm-text-buffer.o
+drm-text-$(CONFIG_DEBUG_FS) += drm-text-debugfs.o
+
+obj-m += drm-text.o
diff --git a/drivers/gpu/drm/drm-text/drm-text-buffer.c b/drivers/gpu/drm/drm-text/drm-text-buffer.c
new file mode 100644
index 0000000..187dd4b
--- /dev/null
+++ b/drivers/gpu/drm/drm-text/drm-text-buffer.c
@@ -0,0 +1,336 @@
+#define DEBUG
+/*
+ * Copyright 2016 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 <drm/drmP.h>
+#include <drm/drm_fb_cma_helper.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_gem_cma_helper.h>
+#include <linux/font.h>
+#include <linux/module.h>
+
+#include "drm-text.h"
+
+struct drm_text_buffer *drm_text_buffers[MAX_DRM_TEXT_BUFFERS];
+
+/**
+ * DOC: overview
+ *
+ * Text buffer format 16-bit: 4-bit bg color, 4-bit fg color, 8-bit character
+ * The CGA color palette is used: 4-bit RGBI: intense red green blue
+ *
+ * drm_text_get(): get a text buffer for a dev->primary->index
+ * Clients can write directly to the text buffer
+ * drm_text_write(): write string to text buffer (handles wrapping)
+ * drm_text_scroll(): scroll text buffer
+ * drm_text_flush(): flush text buffer to pixel buffer (uses worker unless panic)
+ * drm_text_enable/disable(): enabled/disable text buffer (mode setting)
+ *
+ */
+
+static const u32 drm_text_palette888[] = {
+	0x00000000, /*  0 black */
+	0x000000aa, /*  1 blue */
+	0x0000aa00, /*  2 green */
+	0x0000aaaa, /*  3 cyan */
+	0x00aa0000, /*  4 red */
+	0x00aa00aa, /*  5 magenta */
+	0x00aa5500, /*  6 brown */
+	0x00aaaaaa, /*  7 light gray */
+	0x00555555, /*  8 dark gray */
+	0x005555ff, /*  9 bright blue */
+	0x0055ff55, /* 10 bright green */
+	0x0055ffff, /* 11 bright cyan */
+	0x00ff5555, /* 12 bright red */
+	0x00ff55ff, /* 13 bright magenta */
+	0x00ffff55, /* 14 yellow */
+	0x00ffffff  /* 15 white */
+};
+
+static const u16 drm_text_palette565[] = {
+	0x0000, /*  0 black */
+	0x0015, /*  1 blue */
+	0x0540, /*  2 green */
+	0x0555, /*  3 cyan */
+	0xa800, /*  4 red */
+	0xa815, /*  5 magenta */
+	0xaaa0, /*  6 brown */
+	0xad55, /*  7 light gray */
+	0x52aa, /*  8 dark gray */
+	0x52bf, /*  9 bright blue */
+	0x57ea, /* 10 bright green */
+	0x57ff, /* 11 bright cyan */
+	0xfaaa, /* 12 bright red */
+	0xfabf, /* 13 bright magenta */
+	0xffea, /* 14 yellow */
+	0xffff  /* 15 white */
+};
+
+static void drm_text_render_char(struct drm_text_buffer *text, size_t x, size_t y)
+{
+	u16 cc = text->text_buf[x + (y * text->cols)];
+	const struct font_desc *font = text->font;
+	u16 *vmem = text->pixel_buf;
+	char c = cc;
+	unsigned int h, w;
+	const u8 *src;
+	u16 fg_col = drm_text_palette565[(cc & 0x0f00) >> 8];
+	u16 bg_col = drm_text_palette565[cc >> 12];
+
+	src = font->data + c * font->height;
+
+	for (h = 0; h < font->height; h++) {
+		u8 fontline = *(src + h);
+
+		for (w = 0; w < font->width; w++) {
+			size_t i = ((x * font->width) + w) + (((y * font->height) + h) * text->fb->width);
+
+			vmem[i] = fontline & BIT(7 - w) ? fg_col : bg_col;
+		}
+	}
+}
+
+static void drm_text_do_flush(struct drm_text_buffer *text)
+{
+	size_t x, y;
+
+	drm_text_debug("%s - IN\n", __func__);
+
+	for (y = 0; y < text->rows; y++) {
+		for (x = 0; x < text->cols; x++) {
+			/*
+			 * TODO
+			 * A copy of the previously rendered text_buf
+			 * could help speed this up by skipping characters
+			 * that haven't changed
+			 */
+			drm_text_render_char(text, x, y);
+		}
+	}
+
+	drm_text_debug("%s - OUT\n", __func__);
+}
+
+static void drm_text_flush_work(struct work_struct *work)
+{
+	struct drm_text_buffer *text = container_of(work, struct drm_text_buffer,
+						   flush_work);
+	struct drm_framebuffer *fb = text->fb;
+
+	drm_text_debug("%s\n", __func__);
+	drm_text_do_flush(text);
+
+	if (fb->funcs->dirty)
+		fb->funcs->dirty(fb, NULL, 0, 0, NULL, 0);
+}
+
+void drm_text_flush(struct drm_text_buffer *text, bool panic)
+{
+
+	drm_text_debug("%s(panic=%u)\n", __func__, panic);
+	if (panic)
+		drm_text_do_flush(text);
+	else
+		schedule_work(&text->flush_work);
+}
+
+void drm_text_write(struct drm_text_buffer *text, const char *str,
+		    unsigned int num)
+{
+	unsigned int i;
+
+	for (i = 0; i < num; i++) {
+		char c = *str++;
+
+		if (text->y_pos == text->rows) {
+			drm_text_scroll(text, 0, text->rows, 1);
+			text->y_pos--;
+		}
+
+		if (c == '\n') {
+			text->x_pos = 0;
+			text->y_pos++;
+			continue;
+		}
+
+		text->text_buf[text->x_pos + (text->y_pos * text->cols)] = 0x0f00 | c;
+		text->x_pos++;
+
+		if (text->x_pos == text->cols) {
+			text->x_pos = 0;
+			text->y_pos++;
+		}
+	}
+}
+
+/* TODO: Is top and bottom necessary? From VT */
+void drm_text_scroll(struct drm_text_buffer *text, unsigned int top,
+		     unsigned int bottom, int lines)
+{
+	size_t count;
+
+	drm_text_debug("%s(top=%u, bottom=%u, lines=%d)\n", __func__, top, bottom, lines);
+
+	if (lines > 0) {
+		count = text->cols * (text->rows - lines);
+		memmove(text->text_buf, text->text_buf + text->cols, count * sizeof(u16));
+		memset(text->text_buf + count, 0, text->cols * lines * sizeof(u16));
+	} else if (lines < 0) {
+		drm_text_log("%s: TODO\n", __func__);
+	}
+
+}
+
+int drm_text_enable(struct drm_text_buffer *text)
+{
+	int ret;
+
+	ret = drm_fb_helper_set_par(text->fbi);
+	if (ret)
+		drm_text_log("%s: failed to enable %d\n", __func__, ret);
+
+	return ret;
+}
+
+int drm_text_disable(struct drm_text_buffer *text)
+{
+	int ret;
+
+	ret = fb_blank(text->fbi, FB_BLANK_POWERDOWN);
+	if (ret)
+		drm_text_log("%s: failed to disable %d\n", __func__, ret);
+
+	return ret;
+}
+
+struct drm_text_buffer *drm_text_get(unsigned int index)
+{
+	if (index >= MAX_DRM_TEXT_BUFFERS)
+		return NULL;
+
+	return drm_text_buffers[index];
+}
+
+static struct drm_text_buffer *drm_text_create_from_fb_helper(struct drm_fb_helper *fb_helper)
+{
+	struct drm_framebuffer *fb = fb_helper->fb;
+	struct drm_gem_cma_object *cma_obj;
+	struct drm_text_buffer *text;
+	int ret;
+
+	switch (fb->pixel_format) {
+	case DRM_FORMAT_RGB565:
+		break;
+	default:
+		return ERR_PTR(-EINVAL);
+	}
+
+	text = kzalloc(sizeof(*text), GFP_KERNEL);
+	if (!text)
+		return ERR_PTR(-ENOMEM);
+
+	INIT_WORK(&text->flush_work, drm_text_flush_work);
+	text->fb = fb;
+	text->fbi = fb_helper->fbdev;
+
+	DRM_DEBUG_KMS("DRM minor: %d\n", fb->dev->primary->index);
+
+	cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
+	text->pixel_buf = cma_obj->vaddr;
+
+	DRM_DEBUG_KMS("[FB:%d] pixel_format: %s, vaddr=%p\n", fb->base.id,
+		drm_get_format_name(fb->pixel_format), cma_obj->vaddr);
+
+	/* TODO: 10 and 12 width fonts are not supported */
+	text->font = get_default_font(fb->width, fb->height, -1, -1);
+	if (!text->font) {
+		ret = -ENODEV;
+		goto err_free;
+	}
+
+	DRM_DEBUG_KMS("font: %s\n", text->font->name);
+	text->cols = text->fb->width / text->font->width;
+	text->rows = text->fb->height / text->font->height;
+	DRM_DEBUG_KMS("cols=%zu, rows=%zu\n", text->cols, text->rows);
+
+	text->text_buf = kcalloc(text->cols * text->rows, sizeof(*text->text_buf), GFP_KERNEL);
+	if (!text->text_buf) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	drm_text_debugfs_buffer_init(text);
+/* REMOVE, helpful while testing on display that needs fb flushing */
+drm_text_flush(text, false);
+
+	return text;
+
+err_free:
+	DRM_DEBUG_KMS("Error creating buffer %d\n", ret);
+	kfree(text->text_buf);
+	kfree(text);
+
+	return ERR_PTR(ret);
+}
+
+static void drm_text_scan_fbdev(void)
+{
+	struct drm_fb_helper *fb_helper;
+	struct drm_text_buffer *text;
+	unsigned int i;
+
+	for (i = 0; i < FB_MAX; i++) {
+		struct fb_info *info = registered_fb[i];
+
+		if (!info)
+			continue;
+
+		fb_helper = info->par;
+		if (fb_helper && fb_helper->fbdev == info) {
+			text = drm_text_create_from_fb_helper(fb_helper);
+			if (!IS_ERR(text))
+				drm_text_buffers[text->fb->dev->primary->index] = text;
+		}
+	}
+}
+
+static void drm_text_free(struct drm_text_buffer *text)
+{
+	kfree(text->text_buf);
+	kfree(text);
+}
+
+static int __init drm_text_init(void)
+{
+	int ret = 0;
+
+	ret = drm_text_debugfs_init();
+	if (ret)
+		pr_err("Failed to create debugfs entry\n");
+
+	drm_text_scan_fbdev();
+
+
+	return ret;
+}
+module_init(drm_text_init);
+
+static void __exit drm_text_exit(void)
+{
+	unsigned int i;
+
+	drm_text_debugfs_exit();
+
+	for (i = 0; i < MAX_DRM_TEXT_BUFFERS; i++)
+		if (drm_text_buffers[i])
+			drm_text_free(drm_text_buffers[i]);
+}
+module_exit(drm_text_exit);
+
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/drm-text/drm-text-debugfs.c b/drivers/gpu/drm/drm-text/drm-text-debugfs.c
new file mode 100644
index 0000000..d01f995
--- /dev/null
+++ b/drivers/gpu/drm/drm-text/drm-text-debugfs.c
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2016 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 <drm/drmP.h>
+#include <linux/debugfs.h>
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+
+#include "drm-text.h"
+
+#define HACK_NEED_FLUSHING
+
+#define LOG_LINE	64
+#define LOG_ENTRIES	PAGE_SIZE / LOG_LINE
+
+static char *log_buf;
+static size_t log_pos;
+static struct dentry *drm_text_debugfs_root;
+
+static void drm_text_vprintk(int level, const char *fmt, va_list args)
+{
+	u32 rem_nsec;
+	char *text;
+	size_t len;
+	u64 sec;
+
+	if (log_pos >= LOG_ENTRIES)
+		log_pos = 0;
+
+	text = log_buf + (log_pos++ * LOG_LINE);
+	if (log_pos == LOG_ENTRIES)
+		log_pos = 0;
+
+	sec = div_u64_rem(local_clock(), 1000000000, &rem_nsec);
+
+	len = scnprintf(text, LOG_LINE, "[%5llu.%06u] ", sec, rem_nsec / 1000);
+
+	vscnprintf(text + len, LOG_LINE - len, fmt, args);
+
+	/* Make sure to always have a newline in case of overflow */
+	if (text[LOG_LINE - 2] != '\0')
+		text[LOG_LINE - 2] = '\n';
+}
+
+void drm_text_log(const char *fmt, ...)
+{
+	va_list args;
+
+	if (!log_buf)
+		return;
+
+	va_start(args, fmt);
+	drm_text_vprintk(LOGLEVEL_DEFAULT, fmt, args);
+	va_end(args);
+}
+
+static int drm_text_log_show(struct seq_file *m, void *v)
+{
+	size_t pos = log_pos;
+	unsigned int i;
+	char *text;
+
+	for (i = 0; i < LOG_ENTRIES; i++) {
+		text = log_buf + (pos++ * LOG_LINE);
+		if (pos == LOG_ENTRIES)
+			pos = 0;
+		if (*text == '\0')
+			continue;
+		seq_puts(m, text);
+	}
+
+	return 0;
+}
+
+static int drm_text_log_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, drm_text_log_show, NULL);
+}
+
+static const struct file_operations drm_text_log_ops = {
+	.owner   = THIS_MODULE,
+	.open    = drm_text_log_open,
+	.read    = seq_read,
+	.llseek  = seq_lseek,
+	.release = single_release,
+};
+
+static int drm_text_buffer_show(struct seq_file *m, void *v)
+{
+	struct drm_text_buffer *text = m->private;
+	size_t x, y;
+
+	for (y = 0; y < text->rows; y++) {
+		for (x = 0; x < text->cols; x++) {
+			char c = text->text_buf[x + (y * text->cols)];
+
+			if (c < 32 || c == 127 || c == 255)
+				c = ' ';
+			seq_putc(m, c);
+		}
+		seq_putc(m, '\n');
+	}
+
+	return 0;
+}
+
+static int drm_text_buffer_open(struct inode *inode, struct file *file)
+{
+	struct drm_text_buffer *text = inode->i_private;
+
+	return single_open(file, drm_text_buffer_show, text);
+}
+
+static const struct file_operations drm_text_buffer_ops = {
+	.owner   = THIS_MODULE,
+	.open    = drm_text_buffer_open,
+	.read    = seq_read,
+	.llseek  = seq_lseek,
+	.release = single_release,
+};
+
+void drm_text_debugfs_buffer_init(struct drm_text_buffer *text)
+{
+	char name[64];
+
+	snprintf(name, sizeof(name), "%d", text->fb->dev->primary->index);
+	debugfs_create_file(name, S_IRUGO, drm_text_debugfs_root, text,
+			    &drm_text_buffer_ops);
+}
+
+static ssize_t drm_text_flush_write(struct file *file,
+				    const char __user *user_buf,
+				    size_t count, loff_t *ppos)
+{
+	struct drm_text_buffer *text;
+	unsigned long long val;
+	char buf[24];
+	ssize_t ret = 0;
+	size_t size;
+
+	size = min(sizeof(buf) - 1, count);
+	if (copy_from_user(buf, user_buf, size))
+		return -EFAULT;
+
+	buf[size] = '\0';
+	ret = kstrtoull(buf, 0, &val);
+	if (ret)
+		return ret;
+
+	text = drm_text_get(val);
+	if (!text)
+		return -EINVAL;
+
+	//drm_text_flush(text, false);
+	if (text->fb->funcs->dirty)
+		text->fb->funcs->dirty(text->fb, NULL, 0, 0, NULL, 0);
+
+	return count;
+}
+
+static const struct file_operations drm_text_flush_ops = {
+	.write =        drm_text_flush_write,
+	.open =         simple_open,
+	.llseek =       default_llseek,
+};
+
+int drm_text_debugfs_init(void)
+{
+	drm_text_debugfs_root = debugfs_create_dir("drm-text", NULL);
+	if (!drm_text_debugfs_root)
+		return -ENOMEM;
+
+	if (!debugfs_create_file("log", S_IRUGO, drm_text_debugfs_root, NULL,
+			    &drm_text_log_ops))
+		goto err_remove;
+
+	log_buf = kzalloc(PAGE_SIZE, GFP_KERNEL);
+	if (!log_buf)
+		goto err_remove;
+
+	debugfs_create_file("flush", S_IWUSR, drm_text_debugfs_root, NULL,
+			    &drm_text_flush_ops);
+
+	return 0;
+
+err_remove:
+	debugfs_remove_recursive(drm_text_debugfs_root);
+
+	return -ENOMEM;
+}
+
+void drm_text_debugfs_exit(void)
+{
+	debugfs_remove_recursive(drm_text_debugfs_root);
+	kfree(log_buf);
+	log_buf = NULL;
+}
diff --git a/drivers/gpu/drm/drm-text/drm-text.h b/drivers/gpu/drm/drm-text/drm-text.h
new file mode 100644
index 0000000..77e7429
--- /dev/null
+++ b/drivers/gpu/drm/drm-text/drm-text.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 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_DRM_TEXT_H
+#define __LINUX_DRM_TEXT_H
+
+#include <linux/workqueue.h>
+
+#define MAX_DRM_TEXT_BUFFERS 64
+
+extern struct drm_text_buffer *drm_text_buffers[];
+
+struct drm_text_buffer {
+	struct fb_info *fbi;
+
+	struct work_struct flush_work;
+	const struct font_desc *font;
+	struct drm_framebuffer *fb;
+	void *pixel_buf;
+	u16 *text_buf;
+	size_t rows;
+	size_t cols;
+	size_t x_pos;
+	size_t y_pos;
+};
+
+void drm_text_flush(struct drm_text_buffer *text, bool panic);
+void drm_text_write(struct drm_text_buffer *text, const char *str,
+		    unsigned int num);
+void drm_text_scroll(struct drm_text_buffer *text, unsigned int top,
+		     unsigned int bottom, int lines);
+int drm_text_enable(struct drm_text_buffer *text);
+int drm_text_disable(struct drm_text_buffer *text);
+struct drm_text_buffer *drm_text_get(unsigned int index);
+
+#ifdef DEBUG
+#define drm_text_debug(fmt, ...) \
+	drm_text_log(fmt, ##__VA_ARGS__)
+#else
+#define drm_text_debug(fmt, ...)                  \
+do {                                              \
+	if (0)                                    \
+		drm_text_log(fmt, ##__VA_ARGS__); \
+} while (0)
+#endif
+
+#ifdef CONFIG_DEBUG_FS
+
+void drm_text_log(const char *fmt, ...) __printf(1, 2);
+void drm_text_debugfs_buffer_init(struct drm_text_buffer *text);
+int drm_text_debugfs_init(void);
+void drm_text_debugfs_exit(void);
+
+#else
+
+static inline void drm_text_log(const char *fmt, ...) __printf(1, 2)
+{
+}
+
+void drm_text_debugfs_buffer_init(struct drm_text_buffer *text)
+{
+}
+
+static inline int drm_text_debugfs_init(void)
+{
+	return 0;
+}
+
+static inline void drm_text_debugfs_exit(void);
+{
+}
+
+#endif
+
+#endif
