Android HAL硬件抽象层的原理与应用实例

前言

先来看下 Android 的系统架构图:
在这里插入图片描述
HAL 全称 Hardware Abstract Layer,即硬件抽象层,它向下屏蔽了硬件的实现细节,向上提供了抽象接口,HAL是底层硬件和上层框架直接的接口,框架层通过HAL可以操作硬件设备。

为什么需要HAL?

许多早期的计算机系统没有任何形式的硬件抽象,这意味着为该系统编写程序的任何人都必须知道每个硬件设备如何与系统的其余部分进行通信。这对软件开发人员来说是一个巨大的挑战,因为他们必须知道系统中每个硬件设备如何工作才能确保软件的兼容性。使用硬件抽象,而不是直接与硬件设备通信的程序,它将程序传达给操作系统该设备应执行的操作,然后操作系统会向该设备生成硬件相关的指令。这意味着程序员不需要知道特定设备的工作方式,就能使他们的程序与设备兼容。

我们知道 Android 是基于 Linux 进行开发的,传统的 Linux 对硬件的操作基本上都是在内核中,而 Android 把对硬件的操作分为了两部分,HAL 和内核驱动,HAL 实现在用户空间,驱动在内核空间。这是因为 Linux 内核中的代码是需要开源的(遵循GUN License),如果把对硬件的操作放在内核这会损害硬件厂商的利益,因为这是别人厂商的商业机密,而现在有了 HAL 层位于用户空间(该部分代码遵循Apache License,无需对外开放源代码),硬件厂商就可以将自己的核心算法之类的放在 HAL 层,保护了自己的利益,这样 Android 系统才能得到更多厂商的支持。

Windows 下的 HAL 位于操作系统的最底层,它直接操作物理硬件设备,使用抽象接口来隔离不同硬件的具体实现,为上层的操作系统和设备驱动程序提供一个统一接口,起到对硬件抽象作用。Linux 下的 HAL 与 Windows 不同,HAL 层并不位于操作系统的最底层直接操作硬件,而是在操作系统内核层和驱动程序之上,是一个运行在 User Space(用户空间) 的服务程序。

安卓源码编译

参考文章:

  1. Android 系统开发系列(1):Android 12 源代码下载、编译和刷机
  2. [原创]源码编译(1)——Android6.0源码编译详解 ;
  3. 从谷歌官网下载android 6.0源码、编译并刷入nexus 6p手机

HAL体系结构

HAL 层的源码目录主要如下:

1)一般除了以下3个以外都是硬件厂商相关的 hal 目录:
/hardware/libhardware_legacy/   旧的架构、采取链接库模块的方式
/hardware/libhardware         新架构,调整为 HAL stub
/hardware/ril              无线电抽象层

2)libhardware 目录的结构如下:
/hardware/libhardware/hardware.c  编译成libhardware.s置于/system/lib

3/hardware/libhardware/include/hardware目录下包含如下头文件:
hardware.h         通用硬件模块头文件
copybit.h          copybit模块头文件
gralloc.h          gralloc模块头文件
lights.h           背光模块头文件
overlay.h          overlay模块头文件
qemud.h            qemud模块头文件
sensors.h          传感器模块头文件

4/hardware/libhardware/modules  目录下定义了很多硬件模块:
hardware/msm7k 
/hardware/qcom 
/hardware/ti
/device/Samsung 
/device/moto
这些硬件模块都编译成xxx.xxx.so,目标位置为/system/lib/hw目录

HAL新老架构

位于 libhardware_legacy 目录下的 “旧HAL架构” 和位于 libhardware 目录下的 “新HAL架构”,两种框架如下图所示:
在这里插入图片描述
1、老式 Module 架构

Android 用户应用程序或框架层代码由 Java 实现,Java 运行在 Dalvik 虚拟机中,没有办法直接访问底层硬件,只能通过调用 so 本地库代码实现,在 so本地库代码里有对底层硬件操作代码。也就是说,应用层或框架层 Java 代码,通过 JNI 技术调用 C 或 C++ 写的 so 库代码,在 so 库代码中调用底层驱动,实现上层应用的提出的硬件请求操作。实现硬件操作的 so 库为:module。

老的 libhardware_legacy 架构,在 so 动态链接库中实现了对驱动的访问逻辑处理,应用或者框架在 Runtime(JNI 部份)通过 so 动态链接库直接调用函数达到对硬件驱动的访问。

这种设计架构虽然满足了 Java 应用访问硬件的需要,但是它使得我们的代码上下层次间的耦合太高,用户程序或框架代码必须要去加载 module 库,如果底层硬件有变化,moudle 要重新编译,上层也要做相应的变化;同时如果多个应用程序同时访问硬件,都去加载 module,则同一 module 被多个进程映射多次,会有代码的重入问题。因此 Google 又提出了新的HAL架构。

2、新式 HAL Stub 架构

新的架构使用的是 Module Stub 方式,Stub 是存根或桩的意思,其实说白了就是指一个对象代表的意思。由上面的架构可知,上层应用层或框架层代码加载 so 库代码,so 库代码我们称为 module,在 HAL 层注册了每个硬件对象的存根 stub,当上层需要访问硬件的时候,就从当前注册的硬件对象 stub 里查找,找到之后 stub 会向上层 module 提供该硬件对象的 operations interface(操作接口),该操作接口就保存在了 module 中,上层应用或框架再通过这个 module 操作接口来访问硬件。

libhardware 架构采用 Proxy 代理模式,Stub 虽然仍是以*.so的形式存在,但是 HAL 已经将*.的具体实现隐藏了起来。Stub 向 HAL 提供 operations 方法,Runtime 通过 Stub 提供的 so 获取它的 operations 方法,并告知 Runtime 的 callback 接口回调方法。这样,Runtime 和 Stub 都有对方调用的方法,一个应用的请求通过 Runtime 调用 Stub 的 operations 方法,而 Stub 响应 operations 方法并完成后,再调用 Runtime 的 callback 方法进行返回。如下图,以 Led 为例的示意图:
在这里插入图片描述
Led App 为 Android 应用程序,Led App 里的 Java 代码不能操作硬件,将硬件操作工作交给本地 module 库 led_runtime.so,它从当前系统中查找 Led Stub,查找到之后,Led Stub 将硬件驱动操作返回给 module,Led App 操作硬件时,通过保存在 module 中的操作接口间接访问底层硬件。

3、Module 架构与 Stub 构架对比

在 Module 架构中,本地代码由 so 库实现,上层直接将 so 库映射进进程空间,会有代码重入及设备多次打开的问题。新的 Stub 框架虽然也要加载 module 库,但是这个 module 已经不包含操作底层硬件驱动的功能了,它里面保存的只是底层 Stub 提供的操作接口,底层 Stub 扮演了“接口提供者”的角色。当 Stub 第一次被使用时加载到内存,后续再使用时仅返回硬件对象操作接口,不会存在设备多次打开问题,并且由于多进程访问时返回的只是函数指针,代码没有重入问题。

4、应用调用 HAL 层的完整过程

Android 的 HAL 的实现需要通过 JNI(Java Native Interface),JNI 简单来说就是 java 程序可以调用 C/C++ 写的动态链接库,这样的话,HAL 可以使用 C/C++ 语言编写,效率更高。Android APP 可以直接调用 .so(上面第一种 Module 架构方法),也可以通过app->app_manager->service(java)->service(jni)->HAL来调用(上面第二种 Stub 架构方法)。

第二种方法的基本框架如下所示:
在这里插入图片描述
继续以 LED 的调用过程为例:
在这里插入图片描述

描述下调用过程,如下:

  1. JNI -> 通用硬件模块 -> 硬件模块 -> 内核驱动接口;
  2. 具体一点:JNI->libhardware.so->xxx.xxx.so->kernel
  3. 再具体来说:android frameworks 中 JNI 调用 hardware.c 中定义的 hw_get_module 函数来获取硬件模块,然后调用硬件模块中的方法,硬件模块中的方法直接调用内核接口完成相关功能(看不懂没关系,下文会具体解释)。

HAL的结构体

一般来说 HAL moudle 需要涉及的是三个关键结构体:

struct hw_module_t;           //抽象硬件模块结构体
struct hw_module_methods_t;   //硬件模块方法结构体
struct hw_device_t;           //抽象硬件设备结构体

这三个结构体定义在 hardware.h 中,具体的代码路径:
在这里插入图片描述
如下图所示:
在这里插入图片描述
其中头文件 hardware.h 的代码如下:

/**
* Every hardware module must have a data structure named HAL_MODULE_INFO_SYM
* and the fields of this data structure must begin with hw_module_t
* followed by module specific information.
*/
typedef struct hw_module_t
{
    uint32_t tag;
    uint16_t version_major;
    uint16_t version_minor;
    cost char* id;
    const char* author;
    struct hw_module_methods_t* methods;
    void* dso;
    uint32_t reserved[10];
}hw_module_t;

/**
* Create a function list
*/
typedef struct hw_module_methods_t
{
    /** Open a specific device */
    int (*open)(const struct hw_module_t* module, const char* id,
                struct hw_device_t** device);
} hw_module_methods_t; 

/**
* Every device data structure must begin with hw_device_t
* followed by module specific public methods and attributes.
*/
typedef struct hw_device_t
{
    /** tag must be initialized to HARDWARE_DEVICE_TAG */
    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; 

以上关于 HAL 模块的关键变量和结构体,概述如下:
在这里插入图片描述
Android HAL 将各类硬件设备抽象为硬件模块,HAL 使用hw_module_t结构体描述一类硬件抽象模块。每个硬件抽象模块都对应一个动态链接库,一般是由厂商提供的,这个动态链接库必须尊重 HAL 的命名规范才能被 HAL 加载到,我们后面会看到。每一类硬件抽象模块又包含多个独立的硬件设备,HAL 使用hw_device_t结构体描述硬件模块中的独立硬件设备。下面来分析下 HAL moudle 的三个结构体。

1、先看看头文件中定义的通用硬件模块结构体hw_module_t,它声明了 JNI 调用的接口函数hw_get_module。具体定义如下:

/**
* Every hardware module must have a data structure named HAL_MODULE_INFO_SYM
 * and the fields of this data structure must begin with hw_module_t
 * followed by module specific information.
 */
typedef struct hw_module_t {
  /** tag must be initialized to HARDWARE_MODULE_TAG */
  uint32_t tag;
  /** major version number for the module */
  uint16_t version_major;
  /** minor version number of the module */
  uint16_t version_minor;
  /** Identifier of module */
  const char *id;
  /** Name of this module */
  const char *name;
  /** Author/owner/implementor of the module */
  const char *author;
  /** Modules methods */
  struct hw_module_methods_t* methods; //硬件模块的方法
  /** module's dso */
  void* dso;
  /** padding to 128 bytes, reserved for future use */
  uint32_t reserved[32-7];
} hw_module_t;

如注释所说,所有的 HAL 模块都要有一个以HAL_MODULE_INFO_SYM命名的结构,而且这个结构要以hw_module_t为第一个成员(这里可以理解为是一种继承关系,相当于硬件模块的HAL_MODULE_INFO_SYM结构体,继承了hw_module_t,只不过是 C 语言中没有继承的概念,是通过在结构体中包含的方式间接实现的)。咱们以 lights,sensor 为例:

struct sensors_module_t {
    struct hw_module_t common;
    int (*get_sensors_list)(struct sensors_module_t* module,
            struct sensor_t const** list);
};

/*
 * The lights Module
 */
struct light_module_t HAL_MODULE_INFO_SYM = {
    .common: {
        tag: HARDWARE_MODULE_TAG,
        version_major: 1,
        version_minor: 0,
        id: LIGHTS_HARDWARE_MODULE_ID,
        name: "Lights module",
        author: "Rockchip",
        methods: &light_module_methods,
    }
};

const struct sensors_module_t HAL_MODULE_INFO_SYM = {
    .common = {
        .tag = HARDWARE_MODULE_TAG,
        .version_major = 1,
        .version_minor = 0,
        .id = SENSORS_HARDWARE_MODULE_ID,
        .name = "Stingray SENSORS Module",
        .author = "Motorola",
        .methods = &sensors_module_methods,
    },
    .get_sensors_list = sensors__get_sensors_list
};

2、在 hw_module_t中比较重要的是硬件模块方法结构体hw_module_methods_t的定义如下:

typedef struct hw_module_methods_t {
    /** Open a specific device */
    int (*open)(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device);
} hw_module_methods_t;

该方法只有一个函数指针 open,该方法在定义HAL_MODULE_INFO_SYM的时候被初始化。根据参数可以推断出 open 是用来打开硬件模块获取模块中的硬件设备。由于一个硬件抽象模块中可能包含多个设备,因此需要根据传入的设备id来获取相应的硬件设备hw_device_t。所以这里的 device 就表示一个已经打开的硬件设备。

3、hw_module_methods_t中调用的设备结构体参数 hw_device_t ,前面提到 HAL 使用hw_device_t结构体描述硬件模块中的独立硬件设备,其定义如下:

/**
 * Every device data structure must begin with hw_device_t
 * followed by module specific public methods and attributes.
 */
typedef struct hw_device_t 
    /** tag must be initialized to HARDWARE_DEVICE_TAG */
    uint32_t tag;
    /** version number for hw_device_t */
    uint32_t version;
    /** reference to the module this device belongs to */
    struct hw_module_t* module;
    /** padding reserved for future use */
    uint32_t reserved[12];
    /** Close this device */
    int (*close)(struct hw_device_t* device);
} hw_device_t;

HAL 规定每个硬件设备都必须定义一个硬件设备描述结构体,该结构体必须以hw_device_t作为第一个成员变量,后跟设备相关的公开函数和属性。

  • tag:初始化为常量 HARDWARE_DEVICE_TAG;
  • module:表示该硬件设备归属于哪一个硬件抽象模块;
  • close:函数指针,用来关闭硬件设备。

同样来看下实际的例子:

struct light_device_t {
    struct hw_device_t common;
    int (*set_light)(struct light_device_t* dev,
            struct light_state_t const* state);
};

/**
 * Every device data structure must begin with hw_device_t
 * followed by module specific public methods and attributes.
 */
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);
};

到此,HAL 的 3 个核心数据结构体就分析完了。硬件厂商必须遵循HAL规范和命名,实现抽象硬件模块结构体 hw_module_t 和抽象硬件设备结构体hw_device_t,并且在硬件模块方法结构体hw_module_methods_t中提供 open 函数来获取 hw_device_t。下面我们来看看 HAL 到底是怎样获取硬件模块和硬件设备的,以及是如何加载和解析对应的动态共享库的。

模块实现方法

在 Android 的 HAL 框架使用通用的 “321架构”,也就是三个结构体,两个常量,一个函数。所有的硬件抽象模块都遵循321架构,在此基础上扩展自有的功能。三个结构体上面已经分析完了,接着来看下两个常量(HAL_MODULE_INFO_SYM值定义同样在上面的 hardware.h 中):

/**
* Name of the hal_module_info
*/
#define HAL_MODULE_INFO_SYM HMI
/**
* Name of the hal_module_info as a string
*/
#define HAL_MODULE_INFO_SYM_AS_STR "HMI"

最后看下“一个函数”:在 hardware.c 文件中有一个关键的公共 API——hw_get_module,其作用是根据 module_id 去查找注册相对应的硬件对象,然后载入相应的HAL层驱动模块的 so 文件:
在这里插入图片描述
先给个图总结下 hardware.c 文件中定义的 HAL 整体的获取 Module 和 Device 结构对象的流程:
在这里插入图片描述
下面以 lights 模块为例进行分析,其源文件位置:(Android 5.1 源码):

/frameworks/base/services/core/java/com/android/server/lights/
/frameworks/base/services/core/jni/com_android_server_lights_LightsService.cpp
/hardware/libhardware/include/hardware/lights.h

访问设备大概流程:

app--->frameworks--->hardware--->kernel驱动

(1)frameworks 通过 jni 调用hw_get_module()获得 HAL 对应的模块,在 lights.h中定义有 lights 模块的ID:

#define LIGHTS_HARDWARE_MODULE_ID "lights"

(2)在 Android 5.1 版本的源码frameworks/base/services/core/jni/com_android_server_LightsService.cpp的i nit_native 方法中调用hw_ge_module(),代码如下:

static jint init_native(JNIEnv *env, jobject clazz)
{
    int err;
    hw_module_t* module;
    Devices* devices;
    devices = (Devices*)malloc(sizeof(Devices));
    err = hw_get_module(LIGHTS_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
    if (err == 0) {
        devices->lights[LIGHT_INDEX_BACKLIGHT]
                = get_device(module, LIGHT_ID_BACKLIGHT);
    //………………………………………….
}

(3)由上已知hw_get_module函数在 hardware.c 中实现:

int hw_get_module(const char *id, const struct hw_module_t **module)
{
   return hw_get_module_by_class(id, NULL, module);
}

(4)来看看hw_get_module_by_class是如何实现的:

/** Base path of the hal modules */
#define HAL_LIBRARY_PATH1 "/system/lib/hw"
#define HAL_LIBRARY_PATH2 "/vendor/lib/hw"

/**
 * There are a set of variant filename for modules. The form of the filename
 * is "<MODULE_ID>.variant.so" so for the led module the Dream variants
 * of base "ro.product.board", "ro.board.platform" and "ro.arch" would be:
 *
 * led.trout.so
 * led.msm7k.so
 * led.ARMV6.so
 * led.default.so
 */

static const char *variant_keys[] = {
    "ro.hardware",  /* This goes first so that it can pick up a different
                       file on the emulator. */
    "ro.product.board",
    "ro.board.platform",
    "ro.arch"
};

static const int HAL_VARIANT_KEYS_COUNT =
    (sizeof(variant_keys)/sizeof(variant_keys[0]));
    
int hw_get_module_by_class(const char *class_id, const char *inst,
                           const struct hw_module_t **module)
{
    int i = 0;
    char prop[PATH_MAX] = {0};
    char path[PATH_MAX] = {0};
    char name[PATH_MAX] = {0};
    char prop_name[PATH_MAX] = {0};

    //根据id生成module name,这里inst为NULL
    if (inst)
        snprintf(name, PATH_MAX, "%s.%s", class_id, inst);
    else
        strlcpy(name, class_id, PATH_MAX);

    /*
     * Here we rely on the fact that calling dlopen multiple times on
     * the same .so will simply increment a refcount (and not load
     * a new copy of the library).
     * We also assume that dlopen() is thread-safe.
     */

    /* First try a property specific to the class and possibly instance */
    //首先查询特定的属性名称来获取variant值
    snprintf(prop_name, sizeof(prop_name), "ro.hardware.%s", name);
    if (property_get(prop_name, prop, NULL) > 0) {
        //检查目标模块共享库是否存在
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
            goto found; //存在,找到了
        }
    }

    /* Loop through the configuration variants looking for a module */
    //逐一查询variant_keys数组定义的属性名称
    for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) {
        if (property_get(variant_keys[i], prop, NULL) == 0) {
            continue;
        }
        //检查目标模块共享库是否存在
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) {
            goto found;
        }
    }
    //没有找到,尝试默认variant名称为default的共享库
    /* Nothing found, try the default */
    if (hw_module_exists(path, sizeof(path), name, "default") == 0) {
        goto found;
    }

    return -ENOENT;

found:
    /* load the module, if this fails, we're doomed, and we should not try
     * to load a different variant. */
    return load(class_id, path, module); //执行加载和解析共享库的工作
}

可以看到,在hw_get_module_by_class函数中:

  • 首先根据 class_id 生成 module name,这里就是hw_get_module函数传进来的 id;
  • 再通过 property_get 获得 varient_key 中定义的系统属性,如果系统中有定义该属性,就会获得一个模块名.属性名组成的一个 so 的名称;
  • 然后去/system/lib/hw/vendor/lib/hw下查看,该 so 是否存在,如果存在,调用 load 函数,打开.so文件;
  • 如果不存在,则遍历 variant_keys 数组中定义的属性名称来获取属性值,得到目标模块库名字,检查其是否存在;
  • 如果根据属性值都没有找到模块共享库,则尝试检查 default 的库是否存在,如果仍然不存在,返回错误;
  • 如果上述任何一次尝试找到了目标共享库,path 就是目标共享库的文件路径,调用 load 执行真正的加载库的工作。

(5)再看 load 函数的实现:

/**
 * Load the file defined by the variant and if successful
 * return the dlopen handle and the hmi.
 * @return 0 = success, !0 = failure.
 */
static int load(const char *id,
        const char *path,
        const struct hw_module_t **pHmi)
{
    int status = -EINVAL;
    void *handle = NULL;
    struct hw_module_t *hmi = NULL;

    /*
     * load the symbols resolving undefined symbols before
     * dlopen returns. Since RTLD_GLOBAL is not or'd in with
     * RTLD_NOW the external symbols will not be global
     */
    //使用dlopen打开path定义的目标共享库,得到库文件的句柄handle
    handle = dlopen(path, RTLD_NOW);
    if (handle == NULL) {
        //出错,通过dlerror获取错误信息
        char const *err_str = dlerror();
        ALOGE("load: module=%s\n%s", path, err_str?err_str:"unknown");
        status = -EINVAL;
        goto done;
    }

    /* Get the address of the struct hal_module_info. */
    const char *sym = HAL_MODULE_INFO_SYM_AS_STR; //"HMI"
    //使用dlsym找到符号为“HMI”的地址,这里应该是hw_module_t结构体的地址;并且赋给hmi
    hmi = (struct hw_module_t *)dlsym(handle, sym);
    if (hmi == NULL) {
        ALOGE("load: couldn't find symbol %s", sym);
        status = -EINVAL;
        goto done;
    }

    /* Check that the id matches */
    //检查模块id是否匹配
    if (strcmp(id, hmi->id) != 0) {
        ALOGE("load: id=%s != hmi->id=%s", id, hmi->id);
        status = -EINVAL;
        goto done;
    }
    //保存共享库文件的句柄
    hmi->dso = handle;

    /* success */
    status = 0;

    done:
    if (status != 0) {
        hmi = NULL;
        if (handle != NULL) {
            dlclose(handle);
            handle = NULL;
        }
    } else {
        ALOGV("loaded HAL id=%s path=%s hmi=%p handle=%p",
                id, path, *pHmi, handle);
    }
    //返回得到的hw_module_t结构体的指针
    *pHmi = hmi;
    return status;
}

load 函数的主要工作时通过 dlopen 来打开目标模块共享库,打开成功后,使用 dlsym 来得到符号名字为 “HMI” 的地址。这里的 HMI 应该是模块定义的hw_module_t结构体的名字,如此,就得到了模块对应的hw_module_t的指针。

至此,我们终于得到了表示硬件模块的 hw_module_t 的指针,有了这个指针,就可以对硬件模块进行操作了。HAL 是如何查找和加载模块共享库的过程就分析完了,最终还是通过 dlopen 和 dlsym 拿到了模块的hw_module_t的指针,就可以为所欲为了。

(6)HAL 与 frameworks 关联代码如下:

public class LightsService extends SystemService {
    //...
    private void setLightLocked(int color, int mode, int onMS, int offMS, int brightnessMode) {
        if (color != mColor || mode != mMode || onMS != mOnMS || offMS != mOffMS) {
            if (DEBUG) Slog.v(TAG, "setLight #" + mId + ": color=#"
                    + Integer.toHexString(color));
            mColor = color;
            mMode = mode;
            mOnMS = onMS;
            mOffMS = offMS;
            Trace.traceBegin(Trace.TRACE_TAG_POWER, "setLight(" + mId + ", " + color + ")");
            try {
                setLight_native(mNativePointer, mId, color, mode, onMS, offMS, brightnessMode);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_POWER);
            }
        }
    }
    public LightsService(Context context) {
        super(context);
        mNativePointer = init_native();
        //...
    }
    private static native long init_native();//这里调用HAL层的hw_get_module
    private static native void finalize_native(long ptr);
    static native void setLight_native(long ptr, int light, int color, int mode,
            int onMS, int offMS, int brightnessMode);//访问硬件
    private long mNativePointer;//保存hal返回的句柄
}

GPS HAL加载

前面分析完了 HAL 的框架和机制,下面以 GPS HAL 的加载过程为例把上面的知识串起来。我们从 framework 层的hw_get_module函数作为入口点,初步拆解分析。

1、加载 GPS HAL 的入口函数定义在frameworks/base/services/core/jni/com_android_server_location_GpsLocationProvider.cpp

static void android_location_GpsLocationProvider_class_init_native(JNIEnv* env, jclass clazz) {
   // skip unrelate code
    hw_module_t* module;
   //获取GPS模块的hw_module_t指针
    err = hw_get_module(GPS_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
    if (err == 0) {
        hw_device_t* device;
        //调用open函数得到GPS设备的hw_device_t指针
        err = module->methods->open(module, GPS_HARDWARE_MODULE_ID, &device);
        if (err == 0) {
            //指针强转为gps_device_t类型
            gps_device_t* gps_device = (gps_device_t *)device;
            //得到GPS模块的inteface接口,通过sGpsInterface就可以操作GPS设备了
            sGpsInterface = gps_device->get_gps_interface(gps_device);
        }
    }

其中GPS_HARDWARE_MODULE_ID定义在hardware/libhardware/include/hardware/gps.h中:

/**
 * The id of this module
 */
#define GPS_HARDWARE_MODULE_ID "gps"

2、调用hw_get_module得到 GPS 模块的hw_module_t指针,保存在 module 变量中;我们来看下 GPS 模块的hw_module_t长得什么样。以hardware/qcom/gps/loc_api/libloc_api_50001/gps.c为例:

struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .module_api_version = 1,
    .hal_api_version = 0,
    .id = GPS_HARDWARE_MODULE_ID,
    .name = "loc_api GPS Module",
    .author = "Qualcomm USA, Inc.",
    .methods = &gps_module_methods, //自定义的函数指针,这里既是获取hw_device_t的入口了
};

3、接着调用 GPS 模块自定义的 hw_module_t的 methods 中的 open 函数,获取hw_device_t指针。上面的代码中我们看到,GPS 模块的hw_module_t的 methods 成员的值为gps_module_methods,其定义如下:

static struct hw_module_methods_t gps_module_methods = {
    .open = open_gps
};

4、我们来看 open_gps 函数做了什么:

static int open_gps(const struct hw_module_t* module, char const* name,
        struct hw_device_t** device)
{
    //为gps_device_t分配内存空间
    struct gps_device_t *dev = (struct gps_device_t *) malloc(sizeof(struct gps_device_t));

    if(dev == NULL)
        return -1;

    memset(dev, 0, sizeof(*dev));
    //为gps_device_t的common成员变量赋值
    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = 0;
    dev->common.module = (struct hw_module_t*)module;
    //通过下面的函数就能得到GPS模块所有interface
    dev->get_gps_interface = gps__get_gps_interface;
    //将gps_device_t指针强转为hw_device_t指针,赋给device
    *device = (struct hw_device_t*)dev;
    return 0;
}

我们看到 open_gps 创建了gps_device_t结构体,初始化完成后,将其转为hw_device_t。所以module->methods->open得到实际上是gps_device_t结构体指针。这里我们可以理解为gps_device_thw_device_t的子类,将子类对象转为父类对象返回,是很正常的使用方法。为什么可以这么理解,看一下gps_device_t长得什么样子就明白了。代码位置为hardware/libhardware/include/hardware/gps.h

struct gps_device_t {
    struct hw_device_t common;

    /**
     * Set the provided lights to the provided values.
     *
     * Returns: 0 on succes, error code on failure.
     */
    const GpsInterface* (*get_gps_interface)(struct gps_device_t* dev);
};

可以看到,第一个成员是名为 common 的hw_device_t类型的变量;所以可以理解为gps_device_t继承了hw_device_t

5、得到 GPS 的hw_device_t指针后将其强转回gps_devcie_t指针,然后调用 GPS device 定义的get_gps_interface接口,得到保存 GPS 接口的GpsInterface结构体指针。在 open_gps 中,get_gps_interface被赋值为gps__get_gps_interface函数指针,其主要工作就是返回 GPS 模块的GpsInterface结构体指针。GpsInterface 定义在hardware/libhardware/include/hardware/gps.h

/** Represents the standard GPS interface. */
typedef struct {
    /** set to sizeof(GpsInterface) */
    size_t          size;
    /**
     * Opens the interface and provides the callback routines
     * to the implementation of this interface.
     */
    int   (*init)( GpsCallbacks* callbacks );

    /** Starts navigating. */
    int   (*start)( void );

    /** Stops navigating. */
    int   (*stop)( void );

    /** Closes the interface. */
    void  (*cleanup)( void );

    /** Injects the current time. */
    int   (*inject_time)(GpsUtcTime time, int64_t timeReference,
                         int uncertainty);

    /** Injects current location from another location provider
     *  (typically cell ID).
     *  latitude and longitude are measured in degrees
     *  expected accuracy is measured in meters
     */
    int  (*inject_location)(double latitude, double longitude, float accuracy);

    /**
     * Specifies that the next call to start will not use the
     * information defined in the flags. GPS_DELETE_ALL is passed for
     * a cold start.
     */
    void  (*delete_aiding_data)(GpsAidingData flags);

    /**
     * min_interval represents the time between fixes in milliseconds.
     * preferred_accuracy represents the requested fix accuracy in meters.
     * preferred_time represents the requested time to first fix in milliseconds.
     *
     * 'mode' parameter should be one of GPS_POSITION_MODE_MS_BASE
     * or GPS_POSITION_MODE_STANDALONE.
     * It is allowed by the platform (and it is recommended) to fallback to
     * GPS_POSITION_MODE_MS_BASE if GPS_POSITION_MODE_MS_ASSISTED is passed in, and
     * GPS_POSITION_MODE_MS_BASED is supported.
     */
    int   (*set_position_mode)(GpsPositionMode mode, GpsPositionRecurrence recurrence,
            uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time);

    /** Get a pointer to extension information. */
    const void* (*get_extension)(const char* name);
} GpsInterface;

6、GpsInterface 定义了操作 GPS 模块的基本的标准接口,得到了 GpsInterface 就可以通过这些接口操作 GPS 了,终于可以硬件打交道了。某一个具体的 GPS 模块会将 GpsInterface 中的接口初始化为其平台相关的具体实现。比如hardware/qcom/gps/loc_api/libloc_api_50001/loc.cpp:

// Defines the GpsInterface in gps.h
static const GpsInterface sLocEngInterface =
{
   sizeof(GpsInterface),
   loc_init,
   loc_start,
   loc_stop,
   loc_cleanup,
   loc_inject_time,
   loc_inject_location,
   loc_delete_aiding_data,
   loc_set_position_mode,
   loc_get_extension
};

到这里,整个 GPS HAL 的加载过程就结束了,后面就可以通过 GpsInterface 操作 GPS 模块了。

HAL简单示例

接下来就手动实现一个 HAL 服务,用于提供一个加法函数,项目结构如下:
在这里插入图片描述
1、首先在 hardware/libhardware/include/hardware 目录下创建 hello.h 文件:

#include <sys/cdefs.h>
#include <sys/types.h>
#include <hardware/hardware.h>
//HAL模块名
#define HELLO_HARDWARE_MODULE_ID "hello"
//HAL版本号
#define HELLO_MODULE_API_VERSION_1_0 HARDWARE_MODULE_API_VERSION(0, 1)
//设备名
#define HARDWARE_HELLO "hello"

//自定义HAL模块结构体
typedef struct hello_module {
    struct hw_module_t common;
} hello_module_t;

//自定义HAL设备结构体
typedef struct hello_device {
    struct hw_device_t common;
    //加法函数
    int (*additionTest)(const struct hello_device *dev,int a,int b,int* total);
} hello_device_t;

//给外部调用提供打开设备的函数
static inline int _hello_open(const struct hw_module_t *module,
        hello_device_t **device) {
    return module->methods->open(module, HARDWARE_HELLO,
            (struct hw_device_t **) device);
}

我们可以看到在 hello.h 中自定义了两个结构体,分别继承 hw_module_t 和 hw_device_t 且在第一个变量,满足前面说的规则,另外定义在 device 结构体中的函数的第一个参数也必须是 device 结构体。

2、接着在 hardware/libhardware/modules/ 目录下创建一个 hello 的文件夹,里面包含一个 hello.c 和 Android.bp,我们来看 hello.c 文件:

#define LOG_TAG "HelloHal"

#include <malloc.h>
#include <stdint.h>
#include <string.h>

#include <log/log.h>

#include <hardware/hello.h>
#include <hardware/hardware.h>

//加法函数实现
static int additionTest(const struct hello_device *dev,int a,int b,int *total)
{
	if(!dev){
		return -1;
	}
    *total = a + b;
    return 0;
}

//关闭设备函数
static int hello_close(hw_device_t *dev)
{
    if (dev) {
        free(dev);
        return 0;
    } else {
        return -1;
    }
}
//打开设备函数
static int hello_open(const hw_module_t* module,const char __unused *id,
                            hw_device_t** device)
{
    if (device == NULL) {
        ALOGE("NULL device on open");
        return -1;
    }

    hello_device_t *dev = malloc(sizeof(hello_device_t));
    memset(dev, 0, sizeof(hello_device_t));

    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = HELLO_MODULE_API_VERSION_1_0;
    dev->common.module = (struct hw_module_t*) module;
    dev->common.close = hello_close;
    dev->additionTest = additionTest;

    *device = &(dev->common);
    return 0;
}

static struct hw_module_methods_t hello_module_methods = {
    .open = hello_open,
};
//导出符号HAL_MODULE_INFO_SYM,指向自定义模块
hello_module_t HAL_MODULE_INFO_SYM = {
    .common = {
        .tag                = HARDWARE_MODULE_TAG,
        .module_api_version = HELLO_MODULE_API_VERSION_1_0,
        .hal_api_version    = HARDWARE_HAL_API_VERSION,
        .id                 = HELLO_HARDWARE_MODULE_ID,
        .name               = "Demo Hello HAL",
        .author             = "dongjiao.tang@tcl.com",
        .methods            = &hello_module_methods,
    },
};

前面我们说过,要想自定义的 HAL 模块被外部使用需要导出符号 HAL_MODULE_INFO_SYM,导出的这个模块结构体最重要的其实就是 tag,id和 methods,tag 必须定义为HARDWARE_MODULE_TAG,id 就是在 hello.h 中定义的模块名,methods 指向 hello_module_methods 地址, hello_module_methods 指向结构体 hw_module_methods_t,它里面的open函数指针作用是打开模块下的设备,它指向 hello_open 函数。

hello_open 函数也很简单,就是创建一个 hello_device_t 结构体,这是我们自定义 HAL 模块下的设备结构体,然后给这个结构体赋值,我们定义的加法函数 additionTest 赋值给设备结构体的函数指针,外部就可以使用,因为这个 HAL 模块导出了符号HAL_MODULE_INFO_SYM,所以我们能通过模块 id 找到这个 HAL 模块,有了模块之后就可以调用它的 hello_open 函数打开设备,hello_open 接收三个参数,module 代表这个 HAL 模块,id 代表要打开的是这个模块下哪一个设备,最后一个参数是一个二级指针,其实是我们自定义设备结构体指针的地址,传个地址过来就可以给这个设备结构体赋值。

3、这样最简单的一个 HAL 模块就定义好了,它仅仅提供了一个加法函数,对于 Android.bp 如下(最终编译成一个共享 lib 库):

cc_library_shared {
    name: "hello.default",
    relative_install_path: "hw",
    proprietary: true,
    srcs: ["hello.c"],
    header_libs: ["libhardware_headers"],
    shared_libs: ["liblog"],
}

4、我们在根目录执行如下命令来编译一下:

mmm hardware/libhardware/modules/hello/

编译成功后生成了一个hello.default.so共享 lib 库:
在这里插入图片描述
5、我们将这个 so 库 push 进手机/system/lib64路径下,接着需要写一个简单测试程序来测一下。我们在 HAL 模块目录下再创建一个 test 目录,test 目录下创建一个 HelloTest.cpp 测试文件和一个 Android.bp 文件,HelloTest.cpp 源码如下:

#include <hardware/hardware.h>
#include <hardware/hello.h>
#include <log/log.h>
#define TAG "HelloHal"
int main(){
    hello_device_t* hello_device = NULL;

    const hello_module_t * module = NULL;

    int ret = hw_get_module(HELLO_HARDWARE_MODULE_ID,(const struct hw_module_t**)&module);

    if (!ret) {
        ret = _hello_open((const struct hw_module_t*)module,&hello_device);
    }

   
    if (ret < 0) {
          ALOGD("get hello-hal failed.......");
          return -1;
    }

    int total = 0;
    hello_device->additionTest(hello_device,3,5,&total);
    ALOGD("success start hello-hal....total = %d",total);
    return 0;
}

通过 hardware.c 提供的 hw_get_module 函数,传递模块名获取对应的 HAL 模块,接着调用我们自定义的 _hello_open 函数通过获取到的模块打开设备,获取到设备之后,我们调用定义在设备结构体中的加法函数。

6、Android.bp 定义如下:

cc_binary {
    name: "hello_hal",
    srcs: ["HelloTest.cpp"],
    shared_libs: [
        "liblog",
        "libhardware",
    ],
}

这个 test 会被编译成二进制可执行文件 hello_hal :
在这里插入图片描述
7、我们将这个可执行文件 push 进手机 system/bin 目录,然后执行:
在这里插入图片描述
8、可以看到 log 已经成功打印:
在这里插入图片描述
到此我们最简单的 HAL 就已经实现了,这个 HAL 并不设计底层驱动,实际开发中自己可以在设备结构体中定义函数访问底层硬件。

实际上 HAL 服务的访问场景,往往不是以上的二进制客户端文件,而是从 native 层通过 HIDL 访问 HAL,或者应用层 APK 通过 AIDL 访问 framework 层,framework 层通过 JNI 访问 native 层,打通应用层到 HAL 层的整个开发框架。参见如下专栏:https://blog.csdn.net/qq_34211365/category_9903135.html。

HAL发展历程

【官方描述】

在 Android 8.0 及更高版本中,较低级别的层已重新编写以采用更加模块化的新架构。搭载 Android 8.0 或更高版本的设备必须支持使用 HIDL 语言编写的 HAL,下面列出了一些例外情况。这些 HAL 可以是绑定式 HAL 也可以是直通式 HAL。Android 11 也支持使用 AIDL 编写的 HAL。所有 AIDL HAL 均为绑定式。

  1. 绑定式 HAL:以 HAL 接口定义语言 (HIDL) 或 Android 接口定义语言 (AIDL) 表示的 HAL。这些 HAL 取代了早期 Android 版本中使用的传统 HAL 和旧版 HAL。在绑定式 HAL 中,Android 框架和 HAL 之间通过 Binder 进程间通信 (IPC) 调用进行通信。所有在推出时即搭载了 Android 8.0 或后续版本的设备都必须只支持绑定式 HAL。
  2. 直通式 HAL:以 HIDL 封装的传统 HAL 或旧版 HAL。这些 HAL 封装了现有的 HAL,可在绑定模式和 Same-Process(直通)模式下使用。升级到 Android 8.0 的设备可以使用直通式 HAL。

参考释义:Android P HIDL服务绑定模式与直通模式的分析 (原创)

旧版HAL

旧版HAL指 Android 8.0 以前的HAL,此时的 HAL 主要实现的是代码上的规范化,但是从运行方式上来看,此时的HAL是一个一个的so库,与框架的通信通过dlopen进行,本质上 HAL 和 framework 运行在一个进程。这也就意味着,当 framework 中相关的 api 跟随安卓版本更新发生变化的时候,hal 必须要修改并重新编译,而这一过程需要由厂商完成,费时费力,使得众多老旧设备更新安卓版本十分困难。

因此安卓 8.0 实现了模块化的 Project Treble 架构,将属于厂商的东西和属于安卓框架的东西完全解耦,属于框架的内容位于/system分区,而属于厂商的内容被移动到了/vendor分区,并且HAL的实现也发生了翻天覆地的变化

Project Treble

从 Android 8.0 之后,Android 引入 Treble 机制,主要是为了解决目前Android 版本之间升级麻烦的问题,将 OEM 适配的部分 vendor 与 google 对 android 大框架升级的部分 system 部分做了分离,一旦适配了一个版本的 vendor 信息之后,之后的版本再进行升级时,直接升级 system 即可,这个就不会给 OEM 厂商升级带来太大的工作量,直接升级最新功能,可以解决目前市面上 Android 版本过来凌乱的问题。

在理想的 Project Treble 架构下,Framework 进程不需要像上面一样使用 dlopen 加载厂商的共享库,而是使用 binder 进行 ipc 通信,框架与 HAL 运行在不同的进程之中(当然这也意味着HAL要作为一个进程,一直在运行,等待来自框架的调用)。首先 Treble 机制在 Vendor 分区中有两种模式:

  • 一个编译成 so 库,供 System 分区的进程和应用直接加载调用,两者在同一个进程中,这种叫直通式 HAL(passthrough)模式;
  • 另外一种是直接编译成一个 daemon 可运行的服务,然后 System 分区的进程通过 HwBinder 的 IPC 通信方式来调用,两者在二个独立的进程中,这种称为绑定式 HAL(Binderized ) 模式。

大致框架图如下:
在这里插入图片描述

  • 对于 Android 8.0 之前的设备,对应图1;
  • 对于从之前的设备升级到 Android 8.0 的版本,对应图2、图3;
  • 对于直接基于 Android 8.0 开发的设备,对应图4。

上图也可以总结 HAL 的发展历程:

  1. Legacy Hal:Android 8.0 之前版本的 HAL 都是编译成 so,然后动态链接到各个 frameworks service 中去;
  2. Passthrough Hal:该模式是为了兼容旧版的 HAL,旧版 HAL 实现仍以动态库的方式提供,只是 binder service 链接了动态库 HAL 实现,即 binder service 通过 hw_get_module 链接了旧版的 hal 实现,而用户端通过与 binder service IPC 通信,间接实现了与旧版 HAL 的交互;
  3. Binderized HAL:HAL 与 用户调用在不同的进程中,HAL 被写成 binder service,而用户接口如 frameworks 作为 binder client 通过 IPC 机制实现跨进程接口调用。这个是 Google 的最终设计目标。

最后再补充下 Android 镜像的分区:
在这里插入图片描述

总结

本文参考文章:

  1. Android硬件抽象层HAL总结
  2. 浅谈HAL(1)-介绍-知乎
  3. Android HAL教程(含实例)
  4. BiliBili视频:Android HAL编程实战
  5. AndroidQ 打通应用层到HAL层—(HAL模块实现)

版权声明:本文为CSDN博主「Tr0e」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39190897/article/details/121781996

生成海报
点赞 0

Tr0e

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

RT-Thread Studio移植LAN8720A驱动

RTT网络协议栈驱动移植(霸天虎) 1、新建工程 ​ 工程路径不含中文路径名,工程名用纯英文不含任何符号。 2、用CubeMx配置板子外设 2.1、配置时钟 ​ 按照自己板子配置相应时钟。

ESP8266 无限重启踩坑

最近做了一个电子墨水屏万年历,在移植屏幕代码时遇到了esp8266无限软复位的问题,如果你的串口打印是以下图片所示,那么恭喜你问题解决了。 造成软复位的原因是因为,程序里有死循环&#xf

Renode应用:在RISC-V核上运行FreeRTOS

本篇记录通过Renode在RISC-V核上运行FreeRTOS demo的情况。本来不准备写这一篇,但是发现近期工作学习密度实在太大,上周工作的中间结果这周竟然完全想不起来了,不得不又花了一些时间从头摸