4. 点亮一颗Sensor:从零开始编写sensor驱动probe函数

终于到了最激动人心的环节——让Sensor真正跑起来。前面几章我们把驱动框架、I2C通信、电源时序都铺垫好了,现在要做的就是把它们串起来,写一个能用的probe函数。

说实话,我第一次写Sensor驱动时,以为probe就是简单注册一下设备。结果踩了不少坑。后来才明白,probe函数是Sensor的“出生证明”,它决定了硬件能不能被内核正确识别和管理。

4.1 probe函数的核心职责

probe函数说白了就是做三件事:检测硬件是否存在、初始化通信接口、注册V4L2子设备。我习惯把它分成四个阶段:

阶段 主要工作 常见问题
设备树解析 读取reg、clocks、power-gpios等属性 属性名拼写错误、缺少必要属性
硬件检测 读取CHIP_ID寄存器,验证Sensor型号 I2C地址错误、电源未稳定
资源初始化 申请GPIO、时钟、电源、中断 资源冲突、顺序依赖
V4L2注册 初始化子设备、注册到媒体框架 ops未正确赋值、缺少必要回调

4.2 设备树配置——硬件信息的“身份证”

设备树是驱动和硬件之间的桥梁。我见过不少新手直接在代码里硬编码GPIO号,结果换一个平台就得改代码。正确的做法是把所有硬件配置都放到设备树里。

下面是一个典型的Sensor设备树节点:

// imx258_sensor.dtsi
&i2c_2 {
    status = "okay";
    imx258: camera_sensor@1a {
        compatible = "sony,imx258";
        reg = <0x1a>;
        
        clocks = <&camcc CAM_CC_MCLK0_CLK>;
        clock-names = "xvclk";
        clock-frequency = <24000000>;
        
        avdd-supply = <&pm8150_l17>;
        dvdd-supply = <&pm8150_l16>;
        iovdd-supply = <&pm8150_l6>;
        
        reset-gpios = <&tlmm 94 GPIO_ACTIVE_LOW>;
        powerdown-gpios = <&tlmm 105 GPIO_ACTIVE_HIGH>;
        
        port {
            imx258_ep: endpoint {
                remote-endpoint = <&csiphy0_ep>;
                data-lanes = <1 2 3 4>;
                link-frequencies = /bits/ 64 <480000000>;
            };
        };
    };
};
我的习惯:设备树里的reg地址一定要和Sensor数据手册一致。我曾经因为I2C地址写成了0x34(7位地址),结果驱动死活检测不到设备。后来才发现Sensor用的是10位地址格式,需要左移一位。

4.3 驱动probe函数的完整实现

好了,设备树配好了,接下来写probe函数。我会一步步拆解,每个环节都配上实际代码。

4.3.1 解析设备树属性

第一步是从设备树节点里把配置读出来。这里我习惯用of_property_read_*系列函数:

static int imx258_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct device *dev = &client->dev;
    struct imx258 *sensor;
    int ret;

    sensor = devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    // 解析时钟
    sensor->xvclk = devm_clk_get(dev, "xvclk");
    if (IS_ERR(sensor->xvclk)) {
        dev_err(dev, "failed to get xvclk\n");
        return PTR_ERR(sensor->xvclk);
    }

    // 解析电源
    sensor->avdd = devm_regulator_get(dev, "avdd");
    sensor->dvdd = devm_regulator_get(dev, "dvdd");
    sensor->iovdd = devm_regulator_get(dev, "iovdd");

    // 解析GPIO
    sensor->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
    sensor->powerdown_gpio = devm_gpiod_get(dev, "powerdown", GPIOD_OUT_LOW);
    
    // 解析CSI配置
    ret = of_property_read_u32(dev->of_node, "clock-frequency",
                               &sensor->mclk_freq);
    if (ret)
        sensor->mclk_freq = 24000000; // 默认24MHz
}
注意:解析GPIO时,GPIOD_OUT_LOW表示初始状态为低电平。对于复位引脚,通常低电平有效,所以初始状态要保持低电平。我曾经搞反了极性,结果Sensor一直处于复位状态,读ID总是失败。

4.3.2 硬件检测——读CHIP_ID

这是最关键的一步。读不到正确的CHIP_ID,后面所有工作都是白费。我一般会写一个独立的检测函数:

static int imx258_detect(struct imx258 *sensor)
{
    struct i2c_client *client = sensor->i2c_client;
    u16 chip_id;
    int ret;

    // 先上电
    ret = imx258_power_on(sensor);
    if (ret) {
        dev_err(&client->dev, "power on failed\n");
        return ret;
    }

    // 等待电源稳定,至少10ms
    msleep(20);

    // 读取CHIP_ID寄存器(0x000A)
    ret = imx258_read_reg(sensor, 0x000A, &chip_id);
    if (ret) {
        dev_err(&client->dev, "failed to read chip id\n");
        goto power_off;
    }

    dev_info(&client->dev, "chip id: 0x%04x\n", chip_id);

    if (chip_id != IMX258_CHIP_ID) {
        dev_err(&client->dev, "chip id mismatch: 0x%04x != 0x%04x\n",
                chip_id, IMX258_CHIP_ID);
        ret = -ENODEV;
        goto power_off;
    }

    return 0;

power_off:
    imx258_power_off(sensor);
    return ret;
}

你可能会问:为什么要先上电再读ID?嗯,这个问题我当年也犯过。Sensor不上电,I2C总线上的设备根本不会响应,读回来的数据全是0xFF。所以电源时序一定要对。

4.3.3 注册V4L2子设备

硬件检测通过后,就可以把Sensor注册到V4L2框架了。这里我习惯用v4l2_i2c_subdev_init:

    // 初始化V4L2子设备
    v4l2_i2c_subdev_init(&sensor->sd, client, &imx258_subdev_ops);
    sensor->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
    sensor->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;

    // 设置必要的回调
    sensor->sd.internal_ops = &imx258_internal_ops;
    sensor->sd.dev->platform_data = sensor;

    // 注册异步子设备
    ret = v4l2_async_register_subdev(&sensor->sd);
    if (ret) {
        dev_err(dev, "failed to register subdev\n");
        goto power_off;
    }

关键点:注册异步子设备后,驱动会等待CSI PHY和CSID等硬件就绪,然后才完成整个pipeline的绑定。这个机制保证了各个模块的初始化顺序。

4.4 完整的probe流程

把上面几个步骤串起来,就是完整的probe函数。我画了一张流程图,方便你理解整体逻辑:

Sensor驱动probe函数执行流程 probe入口 解析设备树属性 时钟/电源/GPIO/CSI配置 Sensor上电 + 时钟使能 读取CHIP_ID寄存器 ID匹配? 返回 -ENODEV 初始化V4L2子设备 注册异步子设备 写入初始化寄存器序列 probe成功返回0 关键检查点 1. 设备树属性完整性 2. 电源时序是否正确 3. CHIP_ID是否匹配

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

写了这么多年驱动,probe函数里的坑我基本都踩过一遍。分享几个典型的:

  • 电源时序问题:我曾经在某个项目里,Sensor的AVDD和DVDD需要先上电再拉高IOVDD。结果我同时使能了三个电源,导致Sensor内部LDO锁死,读ID一直失败。后来加了20ms延时才解决。
  • GPIO极性搞反:复位引脚低电平有效,但设备树里忘了加GPIO_ACTIVE_LOW标志。驱动拉高复位引脚,Sensor一直处于复位状态。这个问题排查了我整整一天。
  • I2C地址错误:Sensor数据手册里写的是8位地址,但Linux内核用的是7位地址。如果不做转换,驱动会去错误的地址找设备。
  • 时钟频率不匹配:Sensor的MCLK要求24MHz,但平台默认输出19.2MHz。没有正确配置时钟树,Sensor内部PLL无法锁定,输出图像全是花屏。
调试小技巧:如果probe失败,先检查/sys/kernel/debug/gpio看GPIO状态对不对。然后用i2cget手动读一下Sensor的CHIP_ID寄存器,确认I2C通信是否正常。这两个方法能快速定位80%的问题。

4.6 小结

probe函数是Sensor驱动的入口,也是整个驱动稳定性的基石。写的时候记住三个原则:先检测再注册、先上电再通信、先解析再使用。把设备树配置好,把电源时序理清楚,把CHIP_ID读对,剩下的工作就水到渠成了。