Android IoT, Automotive, & Smart TV Customizations

Mastering Android HAL: Build a Custom Driver for I2C IoT Sensors from Scratch

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android HAL and IoT Sensors

The Android Hardware Abstraction Layer (HAL) is a critical component that bridges the gap between high-level Java APIs in the Android framework and the low-level hardware drivers of a device. For IoT devices, embedded systems, and custom hardware, mastering HAL development is essential to integrate proprietary sensors and actuators seamlessly into the Android ecosystem. This guide will walk you through building a custom HAL driver for an I2C-based IoT sensor from scratch, providing a deep dive into the process.

I2C (Inter-Integrated Circuit) is a widely used serial communication protocol for connecting low-speed peripherals over short distances. Many IoT sensors, such as accelerometers, gyroscopes, temperature sensors, and magnetometers, rely on I2C for data exchange. Our goal is to expose a generic I2C temperature sensor’s readings to the Android framework via a custom HAL service.

Prerequisites and Setup

Before diving into HAL development, ensure you have the following:

  • An AOSP (Android Open Source Project) build environment set up. This involves syncing the AOSP source code and configuring your build for a target device (e.g., an Android TV box, a custom embedded board, or even a virtual device if you can simulate I2C).
  • Basic understanding of C++ and Linux kernel device drivers.
  • The datasheet for your specific I2C sensor. For this tutorial, we’ll assume a hypothetical temperature sensor at I2C address 0x48 that provides temperature data by reading two bytes from its data register 0x00 after a single-byte command to initiate conversion.
  • Root access to your target Android device for testing and flashing.

Setting Up the AOSP Build Environment (Brief)

If you haven’t already, set up your AOSP environment:

$ repo init -u <AOSP_GIT_URL> -b <BRANCH>$ repo sync -j8$ source build/envsetup.sh$ lunch <TARGET_DEVICE_BUILD> # e.g., aosp_arm64-userdebug

Step 1: Understanding Your I2C Sensor and Linux Device Node

Every I2C device on a Linux system typically exposes itself via the /dev/i2c-N interface, where ‘N’ is the bus number. You can identify your sensor’s bus by checking /sys/bus/i2c/devices/ or using i2cdetect on a rooted device.

# On your Android device (via adb shell)i2cdetect -y 1 # Scans I2C bus 1 for devices

From our hypothetical sensor datasheet: I2C slave address is 0x48. To read temperature, we need to send a command byte (e.g., 0x00 for ‘read temp register’) and then read two bytes.

Step 2: Defining the HAL Interface with AIDL

Android 10+ primarily uses AIDL (Android Interface Definition Language) for HAL interfaces. We’ll define an interface that allows an Android application or framework service to request temperature readings.

Create AIDL Directory and File

Navigate to your AOSP source and create a new interface path (e.g., hardware/interfaces/temperature/aidl/android/hardware/temperature/). Inside, create ITemperature.aidl:

// hardware/interfaces/temperature/aidl/android/hardware/temperature/ITemperature.aidlpackage android.hardware.temperature;interface ITemperature {    /**     * Reads the current temperature from the sensor.     * @return The temperature value in Celsius, or -999.0 if an error occurs.     */    float readTemperature();    /**     * Sets a configuration register on the sensor.     * @param reg The register address.     * @param value The value to write to the register.     * @return True if successful, false otherwise.     */    boolean writeConfig(byte reg, byte value);    /**     * Reads a configuration register from the sensor.     * @param reg The register address.     * @return The value of the register, or -1 if an error occurs.     */    int readConfig(byte reg);};

Build the AIDL Stubs

Add an Android.bp file in hardware/interfaces/temperature/aidl/ to build the AIDL stubs:

// hardware/interfaces/temperature/aidl/Android.bpaidl_interface {    name: "android.hardware.temperature-V1-aidl",    srcs: [        "android/hardware/temperature/ITemperature.aidl",    ],    stability: "vintf",}

Step 3: Implementing the HAL Service (C++)

Now, we’ll implement the C++ service that provides the actual hardware interaction. This service will live in its own directory, for example, hardware/interfaces/temperature/service/.

Temperature.h (Header File)

// hardware/interfaces/temperature/service/Temperature.h#pragma once#include <android/hardware/temperature/ITemperature.h>#include <hidl/MQDescriptor.h>#include <hidl/Status.h>#include <string>namespace android::hardware::temperature::implementation {class Temperature : public ITemperature {public:    Temperature(const std::string& i2cDevPath);    ~Temperature();    // Methods from ::android::hardware::temperature::ITemperature follow.    ::android::binder::Status readTemperature(float* _aidl_return) override;    ::android::binder::Status writeConfig(int8_t reg, int8_t value, bool* _aidl_return) override;    ::android::binder::Status readConfig(int8_t reg, int32_t* _aidl_return) override;private:    int mI2cFd; // File descriptor for the I2C device    bool initI2c();    bool setSlaveAddress(int addr);    // Helper for I2C communication    bool i2cReadBytes(int reg, uint8_t* buffer, size_t len);    bool i2cWriteBytes(int reg, const uint8_t* buffer, size_t len);};} // namespace android::hardware::temperature::implementation

Temperature.cpp (Implementation File)

// hardware/interfaces/temperature/service/Temperature.cpp#include "Temperature.h"#include <log/log.h>#include <fcntl.h>#include <sys/ioctl.h>#include <unistd.h>#include <linux/i2c-dev.h> // For I2C_SLAVE, I2C_RDWR etc.namespace android::hardware::temperature::implementation {Temperature::Temperature(const std::string& i2cDevPath) : mI2cFd(-1) {    mI2cFd = open(i2cDevPath.c_str(), O_RDWR);    if (mI2cFd < 0) {        ALOGE("Failed to open I2C device %s: %s", i2cDevPath.c_str(), strerror(errno));        return;    }    ALOGI("Successfully opened I2C device %s", i2cDevPath.c_str());}Temperature::~Temperature() {    if (mI2cFd >= 0) {        close(mI2cFd);    }}bool Temperature::setSlaveAddress(int addr) {    if (mI2cFd < 0) {        ALOGE("I2C device not open.");        return false;    }    if (ioctl(mI2cFd, I2C_SLAVE, addr) < 0) {        ALOGE("Failed to set I2C slave address 0x%02X: %s", addr, strerror(errno));        return false;    }    ALOGV("Set I2C slave address to 0x%02X", addr);    return true;}// Helper for reading bytes from a register (simplified for common I2C chips)bool Temperature::i2cReadBytes(int reg, uint8_t* buffer, size_t len) {    if (!setSlaveAddress(0x48)) return false; // Set sensor's I2C address    if (write(mI2cFd, &reg, 1) != 1) {        ALOGE("Failed to write register address 0x%02X: %s", reg, strerror(errno));        return false;    }    if (read(mI2cFd, buffer, len) != (ssize_t)len) {        ALOGE("Failed to read %zu bytes from register 0x%02X: %s", len, reg, strerror(errno));        return false;    }    return true;}bool Temperature::i2cWriteBytes(int reg, const uint8_t* buffer, size_t len) {    if (!setSlaveAddress(0x48)) return false; // Set sensor's I2C address    uint8_t write_buffer[len + 1];    write_buffer[0] = (uint8_t)reg;    memcpy(&write_buffer[1], buffer, len);    if (write(mI2cFd, write_buffer, len + 1) != (ssize_t)(len + 1)) {        ALOGE("Failed to write %zu bytes to register 0x%02X: %s", len, reg, strerror(errno));        return false;    }    return true;}::android::binder::Status Temperature::readTemperature(float* _aidl_return) {    if (mI2cFd < 0) {        *_aidl_return = -999.0f;        return ::android::binder::Status::fromServiceSpecificError(        -ENODEV, "I2C device not open.");    }    uint8_t data[2];    // Assuming reg 0x00 is temperature data, read 2 bytes    if (!i2cReadBytes(0x00, data, 2)) {        *_aidl_return = -999.0f;        return ::android::binder::Status::fromServiceSpecificError(        -EIO, "Failed to read temperature from sensor.");    }    // Convert raw data to temperature (hypothetical conversion)    // Example: 12-bit sensor data, MSB first, where each bit is 0.0625 degrees Celsius    int16_t raw_temp = (data[0] << 8) | data[1];    float temperature_c = (float)raw_temp * 0.0625f;    *_aidl_return = temperature_c;    return ::android::binder::Status::ok();}::android::binder::Status Temperature::writeConfig(int8_t reg, int8_t value, bool* _aidl_return) {    if (mI2cFd < 0) {        *_aidl_return = false;        return ::android::binder::Status::fromServiceSpecificError(        -ENODEV, "I2C device not open.");    }    uint8_t val = (uint8_t)value;    *_aidl_return = i2cWriteBytes(reg, &val, 1);    if (!*_aidl_return) {        return ::android::binder::Status::fromServiceSpecificError(        -EIO, "Failed to write config to sensor.");    }    return ::android::binder::Status::ok();}::android::binder::Status Temperature::readConfig(int8_t reg, int32_t* _aidl_return) {    if (mI2cFd < 0) {        *_aidl_return = -1;        return ::android::binder::Status::fromServiceSpecificError(        -ENODEV, "I2C device not open.");    }    uint8_t val;    if (!i2cReadBytes(reg, &val, 1)) {        *_aidl_return = -1;        return ::android::binder::Status::fromServiceSpecificError(        -EIO, "Failed to read config from sensor.");    }    *_aidl_return = (int32_t)val;    return ::android::binder::Status::ok();}// Main entry point for the serviceextern "C" void main() {    ALOGI("Temperature HAL service starting...");    std::shared_ptr<Temperature> service =    ::ndk::SharedRefBase::make<Temperature>("/dev/i2c-1"); // Adjust I2C bus number    if (!service) {        ALOGE("Failed to create Temperature HAL service instance.");        exit(1);    }    // Add service to ServiceManager    ::android::status_t status = ::android::binder::Status::ok().getStatus();    status = service->publish();    if (status != ::android::OK) {        ALOGE("Failed to register Temperature HAL service: %d", status);        exit(1);    }    ALOGI("Temperature HAL service registered. Ready for requests.");    ::android::base::WaitForDeath(nullptr); // Keep service alive} // extern "C" void main()} // namespace android::hardware::temperature::implementation

Android.bp for the Service

Create an Android.bp in hardware/interfaces/temperature/service/ to build the service:

// hardware/interfaces/temperature/service/Android.bpcc_binary {    name: "android.hardware.temperature-service",    relative_install_path: "hw",    vendor: true,    init_rc: ["android.hardware.temperature-service.rc"],    vintf_fragments: ["android.hardware.temperature-service.xml"],    srcs: [        "Temperature.cpp",        "main.cpp", // Often main is a separate small file that calls into Temperature.cpp.     ],    shared_libs: [        "liblog",        "libutils",        "libbinder_ndk",        "android.hardware.temperature-V1-ndk", // Link against the AIDL stubs    ],    static_libs: [        "libbase",    ],    cflags: [        "-Wall",        "-Werror",    ],    product_variables: {        debuggable: {            cflags: [                "-O0",            ],        },    },    compile_multilib: "64",}

Service Init Script (RC File)

Create android.hardware.temperature-service.rc in the same directory. This script tells Android’s init process how to start our service:

// hardware/interfaces/temperature/service/android.hardware.temperature-service.rcservice vendor.temperature-service /vendor/bin/hw/android.hardware.temperature-service    class hal    user system    group system    capabilities SYS_NICE    onrestart restart vendor.temperature-service

VINTF Manifest Entry

Create android.hardware.temperature-service.xml to declare our HAL service to the VINTF framework. This allows Android to discover and bind to it:

// hardware/interfaces/temperature/service/android.hardware.temperature-service.xml<manifest version="1.0" type="device">    <hal format="aidl">        <name>android.hardware.temperature</name>        <version>1</version>        <interface>            <name>ITemperature</name>            <instance>default</instance>        </interface>    </hal></manifest>

Step 4: Building and Flashing

Now, build your HAL service and integrate it into your AOSP image:

$ source build/envsetup.sh$ lunch <TARGET_DEVICE_BUILD>$ m android.hardware.temperature-service # Build just your service

After building, you’ll typically flash the entire system image to your device. Alternatively, you can use adb push for testing if your device’s /vendor partition is writable, but for a stable integration, rebuilding and flashing is recommended.

# After 'm' completes, you can find the service executable in out/target/product/<device>/vendor/bin/hw/$ adb push out/target/product/<device>/vendor/bin/hw/android.hardware.temperature-service /vendor/bin/hw/$ adb push out/target/product/<device>/vendor/etc/init/android.hardware.temperature-service.rc /vendor/etc/init/$ adb push out/target/product/<device>/vendor/etc/vintf/manifest/android.hardware.temperature-service.xml /vendor/etc/vintf/manifest/# Then reboot or restart init$ adb shell stop$ adb shell start

Step 5: Testing Your HAL Service

Once the device reboots, you can verify your service is running and accessible. You can use dumpsys or a simple Android test application.

$ adb shell dumpsys | grep android.hardware.temperature

You should see an entry for android.hardware.temperature.ITemperature/default indicating the service is registered. To interact from an app:

// Example Java code in an Android App (simplified)import android.hardware.temperature.ITemperature;...try {    ITemperature service = ITemperature.Stub.asInterface(        android.os.ServiceManager.getService("android.hardware.temperature.ITemperature/default"));    if (service != null) {        float temperature = service.readTemperature();        Log.d("TemperatureApp", "Current Temperature: " + temperature + " C");    } else {        Log.e("TemperatureApp", "Temperature HAL service not found!");    }} catch (RemoteException e) {    Log.e("TemperatureApp", "Remote exception: " + e.getMessage());}

You’ll need to include the AIDL interface definitions in your application’s build system to generate the client-side stubs.

Conclusion

Building a custom Android HAL for an I2C sensor opens up a world of possibilities for custom hardware integration in Android IoT, automotive, and smart TV platforms. By defining a clear AIDL interface, implementing a robust C++ service interacting with Linux kernel I2C devices, and correctly integrating with the VINTF framework, you can expose low-level hardware capabilities directly to the high-level Android framework. This detailed guide provides the foundation; remember to consult your specific sensor’s datasheet for accurate register maps and communication sequences.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner