22、编写自定义传感器驱动(理论):Linux内核中的传感器驱动框架、I2C通信基础

好,到了这一章,我们终于要碰一碰驱动开发了。

说实话,很多做Android应用层的朋友,一听到「驱动」两个字就头大。我当年也一样,总觉得那是内核大佬才碰的东西。但后来被项目逼着去调一个霍尔传感器,硬着头皮啃了一遍,才发现——驱动开发没那么玄乎,它其实就是一套固定的套路

今天我们就来拆解这个套路。你不需要成为内核专家,但搞清楚传感器驱动是怎么挂上去的、I2C是怎么读写的,对你理解整个Android传感器体系会非常有帮助。

Linux内核中的传感器驱动框架

Linux内核的驱动模型,说白了就是「分层」和「抽象」。传感器驱动也不例外。

我习惯把传感器驱动分成三层来看:

  • 硬件层:芯片本身,比如霍尔传感器AH49E、SS49E这类。它们通过I2C或GPIO和主控通信。
  • 核心层:内核提供的通用框架,比如IIO(Industrial I/O)子系统。它帮你把数据上报、sysfs接口、中断处理这些脏活累活都干了。
  • 驱动层:你真正要写的部分。负责初始化芯片、配置寄存器、读取数据,然后塞给核心层。

嗯,这里要注意:不是所有传感器都用IIO。霍尔传感器因为数据简单,很多时候直接用input子系统或者自定义misc设备就搞定了。但IIO是趋势,尤其在新版内核里,它专门为传感器类设备设计,接口更规范。

核心观点:写驱动不是从零造轮子,而是把芯片的寄存器操作「翻译」成内核能理解的接口。

I2C通信基础

霍尔传感器最常见的通信接口就是I2C。为什么?因为它只需要两根线(SCL、SDA),引脚少,布线简单。

I2C的通信过程,我总结成三步:

  1. 起始信号:主机拉低SDA,再拉低SCL,告诉从机「我要开始通信了」。
  2. 地址+读写位:主机发送7位从机地址,最后一位是读写标志(0表示写,1表示读)。
  3. 数据收发:每发完一个字节,从机回复一个ACK(应答位)。然后重复,直到主机发送停止信号。

听起来简单,但实际调的时候坑不少。我曾经遇到一个传感器,地址明明是0x48,但读出来全是0xFF。查了半天,发现是地址左移了一位——I2C的7位地址在发送时要左移1位,最后补上读写位。这个细节,手册上写得清清楚楚,但新手很容易忽略。

避坑指南:我曾经因为I2C时钟频率设得太高(400kHz以上),导致传感器数据偶尔跳变。后来降到100kHz,问题消失。不是所有传感器都支持高速模式,先看数据手册再调频率。

驱动代码的核心骨架

下面我贴一段简化版的霍尔传感器驱动代码。它基于IIO框架,通过I2C读取寄存器值。你不需要逐行背下来,但要看懂结构。

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/iio/iio.h>

#define HALL_SENSOR_ADDR  0x48
#define HALL_DATA_REG     0x00

struct hall_chip {
    struct i2c_client *client;
    struct iio_dev *indio_dev;
};

static int hall_read_raw(struct iio_dev *indio_dev,
                         struct iio_chan_spec const *chan,
                         int *val, int *val2, long mask)
{
    struct hall_chip *chip = iio_priv(indio_dev);
    struct i2c_client *client = chip->client;
    int ret;

    // 读取霍尔传感器的数据寄存器
    ret = i2c_smbus_read_byte_data(client, HALL_DATA_REG);
    if (ret < 0) {
        dev_err(&client->dev, "read failed\n");
        return ret;
    }

    *val = ret;
    return IIO_VAL_INT;
}

static const struct iio_info hall_info = {
    .read_raw = hall_read_raw,
};

static int hall_probe(struct i2c_client *client)
{
    struct iio_dev *indio_dev;
    struct hall_chip *chip;

    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*chip));
    if (!indio_dev)
        return -ENOMEM;

    chip = iio_priv(indio_dev);
    chip->client = client;

    indio_dev->info = &hall_info;
    indio_dev->name = "hall_sensor";
    indio_dev->modes = INDIO_DIRECT_MODE;

    // 注册IIO设备
    return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct of_device_id hall_of_match[] = {
    { .compatible = "hall,ah49e" },
    { }
};
MODULE_DEVICE_TABLE(of, hall_of_match);

static struct i2c_driver hall_driver = {
    .driver = {
        .name = "hall_sensor",
        .of_match_table = hall_of_match,
    },
    .probe = hall_probe,
    .id_table = hall_id_table,
};
module_i2c_driver(hall_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

你看,核心逻辑其实就两个:probe函数里注册设备,read_raw函数里读寄存器。其他都是模板代码。

驱动框架流程图

为了让你更直观地理解整个流程,我画了一张图。它展示了从设备树匹配到数据上报的完整链路。

霍尔传感器驱动框架流程图 设备树(DTS)匹配 probe函数被调用 初始化I2C & 注册IIO read_raw读取寄存器 用户空间获取数据 设备树中声明 compatible 内核自动匹配驱动 分配内存、初始化结构体 设置I2C速率、注册IIO设备 通过i2c_smbus读取 sysfs或ioctl获取数据

设备树与驱动匹配

驱动写好了,怎么让内核知道「这个传感器对应这个驱动」?靠设备树。

在设备树里,你只需要这样写:

&i2c1 {
    hall_sensor@48 {
        compatible = "hall,ah49e";
        reg = <0x48>;
    };
};

然后驱动里声明 of_match_table,内核就会自动把两者绑在一起。我刚开始学的时候,总觉得这一步很神奇,其实原理很简单——内核在启动时扫描I2C总线,发现地址0x48上有设备,就去匹配compatible字符串。匹配上了,就调用你的probe函数。

注意:设备树里的reg地址必须是7位I2C地址,不要左移。而驱动里i2c_smbus函数用的地址是8位(左移后的)。这个区别坑过不少人,包括我。

I2C通信的几种常用API

内核里操作I2C,最常用的就这几个函数。我列个表,方便你查阅:

函数 作用 适用场景
i2c_smbus_read_byte_data 读一个寄存器的值 单字节寄存器读取
i2c_smbus_write_byte_data 写一个寄存器的值 配置传感器模式
i2c_smbus_read_i2c_block_data 连续读取多个字节 读取多轴数据(如加速度计)
i2c_transfer 自定义传输(支持重复起始) 复杂协议、非标准I2C设备

我个人习惯,能用smbus就用smbus。它封装好了错误处理和重试逻辑,代码更简洁。只有遇到特殊时序要求时,才用底层的i2c_transfer。

中断还是轮询?

霍尔传感器通常不需要中断,因为它数据变化不快。但如果你做的是高精度位置检测,比如翻盖手机的角度判断,那可以考虑用中断。

中断驱动的流程是:传感器检测到磁场变化 → 拉高INT引脚 → 内核触发中断处理函数 → 读取数据 → 上报。这样CPU不用一直轮询,省电。

不过,中断处理函数里不能做耗时操作。我曾经犯过一个错,在中断里直接调了i2c_smbus_read,结果因为I2C总线被占用,导致系统卡死。正确的做法是用工作队列(workqueue)把读取操作推迟到进程上下文执行。

小技巧:如果你不确定用中断还是轮询,先轮询。等调通了,再优化成中断。不要一开始就上复杂方案。

总结一下

这一章我们聊了Linux内核的传感器驱动框架,核心就是IIO子系统 + I2C通信。你只要记住:

  • 驱动 = 设备树匹配 + probe初始化 + read_raw读数据
  • I2C = 地址 + 寄存器 + 读写操作
  • 先轮询,后中断,别在中断里做I2C读写

下一章我们会实际动手,在开发板上跑一个霍尔传感器的驱动。到时候你会看到,今天讲的这些理论,全都会变成实实在在的代码和数据。


公众号:蓝海资料掘金营,微信deep3321