One of the questions I’m asked most frequently when giving training on Android’s internal workings is how to write a kernel driver.  What makes this question interesting is it’s not always clear what the person asking the question means and usually requires additional conversation.  Generally speaking, it boils down to one of two things: 1) how do I make device hardware work on Android or 2) how do I write a driver for the Linux kernel powering Android.  While there is no doubt these two things are related, the answers are not necessarily the same.  This really stems from how hardware devices are handled in Android and how it is different than traditional Linux systems.  We can answer this by looking at three basic concepts for Android hardware handling:

  1. The Linux kernel is used on Android in part because of its large base of existing drivers as well as easy programming model
  2. Android handles hardware both in the kernel drivers as well as via the Hardware Abstraction Layer (HAL) at the native level
  3. Drivers and HAL modules must be included with the device, they cannot be installed by the user

 

android_tux_tools

 

Linux Kernel Drivers

The Linux kernel provides a robust, extensible and well understood kernel environment.  It’s been around for nearly 25 years and continues to evolve as time goes on.  Android utilizes a forked Linux kernel: there are numerous additions made to the kernel which are Android specific and essential to its operation.  However, in the grand scheme of things the number of changes is quite small.  The Linux kernel has a well documented driver model and includes a multitude of drivers supporting various CPUs, SoCs, controllers, busses and devices.  So what does this mean for Android?  It means if you are already familiar with writing Linux kernel drivers for workstations/servers then you will feel right at home.  That being said, there are several things to keep in mind. First, Android is a mobile operating system and you have to be cognizant of the fact you do not have unlimited resources at your disposal.  Even though modern mobile phone and tablet devices are increasing in RAM and power, they are not workstations.  Good memory management and efficient use of resources is even more critical.  Top notch drivers are generally written to be efficient in terms of resource usage but I have seen many cases on Linux and other OSes where far too much memory was being consumed or slow algorithms used because it was simple to crank it out. Second, power management is treated completely different than traditional Linux kernel based systems.  Unlike Linux workstations, Android devices want to be in a low power state as often as possible.  You may have heard about something called “wakelocks” in Android.  Wakelocks are essentially constructs used to prevent suspending of the system.  They can be acquired in kernel space (e.g. drivers)  or in user space (via the PowerManager).  This means you must code your driver with sleep state in mind: if your device can “wake up” the system from low power, you probably also need to use a wakelock to keep the system awake so user space can handle your event.  Of course, there are other situations a driver can encounter which need similar functionality. Third, the Android Linux kernel does introduce new functionality which may be beneficial for you to leverage.  For example, newer versions of Android are shipping with the “Android sync driver” which allows drivers and HAL components to link different hardware components within an SoC for efficient processing.  This framework allows you to take advantage of hardware synchronization support and tie things like camera, GPU and encoders together for sampling and processing.  This alleviates some overhead and allows your kernel and device to be more efficient. Fourth, debugging the kernel in Android can be more challenging.  Often, the SoCs used in mobile platforms are specialized and there is limited or no support for things like kgdb or kdb.  You may be able to patch in the support, but often that in and of itself is not a minor undertaking.  Another option is the use of a JTAG style debugger which can connect over network or USB to your development machine.  Of course, there is always the old standby, printk, but on Android it only goes into a RAM buffer so you’ll need to hook up a serial console or other mechanism to get access to the output. Another thing to keep in mind with regards to kernel drivers is, unlike the vast majority of the rest of the Android software stack, the Linux kernel is licensed under the GPLv2.  This means your drivers or other software rolled into the kernel must also be released under GPL, which for some businesses is not ideal.  This is where the Android HAL comes into play.

Android HAL

The Android HAL is a framework used in the native (C/C++) layer of Android to bridge the gap between the user and kernel space.  It provides a generic interface which can be used to communicate with underlying kernel drivers or other system components, such as daemons.  Android uses this approach for communicating with the underlying hardware for several different reasons:

  1. While the Linux driver model is well documented and understood, the user space interfaces for devices is not always consistent.  A great example is the Wifi network drivers. While Wifi has been around for quite a while now, vendors are not always consistent with their use of the user space interface, often adding their own extensions or having slightly different behavior.  Just look at wpa_supplicant’s variety of driver interface components and you’ll see what I mean.  The HAL provides a concrete API which OEMs must implement to bridge the gap for defined hardware components.
  2. Since the HAL is in user space, it is purposely licensed using the Apache 2.0 license.  This allows OEMs or vendors to keep their proprietary information just that: proprietary.
  3. The HAL framework is extensible enough to support OEM specific hardware which is not part of the Android Compatibility Definition or supported hardware.

The HAL framework is a native shared library, libhardware, used to “find” and load other shared libraries by name (module ID).  The OEMs are responsible for providing the appropriate shared library.  Writing one for one of the defined, supported hardware devices just means following the structure and documented interface(s) in the headers.  Similarly, writing a custom HAL for a custom component is done exactly the same way.  The existing device HALs (GPS, audio, sensors, etc.) are all derived from the baseline structures/interfaces defined in hardware/libhardware/include/hardware/hardware.h within the AOSP sources.  For Linux kernel developers, the concepts are going to feel very familiar. Essentially, each HAL implementation provides a “module” data structure which is derived from hw_module_t:

typedef struct hw_module_t {
    uint32_t tag;
    uint16_t module_api_version;
#define version_major module_api_version
    uint16_t hal_api_version;
#define version_minor hal_api_version
    const char *id;
    const char *name;
    const char *author;
    struct hw_module_methods_t* methods;
    void* dso;
    uint32_t reserved[32-7];
} hw_module_t;

Remember, though, this is not C++ so in C you essentially use this structure as-is in your own module, or create a new structure and embed this structure at the start of it:

typedef struct my_module_t {
    hw_module_t  common;

    uint32_t     myData1;
    uint16_t     myData2;
} my_module_t;

Each module exposes the usual information via a fixed symbol, HAL_MODULE_INFO_SYM, of the type hw_module_t (or a derivative like above): the module ID (string), version and vendor info, and a function pointer table for module “methods”.  There is only a single pre-defined function in the hw_module_methods_t:

int (*open)(const struct hw_module_t *module,
            const char               *id,
            struct hw_device_t       **device)

Of course, like the hw_module_t structure, the hw_module_methods_t structure can similarly be expands to include module specific methods which are used against the entire module. The open call is used to open a specific device which is supported by the HAL, identified by the id argument, which is a pre-defined string identifier. This allows a single HAL module to support multiple devices.  The sensors HAL is a great example of this: the OEM sensor HAL must expose all sensors provided by the OEM, which can include a variety of unrelated technologies.  Many HALs only expose a single device and ignore this argument. Like the module and module methods structures, it is expected that an individual device will provide its own interface and details via a structure derived from hw_device_t:

typedef struct hw_device_t {
    uint32_t tag;
    uint32_t version;
    struct hw_module_t* module;
    uint32_t reserved[12];
    int (*close)(struct hw_device_t* device);
} hw_device_t;

You can see here the only required method for the device is the close method, which is used to close any operations and backing data for communication with the underlying device.  After the close call the device object is unusable. It’s typical for HALs to define their own device structure derived from hw_device_t and exposing APIs to operate on the device(s). The Android defined HALs do exactly this, as seen here in the Sensors HAL (Sensor device API version 0.1):

struct sensors_poll_device_t {
    struct hw_device_t common;
    int (*activate)(struct sensors_poll_device_t *dev,
            int handle, int enabled);
    int (*setDelay)(struct sensors_poll_device_t *dev,
            int handle, int64_t ns);
    int (*poll)(struct sensors_poll_device_t *dev,
            sensors_event_t* data, int count);
};

OEMs therefore need to provide HAL implementations for all required or Android defined and optionally included hardware components.  OEMs can choose (wisely!) to use a HAL for their own custom components as well.  In many cases, the OEM will also need to provide the necessary kernel support to operate the underlying hardware as well.  Many silicon and peripheral vendors can supply these under license, which can obviously have a positive impact on the overall system development time.

 Device Support After Shipment

As you could not doubt surmise, all of this implies that device support must be included with the device when it ships.  This is generally true.  Android is not intended to be used on workstation/desktop/server type systems which can be expanded by the end user.  At least, not yet.  These systems are provided by an OEM and come with support for all of the built in hardware.  And, usually the support for the hardware is locked down to proprietary binaries (HALs) by the OEM and/or silicon vendors.  Notice I said, “generally.” There are a couple of cases where some extensibility is still available via 3rd parties: USB and Bluetooth devices.  However, even this is pretty limited.  Unlike workstation Linux, the kernel driver support is completely locked down.  Only the core system services are allowed to interface with the kernel and part of the init process loads the necessary driver modules (if used by the OEM).  On stock/commercial Android systems there is no “su” or “sudo” for installing system type executables. For USB, the Android framework does provide some capability for apps to leverage attached USB peripheral devices.  I’m going to stick to just the USB host for this article, Android’s USB accessory mode is a different discussion.  However, in order to use “host” mode the OEM must support it and your app must deal with the USB device specifics itself.  This is done via the UsbManager. Using the manager, some manifest and BroadcastReceiver additions your app can gain access to attached USB peripherals. Android supports USB HID devices, so these will likely not be directly accessible. Once you gain access to the device, you can enumerate its endpoints and issue requests to it, etc. In other words, your app essentially has to build in driver-like support for that specific device. For Bluetooth, the concept is very similar. Using the BluetoothManager and BluetoothAdapter objects you can discover devices, pair with them, and make connections to them. For common BT profiles the system already provides support, but for custom devices you can setup RFCOMM type sockets and communicate with the device(s).

Parting Thoughts

Hopefully this has helped with your understanding of the way Android supports hardware devices.  The differences found in Android compared to traditional Linux workstations is confusing to many new Android platform and application engineers.  However, due to silicon/vendor IP concerns, a completely different deployment environment and security concerns you can see why the system was constructed in this way.  But, Android still provides some flexibility for end apps to take advantage of certain peripheral devices which are not handled by the system.  It’s a decent trade off for the average consumer, even if it means some additional work for OEMs and even 3rd party app authors.

What are some of your thoughts on this paradigm?  Have you encountered any issues because of this design?  What would you like to see done differently and why?