在嵌入式 Linux 平台进行图像传感器驱动移植与调试,通常包括以下几个关键步骤:
-
硬件上电与复位时序配置
依据传感器 datasheet 时序要求,合理控制 vdd、reset、powerdown 及 clk 等信号的上下电顺序,确保传感器可靠启动。
-
传感器寄存器初始化
配置 sensor 内部寄存器,设定所需的分辨率、像素格式、数据输出模式等基本图像参数。
-
实现 V4L2 子设备操作集
编写
struct v4l2_subdev_ops中必要的回调函数,通常包括:-
set_fmt:设置图像格式
-
get_fmt:获取当前图像格式
-
s_stream:启动/停止视频流
-
s_power:设备电源管理
-
-
扩展 V4L2 控制接口
通过 **
v4l2_ctrl_handler**为驱动添加控制项,支持动态调整 fps、曝光时间、增益、测试图等参数。 -
驱动探测与设备注册
在
probe()函数中完成传感器检测、资源分配,并通过 Media Controller 框架注册子设备 ,构建与上层视频设备节点的连接。 -
寄存器配置方式
一般I2C协议为主
来个实际的demo移植参考:rk3566移植豪威的OX05B1S到RK3562

先依葫芦画瓢

但编译过程报错:

修改后:

继续修复驱动,编译内核ok后即可,烧录到新的主板上面看看

接着扩展一下驱动代码的解释,重点是实现 V4L2 子设备代码操作:
static const struct v4l2_subdev_pad_ops ox05b1s_pad_ops = {
.enum_mbus_code = ox05b1s_enum_mbus_code,
.enum_frame_size = ox05b1s_enum_frame_sizes,
.enum_frame_interval = ox05b1s_enum_frame_interval,
.get_fmt = ox05b1s_get_fmt,
.set_fmt = ox05b1s_set_fmt,
.get_mbus_config = ox05b1s_g_mbus_config,
};
内核的V4L2配置
#
# Video4Linux options
#
CONFIG_VIDEO_V4L2=y
CONFIG_VIDEO_V4L2_I2C=y
CONFIG_VIDEO_V4L2_SUBDEV_API=y
# CONFIG_VIDEO_ADV_DEBUG is not set
# CONFIG_VIDEO_FIXED_MINOR_RANGES is not set
CONFIG_V4L2_MEM2MEM_DEV=y
CONFIG_V4L2_FWNODE=y
# end of Video4Linux options
-
编写
struct v4l2_subdev_ops中必要的回调函数,通常包括:-
set_fmt:设置图像格式
-
get_fmt:获取当前图像格式
-
s_stream:启动/停止视频流
-
s_power:设备电源管理
-
1. .set_fmt = ox05b1s_set_fmt, //设置图像格式
static int ox05b1s_set_fmt(struct v4l2_subdev *sd,
struct v4l2_subdev_pad_config *cfg,
struct v4l2_subdev_format *fmt)
{
struct ox05b1s *ox05b1s = to_ox05b1s(sd);
const struct ox05b1s_mode *mode;
s64 h_blank, vblank_def;
mutex_lock(&ox05b1s->mutex);//加入互斥锁,禁止操作期间被其他函数操作
mode = ox05b1s_find_best_fit(fmt);
fmt->format.code = PIX_FORMAT;
fmt->format.width = mode->width;
fmt->format.height = mode->height;
fmt->format.field = V4L2_FIELD_NONE;
if (fmt->which == V4L2_SUBDEV_FORMAT_TRY) {
#ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
*v4l2_subdev_get_try_format(sd, cfg, fmt->pad) = fmt->format;
#else
mutex_unlock(&ox05b1s->mutex);
return -ENOTTY;
#endif
} else {
ox05b1s->cur_mode = mode;
h_blank = mode->hts_def - mode->width;
__v4l2_ctrl_modify_range(ox05b1s->hblank, h_blank,
h_blank, 1, h_blank);
vblank_def = mode->vts_def - mode->height;
__v4l2_ctrl_modify_range(ox05b1s->vblank, vblank_def,
OX05B1S_VTS_MAX - mode->height,
1, vblank_def);
}
mutex_unlock(&ox05b1s->mutex); //释放互斥锁,把资源可以给到其他API操作
return 0;
}
2. .get_fmt = ox05b1s_get_fmt, //获取当前图像格式
static int ox05b1s_get_fmt(struct v4l2_subdev *sd,
struct v4l2_subdev_pad_config *cfg,
struct v4l2_subdev_format *fmt)
{
struct ox05b1s *ox05b1s = to_ox05b1s(sd);
const struct ox05b1s_mode *mode = ox05b1s->cur_mode;
mutex_lock(&ox05b1s->mutex);
if (fmt->which == V4L2_SUBDEV_FORMAT_TRY) {
#ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
fmt->format = *v4l2_subdev_get_try_format(sd, cfg, fmt->pad);
#else
mutex_unlock(&ox05b1s->mutex);
return -ENOTTY;
#endif
} else {
fmt->format.width = mode->width;
fmt->format.height = mode->height;
fmt->format.code = PIX_FORMAT;
fmt->format.field = V4L2_FIELD_NONE;
}
mutex_unlock(&ox05b1s->mutex);
return 0;
}
篇幅有限就不展开CONFIG_VIDEO_V4L2_SUBDEV_API=y,则走的是
*v4l2_subdev_get_try_format(sd, cfg, fmt->pad);//指针函数
static const struct v4l2_subdev_video_ops ox05b1s_video_ops = {
.s_stream = ox05b1s_s_stream,
.g_frame_interval = ox05b1s_g_frame_interval,
//.g_mbus_config = ox05b1s_g_mbus_config,
};
3. .s_stream = ox05b1s_s_stream, //启动/停止视频流
static int ox05b1s_s_stream(struct v4l2_subdev *sd, int on)
{
struct ox05b1s *ox05b1s = to_ox05b1s(sd);
struct i2c_client *client = ox05b1s->client;
int ret = 0;
mutex_lock(&ox05b1s->mutex);
on = !!on;
if (on == ox05b1s->streaming)
goto unlock_and_return;
if (on) { //这个布尔on判断视频流的启停
ret = pm_runtime_get_sync(&client->dev);
if (ret < 0) {
pm_runtime_put_noidle(&client->dev);
goto unlock_and_return;
}
ret = __ox05b1s_start_stream(ox05b1s); //开始视频流
if (ret) {
v4l2_err(sd, "start stream failed while write regs\n");
pm_runtime_put(&client->dev);
goto unlock_and_return;
}
} else {
__ox05b1s_stop_stream(ox05b1s); //停止视频流
pm_runtime_put(&client->dev);
}
ox05b1s->streaming = on;
unlock_and_return:
mutex_unlock(&ox05b1s->mutex);
return ret;
}
static const struct v4l2_subdev_core_ops ox05b1s_core_ops = {
.s_power = ox05b1s_s_power,
.ioctl = ox05b1s_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl32 = ox05b1s_compat_ioctl32,
#endif
};
4. .s_power = ox05b1s_s_power //设备电源管理
static int ox05b1s_s_power(struct v4l2_subdev *sd, int on)
{
struct ox05b1s *ox05b1s = to_ox05b1s(sd);
struct i2c_client *client = ox05b1s->client;
int ret = 0;
mutex_lock(&ox05b1s->mutex);
/* If the power state is not modified - no work to do. */
if (ox05b1s->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 = ox05b1s_write_array(ox05b1s->client, OX05B1S_ARRAY_REG);//上电后,重新初始化
if (ret) {
v4l2_err(sd, "could not set init registers\n");
pm_runtime_put_noidle(&client->dev);
goto unlock_and_return;
}
ox05b1s->power_on = true;
} else {
pm_runtime_put(&client->dev);
ox05b1s->power_on = false;
}
unlock_and_return:
mutex_unlock(&ox05b1s->mutex);
return ret;
}
一、DTS配置
在嵌入式 Linux 设备树(Device Tree)中正确引用 Sensor 驱动,需完成以下关键配置:
-
时钟与管脚复用配置
- 为 Sensor 分配正确的工作时钟,并在 pinmux 节点中配置对应 I/O 口的复用功能,确保信号路径畅通。
-
电源与 GPIO 配置
-
根据原理图,在regulator 节点中定义 Sensor 所需的供电(如 AVDD、DVDD、DOVDD 等)。
-
声明并配置 reset、powerdown 等控制引脚对应的 GPIO,为上电/复位时序提供硬件支持。
-
-
Media Controller 链路连接
- 在 Sensor 节点下添加
port子节点,并通过remote-endpoint属性与主控的 CIF(Camera Interface)或 ISP(Image Signal Processor)节点建立连接,完成视频流管道的硬件绑定。
- 在 Sensor 节点下添加

Linux DTS语法官网:
https://www.kernel.org/doc/html/latest/devicetree/usage-model.html
- 二、时序
需特别关注上电时序的设计,不同传感器对此要求差异显著:
-
大部分型号 :对时序要求相对宽松,只要保证主时钟(mclk)、各档电源(vdd)、复位(reset)与掉电(powerdown) 信号处于有效状态,即可正常进行 I2C 通信并输出图像,无需严格限定上电先后顺序与延时。
-
部分型号 :存在严格的时序要求,必须严格遵循特定顺序与延时控制,否则无法正常启动或工作。
因此,在驱动实现前,务必查阅 Sensor 原厂提供的数据手册(DataSheet),其中通常会包含明确的上电时序图。开发者只需依据该时序,依次配置各信号即可确保硬件正确初始化。
状态流:

① 一般都是从模式,即从外部接收一个主时钟(MCLK)输入
② 主控SoC的时钟发生器(或外部晶振) → 提供MCLK信号给Sensor → Sensor 内部的PLL以此MCLK为参考,生成其内部处理所需的各种时钟
③ Sensor 在外部MCLK的同步下,将图像数据(通过MIPI CSI/DVP等接口)发送给主控SoC

上电:

① 时序要求 :从第一路电源开始上电,到所有电源完全稳定,整个过程应在 200 毫秒 内完成。
- DVDD(数字核心电源,1.2V) 首先为传感器内部的数字逻辑核心供电,通常标注为DVDD1、DVDD2。
- OVDD(接口IO电源,1.8V) 接着为传感器的I/O接口(如I2C、MIPI等)供电,通常标注为OVDD。此电压需与主控SoC的I/O电压匹配。
- AVDD(模拟/感光电源,2.9V) 最后为传感器的模拟电路(如像素阵列、ADC等)供电,通常标注为AVDD1、AVDD2。
② 在摄像头传感器上电后,其内部寄存器的初始状态是未定义的,必须通过系统复位将其恢复至已知的默认值。
具体操作是:在所有电源稳定后,将芯片的硬件复位引脚(XCLR)保持低电平至少 500 纳秒,之后将其释放至高电平。此时寄存器将被初始化为默认值,为后续软件配置做好准备。
③ 在摄像头传感器的初始化过程中,系统复位 是通过将复位引脚XCLR从低电平切换为高电平来完成的。复位信号生效后,传感器内部逻辑才可被正常配置。
④ 在系统复位完成之后,需要通过寄存器配置来设置传感器的工作参数。
- 至此,上电时序算是完成,还需要进行参数配置,这一过程通常通过I2C等串行总线进行,包括设置图像分辨率、输出格式、曝光时间、增益等关键参数(具体可看手册/咨询FAE)。只有完成正确的寄存器配置,传感器才能输出符合预期的图像数据。
下电:

- 三、调试
1.I²C不通、识别不到设备
-
排查I²C硬件接线是否OK
-
上电时序是否正确(可检查驱动日志/直接量一下)
-
测量电压、i2cdetect、检查 DTS 配置
2.出图异常
- 全黑 -> 镜头盖未摘、曝光时间过短、sensor 未正常工作 -> 检查镜头、曝光寄存器、sensor 电源、检查MCLK
- 全白 -> 曝光过度、增益过大 -> 降低曝光值、检查自动曝光是否生效
- 颜色异常 -> 数据格式不匹配、白平衡错误 -> 确认 YUV/RGB 格式、检查白平衡寄存器
- 条纹/噪点 -> 电源噪声、时钟抖动、MIPI 信号质量差 -> 测量电源纹波、检查 MIPI 等长和阻抗
- 图像撕裂 -> VSYNC/HSYNC 不同步、DMA 溢出 -> 检查时序寄存器、增加 DMA 缓冲区
- 使用抓图工具,从各个阶段(raw、isp前后、编码前后等等)抓图,排查异常位置,找到异常点
3.帧率低
-
检查时钟频率MCLK是否足够
-
确认总线带宽(MIPI 速率)
-
优化驱动中的数据传输(如使用 DMA 聚集/分散)
-
调整曝光
-
查看相关日志、状态先确认是哪个环节导致的帧率低
4.延迟大
-
减少处理流水线中的缓冲区数量
-
调整 ISP 处理参数
-
检查 CPU 负载,避免调度延迟
-
排查是否为业务层导致
5.其他
- 一般SOC平台的SDK都可以对工作状态、RAW/YUV图等进行dump,方便分析
针对V4L2的知识扩展一下:
V4L2 (Video For Linux 2) 子设备驱动的典型实现
static const struct v4l2_subdev_core_ops ox05b1s_core_ops = {
.s_power = ox05b1s_s_power,
.ioctl = ox05b1s_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl32 = ox05b1s_compat_ioctl32,
#endif
};
static const struct v4l2_subdev_video_ops ox05b1s_video_ops = {
.s_stream = ox05b1s_s_stream,
.g_frame_interval = ox05b1s_g_frame_interval,
//.g_mbus_config = ox05b1s_g_mbus_config,
};
static const struct v4l2_subdev_pad_ops ox05b1s_pad_ops = {
.enum_mbus_code = ox05b1s_enum_mbus_code,
.enum_frame_size = ox05b1s_enum_frame_sizes,
.enum_frame_interval = ox05b1s_enum_frame_interval,
.get_fmt = ox05b1s_get_fmt,
.set_fmt = ox05b1s_set_fmt,
.get_mbus_config = ox05b1s_g_mbus_config,
};
static const struct v4l2_subdev_ops ox05b1s_subdev_ops = {
.core = &ox05b1s_core_ops,
.video = &ox05b1s_video_ops,
.pad = &ox05b1s_pad_ops,
};
1. 整体结构层次
ox05b1s_subdev_ops
├── .core → ox05b1s_core_ops (核心操作)
├── .video → ox05b1s_video_ops (视频流操作)
└── .pad → ox05b1s_pad_ops (Pad/格式操作)
V4L2 子设备驱动将回调函数按功能分组,便于管理和复用。
2. ox05b1s_core_ops (核心操作)
static const struct v4l2_subdev_core_ops ox05b1s_core_ops = {
.s_power = ox05b1s_s_power, //控制子设备的电源状态(如开启/关闭传感器)
.ioctl = ox05b1s_ioctl, //扩展私有控制命令(如读取传感器寄存器、调试信息)
#ifdef CONFIG_COMPAT
.compat_ioctl32 = ox05b1s_compat_ioctl32, //兼容32bit的系统接口
#endif
};
| 成员 | 作用 |
|---|---|
.s_power |
控制子设备的电源状态(如开启/关闭传感器)。上层调用 v4l2_subdev_call(sd, core, s_power, on) 触发。 |
.ioctl |
自定义 IOCTL 命令处理,用于扩展私有控制命令(如读取传感器寄存器、调试信息)。 |
.compat_ioctl32 |
在 64 位内核上处理 32 位用户空间的 IOCTL,确保兼容性(需要 CONFIG_COMPAT 宏)。 |
3. ox05b1s_video_ops (视频操作)
static const struct v4l2_subdev_video_ops ox05b1s_video_ops = {
.s_stream = ox05b1s_s_stream, //启动/停止视频流,应用VIDIOC_STREAMON/STREAMOFF 触发
.g_frame_interval = ox05b1s_g_frame_interval, //获取当前帧间隔,结合规格书
//.g_mbus_config = ox05b1s_g_mbus_config, // 被注释
};
| 成员 | 作用 |
|---|---|
.s_stream |
启动/停止视频流 。传感器在此函数中配置寄存器开始输出 MIPI/并行数据。调用时机:应用通过 VIDIOC_STREAMON / VIDIOC_STREAMOFF 触发。 |
.g_frame_interval |
获取当前帧间隔(即帧率)。返回结构如 { numerator = 1, denominator = 30 } 表示 30fps。 |
.g_mbus_config (注释) |
获取媒体总线配置(数据 lane 数、时钟极性等)。被注释可能因为该传感器不需要动态查询,或改用 .get_mbus_config(见下面 pad ops)。 |
4. ox05b1s_pad_ops (Pad 操作)
static const struct v4l2_subdev_pad_ops ox05b1s_pad_ops = {
.enum_mbus_code = ox05b1s_enum_mbus_code, //枚举像素格式
.enum_frame_size = ox05b1s_enum_frame_sizes, //枚举像素大小
.enum_frame_interval = ox05b1s_enum_frame_interval, //枚举在特定分辨率下支持的帧率
.get_fmt = ox05b1s_get_fmt,
.set_fmt = ox05b1s_set_fmt,
.get_mbus_config = ox05b1s_g_mbus_config,
};
这是 最核心 的格式协商部分,对应 V4L2 的 Media Controller + Pad 架构。
| 成员 | 作用 |
|---|---|
.enum_mbus_code |
枚举传感器支持的 媒体总线像素格式 (如 MEDIA_BUS_FMT_SBGGR10_1X10 等)。 |
.enum_frame_size |
枚举支持的 分辨率 (如 1920x1080, 1280x720)。用户程序通过 VIDIOC_ENUM_FRAMESIZES 调用。 |
.enum_frame_interval |
枚举在特定分辨率下支持的 帧率(如 30fps, 15fps)。 |
.get_fmt |
获取当前 Pad 上设置的 格式(包括分辨率、像素格式、颜色空间等)。 |
.set_fmt |
设置新的格式,通常还会返回硬件实际调整后的格式(因为传感器可能有对齐或步长限制)。 |
.get_mbus_config |
获取当前 硬件总线配置(如 MIPI CSI-2 lane 数、连续时钟还是非连续时钟等),用于上层或桥接驱动正确配置接收端。 |
5. ox05b1s_subdev_ops (顶层操作集合)
static const struct v4l2_subdev_ops ox05b1s_subdev_ops = {
.core = &ox05b1s_core_ops,
.video = &ox05b1s_video_ops,
.pad = &ox05b1s_pad_ops,
};
这是注册到 V4L2 子设备框架时的 主结构体,包含上述三类操作。当驱动调用:
v4l2_i2c_subdev_init(&sensor->sd, client, &ox05b1s_subdev_ops);//参考下面的probe函数
就将这些回调注册到系统中,后续上层可通过 v4l2_subdev_call 或直接调用相应函数。
static int ox05b1s_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device *dev = &client->dev;
struct device_node *node = dev->of_node;
struct ox05b1s *ox05b1s;
struct v4l2_subdev *sd;
char facing[2];
int ret;
dev_info(dev, "driver version: %02x.%02x.%02x",
DRIVER_VERSION >> 16,
(DRIVER_VERSION & 0xff00) >> 8,
DRIVER_VERSION & 0x00ff);
ox05b1s = devm_kzalloc(dev, sizeof(*ox05b1s), GFP_KERNEL);
if (!ox05b1s)
return -ENOMEM;
ret = of_property_read_u32(node, RKMODULE_CAMERA_MODULE_INDEX,
&ox05b1s->module_index);
.....
if (ret) {
dev_err(dev, "could not get module information!\n");
return -EINVAL;
}
ox05b1s->client = client;
ox05b1s->cur_mode = &supported_modes[0];
ox05b1s->is_thunderboot = IS_ENABLED(CONFIG_VIDEO_ROCKCHIP_THUNDER_BOOT_ISP);
ox05b1s->xvclk = devm_clk_get(dev, "xvclk");
if (IS_ERR(ox05b1s->xvclk)) {
dev_err(dev, "Failed to get xvclk\n");
return -EINVAL;
}
ox05b1s->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(ox05b1s->reset_gpio))
dev_warn(dev, "Failed to get reset-gpios\n");
....
ret = ox05b1s_configure_regulators(ox05b1s);
if (ret) {
dev_err(dev, "Failed to get power regulators\n");
return ret;
}
ox05b1s->pinctrl = devm_pinctrl_get(dev);
if (!IS_ERR(ox05b1s->pinctrl)) {
ox05b1s->pins_default =
pinctrl_lookup_state(ox05b1s->pinctrl,
OF_CAMERA_PINCTRL_STATE_DEFAULT);
if (IS_ERR(ox05b1s->pins_default))
dev_err(dev, "could not get default pinstate\n");
ox05b1s->pins_sleep =
pinctrl_lookup_state(ox05b1s->pinctrl,
OF_CAMERA_PINCTRL_STATE_SLEEP);
if (IS_ERR(ox05b1s->pins_sleep))
dev_err(dev, "could not get sleep pinstate\n");
} else {
dev_err(dev, "no pinctrl\n");
}
mutex_init(&ox05b1s->mutex);
sd = &ox05b1s->subdev;
v4l2_i2c_subdev_init(sd, client, &ox05b1s_subdev_ops);//重点V4L2初始化函数
ret = ox05b1s_initialize_controls(ox05b1s);//这个也都是重点函数
if (ret)
goto err_destroy_mutex;
ret = __ox05b1s_power_on(ox05b1s); //上电
if (ret)
goto err_free_handler;
ret = ox05b1s_check_sensor_id(ox05b1s, client); //检测是否读到ID
if (ret)
goto err_power_off;
#ifdef CONFIG_VIDEO_V4L2_SUBDEV_API
sd->internal_ops = &ox05b1s_internal_ops;
sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE |
V4L2_SUBDEV_FL_HAS_EVENTS;
#endif
#if defined(CONFIG_MEDIA_CONTROLLER)
ox05b1s->pad.flags = MEDIA_PAD_FL_SOURCE;
sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
ret = media_entity_pads_init(&sd->entity, 1, &ox05b1s->pad);
if (ret < 0)
goto err_power_off;
#endif
memset(facing, 0, sizeof(facing));
if (strcmp(ox05b1s->module_facing, "back") == 0)
facing[0] = 'b';
else
facing[0] = 'f';
snprintf(sd->name, sizeof(sd->name), "m%02d_%s_%s %s",
ox05b1s->module_index, facing,
OX05B1S_NAME, dev_name(sd->dev));
ret = v4l2_async_register_subdev_sensor_common(sd);
if (ret) {
dev_err(dev, "v4l2 async register subdev failed\n");
goto err_clean_entity;
}
pm_runtime_set_active(dev);
pm_runtime_enable(dev);
pm_runtime_idle(dev);
return 0;
err_clean_entity:
#if defined(CONFIG_MEDIA_CONTROLLER)
media_entity_cleanup(&sd->entity);
#endif
err_power_off:
__ox05b1s_power_off(ox05b1s);
err_free_handler:
v4l2_ctrl_handler_free(&ox05b1s->ctrl_handler);
err_destroy_mutex:
mutex_destroy(&ox05b1s->mutex);
return ret;
}
6. 调用流程示例(从应用到传感器)
-
应用程序设置分辨率 →
VIDIOC_S_FMT→ 内核调用
.set_fmt→ 传感器驱动配置寄存器。 -
应用程序查询帧率 →
VIDIOC_G_PARM→ 内核调用
.g_frame_interval→ 返回当前帧率。 -
应用程序启动流 →
VIDIOC_STREAMON→ 内核调用
.s_stream(1)→ 传感器开始输出图像数据。 -
调试或自定义操作 →
VIDIOC_DBG_G_REGISTER→ 内核调用
.ioctl→ 读取传感器内部寄存器。
总结
| 结构体 | 主要职责 |
|---|---|
core_ops |
电源、自定义控制、兼容性 |
video_ops |
视频流开关、帧率控制 |
pad_ops |
格式协商(分辨率、像素格式、总线配置) |
subdev_ops |
将上面三个组合成完整的子设备操作集 |
这种分层设计使得 V4L2 子设备驱动易于扩展,并且与媒体控制器(Media Controller)框架无缝配合。对于 OX05B1S 这种图像传感器,pad_ops 是实现正确图像格式传输的关键。