I recently bought a 2.2" TFT display on Ebay (come on, 7 bucks...) and was up to use it with my BeagleBone. Luckily for me there was no Linux driver for the ILI9341 controller so it is just to roll up my sleeves and get to work.
Boot up the BeagleBone
I haven't booted up my bone for a while and support for the board seems to have reached the mainline in v3.8 (currently at v3.15), so the first step is just to get it boot with a custom kernel.
Clone the vanilla kernel from kernel.org:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Use the omap2plus_defconfig as base:
make ARCH=arm omap2plus_defconfig
I will still use my old U-boot version, which does not have support for devicetrees, so I have to make sure that
CONFIG_ARM_APPENDED_DTB=y
This simply tells the boot code to look for a device tree binary (DTB) appended to the zImage. Without this option, the kernel expects the address of a dtb in the r2 register (on ARM architectures), but that does not work on my ancient bootloader.
Next step is to compile the kernel. We are using U-Boot as bootloader, but we do not create an uImage since we have to append the dtb to the zImage before that.:
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
Next, create the device tree blob. We are using the arch/arm/dts/am335x-bone.dts as source.:
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- am33x-bone.dtb
Now we are only two steps behind a booting kernel! First we need to append the dtb to the zImage, and then we need to create an U-boot-friendly kernel image with mkimage.:
cat arch/arm/boot/zImage arch/arm/boot/dts/am335x-bone.dtb > ./zImage_dtb
mkimage -A arm -O linux -T kernel -C none -a 0x80008000 -e 0x80008000 -n 'BeagleBone image' -d ./zImage_dtb uImage
Put the uImage on the uSD-card and boot it up. ..
BeagleBone login:
Victory!
Problems
The driver is quite straight forward and there was no really hard problem with the driver itself. However, I had problem to get a high framerate because the SPI communication took time. All SPI communication is asynchronious and all jobs is stacked on a queue before it gets scheduled. This takes time. One obvious solution is to write bigger chunks with each transfer, and that is what I did.
But the problem was that when I increased the chunk size, the kernel got panic with the DMA transfers.
After an half a hour of code-digging, the problem is derived to the spi-controller for the omap2 (drivers/spi/spi-omap2-mcspi.c). It defines the DMA_MIN_BYTES which is arbitrarily set to 160. The code then compare the data length to this constant and determine if it should use DMA or not. It shows up that the DMA-transfer-code itself is broken.
A temporary solution is to increase the DMA_MIN_BYTES to at least a full frame (240x320x2) bytes until I have looked at the DMA code and submitted a fix :-)
Result
Here is a shell started from Ubuntu
I have also tested to startup Qt and directfb applications. It all works like a charm.
Conclusion
The Deferred IO interface is really nice for such displays. I'm surprised that there is currently so few drivers using it.
(the not so cleaned up) Code:
/\*
\* linux/drivers/video/ili9341.c -- FB driver for ili9341 controller
\*
\* Copyright (C) 2014, Marcus Folkesson
\*
\* This file is subject to the terms and conditions of the GNU General Public
\* License. See the file COPYING in the main directory of this archive for
\* more details.
\*
\*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/uaccess.h>
#include <linux/spi/spi.h>
#include <video/ili9341.h>
#include <linux/regmap.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/debugfs.h>
/\* Display specific information \*/
#define SCREEN_WIDTH (240)
#define SCREEN_HIGHT (320)
#define SCREEN_BPP (16)
#define ID "cbdff49d683b"
#define ID_SZ 12
static unsigned int chunk_size;
struct ili9341_priv {
struct spi_device *spi;
struct regmap *regmap;
struct fb_info *info;
u32 vsize;
int dc;
char *fbmem;
struct dentry *dir;
};
static struct fb_fix_screeninfo ili9341_fix = {
.id = "ili9341",
.type = FB_TYPE_PACKED_PIXELS,
/*.visual = FB_VISUAL_MONO01,*/
.visual = FB_VISUAL_PSEUDOCOLOR,
.xpanstep = 0,
.ypanstep = 0,
.ywrapstep = 0,
.line_length = SCREEN_WIDTH*2,
.accel = FB_ACCEL_NONE,
};
static struct fb_var_screeninfo ili9341_var = {
.xres = SCREEN_WIDTH,
.yres = SCREEN_HIGHT,
.xres_virtual = SCREEN_WIDTH,
.yres_virtual = SCREEN_HIGHT,
.bits_per_pixel = SCREEN_BPP,
.nonstd = 1,
.red = {
.offset = 11,
.length = 5,
},
.green = {
.offset = 5,
.length = 6,
},
.blue = {
.offset = 0,
.length = 5,
},
.transp = {
.offset = 0,
.length = 0,
},
};
static const struct regmap_config ili9341_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.can_multi_write = 1,
};
static void fill(struct ili9341_priv *priv);
static void fill_area(struct ili9341_priv *priv, int y1, int y2);
/* main ili9341 functions */
static void apollo_send_data(struct ili9341_priv *par, unsigned char data)
{
return;
/* set data */
}
static void apollo_send_command(struct ili9341_priv *par, unsigned char data)
{
return;
}
static void ili9341_dpy_update(struct ili9341_priv *par)
{
/*return;*/
fill(par);
}
static void ili9341_dpy_update_area(struct ili9341_priv *par, int y1, int y2 )
{
/*return;*/
fill_area(par, y1, y2);
}
/* this is called back from the deferred io workqueue */
static void ili9341_dpy_deferred_io(struct fb_info *info,
struct list_head *pagelist)
{
struct page *cur;
struct fb_deferred_io *fbdefio = info->fbdefio;
struct ili9341_priv *par = info->par;
struct page *page;
unsigned long beg, end;
int y1, y2, miny, maxy;
miny = INT_MAX;
maxy = 0;
/* stop here if list is empty */
if (list_empty(pagelist)){
dev_err(&par->spi->dev, "pagelist is empty");
return;
}
list_for_each_entry(page, pagelist, lru) {
beg = page->index << PAGE_SHIFT;
end = beg + PAGE_SIZE - 1;
y1 = beg / (info->fix.line_length);
y2 = end / (info->fix.line_length);
if (y2 >= info->var.yres)
y2 = info->var.yres - 1;
if (miny > y1)
miny = y1;
if (maxy < y2)
maxy = y2;
}
ili9341_dpy_update_area(info->par, miny, maxy);
// dev_err(&par->spi->dev, ".");
}
static void ili9341_fillrect(struct fb_info *info,
const struct fb_fillrect *rect)
{
struct ili9341_priv *par = info->par;
sys_fillrect(info, rect);
/*ili9341_dpy_update(par);*/
}
static void ili9341_copyarea(struct fb_info *info,
const struct fb_copyarea *area)
{
struct ili9341_priv *par = info->par;
sys_copyarea(info, area);
/*ili9341_dpy_update(par);*/
}
static void ili9341_imageblit(struct fb_info *info,
const struct fb_image *image)
{
struct ili9341_priv *par = info->par;
sys_imageblit(info, image);
/*ili9341_dpy_update(par);*/
}
/*
* this is the slow path from userspace. they can seek and write to
* the fb. it's inefficient to do anything less than a full screen draw
*/
static ssize_t ili9341_write(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos)
{
struct ili9341_priv *par = info->par;
unsigned long p = *ppos;
void *dst;
int err = 0;
unsigned long total_size;
if (info->state != FBINFO_STATE_RUNNING)
return -EPERM;
total_size = info->fix.smem_len;
if (p > total_size)
return -EFBIG;
if (count > total_size) {
err = -EFBIG;
count = total_size;
}
if (count + p > total_size) {
if (!err)
err = -ENOSPC;
count = total_size - p;
}
dst = (void __force *) (info->screen_base + p);
if (copy_from_user(dst, buf, count))
err = -EFAULT;
if (!err)
*ppos += count;
ili9341_dpy_update(par);
return (err) ? err : count;
}
static struct fb_ops ili9341_ops = {
.owner = THIS_MODULE,
.fb_write = ili9341_write,
.fb_fillrect = ili9341_fillrect,
.fb_copyarea = ili9341_copyarea,
.fb_imageblit = ili9341_imageblit,
};
static struct fb_deferred_io ili9341_defio = {
.delay = HZ/60,
.deferred_io = ili9341_dpy_deferred_io,
};
static void write_command(struct ili9341_priv *priv, u8 data)
{
gpio_set_value(priv->dc, 0);
spi_write(priv->spi, &data, 1);
gpio_set_value(priv->dc, 1);
}
static void write_data(struct ili9341_priv *priv, u8 data)
{
gpio_set_value(priv->dc, 1);
spi_write(priv->spi, &data, 1);
}
static void write_data16(struct ili9341_priv *priv, u8 data)
{
gpio_set_value(priv->dc, 1);
spi_write(priv->spi, &data, 1);
}
static void init(struct ili9341_priv *priv)
{
write_command(priv, 0xCB);
write_data(priv, 0x39);
write_data(priv, 0x2C);
write_data(priv, 0x00);
write_data(priv, 0x34);
write_data(priv, 0x02);
write_command(priv, 0xCF);
write_data(priv, 0x00);
write_data(priv, 0XC1);
write_data(priv, 0X30);
write_command(priv, 0xE8);
write_data(priv, 0x85);
write_data(priv, 0x00);
write_data(priv, 0x78);
write_command(priv, 0xEA);
write_data(priv, 0x00);
write_data(priv, 0x00);
write_command(priv, 0xED);
write_data(priv, 0x64);
write_data(priv, 0x03);
write_data(priv, 0X12);
write_data(priv, 0X81);
write_command(priv, 0xF7);
write_data(priv, 0x20);
write_command(priv, 0xC0); //Power control
write_data(priv, 0x23); //VRH[5:0]
write_command(priv, 0xC1); //Power control
write_data(priv, 0x10); //SAP[2:0];BT[3:0]
write_command(priv, 0xC5); //VCM control
write_data(priv, 0x3e); //Contrast
write_data(priv, 0x28);
write_command(priv, 0xC7); //VCM control2
write_data(priv, 0x86); //--
/* XXX: Hue?! */
write_command(priv, 0x36); // Memory Access Control
write_data(priv, 0x48); //C8 //48 68绔栧睆//28 E8 妯睆
write_command(priv, 0x3A);
write_data(priv, 0x55);
write_command(priv, 0xB1);
write_data(priv, 0x00);
write_data(priv, 0x18);
write_command(priv, 0xB6); // Display Function Control
write_data(priv, 0x08);
write_data(priv, 0x82);
write_data(priv, 0x27);
write_command(priv, 0xF2); // 3Gamma Function Disable
write_data(priv, 0x00);
write_command(priv, 0x26); //Gamma curve selected
write_data(priv, 0x01);
write_command(priv, 0xE0); //Set Gamma
write_data(priv, 0x0F);
write_data(priv, 0x31);
write_data(priv, 0x2B);
write_data(priv, 0x0C);
write_data(priv, 0x0E);
write_data(priv, 0x08);
write_data(priv, 0x4E);
write_data(priv, 0xF1);
write_data(priv, 0x37);
write_data(priv, 0x07);
write_data(priv, 0x10);
write_data(priv, 0x03);
write_data(priv, 0x0E);
write_data(priv, 0x09);
write_data(priv, 0x00);
write_command(priv, 0XE1); //Set Gamma
write_data(priv, 0x00);
write_data(priv, 0x0E);
write_data(priv, 0x14);
write_data(priv, 0x03);
write_data(priv, 0x11);
write_data(priv, 0x07);
write_data(priv, 0x31);
write_data(priv, 0xC1);
write_data(priv, 0x48);
write_data(priv, 0x08);
write_data(priv, 0x0F);
write_data(priv, 0x0C);
write_data(priv, 0x31);
write_data(priv, 0x36);
write_data(priv, 0x0F);
write_command(priv, 0x11); //Exit Sleep
mdelay(100);
write_command(priv, 0x29); //Display on
write_command(priv, 0x2c);
}
static void setCol(struct ili9341_priv *priv, u16 start, u16 end)
{
u8 tmp;
write_command(priv, 0x2a);
tmp = (start & 0xff00) >> 8;
write_data(priv, tmp);
tmp = (start & 0x00ff) >> 0;
write_data(priv, tmp);
tmp = (end & 0xff00) >> 8;
write_data(priv, tmp);
tmp = (end & 0x00ff) >> 0;
write_data(priv, tmp);
}
static void setPage(struct ili9341_priv *priv, u16 start, u16 end)
{
u8 tmp;
write_command(priv, 0x2b);
tmp = (start & 0xff00) >> 8;
write_data(priv, tmp);
tmp = (start & 0x00ff) >> 0;
write_data(priv, tmp);
tmp = (end & 0xff00) >> 8;
write_data(priv, tmp);
tmp = (end & 0x00ff) >> 0;
write_data(priv, tmp);
}
static void setPos(struct ili9341_priv *priv, u16 x1, u16 x2, u16 y1, u16 y2)
{
setPage(priv, y1, y2);
setCol(priv, x1, x2);
}
static void fill_area(struct ili9341_priv *priv, int y1, int y2)
{
int i = 0;
char val = 0xaa;
char *p = priv->fbmem;
int ret;
int start =y1*SCREEN_WIDTH*2 + 1;
int stop = y2*SCREEN_WIDTH*2+1;
int range = stop - start;
if (!chunk_size)
chunk_size = 10;
if (start + range > priv->vsize)
range = priv->vsize - start;
setCol(priv, 0, 239);
setPage(priv, y1, y2);
write_command(priv, 0x2c);
for(i = start; i < stop; i += chunk_size)
{
if ( i + chunk_size > stop )
chunk_size = stop - i;
ret = spi_write(priv->spi, &priv->fbmem[i], chunk_size);
if (ret != 0)
dev_err(&priv->spi->dev, "Error code: %i\n", ret);
}
}
static void fill(struct ili9341_priv *priv)
{
int i = 0;
char val = 0xaa;
char *p = priv->fbmem;
setCol(priv, 0, 239);
setPage(priv, 0, 319);
write_command(priv, 0x2c);
fill_area(priv, 0, 319);
}
static ssize_t id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
sprintf(buf, "%s", ID);
return ID_SZ;
}
static ssize_t id_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
char kbuf[ID_SZ];
if (count != ID_SZ)
return -EINVAL;
memcpy(kbuf, buf, ID_SZ);
if (memcmp(kbuf, ID, ID_SZ) != 0)
return -EINVAL;
return count;
}
DEVICE_ATTR(id, 0666, id_show, id_store);
static struct attribute *ili9341_attrs[] = {
&dev_attr_id.attr,
NULL,
};
ATTRIBUTE_GROUPS(ili9341);
static int ili9341_probe(struct spi_device *spi)
{
struct fb_info *info;
int retval = -ENOMEM;
struct ili9341_priv *priv;
struct device_node *np = spi->dev.of_node;
int ret;
dev_err(&spi->dev, "Hello from I!\n");
priv = kzalloc(sizeof(struct ili9341_priv), GFP_KERNEL);
if(!priv)
return -ENOMEM;
priv->spi = spi;
/* TODO: better fail handling... */
priv->dc = of_get_named_gpio(np, "dc-gpio", 0);
if (priv->dc == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (gpio_is_valid(priv->dc)) {
ret = devm_gpio_request(&spi->dev, priv->dc, "tft dc");
if (ret)
dev_err(&spi->dev, "could not request dc\n");
ret = gpio_direction_output(priv->dc, 1);
if (ret)
dev_err(&spi->dev, "could not set DC to output");
}else
dev_err(&spi->dev, "DC gpio is not valid");
dev_err(&spi->dev, "Initialize regmap");
priv->regmap = devm_regmap_init_spi(spi, &ili9341_regmap_config);
if (IS_ERR(priv->regmap))
goto err_regmap;
dev_err(&spi->dev, "regmap OK");
priv->vsize = (SCREEN_WIDTH*SCREEN_HIGHT)*(SCREEN_BPP/8);
priv->fbmem = vzalloc(priv->vsize);
if (!priv->fbmem)
goto err_videomem_alloc;
init(priv);
fill(priv);
retval = sysfs_create_group(&spi->dev.kobj, *ili9341_groups);
if (retval)
kobject_put(&spi->dev.kobj);
dev_err(&spi->dev, "Allocate framebuffer");
info = framebuffer_alloc(sizeof(struct fb_info), &spi->dev);
if (!info)
goto err_fballoc;
info->par = priv;
priv->info = info;
info->screen_base = priv->fbmem;
info->fbops = &ili9341_ops;
info->var = ili9341_var;
info->fix = ili9341_fix;
info->fix.smem_len = priv->vsize;
/* We are virtual as we only exists in memory */
info->flags = FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB;
info->fbdefio = &ili9341_defio;
fb_deferred_io_init(info);
retval = register_framebuffer(info);
if (retval < 0)
goto err_fbreg;
spi_set_drvdata(spi, info);
fb_info(info, "Hecuba frame buffer device, using %dK of video memory\n",
priv->vsize >> 10);
priv->dir = debugfs_create_dir("ili9341-fb", NULL);
debugfs_create_u32("chunk_size", 0666, priv->dir, &chunk_size);
return 0;
err_fbreg:
framebuffer_release(info);
err_fballoc:
vfree(priv->fbmem);
err_videomem_alloc:
err_regmap:
kfree(priv);
return retval;
}
static int ili9341_remove(struct spi_device *spi)
{
struct fb_info *info = spi_get_drvdata(spi);
if (info) {
struct ili9341_priv *priv = info->par;
fb_deferred_io_cleanup(info);
unregister_framebuffer(info);
vfree(info->screen_base);
framebuffer_release(info);
kfree(priv);
}
return 0;
}
static const struct spi_device_id ili9341_ids[] = {
{"ili9341-fb", 0},
{}
};
MODULE_DEVICE_TABLE(spi, ili9341_ids);
static struct spi_driver ili9341_driver = {
.probe = ili9341_probe,
.remove = ili9341_remove,
.id_table = ili9341_ids,
.driver = {
.owner = THIS_MODULE,
.name = "ili9341-fb",
},
};
module_spi_driver(ili9341_driver);
MODULE_DESCRIPTION("fbdev driver for ili9341 controller");
MODULE_AUTHOR("Marcus Folkesson <marcus.folkesson@gmail.com>");
MODULE_LICENSE("GPL");