纪念再一次使用这里,刚好开通好博客,写下近年来的第一篇。

最近要做一个全志A64平台的vfe驱动培训,组织了下v4l2与vfe驱动分析。这里记录下。

全志A64芯片csi部份不自带isp(其实是有带一个yuv数据处理的小isp,只支持scaler与rotate,没有标识出来,也没有任何的资料,对用户来说基本相当于是黑盒),只是实现了最基本的图像数据采集与格式转换输出,算是入门级的,这里只就驱动、调试与软件框架流程做分析。

A64 csi部份如下所示:

csi支持情况如下所示(包括一路专用i2c(可以配置相关gpio功能脚成普通i2c使用)):

A64 平台csi驱动部份走的是标准的v4l2协议流程,v4l2驱动位于/lichee/linux-3.10/drivers/media/,平台部份驱动位置位于: /lichee/linux-3.10/drivers/media/platform/sunxi-vfe。

整个v4l2驱动是由v4l2-core加上其它组成的,v4l2-core是核心。同时也可以看到v4l2支持的视频设备类型是非常多的,包括:vbi、usb(usb camera)、radio(AM、FM等)、rc(IR设备),dvb、pci等等,还包括platform下面各种平台驱动的支持。

由于这里只研究a64 csi部份,所以内容仅限于v4l2-core + platform/sunxi-vfe。

对于A80机器而言,也就是下面的几个ko内容。

基于V4L2的驱动框架大概如下所示:

图中芯片模块对应Soc的各个子模块,video_device结构体主要用来控制Soc的video模块,v4l2_device会包含多个v4l2_subdev ,每个v4l2_subdev 用来控制各自的子模块,某些驱动不需要v4l2_subdev ,依靠video模块就能实现功能。

A64平台vfe v4l2驱动框架大概如下所示:

ctrl流程大概如下图所示:

2.1 v4l2分析

v4l2相关在v4l2-core中,包括2部份:buffer管理(videobuf/videobuf2)与dev部份(v4l2-device、v4l2-dev、v4l2-subdev、v4l2-ioctl 、v4l2-ctrls 、v4l2-fh)。

一个完整的device由video、若干subdev、ioctl、vdev等组成。

                          

struct v4l2_device:用来描述每一个v4l2设备实例状态的结构。一个v4l2_device可以由多个v4l2_subdev加ctrl组成。

struct v4l2_subdev:对应具体的外围设备,用来初始化和控制子设备的方法。如sensor、actuator、isp、csi、mipi这些都可以是单独的子设备。

struct video_device:用于在/dev目录下生成设备节点文件(如:/dev/video0),把操作设备的接口暴露给用户空间。

struct v4l2_fh:为每一个被打开的节点维护一个文件句柄,方便fileoperation ctrl。

v4l2_fh是用来保存子设备的特有操作方法,也就是下面要分析到的v4l2_ctrl_handler,内核提供一组v4l2_fh的操作方法,通常在打开设备节点时进行v4l2_fh注册。

struct v4l2_ctrl_handler: v4l2_ctrl_handler是用于保存子设备控制方法集的结构体,对于视频设备这些ctrls包括设置亮度、饱和度、对比度和清晰度等,用链表的方式来保存ctrls,可以通过v4l2_ctrl_new_std函数向链表添加ctrls(对应于上层char file操作的ioctl处理)。

videobuf/videobuf2:视频缓冲buffer的处理。

1、videobuf2

videobuf2用来管理视频缓冲buffer。它有一组功能函数集用来实现许多标准的POSIX系统调用,包括read(),poll()和mmap()等等,还有一组功能函数集用来实现流式(streaming)IO的v4l2_ioctl调用,包括缓冲区的分配,入队和出队以及数据流控制等操作。

V4L2核心api提供了一套标准的方法来处理视频缓冲,这些方法允许驱动实现read(),mmap(), overlay()等操作。同样也有方法支持DMA的scatter/gather操作,并且支持vmallocbuffer(这个大多用在USB驱动上)。

该部份主要代码如下:

videobuf2-core.c

videobuf2-memops.c

videobuf2-dma-contig.c

videobuf2-dma-sg.c

videobuf2-vmalloc.c

videobuf2-core是核心实现代码,会建立vb2_queue来控制videobuffer缓冲内存。它调用vb2_mem_ops来向底层申请videobuffer缓冲内存等操作,同时给外部对接平台提供vb2_ops接口来控制vb2_queue。videobuf2-memops为各种缓冲内存分配分式的公共使用部份。videobuf2-dma-contig、videobuf2-dma-sg、videobuf2-vmalloc分别为vb2_mem_ops的具体实现"子类",分别表示:连续的DMA缓冲区、集散的DMA缓冲区以及vmalloc方式创建的缓冲区。

对于以上几种内存分配方式,内核都已经帮我们实现了,具体平台可以根据实际情况来选择使用。A64是DMA连续缓冲区 ION内存分配方式来操作(videobuf2-dma-contig)。

videobuf2-dma-contig分析。

         1.1、videobuf2-dma-contig  videobuf2-dma-sg  videobuf2-vamalloc

        

1.2、vb2_queue  vb2_buffer  vb2_ops

vb2_queue管理videobuffer队列,用链表串接成buffer成员,vb2_buffer是这个队列中的成员,vb2_mem_ops是videobuf2 core对缓冲内存的操作函数集如申请释放等,vb2_ops用来管理队列buffer,留给对接平台去实现,videobuf2-core中会调用到。

vb2_queue由多个vb2_buffer串接的链表组成,有2个链表:done_list(数据已装载,输出)与queued_list(输入,非装载)。vb2_buffer的个数由上层申请,内核会向平台资询(通过留给平台的vb2_ops接口,最小3个),取得可以申请的buffer数量,每个buffer的plane数量,以及plane_size(cmd VIDIOC_REQBUFS)。成员中const struct vb2_mem_ops *mem_ops负责具体的申请视频缓存内存等;const struct vb2_ops *ops留给具体平台来操作vb2_queue队列。对于 capture device 来说,当输入队列中的 buffer 被塞满数据以后会自动变为输出队列,等待调用 VIDIOC_DQBUF 将数据进行处理以后重新调用VIDIOC_QBUF 将 buffer 重新放进输入队列。

vb2_buffer则是一个申请的内存buffer的描述,包括buffer的offset,size,与各plane(如果是multi-plane)信息等。该buffer既可以串接到vb2_queue的done_list(done_entry),也可以串接到queued_list(queued_entry)。

vb2_buffer状态转换图:

vb2_ops接口的定义如下所示:

videobuf2-core中会调用到vb2_ops相关接口。从stream_on到queue、dq,再到stream_off的几乎每一步都有涉及。这里面的接口不一定都需要去实现,取决于平台本身。

queue_setup:用于videobuf2-core向平台查询信息,如:内存申请context(alloc_ctx,videobuf2实现的3种内存分配方式之一)、mplane数、buffer count,buffer size等。

buf_queue:将videobuffer state置成VIDEOBUF_QUEUED状态,并将vb.queue加入到active队列当中,处于active队列当中的buffer,允许csi对其填充数据。

buf_prepare:将当前的size,width,height,field赋值给videobuffer,并将videobuffer state置成VIDEOBUF_PREPARED,以便videobuf2-core对buffer状态进行判断。同时,vb2.boff赋值成buffer对应的物理地址。

a64 vfe驱动中vb2_queue的创建如下所示,在vfe probe_work的workqueue里面完成的。先取alloc_ctx,再设置vb2_queue的2个ops,,timestamp_type类型,再vb2_queue_init()。

mem_opst vb2_dma_contig_memops是内核里面现成实现的。

vb2_queue必须实现以下接口,否则不会vb2_queue_init不会成功。

1.3 vb2_queue的done_wq与fh_event

        

1.4 io_mode vb2 read/ioctl/poll/mmap方式(file_io)

         2、v4l2 dev部份

2.1、v4l2-device

         v4l2-device是设备核心,在v4l2框架中充当所有v4l2_subdev的父设备,管理着注册在其下的子设备,其它subdev、ioctl、ctrls必须依附在具体v4l2-device上操作。

                           

                           

         2.2 v4l2-dev(video-device)

         video_device结构体用于在/dev目录下生成设备节点文件,把操作设备的接口暴露给用户空间。

                               

Video_device分配和释放,用于分配和释放video_device接口:

struct video_device *video_device_alloc(void)

void video_device_release(struct video_device *vdev)

video_device注册和注销,实现video_device结构体的相关成员后,就可以调用下面的接口进行注册:

static inline int __must_checkvideo_register_device(struct video_device *vdev, inttype, int nr)

void video_unregister_device(struct video_device*vdev);

vdev:需要注册和注销的video_device;

type:设备类型,包括VFL_TYPE_GRABBER、VFL_TYPE_VBI、VFL_TYPE_RADIO和VFL_TYPE_SUBDEV。

nr:设备节点名编号,如/dev/video[nr]。

int __video_register_device(struct video_device *vdev, int type, int nr, int warn_if_nr_in_use, struct module *owner)

{

int i = 0;

int ret;

int minor_offset = 0;

int minor_cnt = VIDEO_NUM_DEVICES;

const char *name_base;

/* A minor value of -1 marks this video device as never

having been registered */

vdev->minor = -1;

/* the release callback MUST be present */

if (WARN_ON(!vdev->release))

return -EINVAL;

/* v4l2_fh support */

spin_lock_init(&vdev->fh_lock);

INIT_LIST_HEAD(&vdev->fh_list);

/* Part 1: check device type */

switch (type) {

case VFL_TYPE_GRABBER:

name_base = "video";

break;

case VFL_TYPE_VBI:

name_base = "vbi";

break;

case VFL_TYPE_RADIO:

name_base = "radio";

break;

case VFL_TYPE_SUBDEV:

name_base = "v4l-subdev";

break;

default:

printk(KERN_ERR "%s called with unknown type: %d\n",

__func__, type);

return -EINVAL;

}

vdev->vfl_type = type;

vdev->cdev = NULL;

if (vdev->v4l2_dev) {

if (vdev->v4l2_dev->dev)

vdev->parent = vdev->v4l2_dev->dev;

if (vdev->ctrl_handler == NULL)

vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;

/* If the prio state pointer is NULL, then use the v4l2_device

prio state. */

if (vdev->prio == NULL)

vdev->prio = &vdev->v4l2_dev->prio;

}

/* Part 2: find a free minor, device node number and device index. */

#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES

/* Keep the ranges for the first four types for historical

* reasons.

* Newer devices (not yet in place) should use the range

* of 128-191 and just pick the first free minor there

* (new style). */

switch (type) {

case VFL_TYPE_GRABBER:

minor_offset = 0;

minor_cnt = 64;

break;

case VFL_TYPE_RADIO:

minor_offset = 64;

minor_cnt = 64;

break;

case VFL_TYPE_VBI:

minor_offset = 224;

minor_cnt = 32;

break;

default:

minor_offset = 128;

minor_cnt = 64;

break;

}

#endif

/* Pick a device node number */

mutex_lock(&videodev_lock);

nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);

if (nr == minor_cnt)

nr = devnode_find(vdev, 0, minor_cnt);

if (nr == minor_cnt) {

printk(KERN_ERR "could not get a free device node number\n");

mutex_unlock(&videodev_lock);

return -ENFILE;

}

#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES

/* 1-on-1 mapping of device node number to minor number */

i = nr;

#else

/* The device node number and minor numbers are independent, so

we just find the first free minor number. */

for (i = 0; i < VIDEO_NUM_DEVICES; i++)

if (video_device[i] == NULL)

break;

if (i == VIDEO_NUM_DEVICES) {

mutex_unlock(&videodev_lock);

printk(KERN_ERR "could not get a free minor\n");

return -ENFILE;

}

#endif

vdev->minor = i + minor_offset;

vdev->num = nr;

devnode_set(vdev);

/* Should not happen since we thought this minor was free */

WARN_ON(video_device[vdev->minor] != NULL);

vdev->index = get_index(vdev);

mutex_unlock(&videodev_lock);

if (vdev->ioctl_ops)

determine_valid_ioctls(vdev);

/* Part 3: Initialize the character device */

vdev->cdev = cdev_alloc();

if (vdev->cdev == NULL) {

ret = -ENOMEM;

goto cleanup;

}

vdev->cdev->ops = &v4l2_fops;

vdev->cdev->owner = owner;

ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);

if (ret < 0) {

printk(KERN_ERR "%s: cdev_add failed\n", __func__);

kfree(vdev->cdev);

vdev->cdev = NULL;

goto cleanup;

}

/* Part 4: register the device with sysfs */

vdev->dev.class = &video_class;

vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);

if (vdev->parent)

vdev->dev.parent = vdev->parent;

dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);

ret = device_register(&vdev->dev);

if (ret < 0) {

printk(KERN_ERR "%s: device_register failed\n", __func__);

goto cleanup;

}

/* Register the release callback that will be called when the last

reference to the device goes away. */

vdev->dev.release = v4l2_device_release;

if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)

printk(KERN_WARNING "%s: requested %s%d, got %s\n", __func__,

name_base, nr, video_device_node_name(vdev));

/* Increase v4l2_device refcount */

if (vdev->v4l2_dev)

v4l2_device_get(vdev->v4l2_dev);

#if defined(CONFIG_MEDIA_CONTROLLER)

/* Part 5: Register the entity. */

if (vdev->v4l2_dev && vdev->v4l2_dev->mdev &&

vdev->vfl_type != VFL_TYPE_SUBDEV) {

vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L;

vdev->entity.name = vdev->name;

vdev->entity.info.v4l.major = VIDEO_MAJOR;

vdev->entity.info.v4l.minor = vdev->minor;

ret = media_device_register_entity(vdev->v4l2_dev->mdev,

&vdev->entity);

if (ret < 0)

printk(KERN_WARNING

"%s: media_device_register_entity failed\n",

__func__);

}

#endif

/* Part 6: Activate this minor. The char device can now be used. */

set_bit(V4L2_FL_REGISTERED, &vdev->flags);

mutex_lock(&videodev_lock);

video_device[vdev->minor] = vdev;

mutex_unlock(&videodev_lock);

return 0;

cleanup:

mutex_lock(&videodev_lock);

if (vdev->cdev)

cdev_del(vdev->cdev);

devnode_clear(vdev);

mutex_unlock(&videodev_lock);

/* Mark this video device as never having been registered. */

vdev->minor = -1;

return ret;

}

         2.3、v4l2-ioctl

         在video-device注册/dev/videoX char设备时,看到v4l2-dev.c里面的file_ops操作:

其中的ioctl的处理接口是v4l2_ioctl:

static long v4l2_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)

{

struct video_device *vdev = video_devdata(filp);

int ret = -ENODEV;

if (vdev->fops->unlocked_ioctl) {

struct mutex *lock = v4l2_ioctl_get_lock(vdev, cmd);

if (lock && mutex_lock_interruptible(lock))

return -ERESTARTSYS;

if (video_is_registered(vdev))

ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);

if (lock)

mutex_unlock(lock);

} else if (vdev->fops->ioctl) {

static DEFINE_MUTEX(v4l2_ioctl_mutex);

struct mutex *m = vdev->v4l2_dev ?

&vdev->v4l2_dev->ioctl_lock : &v4l2_ioctl_mutex;

if (cmd != VIDIOC_DQBUF && mutex_lock_interruptible(m))

return -ERESTARTSYS;

if (video_is_registered(vdev))

ret = vdev->fops->ioctl(filp, cmd, arg);

if (cmd != VIDIOC_DQBUF)

mutex_unlock(m);

} else

ret = -ENOTTY;

return ret;

}

可以看到调的是fops,而fops是v4l2-dev封装的一个v4l2_file_operations接口类,留给外部平台来实现。

以A64平台 vfe驱动为例:         

                           

可以看到vfe中fops-> ioctl,用的是v4l2-ioctl中内核现成实现的ioctl现成接口video_ioctl2。而要使用内核现成实现的这个接口,还必须得把v4l2_ioctl_ops给填上。

         v4l2_ioctl_ops是在v4l2-dev注册dev时,要外部实现的接口。在vfe驱动中:

当然如果平台也可以不直接使用内核现成的video_ioctl2,那就得自己实现v4l2_ioctl_ops。

         由上分析:调用v4l2-dev注册设备、节点,我们需要提供v4l2_file_ops与v4l2_ioctl_ops这2个接口给v4l2-dev。其中v4l2_file_ops是用来做文件操作,如open/read/write/map/ioctl等。v4l2_ioctl_ops是专门给ioctl用的,用于实现各类控制,也就是v4l2 api编程中常用的ioctl命令的实现。

来看内核v4l2-ioctl帮我们实现的现成的ioctl处理框架。留了2个接口给外部调用者:video_ioctl2()处理函数用来作统一处理入口;v4l2_ioctl_ops接口给平台对接实现。

         v4l2-ioctl中定义了所有可以操作的ioctl cmd命令,由v4l2_ioctl_ops接口统一给出。这些命令被分为2种类型: STD 与FUNC。

                           

                           

         所有v4l2协议的设备都必须支持下面的ioctl cmd (设备功能查询)。

                    

由于v4l2-ioctl中定义的ioctl cmd命令及处理有限,平台难免有扩展cmd。当ioctl cmd值不在v4l2-ioctl处理cmd范围内时, v4l2_ioctl会统一进行default处理,转发到平台注册的vidioc_default()处理相关cmd。可以理解成v4l2-core扩展ioctl cmd的方法。内核也定义了私有cmd范围。

                           

static long __video_do_ioctl(struct file *file,

unsigned int cmd, void *arg)

{

struct video_device *vfd = video_devdata(file);

const struct v4l2_ioctl_ops *ops = vfd->ioctl_ops;

bool write_only = false;

struct v4l2_ioctl_info default_info;

const struct v4l2_ioctl_info *info;

void *fh = file->private_data;

struct v4l2_fh *vfh = NULL;

int use_fh_prio = 0;

int debug = vfd->debug;

long ret = -ENOTTY;

if (ops == NULL) {

pr_warn("%s: has no ioctl_ops.\n",

video_device_node_name(vfd));

return ret;

}

if (test_bit(V4L2_FL_USES_V4L2_FH, &vfd->flags)) {

vfh = file->private_data;

use_fh_prio = test_bit(V4L2_FL_USE_FH_PRIO, &vfd->flags);

}

if (v4l2_is_known_ioctl(cmd)) {

info = &v4l2_ioctls[_IOC_NR(cmd)];

if (!test_bit(_IOC_NR(cmd), vfd->valid_ioctls) &&

!((info->flags & INFO_FL_CTRL) && vfh && vfh->ctrl_handler))

goto done;

if (use_fh_prio && (info->flags & INFO_FL_PRIO)) {

ret = v4l2_prio_check(vfd->prio, vfh->prio);

if (ret)

goto done;

}

} else { -->如果不在v4l2-ioctl处理cmd范围内

default_info.ioctl = cmd;

default_info.flags = 0;

default_info.debug = v4l_print_default;

info = &default_info;

}

write_only = _IOC_DIR(cmd) == _IOC_WRITE;

if (info->flags & INFO_FL_STD) {

typedef int (*vidioc_op)(struct file *file, void *fh, void *p);

const void *p = vfd->ioctl_ops;

const vidioc_op *vidioc = p + info->u.offset;

ret = (*vidioc)(file, fh, arg);

} else if (info->flags & INFO_FL_FUNC) {

ret = info->u.func(ops, file, fh, arg);

} else if (!ops->vidioc_default) {

ret = -ENOTTY;

} else { -->不在范围内的cmd转到default处理

ret = ops->vidioc_default(file, fh,

use_fh_prio ? v4l2_prio_check(vfd->prio, vfh->prio) >= 0 : 0,

cmd, arg);

}

done:

if (debug) {

v4l_printk_ioctl(video_device_node_name(vfd), cmd);

if (ret < 0)

pr_cont(": error %ld", ret);

if (debug == V4L2_DEBUG_IOCTL)

pr_cont("\n");

else if (_IOC_DIR(cmd) == _IOC_NONE)

info->debug(arg, write_only);

else {

pr_cont(": ");

info->debug(arg, write_only);

}

}

return ret;

}

        

         由上分析ioctl调用总图:

                           

         2.4、v4l2-subdev

V4l2_subdev代表子设备,包含了子设备的相关属性和操作。先来看下结构体原型:

                           

         每个子设备驱动都需要实现一个v4l2_subdev结构体,v4l2_subdev可以内嵌到其它结构体中,也可以独立使用。结构体中包含了对子设备操作的成员v4l2_subdev_ops和v4l2_subdev_internal_ops。

                                                

         视频设备通常需要实现core和video成员,这两个OPS中的操作都是可选的,但是对于视频流设备video->s_stream(开启或关闭流IO)必须要实现。

v4l2_subdev_internal_ops结构体原型如下:

v4l2_subdev_internal_ops是向V4L2框架提供的接口,只能被V4L2框架层调用。在注册或打开子设备时,进行一些辅助性操作。

当我们把v4l2_subdev需要实现的成员都已经实现,就可以调用以下函数把子设备注册到V4L2核心层:

int v4l2_device_register_subdev(struct v4l2_device*v4l2_dev, struct v4l2_subdev *sd)

当卸载子设备时,可以调用以下函数进行注销:

void v4l2_device_unregister_subdev(struct v4l2_subdev*sd)

         2.4v4l2_fh

         v4l2_fh是用来保存子设备的特有操作方法,也就是下面要分析到的v4l2_ctrl_handler,内核提供一组v4l2_fh的操作方法,通常在打开设备节点时进行v4l2_fh注册。

初始化v4l2_fh,添加v4l2_ctrl_handler到v4l2_fh:

void v4l2_fh_init(struct v4l2_fh *fh, structvideo_device *vdev)

添加v4l2_fh到video_device,方便核心层调用到:

void v4l2_fh_add(struct v4l2_fh *fh)

         2.5、v4l2-ctrl  v4l2_ctrl_handler(子设备的handler的串接)

         v4l2_ctrl对象描述的控制特性和跟踪控制的值(包括最近值和新值)。

v4l2_ctrl_handler是用于保存子设备控制方法集的结构体,v4l2-ctrl是具体每一项的控制。对于视频设备这些ctrls包括设置亮度、饱和度、对比度和清晰度等,用链表的方式来保存ctrls,可以通过v4l2_ctrl_new_std函数向链表添加ctrls。

用户空间可以通过ioctl的VIDIOC_S_CTRL指令调用到v4l2_ctrl_handler,id透过arg参数传递。

v4l2-ctrl有几种创建方法:

struct v4l2_ctrl *v4l2_ctrl_new_custom(struct v4l2_ctrl_handler *hdl,const struct v4l2_ctrl_config *cfg, void *priv)

struct v4l2_ctrl *v4l2_ctrl_new_std(struct v4l2_ctrl_handler *hdl, const struct v4l2_ctrl_ops *ops,u32 id, s32 min, s32 max, u32 step, s32 def)

struct v4l2_ctrl *v4l2_ctrl_new_std_menu(struct v4l2_ctrl_handler *hdl,const struct v4l2_ctrl_ops *ops,u32 id, s32 max, s32 mask, s32 def)

struct v4l2_ctrl *v4l2_ctrl_new_std(structv4l2_ctrl_handler *hdl,

conststruct v4l2_ctrl_ops *ops,

u32id, s32 min, s32 max, u32 step, s32 def)

hdl是初始化好的v4l2_ctrl_handler结构体;

ops是v4l2_ctrl_ops结构体,包含ctrls的具体实现;

id是通过IOCTL的arg参数传过来的指令,定义在v4l2-controls.h文件;

min、max用来定义某操作对象的范围。如:

v4l2_ctrl_new_std(hdl, ops, V4L2_CID_BRIGHTNESS,-208, 127, 1, 0);

v4l2-ctrl类型:

v4l2-ctrl class分类:

v4l2_ctrl_handler->ctrl_refs专门用来私有cmd处理参考:

在v4l2-ioctl里面,可以看到v4l2_ctrl_handler是通过handler对象来单独设置参数值的,因此在ioctl里面有些接口函数(v4l_queryctrl、v4l_querymenu、v4l_g_ctrl、v4l_s_ctrl、v4l_g_ext_ctrls、v4l_s_ext_ctrls)就可以放到v4l2_ctrl_handler中去统一实现。

v4l2_ctrl_handler专门针对subdev的接口(与subdev->ctrl_handler联用):

         vfe的v4l2-ctls 与v4l2_ctrl_handler创建:

                           

                           

应用层cmd下到v4l2_ctrl_handler:

2.5、lock(ioctl_lock  buf_lock)

                           

         在videobuf2-core中,wait_for_done_vb操作时,

static int __vb2_wait_for_done_vb(struct vb2_queue *q, int nonblocking)

{

/*

* All operations on vb_done_list are performed under done_lock

* spinlock protection. However, buffers may be removed from

* it and returned to userspace only while holding both driver's

* lock and the done_lock spinlock. Thus we can be sure that as

* long as we hold the driver's lock, the list will remain not

* empty if list_empty() check succeeds.

*/

for (;;) {

int ret;

if (!q->streaming) {

dprintk(1, "Streaming off, will not wait for buffers\n");

return -EINVAL;

}

if (!list_empty(&q->done_list)) {

/*

* Found a buffer that we were waiting for.

*/

break;

}

if (nonblocking) {

dprintk(1, "Nonblocking and no buffers to dequeue, "

"will not wait\n");

return -EAGAIN;

}

/*

* We are streaming and blocking, wait for another buffer to

* become ready or for streamoff. Driver's lock is released to

* allow streamoff or qbuf to be called while waiting.

*/

call_qop(q, wait_prepare, q);

/*

* All locks have been released, it is safe to sleep now.

*/

dprintk(3, "Will sleep waiting for buffers\n");

ret = wait_event_interruptible(q->done_wq,

!list_empty(&q->done_list) || !q->streaming);

/*

* We need to reevaluate both conditions again after reacquiring

* the locks or return an error if one occurred.

*/

call_qop(q, wait_finish, q);

if (ret) {

dprintk(1, "Sleep was interrupted\n");

return ret;

}

}

return 0;

}

可见wait_prepare、wait_finish的锁(vfe_lock、vfe_unlock)是专门用来上层阻塞读取vb用的。

2.6、 IO mode (这里也待理解)

         v4l2-core支持以下几种内存方式,core已经帮我们实现了几种io方式取数据。

         内存映射缓冲区(V4L2_MEMORY_MMAP),是在内核空间开辟缓冲区,应用通过mmap()系统调用映射到用户地址空间。这些缓冲区可以是大而连续DMA缓冲区、通过vmalloc()创建的虚拟缓冲区,或者直接在设备的IO内存中开辟的缓冲区(如果硬件支持);

         用户空间缓冲区(V4L2_MEMORY_USERPTR),是用户空间的应用中开辟缓冲区,用户与内核空间之间交换缓冲区指针。很明显,在这种情况下是不需要mmap()调用的,但驱动为有效的支持用户空间缓冲区,其工作将也会更困难。

read和write,是基本帧IO访问方式,通过read读取每一帧数据,数据需要在内核和用户之间拷贝,这种方式访问速度可能会非常慢;Read和write方式属于帧IO访问方式,每一帧都要通过IO操作,需要用户和内核之间数据拷贝,而后两种是流IO访问方式,不需要内存拷贝,访问速度比较快。内存映射缓冲区访问方式是比较常用的方式。

一般都采用mmap方式,将内核空间内存地址直接映射到应用,避免数据在内核与应用之间多做一次拷贝。体现在应用上就是先select,后ioctl直接取数据(不用read)。

以vfe驱动为例:

videobuf2-core中vb2_poll(),只有num_buffer为0时才会有效,去init_file_io,所以vfe是应该是用不到的(这里为什么是为0??出一帧数据读一帧??)。

再一个就是vb2_read,

显然,并没有用read方式,而是直接的ictl cmd(DQUEUE) ,这里也用不到。

附v4l2 api test code:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <assert.h>

#include <time.h>

#include <getopt.h>

#include <fcntl.h>

#include <unistd.h>

#include <errno.h>

#include <malloc.h>

#include <sys/stat.h>

#include <sys/types.h>

#include <sys/time.h>

#include <sys/mman.h>

#include <sys/ioctl.h>

#include <asm/types.h>

#include "sunxi_camera.h"

#define ALIGN_4K(x) (((x) + (4095)) & ~(4095))

#define ALIGN_16B(x) (((x) + (15)) & ~(15))

#define LOG_OUT(fmt, arg...) printf(fmt, ##arg)

#define LOGD LOG_OUT

#define LOGW LOG_OUT

#define LOGE LOG_OUT

#define MAX_BUF_NUM  (8)

typedef struct frame_map_buffer

{

void *mem;

int length;

}buffer_t;

buffer_t mMapBuffers[MAX_BUF_NUM];

unsigned int mBufferCnt = 0;

unsigned int mFrameRate = 0;

int mCameraFd = -1;

typedef struct frame_size

{

int width;

int height;

}frame_size_t;

struct capture_size

{

frame_size_t main_size;

frame_size_t subch_size;

};

static int openCameraDev(int device_id)

{

int ret;

struct v4l2_input inp;

struct v4l2_capability cap;

char dev_name[64];

sprintf(dev_name, "/dev/video%d", device_id);

mCameraFd = open(dev_name, O_RDWR | O_NONBLOCK, 0);

if(mCameraFd < 0)

{

LOG_OUT("open falied\n");

return -1;

}

ret = ioctl(mCameraFd, VIDIOC_QUERYCAP, &cap);

if (ret < 0)

{

LOGE("Error opening device: unable to query device\n");

goto err_end;

}

if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0)

{

LOGE("Error opening device: video capture not supported\n");

goto err_end;

}

if ((cap.capabilities & V4L2_CAP_STREAMING) == 0)

{

LOGE("Capture device does not support streaming i/o\n");

goto err_end;

}

inp.index = device_id;

if (-1 == ioctl(mCameraFd, VIDIOC_S_INPUT, &inp))

{

LOG_OUT("VIDIOC_S_INPUT %d error!\n", inp.index);

return -1;

}

return 0;

err_end:

close(mCameraFd);

mCameraFd = -1;

return -1;

}

static int closeCameraDev(void)

{

if (mCameraFd > 0)

close(mCameraFd);

mCameraFd = -1;

return 0;

}

static int setVideoFormat(struct capture_size capsize, int subch_en, int angle)

{

struct v4l2_format fmt;

struct rot_channel_cfg rot;

memset(&fmt, 0, sizeof(fmt));

fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;

fmt.fmt.pix.width       = capsize.main_size.width;

fmt.fmt.pix.height      = capsize.main_size.height;

fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420;    //V4L2_PIX_FMT_YUV422P;//V4L2_PIX_FMT_NV12;

fmt.fmt.pix.field       = V4L2_FIELD_NONE;                            //V4L2_FIELD_INTERLACED;

if (-1 == ioctl(mCameraFd, VIDIOC_S_FMT, &fmt))

{

LOGE("VIDIOC_S_FMT error!\n");

return -1;

}

if (subch_en)

{

struct v4l2_pix_format subch_fmt;

subch_fmt.width           = capsize.subch_size.width;

subch_fmt.height           = capsize.subch_size.height;

subch_fmt.pixelformat = V4L2_PIX_FMT_YUV420;   //V4L2_PIX_FMT_YUV422P;//V4L2_PIX_FMT_YUYV;

subch_fmt.field             = V4L2_FIELD_NONE;      //V4L2_FIELD_INTERLACED;

if (-1 == ioctl(mCameraFd, VIDIOC_SET_SUBCHANNEL, &subch_fmt))

{

LOGE("VIDIOC_SET_SUBCHANNEL error!\n");

return -1;

}

rot.sel_ch         = 1;

rot.rotation = angle;

if (-1 == ioctl (mCameraFd, VIDIOC_SET_ROTCHANNEL, &rot))

{

LOGE("VIDIOC_SET_ROTCHANNEL error!\n");

return -1;

}

}

//get format test

if (ioctl(mCameraFd, VIDIOC_G_FMT, &fmt) != -1)

{

LOG_OUT("resolution got from sensor = %d*%d\n",fmt.fmt.pix.width,fmt.fmt.pix.height);

}

return 0;

}

static unsigned int getFrameRate(void)

{

int ret = -1;

struct v4l2_streamparm parms;

parms.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

ret = ioctl(mCameraFd, VIDIOC_G_PARM, &parms);

if (ret < 0)

{

LOGE("VIDIOC_G_PARM getFrameRate error, %s\n", strerror(errno));

return ret;

}

int numerator = parms.parm.capture.timeperframe.numerator;

int denominator = parms.parm.capture.timeperframe.denominator;

LOGD("frame rate: numerator = %d, denominator = %d\n", numerator, denominator);

if ((numerator != 0) && (denominator != 0))

{

return denominator / numerator;

}

else

{

LOGW("unsupported frame rate: %d/%d\n", denominator, numerator);

return 25;

}

}

//static int setFrameRate(int framerate)

static int setCaptureParams(void)

{

struct v4l2_streamparm parms;

int frameRate = getFrameRate();

/*if (frameRate > framerate)

frameRate = framerate;*/

LOGD("set framerate: [%d]\n", frameRate);

parms.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

parms.parm.capture.timeperframe.numerator = 1;

parms.parm.capture.timeperframe.denominator = frameRate;

parms.parm.capture.capturemode = V4L2_MODE_VIDEO; //V4L2_MODE_IMAGE //V4L2_MODE_PREVIEW //

if (-1 == ioctl(mCameraFd, VIDIOC_S_PARM, &parms))

{

LOG_OUT("VIDIOC_S_PARM error\n");

return -1;

}

return 0;

}

static int tryFmt(int format)

{

int i;

struct v4l2_fmtdesc fmtdesc;

fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

for(i = 0; i < 12; i++)

{

fmtdesc.index = i;

if (-1 == ioctl(mCameraFd, VIDIOC_ENUM_FMT, &fmtdesc))

{

break;

}

LOGD("format index = %d, name = %s, v4l2 pixel format = %x\n", i, fmtdesc.description, fmtdesc.pixelformat);

if (fmtdesc.pixelformat == format)

{

return 0;

}

}

return -1;

}

static int requestBufs(int *buf_num)

{

int ret = 0;

struct v4l2_requestbuffers rb;

memset(&rb, 0, sizeof(rb));

rb.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE; //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

rb.memory = V4L2_MEMORY_MMAP;

rb.count  = *buf_num;

ret = ioctl(mCameraFd, VIDIOC_REQBUFS, &rb);

if (ret < 0)

{

LOG_OUT("VIDIOC_REQBUFS failed\n");

ret = -1;

return ret;

}

*buf_num = rb.count;

LOG_OUT("VIDIOC_REQBUFS count: %d\n", *buf_num);

return 0;

}

static int queryBuffers(int buf_num)

{

int i, j;

int ret;

struct v4l2_buffer buf;

assert(buf_num <= MAX_BUF_NUM);

memset(mMapBuffers, 0, sizeof(mMapBuffers));

for (i = 0; i < buf_num; i++)

{

memset(&buf, 0, sizeof(buf));

buf.type    = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

buf.memory      = V4L2_MEMORY_MMAP;

buf.index           = i;

if (-1 == ioctl(mCameraFd, VIDIOC_QUERYBUF, &buf))

{

LOG_OUT("VIDIOC_QUERYBUF [%d] fail\n", i);

for(j = i-1; j >= 0; j--)

{

munmap(mMapBuffers[j].mem, mMapBuffers[j].length);

mMapBuffers[j].length = 0;

}

return -1;

}

mMapBuffers[i].length = buf.length;

mMapBuffers[i].mem    = mmap( NULL,

buf.length,

PROT_READ | PROT_WRITE /* required */,

MAP_SHARED /* recommended */,

mCameraFd,

buf.m.offset );

if ((void *)-1 == mMapBuffers[i].mem)

{

LOG_OUT("[%d] mmap failed\n", i);

mMapBuffers[i].length = 0;

for(j = 0; j < i; j++)

{

munmap(mMapBuffers[j].mem, mMapBuffers[j].length);

mMapBuffers[j].length = 0;

}

return -1;

}

ret = ioctl(mCameraFd, VIDIOC_QBUF, &buf);  //put in a queue

if (ret < 0)

{

LOGE("VIDIOC_QBUF Failed\n");

return ret;

}

}

return 0;

}

static int unmapBuffers(void)

{

int i, ret=0;

for (i = 0; i < mBufferCnt; ++i)

{

if ((mMapBuffers[i].mem != (void *)-1) && (mMapBuffers[i].length > 0))

{

if (-1 == munmap(mMapBuffers[i].mem, mMapBuffers[i].length))

{

LOG_OUT("[%d] munmap error\n", i);

ret = -1;

}

}

}

return ret;

}

static int startStreaming(void)

{

int ret = -1;

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

ret = ioctl(mCameraFd, VIDIOC_STREAMON, &type);

if (ret < 0)

{

LOGE("StartStreaming: Unable to start capture\n");

return ret;

}

return 0;

}

static int stopStreaming(void)

{

int ret = -1;

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

ret = ioctl (mCameraFd, VIDIOC_STREAMOFF, &type);

if (ret < 0)

{

LOGE("StopStreaming: Unable to stop capture: %s\n", strerror(errno));

return ret;

}

LOGD("V4L2Camera::v4l2StopStreaming OK\n");

return 0;

}

static int waitFrame(void)

{

fd_set fds;

struct timeval tv;

int ret;

FD_ZERO(&fds);

FD_SET(mCameraFd, &fds);

/* Timeout */

tv.tv_sec  = 2;

tv.tv_usec = 0;

ret = select(mCameraFd + 1, &fds, NULL, NULL, &tv);

if (ret == -1)

{

LOGE("select err, %s\n", strerror(errno));

return -1;

}

else if (ret == 0)

{

LOGE("select timeout");

return 1;

}

return 0;

}

static int getFrame(struct v4l2_buffer *buf)

{

buf->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

buf->memory = V4L2_MEMORY_MMAP;

if (-1 == ioctl(mCameraFd, VIDIOC_DQBUF, buf))

return 0;

LOGD("get frame: [%d]\n", buf->index);

assert(buf->index < mBufferCnt);

return 1;

}

static int releaseFrame(/*int index*/struct v4l2_buffer *buf)

{

int ret = -1;

#if 1

ret = ioctl(mCameraFd, VIDIOC_QBUF, buf);

#else

struct v4l2_buffer buf;

memset(&buf, 0, sizeof(struct v4l2_buffer));

buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;  //V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE

buf.memory = V4L2_MEMORY_MMAP;

buf.index  = index;

LOGD("release frame: [%d]\n", buf.index);

ret = ioctl(mCameraFd, VIDIOC_QBUF, &buf);

if (ret != 0)

{

LOGE("releaseFrame: VIDIOC_QBUF Failed: index=%d, ret=%d, %s\n", buf.index, ret, strerror(errno));

}

#endif

return ret;

}

static int saveFrame(struct v4l2_buffer *buf, struct capture_size capsize, int subch_en, int angle)

{

int ret = -1;

char file_name[64];

void *yuvStart = NULL;

FILE *fp = NULL;

int i, pic_num=1;

int pic_size[/*3*/2];

LOGD("length: [%d], mem: [%p]\n", mMapBuffers[buf->index].length, mMapBuffers[buf->index].mem);

yuvStart = mMapBuffers[buf->index].mem;

pic_size[0] = ALIGN_16B(capsize.main_size.width) * capsize.main_size.height *3/2;

if (subch_en)

{

pic_num++;

pic_size[1] = ALIGN_16B(capsize.subch_size.width) * capsize.subch_size.height *3/2;

if (angle==90 || angle==270)

{

//pic_num++;

//pic_size[2] = ALIGN_16B(capsize.subch_size.height) * capsize.subch_size.width *3/2;  //90度旋转

pic_size[1] = ALIGN_16B(capsize.subch_size.height) * capsize.subch_size.width *3/2;  //90度旋转

}

}

for (i = 0; i < pic_num; i++)

{

#if 1

sprintf(file_name, "/mnt/sdcard/data_%d_yuv.bin", i);

fp = fopen(file_name, "wb");

fwrite(yuvStart, pic_size[i], 1, fp);

fclose(fp);

#else

sprintf(file_name, "/mnt/sdcard/data_%d_y.bin", i);

fp = fopen(file_name, "wb");

fwrite(yuvStart, pic_size[i]*2/3, 1, fp);

fclose(fp);

sprintf(file_name, "/mnt/sdcard/data_%d_u.bin", i);

fp = fopen(file_name, "wb");

fwrite(yuvStart + pic_size[i]*2/3, pic_size[i]/6, 1, fp);

fclose(fp);

sprintf(file_name, "/mnt/sdcard/data_%d_v.bin", i);

fp = fopen(file_name, "wb");

fwrite(yuvStart + pic_size[i]*2/3 + pic_size[i]/6, pic_size[i]/6, 1, fp);

fclose(fp);

#endif

yuvStart += ALIGN_4K(pic_size[i]);

}

return 0;

}

static int setWhiteBalance(int wb)

{

struct v4l2_control ctrl;

int ret = -1;

ctrl.id = V4L2_CID_AUTO_N_PRESET_WHITE_BALANCE;

ctrl.value = wb;

ret = ioctl(mCameraFd, VIDIOC_S_CTRL, &ctrl);

if (ret < 0)

LOGE("setWhiteBalance failed, %s", strerror(errno));

else

LOGD("setWhiteBalance ok");

return ret;

}

static int setTakePictureCtrl(enum v4l2_take_picture value)

{

struct v4l2_control ctrl;

int ret = -1;

ctrl.id = V4L2_CID_TAKE_PICTURE;

ctrl.value = value;

ret = ioctl(mCameraFd, VIDIOC_S_CTRL, &ctrl);

if (ret < 0)

LOGE("setTakePictureCtrl failed, %s", strerror(errno));

else

LOGD("setTakePictureCtrl ok");

return ret;

}

static int setExposureMode(int mode)

{

int ret = -1;

struct v4l2_control ctrl;

ctrl.id = V4L2_CID_EXPOSURE_AUTO;

ctrl.value = mode;

ret = ioctl(mCameraFd, VIDIOC_S_CTRL, &ctrl);

if (ret < 0)

LOGE("setExposureMode failed, %s", strerror(errno));

else

LOGD("setExposureMode ok");

return ret;

}

static int setAutoFocus(int enable)

{

int ret = -1;

struct v4l2_control ctrl;

if (enable)

ctrl.id = V4L2_CID_AUTO_FOCUS_START;

else

ctrl.id = V4L2_CID_AUTO_FOCUS_STOP;

ctrl.value = 0;

ret = ioctl(mCameraFd, VIDIOC_S_CTRL, &ctrl);

if (ret < 0)

LOGE("setExposureMode failed, %s", strerror(errno));

else

LOGD("setExposureMode ok");

return ret;

}

static void initLocalVar(void)

{

memset(mMapBuffers, 0, sizeof(mMapBuffers));

mBufferCnt = MAX_BUF_NUM;

mFrameRate = 25;

mCameraFd = -1;

}

int main(int argc, char **argv)

{

int ret;

int dev_id = 0;

struct capture_size capsize;

int subch_en=0, angle=0;

int cnt=0, cap_frame_num = 10;

capsize.main_size.width  = 640;

capsize.main_size.height = 480;

capsize.subch_size.width  = 320;

capsize.subch_size.height = 240;

if(argc > 1)

dev_id = atoi(argv[1]);

if(argc > 3)

{

capsize.main_size.width = atoi(argv[2]);

capsize.main_size.height = atoi(argv[3]);

}

if(argc > 4)

mFrameRate = atoi(argv[4]);

if(argc > 5)

cap_frame_num = atoi(argv[5]);

if(argc > 6)

subch_en = atoi(argv[6]);

if(argc > 8)

{

capsize.subch_size.width  = atoi(argv[7]);

capsize.subch_size.height = atoi(argv[8]);

}

if(argc > 9)

angle = atoi(argv[9]);

initLocalVar();

//1

ret = openCameraDev(dev_id);

if (ret)

goto err_out;

//2

setCaptureParams();

setVideoFormat(capsize, subch_en, angle);

//3

requestBufs(&mBufferCnt);

queryBuffers(mBufferCnt);

//setAutoFocus(0); //for test

//4

startStreaming();

//5

struct v4l2_buffer buf;

while (cnt < cap_frame_num)

{

ret = waitFrame();

if (ret > 0)

{

LOGE("error! some thing wrong happen, should stop!!");

break;

}

else if (ret == 0)

{

if (getFrame(&buf))

{

if (cnt++ == cap_frame_num/2)

saveFrame(&buf, capsize, subch_en, angle);

releaseFrame(&buf/*buf.index*/);

}

}

}

//6

stopStreaming();

//7

unmapBuffers();

//8

closeCameraDev();

return 0;

err_out:

return -1;

}

基于全志A64平台v4l2驱动分析相关推荐

  1. 全志A64平台 TP9950 BT656输入驱动调试(1)环境搭建驱动编写

    1. 前言 由于需要在全志A64平台上接入模拟相机,这里选型的模拟转换芯片是TP9950; (1)TP9950可以通过CSI接口输出,也可以通过并行的BT656接口输出: (2)由于A64平台没有MI ...

  2. 旋转机械 | 基于ANSYS WB平台的滑动轴承分析工具(一)

    导读:本文主要针对Tribo-X inside ANSYS的功能及各方向应用实例进行介绍,限于篇幅关系会分五篇进行介绍,第一篇主要结合软件的需求.理论.功能及应用方向进行介绍,第二篇至第五篇将结合具体 ...

  3. V4l2框架-平台V4L2驱动共性

    文章目录 一.前言 二.主要结构体 2.1 v4l2_device 三.应用程序调用系统函数到驱动流程 3.1 使用open函数打开文件结点 3.2 read函数 3.3 ioctl 本文为学习v4l ...

  4. MTK平台闪光灯驱动分析

    MTK平台闪光灯驱动分析   以前没写过博客,总想写着来着,把之前学到的做过的东西都记录下来,但是一直没有时间也没那么大的决心.这次趁着刚换工作,正在学习熟悉平台不是太忙的机会,把自己总结的文档写下来 ...

  5. 【分析笔记】全志 i2c-sunxi.c 控制器驱动分析

    分析平台:全志 A64 内核版本:Linux 4.9 数据手册:Allwinner_A64_User_Manual_V1.1.pdf (whycan.com) 驱动框架 I2C 设备驱动 作为方案应用 ...

  6. 全志A64平台 TP9950 BT656输入驱动调试(2)驱动调试

    1. 前言 承接上篇文章, 这里主要是驱动调试部分 2. 调试记录 (1)硬件引脚梳理 9950没有上电时序要求,直接在dts中将PMU电配好即可 (2) 调试I2C tp9950地址为0x44: 使 ...

  7. 全志t3linux驱动_全志T3 Linux显示驱动分析

    1.总体架构 全志T3处理器的显示框架是基于标准Linux的帧缓冲架构,其结构如图 1.1所示.显示控制器DE的驱动架构如图 1.2所示,包括屏蔽差异的显示管理抽象层,以及显示图层驱动.显示设备驱动. ...

  8. 全志T3 Linux显示驱动分析

    1.总体架构 全志T3处理器的显示框架是基于标准Linux的帧缓冲架构,其结构如图 1.1所示.显示控制器DE的驱动架构如图 1.2所示,包括屏蔽差异的显示管理抽象层,以及显示图层驱动.显示设备驱动. ...

  9. 高通 android平台LCD驱动分析

    目前手机芯片厂家提供的源码里包含整个LCD驱动框架,一般厂家会定义一个xxx_fb.c的源文件,注册一个平台设备和平台驱动,在驱动的probe函数中来调用register_framebuffer(), ...

最新文章

  1. LIST 和 MAP
  2. 深入理解 Android Https
  3. WC前的颓废——带花树
  4. python requests 报错 Connection aborted ConnectionResetError RemoteDisconnected 解决方法
  5. 输入密码查看flag(详解)——Bugku
  6. Parallels Desktop:pd虚拟机 17 for mac
  7. 摄影测量学——解析法相对定向
  8. 计算流体力学编程作业matlab,中科大计算流体力学CFD之大作业二.doc
  9. python基础打卡(9/18)
  10. lisp线段两端同时缩短的命令_cad中有没有办法同时对多条线段偏移?
  11. 1972:【15NOIP普及组】推销员
  12. ps制作台式计算机图标,ps制作计算机图标
  13. 伪原创文章需要注意的地方有哪些
  14. 0基础快速入门CSS技术栈(6)—图解详细阐述说透CSS的浮动及应用、浮动的扩展及清除浮动和详解快速·1photoshop切图(附详细案例源码解析过程)2021-01-07更新
  15. 吴恩达机器学习课程:编程练习 | (7) ex7-kmeans and PCA
  16. 【六】Python全栈之路--for循环
  17. 苹果平板可以用html么,苹果iPad怎么用?iPad新手必备十个使用心得分享(必看)...
  18. 「武汉理工大学 软件工程复习」第一章 | 软件工程概述
  19. JAR 文件打包方法
  20. 如何设计一个三极管放大电路

热门文章

  1. Java练习 -------------抽象
  2. python爬虫爬取天气正则表达式解析案例
  3. 计算机二级刷题先EXCEL,备考篇丨迎接计算机二级考试
  4. 【机器学习周志华】读书笔记 P3 机器学习发展历程(选读)
  5. js实现页面刷新、全屏、退出全屏
  6. git定位追踪一行或若干行代码修改历史记录
  7. 【GDPMS】项目管理实战公益培训第十二期
  8. AI智能加持,景联文科技助力扫地机器人突破技术创新,维护核心竞争力,提供数据采集服务
  9. 【每天一个java设计模式(十八)】 - 观察者模式
  10. C++中虚析构函数和纯虚函数的作用