Rolling your own minimal embedded Linux for the Raspberry Pi -- part five: X

Pi logo This series of articles is about building up a custom, minimal Linux installation for the Raspberry Pi (or similar), for use in an appliance or similar embedded application. I started in the first article by describing how to set up the most minimal of Linux installations, that would boot to a root shell in a kernel console. In subsequent articles I described how to obtain and set up basic utilities, configure service management (using init -- no systemd in an application like this), how to set up wireless networking, and how to get audio working. All the while, I've been presuming a hand-crafted set-up, with every userland process that runs being completely under the developers's control. I've assumed that in an application like this, we would wish to avoid server/desktop heavy-hitters likesystemd, udev, DBus, etc. A fundamental premise all along has been that the installation would be mostly read-only -- at least the root filesystem would be read-only, and requiring no special shut-down procedure.

I've also presumed, and will continue to presume, that we're willing to use existing, compiled binaries from the Raspbian repository, rather than building everything from source. In a related article I described one technique for extracting binaries and their dependencies onto a build host, rather than an existing Raspbian system. That article discussed the advantages and disadvantages of working that way, rather that compiling everything from source. Fundamentally, you're trading convenience for installed size, because the packages in the repository have a high level of "dependency sprawl". But that's not a point of concern in one-off or small-run embedded projects, and time-to-completion might be.

In this last article in the series, I will describe how to run X-based graphical applications in an embedded system, without needing a full desktop or any of the infrastructure that goes with it.

Embedded X?

It's worth asking whether X is appropriate in an embedded system at all. After all, it isn't a natural fit -- X is more common on desktops. I should point out, however, that I'm not talking about running, say, a web browser or word processor on a Linux appliance. There are many lightweight, desktop-focused Pi Linux distributions that are designed for that kind of application -- DietPi is an example. Rather, I'm considering the practicality or running one or two, probably customer-written, X applications. g x sradio screenshot

For example, my gxsradio application (see screenshot) was designed for use on an Internet radio device with a minimal installation of X and GTK. I wrote it such as to avoid the problems that arise from the lack of most desktop-related infrastructure. It isn't always possible to get existing X applications to run in a minimal system.

If you want to run an application with a graphical user interface in an embedded system, the real question to ask is: what alternative is there to X? gxsradio does have a console-based alternative that works reasonably well -- provided you have a keyboard, or some hardware method of making key selections. Touch-screen support in the kernel console is still very difficult. Moreover, the Linux kernel console has no Unicode support, which is increasingly a problem in this post-ASCII world.

The alternative to X would be to write a graphical application that interacts directly with the framebuffer and input hardware. There's no problem in principle with doing this -- it's just very time-consuming and involved. Even relatively subsidiary problems like rendering Unicode fonts can be very complicated. To some extent there are open-source libraries that can be used for this kind of thing but the reality is, for better or worse, most libraries intended for graphical displays are designed for X (or its equally heavy alternatives, like Wayland).

There was a short-lived project to adapt the GTK libraries to work directly on a framebuffer, without X. So far as I know, this is not being developed further. Even SDL doesn't really work without X any more. For better or worse, for GUI-based applications X may be the only game in town.

People are often reluctant to use X in an embedded project, and understandably so. In fact, though, it's not really the basic X server that is slow and memory-intensive -- it's all the infrastructure that goes with it. In an embedded application you don't need a display manager, or a configuration manager, or perhaps even a window manager -- mostly you just need a drawing surface and/or a basic widget library. I think this is not impractical to implement in an embedded project.

Recap -- how X actually works

Twenty years ago it would not have been necessary to explain to a Linux developer how X works -- everybody knew. Since the rise of the "Linux desktop", however, the internal operation of X -- which is really quite simple -- has become submerged by a heap of largely-incomprehensible supporting software. To run X on a Linux appliance, we really have to get back to basics.

The key component of X is the X server. For many years now everything that runs X has used the Xorg X server, that was originally derived from the XFree86 project back in the 90s. One of the major improvements to the X server since then has been in the area of auto-configuration -- back in the day, hand-crafting an X configuration file for all your hardware was the stuff of nightmares. Unfortunately, a lot of the auto-configuration support relies on infrastructure like DBus and udev that won't exist on a minimal system, so to some extent we're going to be back in the 90s when it comes to set-up -- more on this later.

The role of the X server is to carry out primitive drawing operations on behalf its clients -- the applications. X was always envisaged as a client-server protocol and, back in the day, the X server would have run on a desktop workstation, and its clients on some kind of mainframe. Although such an architecture hardly exists today, X is still a client-server protocol, with Xorg as the server and the applications as its clients. The X server will communicate with the display hardware, either via a framebuffer interface provided by the kernel, or using some more direct method. It will also communicate with input devices -- mouse, touch panel, etc. It's conventional to run the X server as root, because of this close coupling to hardware, but there's rarely a need to run X clients as root

The X server understands the concept of a window, that is, of a region of screen that belongs to some particular application. Within that window it can draw lines, shapes, and basic text glyphs; all functionality beyond that is supplied by something else.

Applications rarely, if ever, communicate with the X server using its native protocol: they use libraries of varying degrees of complexity, which usually build on top of one another. These are typically called 'widget libraries', although the modern ones provide services that go far beyond handling screen widgets.

 

One of the most important X clients in most installations is the window manager. The basic purpose of a window manager is to move and re-order windows on behalf of the user. The window manager will usually 'decorate' windows, that is, draw a box around them with various controls. Many window managers provide application management functions as well -- that is, they provide ways to start and stop applications, typically using icons or menus. Some Linux desktop systems delegate these responsibilities to a separate software component. Some even delegate fundamental window-manager functions like window decoration to separate components.

Although it surprises some developers, many X applications will run perfectly well without a window manager. Old stalwarts like xclock and xcalc work just the same with or without one -- so long as you don't mind positioning their windows using the command line. If you're building an appliance with a single application, whose main window should fill the entire screen, question whether you need a window manager at all. If you do, you almost certainly don't need a desktop heavyweight like Gnome Shell. You probably don't even need a lightweight desktop window manager like xfwm or IceWM -- even the lightest of the lightweight desktop window managers is too sophisticated for an embedded system.

For embedded systems that need a window manager, I normally use matchbox. For Raspberry Pi, this is available from the Raspbian repository, although I doubt that anybody running Raspbian actually uses it. The key feature of matchbox -- apart from its speed and size -- is that it forces a single, full-screen window. You can switch between multiple applications if you have them, but each will occupy the whole screen. This is exactly the behaviour that is required when using a 5" or 7" touch panel as your display device.

The window manager is usually started very soon after the X server starts, and has a lifetime the same as that of the X server. In an embedded system the window manager may be largely invisible to users.

 

A key part of an X installation on a desktop system is a display manager. The display manager is a small X client application that authenticates users, and creates a new desktop session when a user logs in. There's almost certainly no need for such a thing in an embedded system -- you would just arrange for a startup script to run the required application with the appropriate user permissions.

In short, to run an X application on an embedded Linux system you need an X server, whatever X-related libraries your application requires, and perhaps a window manager. And that's probably about all.

Obtaining the X server

The standard Xorg server for the Raspberry Pi is available in the Raspbian repository, in the xorg package. The full package contains a lot of code that you'll never need, but the alternative is to build the server from source, which will give you better control. This isn't a job for the faint-hearted, however.

The driver for the Linux framebuffer is included in the xorg package, but you'll probably need additional kernel modules for input devices. In particular, the evdev module (in package xserver-xorg-input-evdev will enable input from many standard input devices, including almost all USB devices, and the Raspberry Pi official 7" touchscreen (I'm unsure if 3rd-party touch panels are supported using the same interface).

Configuring the X server

This is where we have to take a step back in time, because in a system without desktop infrastructure the configuration will have to be defined manually. The display configuration is probably straightforward enough, if you have only one display device. On the Raspberry Pi the only supported display interface is the Linux framebuffer, whether you're using a touch panel or an external HDMI monitor, so this configuration will be boilerplate. For input devices, however, you might have to experiment.

If you're using the evdev module, then most (perhaps all) input devices will be aggregated as device files under /dev/input. Lacking something like udev to categorize these devices and give them helpful names, you'll have to figure out which eventXXX file corresponds to which device. Although there are utilities for probing the /dev/input devices, I don't really think there's a better method than cat /dev/input/eventXXX while wiggling the various input devices, to see which shows activity.

I've found that the assignment of eventXXX devices to hardware is stable between reboots -- but it will probably change if hardware devices are added or removed.

The following is a sample configuration for a Pi 3B that has a USB keypad, a USB trackball (mouse), and a touch panel. You probably shouldn't assume that the input device numbers will be transferrable to any other system.

Section "ServerFlags"
  Option       "AutoAddDevices"   "false"
  Option       "AutoEnableDevices"   "false"
  Option       "AllowEmptyInput"   "false"
EndSection

Section "Screen"
  Identifier "foo"
EndSection

Section "ServerLayout"
  Identifier "bar"
  Screen "foo"
  InputDevice "mouse"
  InputDevice "keyboard"
  InputDevice "touchscreen"
EndSection

Section "Device"
    Identifier "Raspberry Pi FBDEV"
    Driver "fbdev"
    Option "fbdev" "/dev/fb0"
    Option "SwapbuffersWait" "true"
EndSection

Section "InputDevice"
  Identifier "mouse"
  Driver "evdev"
  Option "Device" "/dev/input/event1"
  Option "Emulate3Buttons" "True"
  Option "Emulate3Timeout" "50"
  Option "GrabDevice" "False"   
EndSection

Section "InputDevice"
  Identifier "keyboard"
  Driver "evdev"
  Option "Device" "/dev/input/event4"
  Option "GrabDevice" "False"   
EndSection

Section "InputDevice"
  Identifier "touchscreen"
  Driver "evdev"
  Option "Device" "/dev/input/event8"
  Option "GrabDevice" "False"   
EndSection

The options in the ServerFlags section are essentially to disable auto-configuration, which won't work without udev and DBus. The serverLayout section has one entry for each hardware device -- one display and three input devices. The InputDevice sections simply associate a name with a file in /dev/input.

This is a fairly simple X configuration, compared with the kind we had to create in the 90s. At least we don't have to configure the display hardware timing parameters for different display modes -- that is all handled in the kernel's framebuffer device.

Starting X

The following script, which is run from an init script, is sufficient to start the X server, the matchbox window manager, and my Internet radio application. Starting the X server and window manager takes about a half-second on a Pi 3B -- starting a Gnome desktop session takes sixty times that long.

mkdir /var/log
modprobe evdev
Xorg -config /etc/X11/X.conf
su - user -c "DISPLAY=:0 matchbox-window-manager &"
su - user -c "DISPLAY=:0 gxsradio &"

The mkdir /var/log command is necessary if /var is an in-memory filesystem -- which it will have to be if you're running a read-only root filesystem. It's good security discipline to run application code as an unprivileged user so far as possible, hence the su invocations.

Summary

Running X on an embedded Linux system might not be the most obvious way to get a graphical user interface but, in fact, there are few alternatives that don't involve massive amounts of work. With careful configuration, running X need not be slow or unreasonably memory-intensive, even on a single-board computer.

Closing remarks

Building a complete, functional Linux distribution for an embedded or appliance application is not straightforward, as you'll have seen if you've followed the articles in this series. An awful lot of Linux software -- even low-level system software -- assumes a large-scale installation, for a desktop or server. There are many, many embedded Linux appliances in the world, but pre-packaged, supported distributions for such applications are -- well, non-existent.

Happily, building your own is interesting and educational, which is a good thing, considering how much work is involved.

This is the last article in this series, at least for now. If you found any of these articles useful, please get in touch, and I might be inspired to write some more.

Back to the series index.