之前需要移植MIPI 摄像头,整理了一下学习笔记
开发MIPI摄像头驱动需要实现V4L2框架,并且满足I2C通信控制
本文以RK3588外接GC8034驱动为例进行分析
1.gc8034输入输出
输入:
RESET PWDN信号
MCLK 外部时钟信号 一般是24Mhz
SBCL SBDA 信号
输出:
4lanes 信号线 MDN<3:0> MDP<3:0>
MCN MCP 时钟信号线
2.基本参数
最大分辨率@帧率 : 3264X2448@30Hz
帧率 fps = denominator/numerator
媒体总线格式MEDIA_BUS_FMT 根据图像格式和像素深度设置
2.2 blank 的计算
MIPI CSI-2 数字图像传输过程中,有会帧传输间隔,和行传输间隔。
帧传输间隔:垂直方向,FE(Frame end)到FS(Frame start)的空闲时间。
行传输间隔:水平方向,LE(line end)到FS(line start)的空闲时间。
在驱动程序中,set_fmt 函数里需要设置消隐时间。
vts_def = height + vblank_def
hts_def = width + h_blank
vts_def和hts_def是垂直总时长和水平总时长。
2.3 时钟频率和像素率关系
MIPI摄像头会输出一对差分的时钟信号(CLKp和CLKn),用于同步信号的接收。
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子设备。
V4L2_subdev_ops(子设备操作)包含三个V4L2的子设备操作:core_ops、video_ops、pad_ops
4.core_ops
上电时序
- 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基本设置函数,不支持睡眠。
下电时序
顺序: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);