第4章:HAL模块结构——模块的骨架与血肉

好,咱们今天来聊聊HAL模块的内部结构。说白了,就是搞清楚一个HAL模块到底长什么样,由哪些零件拼起来的。

我记得刚入行那会儿,第一次打开一个HAL模块的源码,看到一堆结构体定义,说实话有点懵。后来做多了才发现,其实核心就两个结构体:hw_module_thw_device_t。嗯,搞懂这俩,你就抓住了HAL模块的命门。

4.1 HAL模块的组成

一个完整的HAL模块,通常包含三大部分:

  • 模块描述部分:用 hw_module_t 结构体来描述模块的基本信息,比如模块ID、版本号、作者等。
  • 设备操作部分:用 hw_device_t 结构体来描述具体的硬件设备,以及你能够对它做什么操作(打开、关闭、读写等)。
  • 实现代码部分:就是那些真正干活的函数,比如初始化硬件、发送数据、读取状态等。

你想想看,这就像一辆车的说明书:模块描述是封面和目录,设备操作是方向盘和油门刹车,实现代码就是发动机和变速箱。缺一不可。

4.2 hw_module_t 结构体——模块的身份证

hw_module_t 是每个HAL模块必须有的第一个结构体。它定义在 hardware.h 里,我直接贴出来给你看:

typedef struct hw_module_t {
    uint32_t tag;              // 必须设为 HARDWARE_MODULE_TAG
    uint16_t module_api_version; // 模块API版本
    uint16_t hal_api_version;    // HAL层API版本
    const char *id;             // 模块ID,比如 "audio"、"gps"
    const char *name;           // 模块名称
    const char *author;         // 作者
    struct hw_module_methods_t *methods; // 模块方法表
    void *dso;                  // 动态库句柄
    uint32_t reserved[32-7];    // 保留字段
} hw_module_t;

这里有几个关键点,我特别想强调一下:

  • tag 字段:必须设为 HARDWARE_MODULE_TAG,这是一个魔数,用来校验结构体是否被正确初始化。我曾经见过有人忘了设这个,结果模块加载时直接崩溃,查了半天才发现是这里的问题。
  • id 字段:这是模块的唯一标识符。比如音频模块的ID是 "audio",GPS模块是 "gps"。系统通过这个ID来找到对应的模块。
  • methods 字段:指向一个 hw_module_methods_t 结构体,里面目前只有一个函数指针——open。没错,就是用来打开设备的那个 open

重点hw_module_t 是模块的入口点。系统加载你的动态库后,第一件事就是找这个结构体。所以它必须被导出,而且名字要固定为 HMI(具体看你的实现)。

4.3 hw_device_t 结构体——设备的操作手柄

有了模块,接下来就是设备了。hw_device_t 代表一个具体的硬件设备实例。它的定义也很简洁:

typedef struct hw_device_t {
    uint32_t tag;              // 必须设为 HARDWARE_DEVICE_TAG
    uint32_t version;          // 设备版本
    struct hw_module_t *module; // 指向所属模块
    uint32_t reserved[12];     // 保留字段
    int (*close)(struct hw_device_t *device); // 关闭设备
} hw_device_t;

你可能会问:就这么点东西?怎么操作硬件?

嗯,这里有个设计上的巧妙之处。hw_device_t 本身只定义了最基本的框架——标签、版本、所属模块、关闭函数。真正的操作函数,是通过继承这个结构体来扩展的。

举个例子,假设你要实现一个LED控制模块,你可以这样定义你的设备结构体:

struct led_device_t {
    struct hw_device_t common;  // 必须放在第一个字段

    // 以下是LED特有的操作
    int (*set_on)(struct led_device_t *dev, int led_id);
    int (*set_off)(struct led_device_t *dev, int led_id);
    int (*get_status)(struct led_device_t *dev, int led_id);
};

看到了吗?led_device_t 的第一个字段就是 hw_device_t common。这样,系统可以通过 hw_device_t 的指针来统一管理所有设备,而你自己的代码则可以通过强制类型转换来访问LED特有的函数。

个人经验:我在项目中遇到过一种情况,有人把 common 字段放到了第二个位置,结果强制类型转换后数据全乱了。记住,common 必须是结构体的第一个成员,这是HAL框架的硬性要求。

4.4 模块与设备的关系——一对多的艺术

一个模块可以管理多个设备。比如一个音频模块,可能同时管理扬声器、耳机、麦克风三个设备。每个设备都有自己的 hw_device_t 实例。

关系图大概是这样的:

hw_module_t (音频模块)
  ├── hw_device_t (扬声器设备)
  │     ├── open()
  │     ├── close()
  │     └── 音频特有操作...
  ├── hw_device_t (耳机设备)
  │     ├── open()
  │     ├── close()
  │     └── 音频特有操作...
  └── hw_device_t (麦克风设备)
        ├── open()
        ├── close()
        └── 音频特有操作...

系统通过 hw_module_methods_t 中的 open 函数来创建设备实例。每次调用 open,都会返回一个新的 hw_device_t 指针。

4.5 避坑指南——我踩过的那些坑

做HAL模块开发,有几个地方特别容易出问题。我把自己踩过的坑分享给你:

  • 结构体对齐问题:不同编译器对结构体的对齐方式可能不同。我曾经在GCC和ARMCC之间切换时,发现 hw_device_t 的大小不一样,导致模块加载失败。解决办法是显式指定对齐方式,或者用 __attribute__((packed))
  • 函数指针类型不匹配open 函数的签名是固定的,不能随意改。我见过有人把参数类型写错了,结果调用时栈崩溃。嗯,这种错误很难查,因为编译时不会报错。
  • 忘记初始化 tag:这个前面提过,但还是要再说一遍。hw_module_thw_device_ttag 字段必须正确设置,否则系统会拒绝加载。

警告:千万不要在 hw_module_thw_device_t 中随意添加字段。这些结构体的大小和布局是HAL框架规定的,你只能通过继承来扩展。擅自修改会导致兼容性问题。

4.6 一个完整的模块示例

最后,我给你看一个最简单的HAL模块实现。假设我们要做一个虚拟的LED模块:

// led_module.c
#include <hardware/hardware.h>

// 设备结构体
struct led_device_t {
    struct hw_device_t common;
    int (*set_on)(struct led_device_t *dev, int id);
    int (*set_off)(struct led_device_t *dev, int id);
};

// 打开设备
static int led_device_open(const struct hw_module_t* module, 
                           const char* name,
                           struct hw_device_t** device) {
    struct led_device_t *dev = malloc(sizeof(struct led_device_t));
    memset(dev, 0, sizeof(*dev));

    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.module = module;
    dev->common.close = led_device_close;
    dev->set_on = led_set_on;
    dev->set_off = led_set_off;

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

// 模块方法表
static struct hw_module_methods_t led_module_methods = {
    .open = led_device_open,
};

// 模块实例
struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .id = "led",
    .name = "LED Control Module",
    .author = "Your Name",
    .methods = &led_module_methods,
};

这个例子虽然简单,但包含了HAL模块的所有核心要素。你把它跑通了,后面再复杂的模块也就是在这个基础上加加减减。

好了,这一章就到这里。记住,hw_module_t 是模块的身份证,hw_device_t 是设备的操作手柄。搞懂这俩,HAL模块的结构你就掌握了八成。下一章我们聊聊模块的加载流程,看看系统是怎么找到并初始化你的模块的。