Writing a Basic Framebuffer Driver

0
21455
laptop with binary code
This article deals with the basic structure of a framebuffer and will interest those who know how to write a character device driver. The author has tried to simplify the topic as much as possible so as to make it accessible to more readers.

A framebuffer driver is an intermediate layer in Linux, which hides the complexities of the underlying video device from the user space applications. From the point of view of the user space, if the display device needs to be accessed for reading or writing, then only the framebuffer device such as /dev/fb0 has to be accessed. If you have more than one video card in your computer, then each will be assigned separate device nodes like /dev/fb0.. /dev/fb1.. /dev/fbX (X being the minor number of the device).

We also have many different ways to access a framebuffer. To get a snapshot of your screen, you can just use the cp command, as follows:

cp /dev/fb0 myscreen

From a programmer’s point of view, you can read and write to this device, the main use being mmap. You can map the video memory in the user space memory, and then you can control each and every pixel of your screen. We also have a set of ioctl calls to get and set different parameters of the video hardware. A few ioctl commands include FBIOGET_VSCREENINFO and FBIOPUT_VSCREENINFO. As you may have guessed, the FBIOGET_VSCREENINFO call will ask the hardware about the screen information, and FBIOPUT_VSCREENINFO will set the screen information. Some other ioctl commands include FBIOGETCMAP, FBIOPAN_DISPLAY, etc. You can find the list of commands in the uapi/linux/fb.h file in the kernel source.

Now, let’s look at how to write a module that will work as a very minimal framebuffer driver, with only the required components.
We need a few header files in our module, and with the basic structure of a module our starting point becomes…

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/pci.h>

static int __init ourfb_init(void)
{
return pci_register_driver(&ourfb_driver);
}
static void __exit ourfb_exit(void)
{
pci_unregister_driver(&ourfb_driver);
}
module_init(ourfb_init);
module_exit(ourfb_exit);
MODULE_LICENSE(“GPL”);

So now we have a very basic module with the required init and exit function, but we have not yet defined ‘ourfb_driver’ which we are using in the code to register in the PCI subsystem.

static struct pci_driver ourfb_driver = {
.name = “ourfb”,
.id_table = ourfb_pci_tbl,
.probe = ourfb_pci_init,
.remove = ourfb_pci_remove,
};

In the driver structure, we have defined only the required part. If you want to use power management in your driver code and provide Suspend and Resume functionality to your hardware, then you can define that in the driver. But here we are leaving them out as they are optional.
In ourfb_pci_tbl, we have to mention the vendor ID and the device ID of the hardware for which you are writing the driver.

static struct pci_device_id ourfb_pci_tbl[] = {
{ PCI_VENDOR_ID_XXX, PCI_DEVICE_ID_XXX, PCI_ANY_ID, PCI_ANY_ID },
{ 0 }
};
MODULE_DEVICE_TABLE(pci, ourfb_pci_tbl);

We are assuming that PCI_VENDOR_ID_XXX and PCI_DEVICE_ID_XXX are defined elsewhere with the correct vendor and device ID. This table should always be null terminated. To explain MODULE_DEVICE_TABLE in a very simple way –- it informs the kernel that this driver is to be used if it finds any PCI device with the mentioned vendor and device IDs. When the kernel finds the device, it calls the probe callback function; and if it sees that the device is removed, then it will call the remove callback function. A detailed discussion on PCI drivers is outside the scope of this article.

static int ourfb_pci_init(struct pci_dev *dev, const struct pci_device_id *ent)
{
struct fb_info *info;
struct ourfb_par *par;
struct device *device = &dev->dev;
int cmap_len, retval;
info = framebuffer_alloc(sizeof(struct ourfb_par), device);
if (!info) {
return -ENOMEM;
}
par = info->par;
info->screen_base = framebuffer_virtual_memory;
info->fbops = &ourfb_ops;
info->fix = ourfb_fix;
info->pseudo_palette = pseudo_palette;
info->flags = FBINFO_DEFAULT;
if (fb_alloc_cmap(&info->cmap, cmap_len, 0))
return -ENOMEM;
if (register_framebuffer(info) < 0) {
fb_dealloc_cmap(&info->cmap);
return -EINVAL;
}
pci_set_drvdata(dev, info);
return 0;
}
static void ourfb_pci_remove(struct pci_dev *dp)
{
struct fb_info *p = pci_get_drvdata(dp);
unregister_framebuffer(p);
fb_dealloc_cmap(&p->cmap);
iounmap(p->screen_base);
framebuffer_release(p);
}

The above code is an example of a very simple probe function. Now, let’s talk a little about what we have used in our probe function.

framebuffer_alloc: This creates a new framebuffer information structure. It also reserves the size for the driver’s private data (info->par). You might have guessed by now that ourfb_par is our private data. This function will return a pointer to struct fb_info or it returns NULL if it fails.

ourfb_par: As already said, this is the private data of our driver. It is not a required component but it is recommended that you have it in your driver. At any point of time, this structure will have information about the hardware state of the graphics card.

framebuffer_virtual_memory: This is not defined in our code, but it is actually the virtual memory address for the framebuffer. We get this address after remapping the resource address. The following code will show you how we get this address:

unsigned long addr, size;
addr = pci_resource_start(dev, 0);
size = pci_resource_len(dev, 0);
if (addr == 0)
return -ENODEV;
framebuffer_virtual_memory = ioremap(addr, 0x800000);

ourfb_ops: This structure will define the operations that we will allow on our framebuffer. We can have many operations here which are similar to any character device driver, like open, read, write, release, ioctl, etc. But the operations that we have to provide are fb_fillrect, fb_copyarea and fb_imageblit. Regarding the other operations, even if we do not mention them in our ops structure, they will be taken care of automatically by the framebuffer layer by default. If we need any customised functions for our driver, then we have to provide for them also.

Fortunately, the framebuffer layer has three functions: cfb_fillrect, cfb_imageblit and cfb_copyarea. We can use these functions in our driver instead of writing our own fb_fillrect, fb_copyarea and fb_imageblit functions. But if you are writing a driver for hardware that supports hardware acceleration, then you have to write your own functions.

static struct fb_ops ourfb_ops = {
.owner = THIS_MODULE,
.fb_fillrect = cfb_fillrect,
.fb_imageblit = cfb_imageblit,
.fb_copyarea = cfb_copyarea,
};

ourfb_fix: This structure will contain fixed information about our hardware. The following is the structure as defined in the header file:

struct fb_fix_screeninfo {
char id[16]; /* identification string */
unsigned long smem_start; /* Start of frame buffer mem (physical address) */
__u32 smem_len; /* Length of frame buffer mem */
__u32 type; /* FB_TYPE_* */
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */
unsigned long mmio_start; /* Start of Memory Mapped I/O (physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which specific chip/card we have */
__u16 capabilities; /* FB_CAP_* */
__u16 reserved[2]; /* Reserved for future compatibility */
};

flags: This will indicate the type of hardware acceleration our driver is capable of.
fb_alloc_cmap: This allocates memory for the colour map, which our driver is going to use.
register_framebuffer: This is the most important function that will actually register our framebuffer with the framebuffer layer. This function is responsible for creating the device nodes /dev/fb*.

Note: In the probe function, at any stage, if the initialisation fails, we have to undo whatever we have done before that, and then need to return an appropriate error value.

In the ourfb_remove function, we are unwinding what we have done in the probe function in a reverse manner.

We have just looked at how to create a very simple framebuffer device, though we have not touched upon any hardware related topics here. But when we work with an actual framebuffer driver that is responsible for video hardware, we need to give all the hardware initialisation routines in the probe function. If you want to see the code of any framebuffer device driver, you can find it in the drivers/video/fbdev/ folder of the Linux source tree.

Another complicated scenario: Some graphical hardware will have support for two monitors, where each display can have its own unique data. In this case, each display should have its own separate framebuffer device, which means separate struct fb_info. Some hardware will have the double-buffering capability.

LEAVE A REPLY

Please enter your comment!
Please enter your name here