Using the Linux framebuffer in C/C++ -- just the essentials
There's rarely a need to write code that operates directly on a framebuffer -- it's a very low-level way of doing graphics, and an even lower-level way of writing text. In desktop applications, it's almost always easier to use a graphical desktop, and draw the screen using the tools and libraries provided. It's enormously easier, and the more complicated the display becomes, the more apparent it gets that working on the framebuffer is painful.
Working on an embedded Linux system is the exception to this rule, as it is to so many others. Getting a full graphical desktop environment working on an embedded system can be difficult, and it might not be at all appropriate, particular if you're working with low memory and sub-second boot times. It's often necessary to work with the framebuffer, even if it's inconvenient.
This article describes how to set the colour of individual pixels, which is the basic drawing operation from which everything else is derived. I'm just giving the bare minimum of information -- no detailed explanation, just the code.
By popular(-ish) request, I have also written a Part 2 article, that deals with some of the complexities that I skip over hear. Please bear in mind, however, that Part 2 is a direct continuation of this article -- it won't make sense on its own.
Before getting started...
1. Ensure that you actually have framebuffer support.
Framebuffer devices usually appear as /dev/fbXXX
.
2. Ensure that you have access rights to read and write the
framebuffer device. The ownership and permissions of the device
will vary from system to system. The device is usually owned
by root
, but might have a read-write group assigned,
so you don't necessarily need to run your application as root
.
3. Don't use X or a Linux desktop. Either the desktop will suppress framebuffer output, or your code will be fighting it for access to the framebuffer. On many Linux desktops you can press ctrl+alt+F-key to switch to a different (text) console for testing.
4. Be aware that there are some odd framebuffers around, particularly in the embedded world. On all Linux systems I've used, even embedded ones, the framebuffer memory is organized into four-byte pixels, with the first three bytes storing the blue, green, and red colour levels (in that order), and the fourth being unused. The fourth byte is often referred to as the 'alpha' or 'transparency' channel, although I'm not aware of any device that implements it. Similarly, I'm aware that there are framebuffers with two-byte and even one-byte pixels, although I've never used any. Similarly, although most framebuffers are organized row-first (so consecutive blocks of data form rows on the screen), this isn't guaranteed. In short, be prepared for a bit of trial and error if you have unusual hardware.
The code
1. Include the appropriate C header
You'll need this for the constants and data structures
needed for the ioctl()
call.
#include <linux/fb.h>
2. Open the framebuffer device
Open it for reading and writing.int fbfd = open (fbdev, O_RDWR); if (fbfd >= 0) { ...
3. Find the dimensions of the screen
struct fb_var_screeninfo vinfo; ioctl (fbfd, FBIOGET_VSCREENINFO, &vinfo); int fb_width = vinfo.xres; int fb_height = vinfo.yres; int fb_bpp = vinfo.bits_per_pixel; int fb_bytes = fb_bpp / 8;
The minimum information you need is the horizontal resolution
vinfo.xres
. Of course, both dimensions are necessary
if you want to avoid writing off the end of the screen. You'll
need both dimensions and the number of bytes per pixel if you
want to map the whole display into memory.
In practice, vinfo.bits_per_pixel
will always
by 32 on a modern Linux system.
There are many, many other ioctrl()
calls that can
be made on the framebuffer. On some systems you can change the
display characteristics, as well as reading them. In embedded
Linux systems, the framebuffer is often of fixed size, so your
application may have to adapt to whatever size it is offered.
4. Map the screen into memory
If you want to map the whole screen, you'll need to calculate the total amount of memory it occupies.
int fb_data_size = fb_width * fb_height * fb_bytes; char *fbdata = mmap (0, fb_data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, (off_t)0);
In some applications it might be more elegant to refer to the
screen using 32-bit quantities, e.g., uint32_t
,
since each pixel is four bytes in size. This saves a certain amount
of calculation when indexing the screen data, but leaves the developer
job of packing and unpacking the individual colour channels from
the integer. Alternatively, you may prefer to use unsigned
char
, since the pixel data is not signed in any meaningful
sense. The best choice depends on the application. In what
follows, I assume you're indexing the screen in byte-sized pieces
-- signed or unsigned makes no difference in this trivial example.
It's not strictly necessary to map the screen into memory -- you
can just make ordinary write()
calls on the
framebuffer's filehandle. Mapping the screen into memory opens the
possibility of random access to the screen's data -- but
consider whether you want to operate this way. In many applications
it will look more appealing if you build up the screen image
in a separate buffer, and then copy the whole thing to the framebuffer.
Still, mapping the screen is usually faster and more flexible, and
probably the best way to proceed if your system supports it.
5. Read or write the mapped screen
To blank the entire screen, just set the entire area to zeros. For example:
memset (fbdata, 0, fb_data_size);
To write a specific pixel, work out its position as follows[1]:
int offset = (y * fb_width + x) * 4;
If you've mapped the screen using a 32-bit data type, then you won't need the '* 4' here -- the compiler will take care of it.
Then just write the RGB values into the array, paying attention to the B-G-R ordering.
r = ... ; g = ...; b = ...; fbdata [offset + 0] = b; fbdata [offset + 1] = g; fbdata [offset + 2] = r; fbdata [offset + 3] = 0; // May not be neeeded
Note that the changes will usually take place immediately -- there is no need to flush a buffer or anything like that, unless it's a buffer you're maintaining yourself.
6. Tidy up
munmap (fbdata, fb_data_size); close (fbfd);
Summary
Low-level framebuffer operations aren't difficult to code on Linux. It's not even particularly difficult to draw crude horizontal lines and boxes. You can do rudimentary, fixed-pitch text output using one of the many open-source bitmap fonts that are to be found on the internet. Even writing images to the screen from files is relatively straightforward.
You'll quickly find, though, that doing serious text rendering, or even drawing oblique lines with anti-aliasing, are a challenge. There are libraries for doing this kind of thing, but getting them working with an embedded application can be a little awkward, to say the least.
[1] Almost within hours of writing this article, people were contacting
me to remind me that you can't assume that the rows of pixels are
contiguous in the framebuffer's mapped memory. Most of the hardware
I've worked on has contiguous pixels but there are a few devices,
including some popular ones, that don't. The proper way to work
out where a line of pixels starts is to use the FBIOGET_FSCREENINFO
ioctl()
call to determine hardware line length.
See my
fbclock
or
jpegtofb
applications on GitHub for a specific example.
The problem with writing code that accommodates a variety of different hardware is that it requires more computation, which can be undesirable in a device with limited CPU power.