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
0x48that provides temperature data by reading two bytes from its data register0x00after 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, ®, 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 →