Tuesday, 30 January, 2018

Previously, I installed Linux on a small ARM Chromebook and set it up without X.Org, using only the console. After neglecting that machine for quite some time, I came back to it because I wanted to try some pixel-graphics programming and the framebuffer seemed like it might be an easy target.

I started by bringing my configuration up to date on the Chromebook and noticed again how good it feels to type on the console with such low latency. I then got to messing around, and it turns out the framebuffer is easy to program, but it’s unfortunately a bit obscure. While I was playing with it, I found myself running date every so often to check the time, so decided a clock panel would be a good first framebuffer program.

Programming a “panel” application, i.e. one that stays visible in a corner, is straightforward on the framebuffer because programs just share the buffer with the console itself. Since the console only writes to the buffer as necessary, redrawing the panel periodically is sufficient to keep it visible.

The Device

As one might expect of old-timey Unix things, the framebuffer can be accessed as a file, the default path of which is /dev/fb0 . You can basically just read and write pixel data to it. For example, try running this to put a white pixel in the top-left corner of the screen (you will need to be root or add yourself to the video group):

echo -en '\xFF\xFF\xFF\x00' > /dev/fb0

In order to put pixels wherever we want without overwriting the whole screen, we’ll want to mmap the file instead, and in order to do that we need to know the dimensions of the buffer. The linux/fb.h header defines some ioctl calls which can be used to interrogate the framebuffer file: FBIOGET_VSCREENINFO and FBIOGET_FSCREENINFO .

These return a lot of information about the format of the buffer, but my assumption is that most of it is irrelevant on modern hardware. I assume the buffer to be packed pixels of 32-bit (A)RGB and only check fb_var_screeninfo.xres and .yres . In that case, the buffer can be mapped as uint32_t * :

int fb = open("/dev/fb0", O_RDWR); assert(fb > 0); struct fb_var_screeninfo info; assert(0 == ioctl(fb, FBIOGET_VSCREENINFO, &info)); size_t len = 4 * info.xres * info.yres; uint32_t *buf = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0); assert(buf != MAP_FAILED);

Placing a pixel is now as simple as assigning buf[y * info.xres + x] an RGB value.

The Font

In order to display text as pixels we’ll need a font. Bitmap fonts for the console are provided by kbd in /usr/share/kbd/consolefonts . Most of these are in PSF2 format, defined in psf.h . The files are gzipped, so we’ll need to use the stdio wrappers from zlib :

gzFile font = gzopen("/usr/share/kbd/consolefonts/default8x16.psfu.gz", "r"); assert(font); struct psf2_header header; assert(1 == gzfread(&header, sizeof(header), 1, font)); assert(PSF2_MAGIC_OK(header.magic)); uint8_t glyphs[header.length][header.charsize]; assert(header.length == gzfread(glyphs, header.charsize, header.length, font);

Now we can index glyphs by a char to get a bitmap header.width bits wide and header.height bits tall. The width is rounded up to the next byte, so a 9×16 bitmap will be 2×16 bytes. To render it, we translate each bit to a pixel:

static void renderChar(uint32_t left, uint32_t top, char c) { uint8_t *glyph = glyphs[c]; uint32_t stride = header.charsize / header.height; for (uint32_t y = 0; y < header.height; ++y) { for (uint32_t x = 0; x < header.width; ++x) { uint8_t bits = glyph[y * stride + x / 8]; uint8_t bit = bits >> (7 - x % 8) & 1; buf[(top + y) * info.xres + left + x] = bit ? 0xFFFFFF : 0x000000; } } }

For strings, we just move left by the width for each character:

static void renderStr(uint32_t left, uint32_t top, const char *s) { for (; *s; ++s) { renderChar(left, top, *s); left += header.width; } }

The Time

To display the time, we call strftime in a loop and render the text every second until the minute changes to keep it visible over the console:

for (;;) { time_t t = time(NULL); assert(t > 0); const struct tm *local = localtime(&t); assert(local); char str[64]; size_t len = strftime(str, sizeof(str), "%H:%M", local); assert(len); for (int i = 0; i < (60 - local->tm_sec); ++i) { renderStr(info.xres - header.width * len, 0, str); sleep(1); } }

This renders the time in the top-right corner. To make it more visually clear, we can add a simple border:

uint32_t left = info.xres - header.width * len - 1; uint32_t bottom = header.height; for (uint32_t y = 0; y < bottom; ++y) { buf[y * info.xres + left] = 0xFFFFFF; } for (uint32_t x = left; x < info.xres; ++x) { buf[bottom * info.xres + x] = 0xFFFFFF; }

The result, with my preferred font and colours, displayed over my editor:

My full implementation is available on GitHub. It can be compiled through the accompanying Makefile or with cc -lz -o fbclock fbclock.c . It’s rather short and simple, so in the spirit of the (A)GPL, I encourage you to copy the file and modify it to your needs.

I’ll probably code up a battery charge indicator next, then move on to what I originally intended to program, which may appear as a new post in the future!

Update: battery charge indicator.