MIPI 摄像头驱动学习笔记

90 阅读11分钟

之前需要移植MIPI 摄像头,整理了一下学习笔记

开发MIPI摄像头驱动需要实现V4L2框架,并且满足I2C通信控制

本文以RK3588外接GC8034驱动为例进行分析

1.gc8034输入输出

gc8034输入.png

输入:

RESET PWDN信号

MCLK 外部时钟信号 一般是24Mhz

SBCL SBDA 信号

输出:

4lanes 信号线 MDN<3:0> MDP<3:0>

MCN MCP 时钟信号线

2.基本参数

NV%AVNIH35ELB@N8S$S2U{H.png

最大分辨率@帧率 : 3264X2448@30Hz

帧率 fps = denominator/numerator

1752142847830.png

媒体总线格式MEDIA_BUS_FMT 根据图像格式和像素深度设置

2.2 blank 的计算

MIPI CSI-2 数字图像传输过程中,有会帧传输间隔,和行传输间隔。

9}E9P8FCC3JNQK}_)XRTB1V.png

帧传输间隔:垂直方向,FE(Frame end)到FS(Frame start)的空闲时间。

行传输间隔:水平方向,LE(line end)到FS(line start)的空闲时间。

在驱动程序中,set_fmt 函数里需要设置消隐时间。

blank计算.png

vts_def = height + vblank_def

hts_def = width + h_blank

vts_def和hts_def是垂直总时长和水平总时长。

2.3 时钟频率和像素率关系

MIPI摄像头会输出一对差分的时钟信号(CLKp和CLKn),用于同步信号的接收。

DDR传输.png

MIPI CSI采取双倍数据速率(DDR)传输,上下边沿都有数据传输,一个时钟周期,就可以传输2位数据。

这里可以得出像素率和时钟频率的关系:

pixel rate = link frequency * 2 * lanes / BITS_PER_SAMPLE

lanes为通道数量,BITS_PER_SAMPLE 为每个像素的位数。

一般情况下,摄像头输出的频率是已知的,在驱动程序中必须设置正确,像素率可以按照上面的这个公式推算后设置。

3. V4L2框架

V4L2(Video for Linux 2)是 Linux 内核中处理视频设备的核心框架,为摄像头、编解码器等视频设备提供了同一的驱动接口和用户空间API。

在嵌入式Linux平台上,需要把MIPI摄像头注册为 v4l2子设备。

image.png

V4L2_subdev_ops(子设备操作)包含三个V4L2的子设备操作:core_ops、video_ops、pad_ops

4.core_ops

上电时序

上电时序1.png

上电时序2.png

  • PWDN图像传感器中的关键控制引脚,主要功能是控制传感器的低功耗休眠模式

低电平 (0): 传感器完全上电,可进行图像采集、数据传输和寄存器配置

高电平 (1): 传感器进入低功耗状态

  • RESETB 复位信号是硬件设备初始化或恢复的关键控制信号。 “B” 表示低电平处罚复位。

  • SCCB是OmniVision开发的专有串行通信协议,主要功能是读取和配置传感器寄存器。 GC8034自带8k bits的OTP(One Time Programmable),可以通过 SCCB 协议访问。

在代码中的体现:

 static int __gc8034_power_on(struct gc8034 *gc8034)
 {
int ret;
u32 delay_us;
struct device *dev = &gc8034->client->dev;

if (!IS_ERR(gc8034->power_gpio))          
	gpiod_set_value_cansleep(gc8034->power_gpio, 1);

usleep_range(1000, 2000);                 //延时1~2ms  >t0+t1+t2+t3+t4

if (!IS_ERR_OR_NULL(gc8034->pins_default)) {
	ret = pinctrl_select_state(gc8034->pinctrl,              //pinctl default
				   gc8034->pins_default);
	if (ret < 0)
		dev_err(dev, "could not set pins\n");
}
ret = clk_set_rate(gc8034->xvclk, GC8034_XVCLK_FREQ);         //set MCLK
if (ret < 0)
	dev_warn(dev, "Failed to set xvclk rate (24MHz)\n");
if (clk_get_rate(gc8034->xvclk) != GC8034_XVCLK_FREQ)
	dev_warn(dev, "xvclk mismatched, modes are based on 24MHz\n");

if (!IS_ERR(gc8034->reset_gpio))
	gpiod_set_value_cansleep(gc8034->reset_gpio, 1);

ret = gc8034_enable_regulators(gc8034, gc8034->supplies);   // 开启电源轨
if (ret < 0) {
	dev_err(dev, "Failed to enable regulators\n");
	goto disable_clk;
}

usleep_range(100, 200);            //100us~200us >t5
ret = clk_prepare_enable(gc8034->xvclk); 
if (ret < 0) {
	dev_err(dev, "Failed to enable xvclk\n");
	return ret;
}

usleep_range(1000, 1100);          // t6
if (!IS_ERR(gc8034->pwdn_gpio))
	gpiod_set_value_cansleep(gc8034->pwdn_gpio, 0);

usleep_range(500, 1000);           // t7
if (!IS_ERR(gc8034->reset_gpio))
	gpiod_set_value_cansleep(gc8034->reset_gpio, 0);

usleep_range(6000, 7000);         // t8

/* 8192 cycles prior to first SCCB transaction */
delay_us = gc8034_cal_delay(8192);
usleep_range(delay_us, delay_us * 2);

return 0;

disable_clk:
clk_disable_unprepare(gc8034->xvclk);

return ret;
  }`

pinctrl_select_state() 是 Linux 内核中 Pin Control 子系统 的核心函数,用于动态切换引脚的配置状态。

gpiod_set_value_cansleep 是 Linux 内核 GPIO 子系统中的一个函数,用于在 可能睡眠的上下文 中设置 GPIO 的值。它是 GPIO Descriptor API 的一部分。

同样的 gpiod_set_value() 是GPIO基本设置函数,不支持睡眠。

下电时序

下电时序.png

顺序:PWDN → RESET → MCLK → Power

先控制信号 → 再断时钟 → 切换引脚状态 → 关闭电源

代码:

 static void __gc8034_power_off(struct gc8034 *gc8034)
 {
int ret;

if (!IS_ERR(gc8034->pwdn_gpio))
	gpiod_set_value_cansleep(gc8034->pwdn_gpio, 1);

if (!IS_ERR(gc8034->reset_gpio))
	gpiod_set_value_cansleep(gc8034->reset_gpio, 1);

clk_disable_unprepare(gc8034->xvclk);
if (!IS_ERR_OR_NULL(gc8034->pins_sleep)) {
	ret = pinctrl_select_state(gc8034->pinctrl,
				   gc8034->pins_sleep);              //切换引脚至sleep状态
	if (ret < 0)
		dev_dbg(&gc8034->client->dev, "could not set pins\n");
}
if (!IS_ERR(gc8034->power_gpio))
	gpiod_set_value_cansleep(gc8034->power_gpio, 0);

regulator_bulk_disable(GC8034_NUM_SUPPLIES, gc8034->supplies);
}

硬件实现

static int __maybe_unused gc8034_runtime_resume(struct device *dev)
{
        struct i2c_client *client = to_i2c_client(dev);
        struct v4l2_subdev *sd = i2c_get_clientdata(client);
        struct gc8034 *gc8034 = to_gc8034(sd);

        return __gc8034_power_on(gc8034);
}

static int __maybe_unused gc8034_runtime_suspend(struct device *dev)
{
        struct i2c_client *client = to_i2c_client(dev);
        struct v4l2_subdev *sd = i2c_get_clientdata(client);
        struct gc8034 *gc8034 = to_gc8034(sd);

        __gc8034_power_off(gc8034);

        return 0;
}

这里的 maybe_unused

注册进了 dev_pm_ops,然后注册进i2c_driver

    static const struct dev_pm_ops gc8034_pm_ops = {
            SET_RUNTIME_PM_OPS(gc8034_runtime_suspend,
                            gc8034_runtime_resume, NULL)
    }; 
    
static struct i2c_driver gc8034_i2c_driver = {
        .driver = {
                .name = GC8034_NAME, 
                .pm = &gc8034_pm_ops,
                .of_match_table = of_match_ptr(gc8034_of_match),
        },
        .probe		= &gc8034_probe,
        .remove		= &gc8034_remove,
        .id_table	= gc8034_match_id,
};

软件API:

static int gc8034_s_power(struct v4l2_subdev *sd, int on)
 {
struct gc8034 *gc8034 = to_gc8034(sd);
struct i2c_client *client = gc8034->client;
const struct gc8034_mode *mode = gc8034->cur_mode;
int ret = 0;

dev_info(&client->dev, "%s(%d) on(%d)\n", __func__, __LINE__, on);
mutex_lock(&gc8034->mutex);         // 加锁防止并发操作

/* If the power state is not modified - no work to do. */
if (gc8034->power_on == !!on)      
	goto unlock_and_return;

if (on) {
	ret = pm_runtime_get_sync(&client->dev);     //同步激活设备电源并增加使用计数
	if (ret < 0) {
		pm_runtime_put_noidle(&client->dev);     //减少使用计数但不挂起设备
		goto unlock_and_return;
	}

	ret = gc8034_write_array(gc8034->client, mode->global_reg_list);
	if (ret) {
		v4l2_err(sd, "could not set init registers\n");
		pm_runtime_put_noidle(&client->dev);
		goto unlock_and_return;
	}

	gc8034->power_on = true;
} else {
	pm_runtime_put(&client->dev);      //减少使用计数并可能挂起设备
	gc8034->power_on = false;
}

unlock_and_return:
        mutex_unlock(&gc8034->mutex);

        return ret;
}

出现三个函数,是 Linux 内核运行时电源管理 (Runtime PM)  框架的核心接口,用于智能管理设备的电源状态。

pm_runtime_get_sync()

pm_runtime_put_noidle()

pm_runtime_put()

通过合理使用这三个API,可使设备在空闲时自动进入低功耗状态,平衡性能和能耗。

s_power 注册进了v4l2_subdev_core_ops 里面

ioctl

用户层和内核层之间的通信,完全暴露给用户,典型操作 读写寄存器。

5. I2C通信

地址的获取与初始化:

MIPI 摄像头是挂载在I2C总线上工作的,这点可以从设备树中看出

&i2c3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&i2c3m0_xfer>;

gc8034_2: gc8034_2@37 {
    compatible = "firefly,xc7160";
    status = "okay";
    reg = <0x37>;
    clocks = <&cru CLK_MIPI_CAMARAOUT_M3>;
    clock-names = "xvclk";
    power-domains = <&power RK3588_PD_VI>;
    pinctrl-names = "default";
    pinctrl-0 = <&mipim0_camera3_clk>;
    rockchip,grf = <&sys_grf>;
    pwdn-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
    rockchip,camera-module-index = <2>;
    rockchip,camera-module-facing = "back";
    rockchip,camera-module-name = "RK-CMK-8M-2-v1";
    rockchip,camera-module-lens-name = "CK8401-4";        

    port {
        gc8034_out2: endpoint {
            remote-endpoint = <&mipi_in_ucam2>;
            data-lanes = <1 2 3 4>;
        };
    };
};

};

可以看到 gc8034的i2c地址配置为 <0x37> 消息结构

struct i2c_msg {
	__u16 addr;    // 从设备地址
	__u16 flags;   // 标志位(I2C_M_RD表示读操作)
	__u16 len;     // 数据长度
	__u8 *buf;     // 数据缓冲区
};

驱动在加载的时候,首先会调用probe() 函数,在probe()函数初始化的时候

// client->addr 包含了从设备树解析出的I2C地址
struct device *dev = &client->dev;
gc8034->client = client;    // 保存client指针,其中包含地址信息 

然后通过v4l2_i2c_subdev_init() 初始化V4L2子设备

   v4l2_i2c_subdev_init(sd, client, &gc8034_subdev_ops);

将 I2C 设备的硬件特性与 V4L2 框架的子设备抽象结合起来,从而实现对设备的统一管理和操作

后续的寄存器操作都会通过 client->addr 获取gc8034的地址

寄存器操作

寄存器写操作

static int gc8034_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
	struct i2c_msg msg;
	u8 buf[2];
	int ret;

	// 构造数据包:寄存器地址 + 数据值
	buf[0] = reg & 0xFF;  // 寄存器地址
	buf[1] = val;         // 要写入的值

	msg.addr = client->addr;     // 传感器I2C地址
	msg.flags = client->flags;   // 标志位(通常是0,表示写操作)
	msg.buf = buf;              // 数据缓冲区
	msg.len = sizeof(buf);      // 数据长度:2字节

	// 执行I2C传输:单次消息传输
	ret = i2c_transfer(client->adapter, &msg, 1);
	if (ret >= 0)
		return 0;

	dev_err(&client->dev,
		"gc8034 write reg(0x%x val:0x%x) failed !\n", reg, val);

	return ret;
}

寄存器读操作

static int gc8034_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
	struct i2c_msg msg[2];  // 需要2个消息:先写地址,再读数据
	u8 buf[1];
	int ret;

	buf[0] = reg & 0xFF;

	// 第一个消息:写入要读取的寄存器地址
	msg[0].addr = client->addr;
	msg[0].flags = client->flags;      // 写标志
	msg[0].buf = buf;                  // 包含寄存器地址
	msg[0].len = sizeof(buf);          // 1字节

	// 第二个消息:读取数据
	msg[1].addr = client->addr;
	msg[1].flags = client->flags | I2C_M_RD;  // 读标志
	msg[1].buf = buf;                  // 用于存放读取的数据
	msg[1].len = 1;                    // 读取1字节

	// 执行I2C传输:两次消息传输
	ret = i2c_transfer(client->adapter, msg, 2);
	if (ret >= 0) {
		*val = buf[0];  // 返回读取的值
		return 0;
	}

	dev_err(&client->dev,
		"gc8034 read reg:0x%x failed !\n", reg);

	return ret;
}

I2C控制的核心功能

流控制

// 启动视频流
static int __gc8034_start_stream(struct gc8034 *gc8034)
{
    // 1. 写入初始化寄存器配置
    ret = gc8034_write_array(gc8034->client, gc8034->cur_mode->reg_list);
    
    // 2. 设置流控制寄存器
    ret |= gc8034_write_reg(gc8034->client,
        GC8034_REG_SET_PAGE, GC8034_SET_PAGE_ZERO);
    
    // 3. 启动流模式
    if (2 == gc8034->lane_num) {
        ret |= gc8034_write_reg(gc8034->client,
            GC8034_REG_CTRL_MODE, 0x91);  // 2-lane流模式
    } else {
        ret |= gc8034_write_reg(gc8034->client,
            GC8034_REG_CTRL_MODE, GC8034_MODE_STREAMING);  // 正常流模式
    }
    return ret;
}

// 停止视频流
static int __gc8034_stop_stream(struct gc8034 *gc8034)
{
    // 设置待机模式
    ret = gc8034_write_reg(gc8034->client,
        GC8034_REG_SET_PAGE, GC8034_SET_PAGE_ZERO);
    ret |= gc8034_write_reg(gc8034->client,
        GC8034_REG_CTRL_MODE, GC8034_MODE_SW_STANDBY);
    return ret;
}

参数控制

// 设置曝光时间
ret = gc8034_write_reg(gc8034->client,
    GC8034_REG_EXPOSURE_H,
    GC8034_FETCH_HIGH_BYTE_EXP(cal_shutter));
ret |= gc8034_write_reg(gc8034->client,
    GC8034_REG_EXPOSURE_L,
    GC8034_FETCH_LOW_BYTE_EXP(cal_shutter));

// 设置增益
ret |= gc8034_write_reg(gc8034->client, 0xb6, gain_index);