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用光了。
我个人习惯的做法是:
- 优先使用专用GPIO控制器:比如Rockchip平台的GPIO0-GPIO4,每个控制器有32个引脚。尽量把摄像头相关的GPIO集中在一个控制器上,方便管理。
- 避免与其它外设冲突:检查原理图,确保摄像头用的GPIO没有被UART、SPI、I2C等外设占用。我在一个项目里就踩过这个坑——摄像头复位引脚和调试串口的TX复用,每次插上调试线摄像头就复位。
- 考虑上电时序:有些摄像头对电源和复位的时序有严格要求。比如先上电,等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配置要确保引脚不冲突、功能正确
- 设备树只是硬件描述,时序控制还得靠驱动代码
设备树配置这块,说白了就是细心活。多检查几遍原理图,多核对几遍引脚号,能省去后面调试时的大把时间。我每次写完设备树都会做一次「三对照」:对照原理图、对照芯片手册、对照内核文档。三遍下来,基本不会出大问题。