2. 驱动移植基础:Linux内核驱动与HAL层的关系、设备节点(/dev)与HAL模块的绑定机制
在Android系统中,硬件访问从底层到上层依次经过:Linux内核驱动 → 设备节点 → HAL层 → Framework层。理解内核驱动与HAL层的关系,以及设备节点与HAL模块的绑定机制,是进行驱动移植的首要前提。本章将拆解这一链路,明确各层职责与交互方式。
2.1 Linux内核驱动与HAL层的分层职责
Android系统并非直接在内核空间实现所有硬件逻辑,而是通过HAL层将内核驱动细节与上层框架解耦。下表对比了内核驱动与HAL层的核心差异:
| 对比维度 | Linux内核驱动 | Android HAL层 |
|---|---|---|
| 运行空间 | 内核空间(Kernel Space) | 用户空间(User Space) |
| 主要语言 | C语言 | C / C++ |
| 核心职责 | 硬件寄存器操作、中断处理、DMA、内存映射 | 封装内核接口,提供稳定上层API |
| 对外接口 | 设备节点(/dev/xxx)、sysfs、procfs | HAL Module 结构体(hw_module_t / hw_device_t) |
| 移植关注点 | 设备树配置、内核编译、驱动 probe 流程 | 设备节点路径、HAL stub 实现、权限配置 |
关键原则:HAL层不直接操作硬件寄存器,而是通过读写设备节点(/dev/xxx)或调用ioctl与内核驱动通信。内核驱动负责硬件底层控制,HAL层负责将硬件能力抽象为Android框架可调用的标准接口。
2.2 设备节点(/dev)的创建与访问
设备节点是用户空间访问内核驱动的“文件接口”。内核驱动在初始化时,通过以下步骤创建设备节点:
- 注册设备号:使用
alloc_chrdev_region()或register_chrdev_region()分配主设备号和次设备号。 - 初始化字符设备:使用
cdev_init()和cdev_add()将设备添加到内核。 - 创建设备类:使用
class_create()创建设备类。 - 创建设备节点:使用
device_create()在/dev目录下生成节点文件。
以下是一个典型的内核驱动创建设备节点的代码片段:
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int __init my_driver_init(void) {
// 1. 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 2. 初始化字符设备
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev_num, 1);
// 3. 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
// 4. 创建设备节点 /dev/my_device
device_create(my_class, NULL, dev_num, NULL, "my_device");
printk(KERN_INFO "my_driver: device node created\n");
return 0;
}
static void __exit my_driver_exit(void) {
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
移植提示:在驱动移植时,务必确认设备节点路径与HAL层代码中定义的路径一致。常见错误是内核驱动创建了
/dev/my_device_v1,而HAL层却尝试打开 /dev/my_device,导致 open() 失败返回 -1。
2.3 HAL模块与设备节点的绑定机制
HAL模块通过标准结构体 hw_module_t 和 hw_device_t 与内核设备节点绑定。绑定流程分为以下步骤:
- 模块加载:Android Framework 通过
hw_get_module()加载指定ID的HAL模块(如GRALLOC_HARDWARE_MODULE_ID)。 - 设备打开:HAL模块实现
hw_module_methods_t.open()回调,在该回调中执行open("/dev/xxx", O_RDWR)。 - 设备句柄封装:将
open()返回的文件描述符(fd)存入hw_device_t的自定义结构体中,供上层调用。
下图展示了HAL模块与设备节点的绑定关系:
2.4 绑定过程中的关键数据结构
HAL层与设备节点绑定时,核心数据结构定义在 hardware/libhardware/include/hardware/hardware.h 中。以下是最关键的两个结构体:
// 硬件模块结构体(每个HAL库只有一个)
typedef struct hw_module_t {
uint32_t tag; // 必须为 HARDWARE_MODULE_TAG
uint16_t module_api_version;
uint16_t hal_api_version;
const char *id; // 模块ID,如 "gralloc"
const char *name;
const char *author;
struct hw_module_methods_t* methods; // 指向方法表
void* dso;
uint32_t reserved[32];
} hw_module_t;
// 硬件设备结构体(每个设备实例一个)
typedef struct hw_device_t {
uint32_t tag; // 必须为 HARDWARE_DEVICE_TAG
uint32_t version;
struct hw_module_t* module; // 指向所属模块
int (*close)(struct hw_device_t* device);
int fd; // 设备节点文件描述符(自定义扩展)
} hw_device_t;
常见陷阱:许多移植新手在自定义
hw_device_t 的扩展结构体时,忘记将 fd 初始化为 -1,导致在 HAL 模块卸载时执行 close(fd) 关闭了一个无效的文件描述符,引发进程崩溃。建议在 open() 失败时显式将 fd 置为 -1。
2.5 绑定流程的完整时序
从Framework调用到内核驱动响应的完整时序如下:
- hw_get_module() 根据模块ID查找并加载
/vendor/lib/hw/xxx.default.so或/system/lib/hw/xxx.$(ro.board.platform).so。 - 加载成功后,获取
hw_module_t指针,并调用module->methods->open()。 - open() 回调 内部执行
open("/dev/xxx", O_RDWR),返回文件描述符。 - 将文件描述符存入自定义设备结构体,并通过
hw_device_t**返回给Framework。 - Framework 通过
hw_device_t中的函数指针(如device->read())间接调用ioctl()或read()/write()与内核驱动交互。
以下时序图展示了这一完整过程:
2.6 移植检查清单
在进行驱动移植时,请逐项核对以下清单,确保内核驱动与HAL层的绑定正确:
| 检查项 | 检查内容 | 常见问题 |
|---|---|---|
| 设备节点路径 | HAL层 open() 路径与内核 device_create() 名称一致 |
路径写死为 /dev/xxx,但内核创建了 /dev/xxx_v2 |
| 设备节点权限 | /dev/xxx 的权限为 0660 或 0666,所属组为 system 或 root |
权限为 0600 导致 HAL 进程无权限打开 |
| 设备节点存在性 | 内核驱动加载后,ls -l /dev/xxx 能正常显示 |
驱动 probe 失败,设备节点未创建 |
| HAL模块ID | hw_module_t.id 与 hw_get_module() 传入的ID一致 |
ID 大小写不匹配或拼写错误 |
| 文件描述符管理 | open() 失败时 fd 置为 -1,close() 前检查 fd 有效性 |
重复 close 或 close 无效 fd 导致 crash |
| ioctl 命令码 | HAL层与内核驱动的 ioctl 命令码(cmd)定义一致 |
命令码使用 _IOR/_IOW 宏时方向或大小不匹配 |
调试技巧:在HAL层的
open() 回调中加入 ALOGD 日志,打印 open() 的返回值以及 errno。如果 open() 返回 -1,通过 strerror(errno) 可以快速定位是权限问题(EACCES)、设备不存在(ENOENT)还是驱动未响应(EIO)。