LCD driver development for embedded linux board

By: Segiy Sergienko, 30 Aug 2016
6   min read
Reading Time: 6 minutes

In this article I want to share my experience writing a Linux driver for 320×240 color display from New Haven Displays, namely the NHD-5.7-320240WFB-CTXI-T1. The idea to write an article came about since there are not many resources for writing framebuffer (FB) drivers on the web. The module was written for quite an old kernel version ( 2.6.30 ), so I assume that some interfaces for FB may be different in newer versions. But, nevertheless, I hope that the article will be of interest for those interested in Linux kernel development. I suspect, that the implementation could be made simpler and more elegant, so your comments are very welcome. Also, you can find entire source code of the module on the GitHub. Additionally, our FPGA design services could be of value to you in the future.


The task was to create a driver, which can be accessed by standard tools, such as QT embedded, to eventually build a simple menu with icons and text for user interaction. The platform was an embedded board from Corewind – AT91SAM9G45, more information can be found here

It wasn’t planned to support a refresh rate sufficient for video streaming. The AT91SAM9G45 contains quite a workable built-in LCD controller with DMA support and a fairly high speed data bus, which could potentially achieve a speed sufficient for video playback, but alas, it is not hardware compatible with the SSD1963 controller. Therefore, it was decided to use an ordinary GPIO interface for this purpose, as the only available alternative.

Interface of SSD1963 controller

The interface to controller is well-described in the data-sheet of the display ( Figure 1):

Figure 1

From a developer’s perspective, we are interested in the driver pins DB0 – DB7. This is an 8 – bit data bus, and DC, RD, WR, CS, RES pins are used for data flow control on the SSD1963.

This display uses 888 pixel format . It means that : 8 bytes are used for Red, 8 bytes – for Green, 8 bytes – for Blue. Quite frequently one can found displays with 555 , 565 and other pixel formats, but that’s not our case. Format of transmitted data is shown in Figure 2 .

Figure 2

Before the first data byte will be put on the bus, one should do the switching of the CS and WR pins from 1 to 0. And after the data byte is set, you should switch CS and WR from 0 to 1, that actually transmits the data byte to the controller SSD1963. More detailed waveforms can be found in the data-sheet for the controller.

In the source code the interface is defined by 2 arrays of GPIO pins. Please note that the driver was designed to work only with the AT91SAM9G45 board, and it was never the plan to use it on other platforms. That’s why the driver code uses AT91_PINs and at91_set_gpio* functions. To make it more portable, one can use gpiolib kernel API.

static unsigned int nhd_data_pin_config[] = {
AT91_PIN_PE13, AT91_PIN_PE14, AT91_PIN_PE17, AT91_PIN_PE18,
AT91_PIN_PE19, AT91_PIN_PE20, AT91_PIN_PE21, AT91_PIN_PE22

static unsigned int nhd_gpio_pin_config[] = {
AT91_PIN_PE2, // DC
AT91_PIN_PE5, // CLK
AT91_PIN_PE6, // RD
AT91_PIN_PE1  // WR

The function sending bytes over this interface:

static void nhd_write_data(int command, unsigned short value)
int i;
at91_set_gpio_output(AT91_PIN_PE12, 1); //R/D

for (i=0; i<ARRAY_SIZE(nhd_data_pin_config); i++)
at91_set_gpio_output(nhd_data_pin_config[i], (value>>i)&0x01);

if (command)
at91_set_gpio_output(AT91_PIN_PE10, 0); //D/C
at91_set_gpio_output(AT91_PIN_PE10, 1); //D/C

at91_set_gpio_output(AT91_PIN_PE11, 0); //WR
at91_set_gpio_output(AT91_PIN_PE26, 0); //CS
at91_set_gpio_output(AT91_PIN_PE26, 1); //CS
at91_set_gpio_output(AT91_PIN_PE11, 1); //WR

As you can see, using this function, you can send both commands (for example, to configure display settings) and data (pixels) to the LCD controller.

Framebuffer kernel model

As you know, the Linux kernel provides interfaces for different types of device drivers – char drivers, block drivers, usb drivers, etc. Framebuffer drivers are also the separate subsystem in  Linux device driver model. The main structure, which is used to represent the FB driver is struct fb_info in linux / fb.h. By the way, this header file could also be interesting to fans of humor in Linux kernel code, since it contains an interesting definition:

I think, the definition speaks for itself . But, moving back to the structure fb_info. Two structures there are of particular interest – fb_var_screeninfo and fb_fix_screeninfo. Let’s initialize those structures with parameters of our display.

static struct fb_fix_screeninfo ssd1963_fix __initdata = {
.id          = "SSD1963",
.type        = FB_TYPE_PACKED_PIXELS,
.visual      = FB_VISUAL_TRUECOLOR,
.accel       = FB_ACCEL_NONE,
.line_length = 320 * 4,

static struct fb_var_screeninfo ssd1963_var __initdata = {
.xres        = 320,
.yres        = 240,
.xres_virtual    = 320,
.yres_virtual    = 240,
.width        = 320,
.height        = 240,
.bits_per_pixel = 32,
.transp      = {24, 8, 0},
.red        = {16, 8, 0},
.green        = {8, 8, 0},
.blue        = {0, 8, 0},
.activate    = FB_ACTIVATE_NOW,

In our case, 4 bytes will be allocated for pixel: 8-Red, 8-Green, 8-Blue, 8-Transparent

Let’s explain some of the fields of this structure:

.type – that’s the way the bits, which define pixels, are layed out in memory. “Packed pixels” means, that bytes(8888 in our case), would be placed coherently, one after another.

.visual – color depth of the display. In our driver it’s set to “truecolor” – color depth 24bit;

.accel – hardware acceleration;

.transp, red, green, blue – that’s, namely, setting 8888 format with 3 fields – offset, length and msb_right.

In order to register our driver in kernel, one needs to define 2 more objects – the device and the driver). Let’s define our framebuffer device(struct ssd1963), which will store pages of our video memory (struct ssd1963_page):

struct ssd1963_page {
unsigned short x;
unsigned short y;
unsigned long *buffer;
unsigned short len;
int must_update;

struct ssd1963 {
struct device *dev;
struct fb_info *info;
unsigned int pages_count;
struct ssd1963_page *pages;

struct platform_driver ssd1963_driver = {
.probe = ssd1963_probe,
.remove = ssd1963_remove,
.driver = { .name = “ssd1963” }


As for any other Linux kernel module, we need the init/remove function pair. Let’s start with init. Framebuffer drivers are usually registered as platform_driver:

static int __init ssd1963_init(void)
int ret = 0;
ret = platform_driver_register(&ssd1963_driver);
if (ret) {
pr_err("%s: unable to platform_driver_register\n", __func__);
return ret;

Platform driver, in its turn calls probe() function for particular driver, which performs all necessary operations – memory allocation, resources reservation, initialization of nested structs etc. Here’s the example of ssd1963_probe function:

static int __init ssd1963_probe(struct platform_device *dev)
int ret = 0;
struct ssd1963 *item;
struct fb_info *info;

// Allocating memory for ssd1663 device
item = kzalloc(sizeof(struct ssd1963), GFP_KERNEL);
if (!item) {
“%s: unable to kzalloc for ssd1963\n”, __func__);
ret = -ENOMEM;
goto out;
item->dev = &dev->dev;
dev_set_drvdata(&dev->dev, item);

// Initializing fb_info struct using kernel framebuffer API
info = framebuffer_alloc(sizeof(struct ssd1963), &dev->dev);
if (!info) {
ret = -ENOMEM;
“%s: unable to framebuffer_alloc\n”, __func__);
goto out_item;
item->info = info;

//Here  info->par pointer is commonly used to store private data
// In our case, we can use it to store pointer to ssd1963 device
info->par = item;
info->dev = &dev->dev;
info->fbops = &ssd1963_fbops;
info->flags = FBINFO_FLAG_DEFAULT;
info->fix = ssd1963_fix;
info->var = ssd1963_var;

ret = ssd1963_video_alloc(item);
if (ret) {
“%s: unable to ssd1963_video_alloc\n”, __func__);
goto out_info;
info->screen_base = (char __iomem *)item->info->fix.smem_start;
ret = ssd1963_pages_alloc(item);
if (ret < 0) {
“%s: unable to ssd1963_pages_init\n”, __func__);
goto out_video;

info->fbdefio = &ssd1963_defio;

ret = register_framebuffer(info);
if (ret < 0) {
“%s: unable to register_frambuffer\n”, __func__);
goto out_pages;

return ret;

return ret;

The full source code can be found on the github: You might also be interested in the custom PCB design services that we offer.

Background form

Latest articles

RISC-V Unleashed: The definitive guide to next-gen computing

Inside RISC-V microarchitecture

7 Stages of Development a Consumer Electronics Product + Challenges