4、摄像头驱动初始化:probe函数实现、时钟与电源管理、GPIO与复位时序

好,咱们今天聊点硬核的。摄像头驱动能不能跑起来,全看probe函数这一哆嗦。我见过太多新手,在设备树里配得漂漂亮亮,结果probe函数写成一团浆糊,摄像头死活不干活。说白了,probe就是驱动和硬件的「第一次握手」,握不好,后面全白搭。

4.1 probe函数:驱动的入场券

probe函数什么时候被调用?当内核发现设备树里的节点和驱动里的compatible匹配上了,就会调用你的probe。嗯,这里要注意,匹配成功不代表万事大吉,你还要做一堆初始化工作。

我个人习惯,probe函数里按这个顺序来:

  1. 获取设备树资源——reg、中断、GPIO、时钟,一个都不能少
  2. 时钟使能——不给摄像头喂时钟,它就是个哑巴
  3. 电源上电——电压不对,芯片直接冒烟
  4. GPIO复位时序——这个最坑,时序不对摄像头不认你
  5. 初始化i2c通信——读写传感器寄存器
  6. 注册v4l2设备——让上层应用能看到你

来看一个精简的probe框架:

static int ov5640_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct ov5640_dev *sensor;
    int ret;

    // 1. 分配设备结构体
    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    // 2. 获取时钟
    sensor->xvclk = devm_clk_get(&client->dev, "xvclk");
    if (IS_ERR(sensor->xvclk))
        return PTR_ERR(sensor->xvclk);

    // 3. 获取复位GPIO
    sensor->reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_HIGH);
    if (IS_ERR(sensor->reset_gpio))
        return PTR_ERR(sensor->reset_gpio);

    // 4. 获取电源
    sensor->avdd = devm_regulator_get(&client->dev, "avdd");
    sensor->dvdd = devm_regulator_get(&client->dev, "dvdd");

    // 5. 上电复位时序
    ret = ov5640_power_on(sensor);
    if (ret)
        return ret;

    // 6. 读取芯片ID确认通信
    ret = ov5640_check_chip_id(sensor);
    if (ret)
        goto power_off;

    // 7. 注册v4l2子设备
    ret = v4l2_i2c_subdev_init(&sensor->sd, client, &ov5640_subdev_ops);
    if (ret)
        goto power_off;

    return 0;

power_off:
    ov5640_power_off(sensor);
    return ret;
}

你看,每一步都有对应的错误处理。我在项目中遇到过有人probe函数写了200行,一个错误处理都没有,结果摄像头一上电就挂,查了三天发现是电源没使能。所以,probe函数里每个资源申请都要检查返回值,这是底线。

4.2 时钟与电源管理:别让摄像头饿着

摄像头是个耗电大户,尤其是VGA以上的传感器。时钟和电源管理做不好,轻则图像花屏,重则芯片烧毁。

4.2.1 时钟配置

大部分摄像头需要外部提供主时钟(MCLK/XCLK),频率通常是24MHz、27MHz或48MHz。你想想看,时钟频率不对,内部PLL锁不住,像素时钟就乱套了。

我建议在设备树里这样配:

ov5640: camera@3c {
    compatible = "ovti,ov5640";
    reg = <0x3c>;
    clocks = <&clks IMX6QDL_CLK_CKO>;
    clock-names = "xvclk";
    assigned-clocks = <&clks IMX6QDL_CLK_CKO>;
    assigned-clock-rates = <24000000>;
    ...
};

驱动里使能时钟的代码:

static int ov5640_set_clock(struct ov5640_dev *sensor)
{
    int ret;

    ret = clk_set_rate(sensor->xvclk, 24000000);
    if (ret < 0)
        return ret;

    ret = clk_prepare_enable(sensor->xvclk);
    if (ret)
        return ret;

    // 等时钟稳定,至少1ms
    usleep_range(1000, 2000);

    return 0;
}

这里有个坑:clk_prepare_enable可能会睡眠,所以不能在原子上下文调用。probe函数里没问题,但如果你在中断里调,系统就崩了。我曾经在中断里调了clk_prepare_enable,结果内核直接panic,教训深刻。

4.2.2 电源管理

摄像头一般需要三路电源:

电源名称 典型电压 用途
AVDD 2.8V 模拟电路供电
DVDD 1.5V / 1.8V 数字核心供电
IOVDD 1.8V / 2.8V IO接口供电

上电顺序很重要。大部分传感器要求:先上AVDD,再上DVDD,最后上IOVDD。顺序反了,芯片内部闩锁效应可能直接烧掉。

看一个规范的上电函数:

static int ov5640_power_on(struct ov5640_dev *sensor)
{
    int ret;

    // 1. 先使能AVDD 2.8V
    ret = regulator_enable(sensor->avdd);
    if (ret)
        return ret;
    usleep_range(1000, 2000);  // 等1ms

    // 2. 再使能DVDD 1.5V
    ret = regulator_enable(sensor->dvdd);
    if (ret)
        goto disable_avdd;
    usleep_range(1000, 2000);

    // 3. 最后使能IOVDD 1.8V
    ret = regulator_enable(sensor->iovdd);
    if (ret)
        goto disable_dvdd;
    usleep_range(5000, 10000); // 等5ms让电源稳定

    // 4. 使能时钟
    ret = ov5640_set_clock(sensor);
    if (ret)
        goto disable_iovdd;

    return 0;

disable_iovdd:
    regulator_disable(sensor->iovdd);
disable_dvdd:
    regulator_disable(sensor->dvdd);
disable_avdd:
    regulator_disable(sensor->avdd);
    return ret;
}

你可能会问:为什么每个电源之间要等1ms?因为电源芯片的启动需要时间,电压爬坡不是瞬间完成的。我见过有人把延时去掉,结果摄像头10次里有3次初始化失败,加上延时后一次都没出过问题。

4.3 GPIO与复位时序:最容易被忽视的细节

好,终于到了最坑的部分——复位时序。说白了,摄像头芯片内部有个状态机,上电后需要给它一个特定的复位信号,它才能进入正常工作模式。

典型的复位时序是这样的:

  1. 上电完成后,保持复位引脚为低电平(复位状态)
  2. 等待至少T1时间(通常1ms~10ms)
  3. 将复位引脚拉高(释放复位)
  4. 等待至少T2时间(通常5ms~20ms),让内部PLL锁定
  5. 然后才能通过I2C访问传感器寄存器

来看代码实现:

static int ov5640_reset(struct ov5640_dev *sensor)
{
    // 1. 拉低复位引脚,进入复位状态
    gpiod_set_value_cansleep(sensor->reset_gpio, 0);
    usleep_range(1000, 2000);  // T1: 至少1ms

    // 2. 拉高复位引脚,释放复位
    gpiod_set_value_cansleep(sensor->reset_gpio, 1);
    usleep_range(5000, 10000); // T2: 等待5ms

    // 3. 再拉低一次,有些传感器需要脉冲复位
    gpiod_set_value_cansleep(sensor->reset_gpio, 0);
    usleep_range(1000, 2000);

    // 4. 最终拉高,完成复位
    gpiod_set_value_cansleep(sensor->reset_gpio, 1);
    usleep_range(20000, 30000); // 等20ms让内部初始化完成

    return 0;
}
⚠️ 重要警告: 不同传感器的复位时序完全不同!有的需要低电平复位,有的需要高电平复位,有的需要脉冲复位。一定要看数据手册里的时序图,别想当然。我曾经因为没仔细看时序图,把复位极性搞反了,摄像头一直处于复位状态,I2C怎么都读不到数据,折腾了两天。

另外,GPIO的操作要注意:

  • 使用devm_gpiod_get获取GPIO,这样驱动卸载时会自动释放
  • GPIO的极性在设备树里配,比如GPIO_ACTIVE_LOW,驱动里统一用1表示有效
  • 复位操作要放在电源使能之后,顺序不能乱
💡 个人经验: 调试复位时序时,我习惯用示波器同时抓复位引脚和I2C的SCL线。如果复位完成后立即去读I2C,但SCL上没有时钟信号,说明复位还没完成或者时钟没使能。这个技巧帮我快速定位过好几次问题。

4.4 完整的初始化流程

把上面这些串起来,一个完整的初始化流程应该是:

static int ov5640_initialize(struct ov5640_dev *sensor)
{
    int ret;

    // 1. 时钟使能
    ret = ov5640_set_clock(sensor);
    if (ret) {
        dev_err(&sensor->client->dev, "failed to set clock\n");
        return ret;
    }

    // 2. 电源上电(含顺序控制)
    ret = ov5640_power_on(sensor);
    if (ret) {
        dev_err(&sensor->client->dev, "failed to power on\n");
        goto disable_clk;
    }

    // 3. 复位时序
    ret = ov5640_reset(sensor);
    if (ret) {
        dev_err(&sensor->client->dev, "failed to reset\n");
        goto power_off;
    }

    // 4. 等待传感器稳定
    msleep(30);

    // 5. 读取芯片ID验证通信
    ret = ov5640_read_reg(sensor, REG_CHIP_ID_HIGH, &id_high);
    if (ret || id_high != 0x56) {
        dev_err(&sensor->client->dev, "chip ID mismatch\n");
        goto power_off;
    }

    // 6. 加载初始化寄存器序列
    ret = ov5640_load_init_seq(sensor);
    if (ret) {
        dev_err(&sensor->client->dev, "failed to load init seq\n");
        goto power_off;
    }

    return 0;

power_off:
    ov5640_power_off(sensor);
disable_clk:
    clk_disable_unprepare(sensor->xvclk);
    return ret;
}

嗯,到这里,probe函数的核心逻辑就完整了。你想想看,从设备树匹配到时钟使能,从电源上电到复位时序,每一步都环环相扣。哪个环节出问题,摄像头都起不来。

最后说一句:调试摄像头驱动,示波器和逻辑分析仪是必备的。别光靠printk,你看不到时钟波形,看不到复位时序,看不到I2C通信,很多问题根本定位不了。我刚开始做驱动时,全靠printk打日志,一个复位时序问题查了一周。后来买了台二手示波器,半天就搞定了。

下一章咱们聊I2C通信和传感器寄存器配置,那又是另一片天地了。