4、摄像头设备树配置:在设备树中描述多路摄像头节点、GPIO与时钟资源分配、pinctrl引脚复用配置

设备树配置,说白了就是给内核画一张「硬件地图」。

多摄像头系统里,这张地图尤其重要。你得告诉内核:

  • 有几路摄像头?
  • 它们挂在哪个I2C总线上?
  • 用哪根GPIO做复位?
  • 时钟从哪里来?
  • 引脚怎么复用?

一个地方写错,摄像头就起不来。我刚开始做多摄像头驱动时,就吃过这个亏——设备树里少配了一个时钟,结果三路摄像头只有两路能出图,查了整整两天。

4.1 多路摄像头节点的基本结构

先看一个典型的多路摄像头设备树片段。假设我们有四路摄像头,都挂在I2C总线上:

/ {
    /* 四路摄像头的公共资源 */
    camera_clk: camera-clock {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <24000000>;  /* 24MHz主时钟 */
    };

    /* I2C总线节点 */
    &i2c3 {
        status = "okay";
        clock-frequency = <400000>;    /* 400KHz快速模式 */
        pinctrl-names = "default";
        pinctrl-0 = <&i2c3_pins>;

        /* 摄像头0:主摄像头 */
        camera0: camera@30 {
            compatible = "sony,imx290";
            reg = <0x30>;
            clocks = <&camera_clk>;
            clock-names = "xvclk";
            reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
            power-gpios = <&gpio1 16 GPIO_ACTIVE_HIGH>;
            pinctrl-names = "default";
            pinctrl-0 = <&camera0_pins>;
            status = "okay";

            port {
                camera0_ep: endpoint {
                    remote-endpoint = <&csi0_ep>;
                    data-lanes = <1 2 3 4>;
                    clock-noncontinuous;
                };
            };
        };

        /* 摄像头1:副摄像头 */
        camera1: camera@32 {
            compatible = "sony,imx290";
            reg = <0x32>;
            clocks = <&camera_clk>;
            clock-names = "xvclk";
            reset-gpios = <&gpio1 17 GPIO_ACTIVE_LOW>;
            power-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
            pinctrl-names = "default";
            pinctrl-0 = <&camera1_pins>;
            status = "okay";

            port {
                camera1_ep: endpoint {
                    remote-endpoint = <&csi1_ep>;
                    data-lanes = <1 2 3 4>;
                    clock-noncontinuous;
                };
            };
        };
    };
};

嗯,这里要注意几个关键点:

  • reg 地址不能冲突:每路摄像头的I2C地址必须唯一。我见过有人把四路摄像头都配成0x30,结果只有最后一路上电的能工作。
  • GPIO 资源要独立:每路摄像头的复位、电源引脚必须分开。共用GPIO会导致摄像头之间互相干扰。
  • 时钟可以共享:如果多路摄像头使用相同的时钟频率,可以共用一个固定时钟节点。但要注意驱动能力——我曾经遇到过24MHz时钟同时驱动四路摄像头时波形失真,后来加了一级时钟缓冲器才解决。

4.2 GPIO资源分配策略

多摄像头系统里,GPIO是最容易出问题的资源。为什么?

因为GPIO数量有限,而且每个摄像头至少需要2个GPIO(复位+电源),四路就是8个。再加上一些传感器还有额外的GPIO(比如帧同步、中断),很容易就把GPIO用光了。

我个人习惯的做法是:

  1. 优先使用专用GPIO控制器:比如Rockchip平台的GPIO0-GPIO4,每个控制器有32个引脚。尽量把摄像头相关的GPIO集中在一个控制器上,方便管理。
  2. 避免与其它外设冲突:检查原理图,确保摄像头用的GPIO没有被UART、SPI、I2C等外设占用。我在一个项目里就踩过这个坑——摄像头复位引脚和调试串口的TX复用,每次插上调试线摄像头就复位。
  3. 考虑上电时序:有些摄像头对电源和复位的时序有严格要求。比如先上电,等10ms后再释放复位。设备树里虽然不能直接描述时序,但可以通过GPIO的默认状态来辅助:
/* 确保复位引脚默认拉低 */
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
/* 电源引脚默认拉高 */
power-gpios = <&gpio1 16 GPIO_ACTIVE_HIGH>;

注意:设备树中的GPIO默认状态只保证在驱动加载前的初始状态。如果你需要精确的时序控制,必须在驱动代码中通过gpiod_set_value_cansleep()等API来实现。

4.3 时钟资源分配

时钟是多摄像头同步的核心。设备树里主要配两种时钟:

时钟类型 作用 典型频率 注意事项
主时钟(xvclk) 摄像头传感器的工作时钟 24MHz / 27MHz 频率精度要求高,通常用晶振或PLL生成
MIPI时钟 CSI接口的传输时钟 根据分辨率动态调整 由SoC的CSI控制器自动配置

对于多摄像头同步,我建议使用同一个时钟源来驱动所有摄像头。这样能保证每路摄像头的像素时钟同频同相,帧同步信号才能对齐。

设备树里可以这样配:

/* 使用同一个PLL输出 */
camera_pll: camera-pll {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;
    clock-output-names = "camera_pll";
};

/* 每路摄像头都引用同一个时钟 */
camera0: camera@30 {
    clocks = <&camera_pll>;
    ...
};
camera1: camera@32 {
    clocks = <&camera_pll>;
    ...
};

小技巧:如果硬件设计上每路摄像头有独立的晶振,可以在设备树里为每路摄像头单独配一个固定时钟。但这样帧同步会困难一些,因为晶振之间总有微小频率偏差。我一般会在驱动里做软件同步补偿。

4.4 pinctrl引脚复用配置

pinctrl配置,说白了就是告诉SoC:这些引脚现在归摄像头用了,别让其他外设乱动。

多摄像头系统里,引脚复用配置要特别注意:

  • MIPI数据线:通常是专用的,不需要pinctrl配置
  • I2C总线:所有摄像头共享,配置一次即可
  • GPIO引脚:每路摄像头独立,需要单独配置

来看一个实际的pinctrl配置示例:

&pinctrl {
    /* I2C3总线引脚 */
    i2c3_pins: i2c3-pins {
        rockchip,pins =
            <1 RK_PA0 2 &pcfg_pull_up>,  /* I2C3_SCL */
            <1 RK_PA1 2 &pcfg_pull_up>;  /* I2C3_SDA */
    };

    /* 摄像头0的GPIO引脚 */
    camera0_pins: camera0-pins {
        rockchip,pins =
            <1 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>,  /* 复位 */
            <1 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>;  /* 电源 */
    };

    /* 摄像头1的GPIO引脚 */
    camera1_pins: camera1-pins {
        rockchip,pins =
            <1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>,  /* 复位 */
            <1 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>;  /* 电源 */
    };
};

这里有几个坑,我一个个说:

  • 引脚号不能重复:每个摄像头的GPIO引脚必须不同。我曾经图省事,把两路摄像头的复位引脚配成同一个,结果一路复位另一路也跟着复位。
  • 上拉/下拉要合理:I2C总线需要上拉,GPIO一般用无上下拉(由外部电路决定)。如果摄像头模块内部已经有上拉电阻,设备树里再配上拉就会导致电流过大。
  • 功能选择要正确:GPIO引脚要配成RK_FUNC_GPIO,I2C引脚要配成对应的功能号(比如2)。配错了摄像头就认不到。

4.5 完整的多摄像头设备树示例

最后,给一个四路摄像头设备树的完整框架。这个框架我在多个项目里验证过,可以直接套用:

/ {
    /* 时钟资源 */
    camera_clk: camera-clk {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <24000000>;
    };

    /* I2C总线 - 挂载所有摄像头 */
    &i2c3 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&i2c3_pins>;

        /* 四路摄像头节点 */
        camera0: camera@30 { ... };
        camera1: camera@32 { ... };
        camera2: camera@34 { ... };
        camera3: camera@36 { ... };
    };

    /* CSI接收器 - 连接所有摄像头 */
    &csi_host0 {
        status = "okay";
        ports {
            #address-cells = <1>;
            #size-cells = <0>;

            port@0 { reg = <0>; ... };  /* 连接camera0 */
            port@1 { reg = <1>; ... };  /* 连接camera1 */
            port@2 { reg = <2>; ... };  /* 连接camera2 */
            port@3 { reg = <3>; ... };  /* 连接camera3 */
        };
    };
};

核心要点总结

  • 每路摄像头要有独立的I2C地址和GPIO资源
  • 时钟尽量共享同一个源,便于同步
  • pinctrl配置要确保引脚不冲突、功能正确
  • 设备树只是硬件描述,时序控制还得靠驱动代码

设备树配置这块,说白了就是细心活。多检查几遍原理图,多核对几遍引脚号,能省去后面调试时的大把时间。我每次写完设备树都会做一次「三对照」:对照原理图、对照芯片手册、对照内核文档。三遍下来,基本不会出大问题。