2.2″ TFT and BeagleBone

2.2" TFT display on Beaglebone

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!

Enable SPI

First of all, we need to setup the pinmux for the spi-bus. This is done with the pinctrl subsystem in the devicetree interface file (arch/arm/boot/dts/am335x-bone-common.dtsi).

Create the pins. For more detailed explaination of the values, see the BeagleBone System Reference Manual.

spi1_pins: spi1_pins_s0 {
   pinctrl-single,pins = <
     0x190 0x33      /* mcasp0_aclkx.spi1_sclk, INPUT_PULLUP | MODE3 */
     0x194 0x33      /* mcasp0_fsx.spi1_d0, INPUT_PULLUP | MODE3 */
     0x198 0x13      /* mcasp0_axr0.spi1_d1, OUTPUT_PULLUP | MODE3 */
     0x19c 0x13      /* mcasp0_ahclkr.spi1_cs0, OUTPUT_PULLUP | MODE3 */
 >;
};

Then override the spi1 entry and create an instance of our device driver. The driver will have the name "ili9341-fb".

&spi1{
 status = "okay";
 pinctrl-names = "default";
 pinctrl-0 = <&spi1_pins>;
 ili9341: ili9341@0 {
  compatible = "ili9341-fb";
  reg = <0>;
  spi-max-frequency = <16000000>;
  dc-gpio = <&gpio3 19 GPIO_ACTIVE_HIGH>;
 };
};

Create an entry in the Kbuild system

I always integrate the modules into the kbuild system as the first step. This for several reasons:
– I use one kernel for all of my projects, just different branches
– It is simple to jump around with cscope/ctags
– It gives you control when the kernel version and your driver follow eachother
– Out-of-tree modules is evil (gives you a tainted kernel and everyone will spit on you)

Those who don’t know how to put a module into the kbuild system – get ready to be surprised how simple it is!

Every directory in the kernel structure contains at least two files, a Makefile and a Kconfig. The Makefile tells the make buildsystem which files to compile and the Kconfig file is interpreted by (menu|k|x|old|….)config.

Here is what’s needed:

diff --git a/drivers/video/fbdev/Kconfig b/drivers/video/fbdev/Kconfig
index e1f4727..be4ec8f 100644
--- a/drivers/video/fbdev/Kconfig
+++ b/drivers/video/fbdev/Kconfig
@@ -163,6 +163,18 @@ config FB_DEFERRED_IO
        bool
        depends on FB
+config FB_ILI9341
+       tristate "ILI9341 TFT driver"
+       depends on FB
+       select FB_SYS_FILLRECT
+       select FB_SYS_COPYAREA
+       select FB_SYS_IMAGEBLIT
+       select FB_SYS_READ
+       select FB_DEFERRED_IO
+       ---help---
+       This enables functions for handling video modes using the ili9341 controller
+
 config FB_HECUBA
        tristate
        depends on FB
diff --git a/drivers/video/fbdev/Makefile b/drivers/video/fbdev/Makefile
index 0284f2a..105166a 100644
--- a/drivers/video/fbdev/Makefile
+++ b/drivers/video/fbdev/Makefile
@@ -60,6 +60,7 @@ obj-$(CONFIG_FB_ATARI)            += atafb.o c2p_iplan2.o atafb_mfb.o
                                      atafb_iplan2p2.o atafb_iplan2p4.o atafb_iplan2p8.o
 obj-$(CONFIG_FB_MAC)              += macfb.o
 obj-$(CONFIG_FB_HECUBA)           += hecubafb.o
+obj-$(CONFIG_FB_ILI9341)          += ili9341.o
 obj-$(CONFIG_FB_N411)             += n411.o
 obj-$(CONFIG_FB_HGA)              += hgafb.o
 obj-$(CONFIG_FB_XVR500)           += sunxvr500.o
diff --git a/drivers/video/fbdev/ili9341.c b/drivers/video/fbdev/ili9341.c

Deferred IO

Deferred IO is a way to delay and repurpose IO. It uses host memory as a buffer and the MMU pagefault as a pretrigger for when to perform the device IO.
You simple tell the kernel the minimum delay between the triggers should occours, this allows you to do burst transfers to the device at a given framerate. This has the big benefit that if the userspace updates the framebuffer several times in this period, we will only write it once.

The interface is _really_ simple. All you need to follow is these four steps (see Documentation/fb/deferred_io.txt):

  1. Setup your structure.

    static struct fb_deferred_io hecubafb_defio = {
     .delay  = HZ,
     .deferred_io = hecubafb_dpy_deferred_io,
    };
    

The delay is the minimum delay between when the page_mkwrite trigger occurs
and when the deferred_io callback is called. The deferred_io callback is
explained below.

  1. Setup your deferred IO callback.

    static void hecubafb_dpy_deferred_io(struct fb_info *info,
        struct list_head *pagelist)
    

The deferred_io callback is where you would perform all your IO to the display
device. You receive the pagelist which is the list of pages that were written
to during the delay. You must not modify this list. This callback is called
from a workqueue.

  1. Call init:

    info->fbdefio = &hecubafb_defio;
    fb_deferred_io_init(info);
    
  2. Call cleanup:

    fb_deferred_io_cleanup(info);
    

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: %in", 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 dcn");
  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 memoryn",
  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");

Take control of your Buffalo Linkstation NAS

Take control of your Buffalo Linkstation NAS

I finally bought a NAS for all of my super-important stuff.
It became a Buffalo Linkstation LS200, most because of the price ($300 for 4TB). It supports all of the standard protocols such as FTP, SAMBA, ATP and so on.

However, it would be really useful to use some sane protocols like sftp so you could use rsync for your backup scripts.

Bring a big coffee mug and let the hacking begin….

I knew that the NAS was based on the ARM architecture and supports a whole set of high level protocols, so one qualified guess is that there lives a little penguin in the box.

Lets start with download the latest firmware from the Buffalo webpage.
When the firmware is unzipped we have these files:

marcus@tuxie:~/shared/buffalo$ ls -al
total 780176
drwxr-xr-x  4 marcus marcus      4096 Jun 13 00:06 .
drwxr-xr-x 18 marcus marcus      4096 Jun 12 23:27 ..
-rw-r--r--  1 marcus marcus 190409979 Jun 13 00:06 hddrootfs.img
drwxr-xr-x  2 marcus marcus      4096 Apr  8 13:25 img
-rw-r--r--  1 marcus marcus  12602325 Apr  1 15:25 initrd.img
-rw-r--r--  1 marcus marcus       656 Apr  1 15:25 linkstation_version.ini
-rw-r--r--  1 marcus marcus       198 Apr  1 15:25 linkstation_version.txt
-rw-r--r--  1 marcus marcus 205568610 Jun 12 23:13 LS200_series_FW_1.44.zip
-rw-r--r--  1 marcus marcus    350104 Apr  1 15:25 LSUpdater.exe
-rw-r--r--  1 marcus marcus       327 Apr  1 15:25 LSUpdater.ini
-rw-r--r--  1 marcus marcus    674181 Apr  1 15:25 u-boot.img
-rw-r--r--  1 marcus marcus   2861933 Apr  1 15:25 uImage.img
-rw-r--r--  1 marcus marcus      4880 Apr  8 13:23 update.html

Ok, u-boot.img, uImage.img, initrd.img and hddrootfs.img tells us that I have a black little penguin cage
in front of me.
First of all, find out what kind of file these *.img files really are.:

System Message: WARNING/2 (<stdin>, line 34); backlink

Inline emphasis start-string without end-string.

marcus@tuxie:~/shared/buffalo$ file ./hddrootfs.img
./hddrootfs.img: Zip archive data, at least v2.0 to extract

Really? It is just an zip-file. Lets extract it then.:

marcus@tuxie:~/shared/buffalo$ unzip hddrootfs.img
Archive:  hddrootfs.img
[hddrootfs.img] hddrootfs.buffalo.updated password:

Of course, it is protected with a password. I will let my old friend John the Ripper take a look at it (I guess I had a great luck, the brute force attack only took 2.5 hours).
The password for the file is: aAhvlM1Yp7_2VSm6BhgkmTOrCN1JyE0C5Q6cB3oBB

marcus@tuxie:~/shared/buffalo$ unzip hddrootfs.img
Archive:  hddrootfs.img
[hddrootfs.img] hddrootfs.buffalo.updated password:
  inflating: hddrootfs.buffalo.updated

Terrific. We got a hddrootfs.buffalo.updated file. What is it anyway?:

marcus@tuxie:~/shared/buffalo$ file hddrootfs.buffalo.updated
hddrootfs.buffalo.updated: gzip compressed data, was "rootfs.tar", from Unix, last modified: Tue Apr  1 08:24:05 2014, max compression

It is just a gzip compressed tar archive, couldn’t be better! Extract it.:

marcus@tuxie:~/shared/buffalo$ mkdir rootfs
marcus@tuxie:~/shared/buffalo$ tar -xz --numeric-owner -f hddrootfs.buffalo.updated  -C ./rootfs/
marcus@tuxie:~/shared/buffalo$ ls -1l rootfs/
total 80
drwxr-xr-x  2 root root 4096 Apr  1 08:23 bin
-rwxr-xr-x  1 root root 1140 Feb  3 07:51 chroot.sh
drwxr-xr-x  2 root root 4096 Apr  1 08:23 debugtool
drwxr-xr-x  5 root root 4096 Apr  1 08:23 dev
drwxr-xr-x 33 root root 4096 Jun 12 23:17 etc
drwxr-xr-x  4 root root 4096 Apr  1 08:23 home
drwxr-xr-x  9 root root 4096 Apr  1 08:23 lib
drwxr-xr-x  3 root root 4096 Feb  3 07:51 mnt
drwxr-xr-x  2 root root 4096 Apr  1 07:35 opt
-rwxr-xr-x  1 root root 2741 Feb  3 07:51 prepare.sh
drwxr-xr-x  2 root root 4096 Apr  1 07:35 proc
drwxr-xr-x  3 root root 4096 Apr  1 08:23 root
drwxr-xr-x  3 root root 4096 Apr  1 08:22 run
drwxr-xr-x  2 root root 4096 Apr  1 08:23 sbin
drwxr-xr-x  2 root root 4096 Apr  1 07:35 sys
-rwxr-xr-x  1 root root 3751 Feb  3 07:51 test.sh
drwxrwxrwt  3 root root 4096 Apr  1 08:23 tmp
drwxr-xr-x 11 root root 4096 Apr  1 08:23 usr
drwxr-xr-x  9 root root 4096 Apr  1 08:22 var
drwxrwxrwx  6 root root 4096 Apr  1 07:53 www

Here we go!

Modify the root filesystem

First of all, I really would like to have SSH access to the box, and I found that there is a SSH daemon in here (/usr/bin/sshd), but why is it not activated?
Take a look in one of the scripts that seems to be related to ssh:

marcus@tuxie:~/shared/buffalo/rootfs$ head -n 20 etc/init.d/sshd.sh
#!/bin/sh
[ -f /etc/nas_feature ] && . /etc/nas_feature
SSHD_DSA=/etc/ssh_host_dsa_key
SSHD_RSA=/etc/ssh_host_rsa_key
SSHD_KEY=/etc/ssh_host_key
SSHD=`which sshd`
if [ "${SSHD}" = "" -o ! -x ${SSHD} ] ; then
 echo "sshd is not supported on this platform!!!"
fi
if [ "${SUPPORT_SFTP}" = "0" ] ; then
        echo "Not support sftp on this model." > /dev/console
        exit 0
fi
umask 000

What about the second if-statement? SUPPORT_SFTP? And what is the /etc/nas_feature file? It does not exist in the package. Is it auto generated at boot?
Anyway, I remove the second statement, it seems evil.
So, if this starts up the ssh daemon, we would like to login as root, uncomment PermitRootLogin in sshd_config:

marcus@tuxie:~/shared/buffalo/rootfs$ sed -i 's/#PermitRootLogin/ PermitRootLogin/' etc/sshd_config

Then copy your public rsa-key to /root/.ssh/authorized_keys. If you don’t have a key, generate it with:

marcus@tuxie:~/shared/buffalo/rootfs$ ssh-keygen

Copy the key to target:

marcus@tuxie:~/shared/buffalo/rootfs$ mkdir ./root/.ssh
marcus@tuxie:~/shared/buffalo/rootfs$ cat ~/.ssh/id_rsa.pub > ./root/.ssh/authorized_keys

This will let you to login without give any password.

Lets try to re-pack the whole thing.:

marcus@tuxie:~/shared/buffalo/rootfs$ mv ../hddrootfs.buffalo.updated{,.old}
marcus@tuxie:~/shared/buffalo/rootfs$ tar -czf ../hddrootfs.buffalo.updated *
marcus@tuxie:~/shared/buffalo/rootfs$ cd ..
marcus@tuxie:~/shared/buffalo$ zip -e hddrootfs.img hddrootfs.buffalo.updated

I encrypt the file with the same password as before, I dare not think about what happens if I don’t.

Time to update firmware

I execute the LSUpdater.exe from a virtual Windows machine and hold my thumbs…
The update process takes about 8 minutes and is a real pain, would it brick my NAS..?

After a while the power LED is indicating that the NAS is up and running. Wow.
Quick! Do a portscan!:

marcus@tuxie:~/shared/buffalo$ sudo nmap -sS 10.0.0.4
[sudo] password for marcus:
Starting Nmap 5.21 ( http://nmap.org ) at 2014-06-13 11:59 CEST
Nmap scan report for nas (10.0.0.4)
Host is up (0.00049s latency).
Not shown: 990 closed ports
PORT      STATE SERVICE
21/tcp    open  ftp
22/tcp    open  ssh
80/tcp    open  http
139/tcp   open  netbios-ssn
443/tcp   open  https
445/tcp   open  microsoft-ds
548/tcp   open  afp
873/tcp   open  rsync
8873/tcp  open  unknown
22939/tcp open  unknown
MAC Address: DC:FB:02:EB:06:A8 (Unknown)
Nmap done: 1 IP address (1 host up) scanned in 0.27 seconds

And there it is! The SSH daemon is running on port 22.:

marcus@tuxie:~$ ssh admin@10.0.0.4
admin@10.0.0.4's password:
[admin@LS220D6A8 ~]$ ls /
bin/        debugtool/  home/       mnt/        proc/       sbin/       tmp/        www/
boot/       dev/        lib/        opt/        root/       sys/        usr/
chroot.sh*  etc/        lost+found/ prepare.sh* run/        test.sh*    var/
[admin@LS220D6A8 ~]$

It is just beautiful!

Wait, what about the /etc/nas_features file?

[admin@LS220D6A8 ~]$ cat /etc/nas_feature
DEFAULT_LANG=english
DEFAULT_CODEPAGE=CP437
REGION_CODE=EU
PRODUCT_CAPACITY="040"
PID=0x0000300D
SERIES_NAME="LinkStation"
PRODUCT_SERIES="LS200"
PRODUCT_NAME="LS220D(SANJO)"
SUPPORT_NTFS_WRITE=on
NTFS_DRIVER="tuxera"
SUPPORT_DIRECT_COPY=on
SUPPORT_RAID=on
SUPPORT_RAID_DEGRADE=off
SUPPORT_FAN=on
SUPPORT_AUTOIP=on
SUPPORT_NEW_DISK_AUTO_REBUILD=off
SUPPORT_2STEP_INSPECTION=off
SUPPORT_RESYNC_DELAY=off
SUPPORT_PRINTER_SERVER=on
SUPPORT_ITUNES_SERVER=on
SUPPORT_DLNA_SERVER=on
SUPPORT_NAS_FIREWALL=off
SUPPORT_IPV6=off
SUPPORT_DHCPS=off
SUPPORT_UPNP=off
SUPPORT_SLIDE_POWER_SWITCH=on
SUPPORT_BITTORRENT=on
BITTORRENT_CLIENT="transmission"
SUPPORT_USER_QUOTA=on
SUPPORT_GROUP_QUOTA=on
SUPPORT_ACL=on
SUPPORT_TIME_MACHINE=on
SUPPORT_SLEEP_TIMER=on
SUPPORT_AD_NT_DOMAIN=on
SUPPORT_RAID0=1
SUPPORT_RAID1=1
SUPPORT_RAID5=0
SUPPORT_RAID6=0
SUPPORT_RAID10=0
SUPPORT_RAID50=0
SUPPORT_RAID60=0
SUPPORT_RAID51=0
SUPPORT_RAID61=0
SUPPORT_NORAID=0
SUPPORT_RAID_REBUILD=1
SUPPORT_AUTH_EXTERNAL=1
SUPPORT_SAMBA_DFS=0
SUPPORT_LINKDEREC_ANALOG=0
SUPPORT_LINKDEREC_DIGITAL=0
SUPPORT_WEBAXS=1
SUPPORT_UPS_SERIAL=0
SUPPORT_UPS_USB=0
SUPPORT_UPS_RECOVER=0
SUPPORT_NUT=0
SUPPORT_SYSLOG_FORWARD=0
SUPPORT_SYSLOG_DOWNLOAD=0
SUPPORT_SHUTDOWN_FROMWEB=0
SUPPORT_REBOOT_FROMWEB=1
SUPPORT_IMHERE=0
SUPPORT_POWER_INTERLOCK=1
SUPPORT_SMTP_AUTH=1
NEED_MICONMON=off
ROOTFS_FS=ext3
USERLAND_FS=xfs
NASFEAT_VM_WRITEBACK=default
NASFEAT_VM_EXPIRE=default
MAX_DISK_NUM=2
MAX_USBDISK_NUM=1
MAX_ARRAY_NUM=1
DEV_BOOT=md0
DEV_ROOTFS1=md1
DEV_SWAP1=md2
SDK_VERSION=2
DEVICE_NETWORK_PRIMARY=eth1
DEVICE_NETWORK_SECONDARY=
DEVICE_NETWORK_NUM=1
DEVICE_HDD1_LINK=disk1_6
DEVICE_HDD2_LINK=disk2_6
DEVICE_HDD3_LINK=disk3_6
DEVICE_HDD4_LINK=disk4_6
DEVICE_HDD5_LINK=disk5_6
DEVICE_HDD6_LINK=disk6_6
DEVICE_HDD7_LINK=disk7_6
DEVICE_HDD8_LINK=disk8_6
DEVICE_HDD_BASE_EDP=md100
DEVICE_HDD1_EDP=md101
DEVICE_HDD2_EDP=md102
DEVICE_HDD3_EDP=md103
DEVICE_HDD4_EDP=md104
DEVICE_HDD5_EDP=md105
DEVICE_HDD6_EDP=md106
DEVICE_HDD7_EDP=md107
DEVICE_HDD8_EDP=md108
DEVICE_MD1_REAL=md10
DEVICE_MD2_REAL=md20
DEVICE_MD3_REAL=md30
DEVICE_MD4_REAL=md40
DEVICE_USB1_LINK=usbdisk1_1
DEVICE_USB2_LINK=usbdisk2_1
DEVICE_USB3_LINK=usbdisk1_5
DEVICE_USB4_LINK=usbdisk2_5
MOUNT_GLOBAL=/mnt
MOUNT_LVM_BASE=/mnt/lvm
MOUNT_HDD1=/mnt/disk1
MOUNT_HDD2=/mnt/disk2
MOUNT_HDD3=/mnt/disk3
MOUNT_HDD4=/mnt/disk4
MOUNT_HDD5=/mnt/disk5
MOUNT_HDD6=/mnt/disk6
MOUNT_HDD7=/mnt/disk7
MOUNT_HDD8=/mnt/disk8
MOUNT_ARRAY1=/mnt/array1
MOUNT_ARRAY2=/mnt/array2
MOUNT_ARRAY3=/mnt/array3
MOUNT_ARRAY4=/mnt/array4
MOUNT_USB1=/mnt/usbdisk1
MOUNT_USB2=/mnt/usbdisk2
MOUNT_USB3=/mnt/usbdisk3
MOUNT_USB4=/mnt/usbdisk4
MOUNT_USB5=/mnt/usbdisk5
MOUNT_MC_BASE=/mnt/mediacartridge
SUPPORT_MC_VER=1
SUPPORT_INTERNAL_DISK_APPEND=0
STORAGE_TYPE=HDD
BODY_COLOR=NORMAL
SUPPORT_MICON=0
SUPPORT_LCD=0
SUPPORT_USER_QUOTA_SOFT=0
SUPPORT_GROUP_QUOTA_SOFT=0
SUPPORT_NFS=0
SUPPORT_LVM=0
SUPPORT_OFFLINEFILE=0
SUPPORT_HIDDEN_SHARE=0
SUPPORT_HOT_SWAP=0
SUPPORT_LCD_LED=0
SUPPORT_ALERT=0
SUPPORT_PORT_TRUNKING=0
SUPPORT_REPLICATION=0
SUPPORT_USER_GROUP_CSV=0
SUPPORT_SFTP=0
SUPPORT_SERVICE_MAPPING=0
SUPPORT_SSLKEY_IMPORT=1
SUPPORT_SLEEPTIMER_DATE=0
SUPPORT_TERA_SEARCH=0
SUPPORT_SECURE_BOOT=0
SUPPORT_PACKAGE_UPDATE=0
SUPPORT_HDD_SPINDOWN=0
SUPPORT_DISK_ENCRYPT=0
SUPPORT_FTPS=1
SUPPORT_CLEANUP_ALL_TRASHBOX=0
SUPPORT_WAKEUP_BY_REBOOT=1
SUPPORT_DTCP_IP=0
SUPPORT_MYSQL=0
SUPPORT_APACHE=0
SUPPORT_PHP=0
SUPPORT_UPS_STANDBY=0
SUPPORT_HIDDEN_RAID_MENU=0
SUPPORT_ISCSI=0
SUPPORT_ISCSI_TYPE=
DEFAULT_WORKINGMODE=
MAX_LVM_VOLUME_NUM=0
MAX_ISCSI_VOLUME_NUM=0
INTERNAL_SCSI_TYPE=multi-host
SUPPORT_ELIMINATE_ADLIMIT=
USB_TREE_TYPE=
SUPPORT_WOL=0
WOL_TYPE=
SUPPORT_HARDLINK_BACKUP=0
SUPPORT_SNMP=0
SUPPORT_EDP=1
SUPPORT_POCKETU=0
SUPPORT_MC=0
SUPPORT_FOFB=0
SUPPORT_EDP_PLUS=0
SUPPORT_INIT_SW=1
SUPPORT_SQUEEZEBOX=0
SUPPORT_OL_UPDATE=1
SUPPORT_AMAZONS3=0
SUPPORT_SURVEILLANCE=0
SUPPORT_WAFS=0
SUPPORT_SUGARSYNC=0
SUPPORT_INTERNAL_DISK_REMOVE=1
SUPPORT_SETTING_RECOVERY_USB=0
SUPPORT_PASSWORD_RECOVERY_USB=0
SUPPORT_AV=
SUPPORT_TUNEUP_RAID=on
SUPPORT_FLICKRFS=0
SUPPORT_WOL_INT=1
TUNE=0
SUPPORT_EDP_PLUS=0
SUPPORT_SXUPTP=0
SUPPORT_SQUEEZEBOX=0
SUPPORT_EYEFI=0
SUPPORT_OL_UPDATE=1
SUPPORT_INIT_SW=1
SUPPORT_USB=1
SUPPORT_WAFS=0
SUPPORT_INFO_LED=1
SUPPORT_ALARM_LED=1
POWER_SWITCH_TYPE=none
SUPPORT_FUNC_SW=1
DLNA_SERVER="twonky"
SUPPORT_TRANSCODER=0
SUPPORT_LAYOUT_SWITCH=1
SUPPORT_UTILITY_DOWNLOAD=1
SUPPORT_BT_CLOUD=0
DEFAULT_VALUE_DLNA=1
DEFAULT_VALUE_BT_CLOUD=0
SUPPORT_EXCLUSION_LED_POWER_INFO_ERROR=1
DEFAULT_DLNA_SERVICE=off
SUPPORT_MOBILE_WEBUI=1
SUPPORT_SHUTDOWN_DEPEND_ON_SW=1
SUPPORT_SPARE_DISK=0

It seems that the files is generated. It also has the SUPPORT_SFTP config that we saw in sshd.sh.

What about the kernel

In the current vanilla kernel, there is devicetrees that seems to be for the Buffalo linkstation.:

marcus@tuxie:~/marcus/linux/linux$ grep -i buffalo arch/arm/boot/dts/*.dts
arch/arm/boot/dts/kirkwood-lschlv2.dts: model = "Buffalo Linkstation LS-CHLv2";
arch/arm/boot/dts/kirkwood-lschlv2.dts: compatible = "buffalo,lschlv2", "buffalo,lsxl", "marvell,kirkwood-88f6281", "marvell,kirkwood";
arch/arm/boot/dts/kirkwood-lsxhl.dts:   model = "Buffalo Linkstation LS-XHL";
arch/arm/boot/dts/kirkwood-lsxhl.dts:   compatible = "buffalo,lsxhl", "buffalo,lsxl", "marvell,kirkwood-88f6281", "marvell,kirkwood";

The device tree seems to match the current CPU:

[admin@LS220D6A8 ~]$ cat /proc/cpuinfo
Processor : Marvell PJ4Bv7 Processor rev 1 (v7l)
BogoMIPS : 795.44
Features : swp half thumb fastmult vfp edsp vfpv3 vfpv3d16 tls
CPU implementer : 0x56
CPU architecture: 7
CPU variant : 0x1
CPU part : 0x581
CPU revision : 1
Hardware : Marvell Armada-370
Revision : 0000
Serial  : 0000000000000000

Also, the kernel is not tainted indicating that there is no out-of-tree modules.:

[admin@LS220D6A8 ~]$ cat /proc/sys/kernel/tainted
0

It could therefor be possible to compile a custom kernel with support for more USB-devices that may be plugged into the NAS.

Other tips

The LSUpdater.exe will refuse to update if the same version of software is already on the target. This means that you cannot upload the same firmware again… unless you change the version!

Together with the uploader application, there is a linkstation_version.ini file that contains information about each of the *.img.
The first thing I tried was just to increase the VERSION by one. This makes the LSUpdater.exe move a little forward, It stops complain about the same version, instead it complains about that this firmware is already on the target.
However, I needed to change the timestamp of each binary (increased the day by one), then it updates the firmware without any problem.

System Message: WARNING/2 (<stdin>, line 435); backlink

Inline emphasis start-string without end-string.

marcus@tuxie:~/shared/buffalo$ cat linkstation_version.ini
[COMMON]
VERSION=1.44-0.36
BOOT=0.20
KERNEL=2014/04/01 14:33:46
INITRD=2014/04/01 14:35:10
ROOTFS=2014/04/01 15:23:41
FILE_BOOT = u-boot.img
FILE_KERNEL = uImage.img
FILE_INITRD = initrd.img
FILE_ROOTFS = hddrootfs.img

[TARGET_INFO1]
PID=0x0000001D
FILE_KERNEL=uImage.img
KERNEL=2014/04/01 14:33:46
FILE_BOOT_APPLY=u-boot.img
BOOT=0.20
BOOT_UP_CMD=""
[TARGET_INFO2]
PID=0x0000300D
FILE_KERNEL=uImage.img
KERNEL=2014/04/01 14:33:46
FILE_BOOT_APPLY=u-boot.img
BOOT=0.20
BOOT_UP_CMD=""
[TARGET_INFO3]
PID=0x0000300E
FILE_KERNEL=uImage.img
KERNEL=2014/04/01 14:33:46
FILE_BOOT_APPLY=u-boot.img
BOOT=0.20
BOOT_UP_CMD=""

Magic SysRq

Magic SysRq

Every kernel-hacker should knows about the magic sysrq already, so this post is kind of unnecessary.
To enable the magic, make sure CONFIG_MAGIC_SYSRQ is set in your Kernel hacking tab.

I use this feature… a lot. Mostly for set loglevel and reboot the system, but it is also very useful when debugging.
So, how to use it?

As everybody is using GNU Screen (what else) as their TTY terminal, the keyboard combination is:
ctrl+A B. And here the magic begins!
This combination simply sends a SysRq keycode to the target system.

To get information about available commands, press ctrl+A B h. h as in help.:

[669510.910125] SysRq : HELP : loglevel(0-9) reBoot Crash terminate-all-tasks(E) memory-full-oom-kill(F) kill-all-tasks(I) thaw-filesystems(J) saK show-memory-usage(M) nice-all-RT-tasks(N) powerOff show-registers(P) show-all-timers(Q) unRaw Sync show-task-states(T) Unmount show-blocked-tasks(W) dump-ftrace-buffer(Z)
And here we go! My favorites are:
0-9, set debug level
b, reboot the system (ingrained...)
p, show a register dump ( useful if the system hangs)
g, switch console to KGDB (I love KGDB)
1, Show all timers (good when looking for power-thieves)

The best part is that Magic SysRq works in allmost every situation, even if the system is frozen.

For further details see the kernel source file Documentation/sysrq.txt