camera_kernel之---videobuffer的申请与map(4)

越伯寅
2023-12-01

对于camera 模块除了逻辑之外,我关注的点就内存的使用,后续还会写些内存相关的部分。这里只探讨如何申请buffer。
当前buffer申请分为
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1,
V4L2_MEMORY_USERPTR = 2,
V4L2_MEMORY_OVERLAY = 3,
V4L2_MEMORY_DMABUF = 4,
};
mmap是下面详细要讲的。OVERLAY这种方式似乎没用过,之前咨询过相关人员,似乎overlay是早期类似gralloc buffer这种显存没有统一框架,然后实现的一种camera数据直显的一种方式。
DMABUF和USERPTR很类似,都是在上层申请buffer,然后将指针直接赋值给vb2模块,当然这种申请都要保证物理地址连续。DMABUF需要通过DMA的接口,export出一个fd,在vb2记录的是这个fd的值。接下来看mmap方式

1.hal层调用
V4L2VideoNode::requestBuffers
里面调用

req_buf.memory = memType;
req_buf.count = num_buffers;
req_buf.type = mBufType;
ret = pioctl(mFd, VIDIOC_REQBUFS, &req_buf, mName.c_str());

#define pioctl(fd, ctrlId, attr, name)
SysCall::ioctl(fd, ctrlId, attr)
所以执行的是

SysCall::ioctl(mFd, VIDIOC_REQBUFS, &req_buf);
int SysCall::ioctl(int fd, int request, void *arg)
{
    return ::ioctl(fd, request, (void *)arg);
}

Rk对ioctl做了几次封装,只管传入的参数。
memType,num_buffers,mBufType
memType就是前面说的几种buffer类型
num_buffers 一般是申请4个buffer轮训
mBufType,对于capture设备来说,一般都是V4L2_BUF_TYPE_VIDEO_CAPTURE,但是rk用的V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,这样可以支持多个平面,可以支持更多数据类型。

2.驱动函数

static int v4l_reqbufs(const struct v4l2_ioctl_ops *ops,
				struct file *file, void *fh, void *arg)
{
	struct v4l2_requestbuffers *p = arg;
	int ret = check_fmt(file, p->type);
……
	CLEAR_AFTER_FIELD(p, memory);
	return ops->vidioc_reqbufs(file, fh, p);//执行video节点的ioctl_ops
}

static const struct v4l2_ioctl_ops rkisp1_v4l2_ioctl_ops = {
.vidioc_reqbufs = vb2_ioctl_reqbufs,

vb2_ioctl_reqbufs执行的是
vb2_core_reqbufs(vdev->queue, p->memory, &p->count);//进入到vb2_queue

2.1
ret = call_qop(q, queue_setup, q, NULL, &num_buffers, &num_planes,
q->plane_sizes, q->alloc_ctx);
看call_qop执行的是q->ops->queue_setup函数
q->ops = &rkisp1_vb2_ops;
.queue_setup = rkisp1_queue_setup,
函数定义

static int rkisp1_queue_setup(struct vb2_queue *queue,
			      const void *parg,
			      unsigned int *num_buffers,
			      unsigned int *num_planes,
			      unsigned int sizes[],
			      void *alloc_ctxs[])
{
……
		pixm = &stream->out_fmt;//输出格式
		isp_fmt = &stream->out_isp_fmt;//capture格式,会在s_fmt赋值
……
*num_planes = isp_fmt->mplanes;//capture_fmt sp_fmts,根据格式找到对应plane
	for (i = 0; i < isp_fmt->mplanes; i++) {
		const struct v4l2_plane_pix_format *plane_fmt;

		plane_fmt = &pixm->plane_fmt[i];
		sizes[i] = plane_fmt->sizeimage;// sizeimage也是在s_fmt中根据rk定义的每种格式的bpp,也就是每个pix要占用的字节数算出来的。
		alloc_ctxs[i] = dev->alloc_ctx;//在初始化的时候,dev->alloc_ctx = vb2_dma_contig_init_ctx(dev->v4l2_dev.dev),是vb2_dc_conf结构体,
	}

//rk代码里只有yuv格式最后带M的才有多个plane,所以一般都是一个plane

2.2 然后调用 __vb2_queue_alloc申请内存
这个函数是初始化vb2_buffer结构,保存在vb2_queue里
__vb2_queue_alloc(q, memory, num_buffers, num_planes);

static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,
			     unsigned int num_buffers, unsigned int num_planes)
{
……
vb->state = VB2_BUF_STATE_DEQUEUED;//申请默认为dq状态
……
		/* Allocate video buffer memory for the MMAP type */
		if (memory == VB2_MEMORY_MMAP) {//为什么没看到DMABUF?
			ret = __vb2_buf_mem_alloc(vb);
……
ret = call_vb_qop(vb, buf_init, vb);
……
		q->bufs[q->num_buffers + buffer] = vb;//保存到vb2_queue
……
}
static int __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{
	struct vb2_queue *q = vb->vb2_queue;
	void *mem_priv;
	int plane;
	for (plane = 0; plane < vb->num_planes; ++plane) {
		unsigned long size = PAGE_ALIGN(q->plane_sizes[plane]);
		mem_priv = call_ptr_memop(vb, alloc, q->alloc_ctx[plane],
				      size, q->dma_dir, q->gfp_flags);//gpf表示内存标志,比如GFP_KERNEL,GFP_DMA(DMA应该是预留了空间)
……
		vb->planes[plane].mem_priv = mem_priv;//保存参数是vb2_dc_buf类型
		vb->planes[plane].length = q->plane_sizes[plane];
	}

在kernel/drivers/media/platform/rockchip/isp1/capture.c
rkisp_init_vb2_queue
q->mem_ops = &vb2_dma_contig_memops;
memops里面的alloc是vb2_dc_alloc
这个函数是videobuf2提供的

static void *vb2_dc_alloc(void *alloc_ctx, unsigned long size,
			  enum dma_data_direction dma_dir, gfp_t gfp_flags)
{
	struct vb2_dc_conf *conf = alloc_ctx;
	struct device *dev = conf->dev;
	struct vb2_dc_buf *buf;

	buf = kzalloc(sizeof *buf, GFP_KERNEL);
	if (!buf)
		return ERR_PTR(-ENOMEM);

	buf->attrs = conf->attrs;//这个为NULL
	buf->cookie = dma_alloc_attrs(dev, size, &buf->dma_addr,
					GFP_KERNEL | gfp_flags, &buf->attrs);
static inline void *dma_alloc_attrs(struct device *dev, size_t size,
				       dma_addr_t *dma_handle, gfp_t flag,
				       struct dma_attrs *attrs)
{
	struct dma_map_ops *ops = get_dma_ops(dev);
	void *cpu_addr;

	BUG_ON(!ops);
	if (dma_alloc_from_coherent(dev, size, dma_handle, &cpu_addr))
		return cpu_addr;

	if (!arch_dma_alloc_attrs(&dev, &flag))
		return NULL;
	if (!ops->alloc)
		return NULL;
	cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
	debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
	return cpu_addr;
}

dma_alloc_from_coherent从预留连续内存池分配,当前用的预留内存有两种形式
1.cma 这个是针对所有的模块都可以申请使用
2.device专属内存,会在dts中配置专属内存起始地址等,只给当前device使用。
rk早期是用过这两种实现方式,后期rk对isp使用iommu。这样就不需要预分配连续内存了,这样可以节省内存,只在申请的时候用。iommu可以把离散的内存块,映射成连续总线地址,这样dma也可以使用。

所以执行的是ops->alloc(dev, size, dma_handle, flag, attrs);
关于rkiommu的文章写了alloc对应的函数
__iommu_alloc_attrs
这其实是分配iova,可以对应多段物理内存。对于dma接口,他是连续的,是以页为单位申请。然后iova和申请的page生成译码表。

注意这个attrs,申请内存的相关属性,在iommu申请的代码中,如果没有指定属性,iommu默认配置成writeombine的,这个其实是读uncached。所以如果发现copy这段内存比较慢,可以改成正常能使用cache的。

一般在申请buffer之后会调用VIDIOC_QUERYBUF
pioctl(mFd , VIDIOC_QUERYBUF, vbuf.get(), mName.c_str())
QUERYBUF传入的是v4l2_buffer结构作为返回。
ops->vidioc_querybuf(file, fh, p); -> vb2_querybuf(vdev->queue, p)
vb2_querybuf先调用vb = q->bufs[b->index];就是获取之前申请的vb2_buffer封装的内存,然后调用vb2_core_querybuf(q, b->index, b)
call_bufop(q, fill_user_buffer, q->bufs[index], pb);
执行的是q->buf_ops-> fill_user_buffer
.fill_user_buffer = __fill_v4l2_buffer,

static int __fill_v4l2_buffer(struct vb2_buffer *vb, void *pb)
{
……
else {
		/*
		 * We use length and offset in v4l2_planes array even for
		 * single-planar buffers, but userspace does not.
		 */
		b->length = vb->planes[0].length;
		b->bytesused = vb->planes[0].bytesused;
		if (q->memory == VB2_MEMORY_MMAP)
			b->m.offset = vb->planes[0].m.offset;
		else if (q->memory == VB2_MEMORY_USERPTR)
			b->m.userptr = vb->planes[0].m.userptr;
		else if (q->memory == VB2_MEMORY_DMABUF)
			b->m.fd = vb->planes[0].m.fd;
	}

其实就是vb的信息转换或者说赋值到用户态的v4l2_buffer,但是前面看mem_priv里面保存了vb2_dc_buf信息(包含了申请的虚拟地址)

在调用QUERYBUF之后,会调用mmap函数。返回映射的handle或者叫地址
来看看mmap函数,原本我以为是公共函数,跟节点没关系,但既然传入节点fd。
发现video节点操作函数有.mmap = vb2_fop_mmap,
vb2_fop_mmap-- > vb2_mmap
快捷键目录标题文本样式列表链接代码片表格注脚注释自定义列表LaTeX 数学公式插入甘特图插入UML图插入Mermaid流程图插入Flowchart流程图插入类图
代码片复制

下面展示一些 内联代码片

// A code block
var foo = 'bar';
int vb2_mmap(struct vb2_queue *q, struct vm_area_struct *vma)
{
……//通过传下来的offset找到对应的plane
__find_plane_by_offset(q, off, &buffer, &plane);
……//调用mmap函数
call_memop(vb, mmap, vb->planes[plane].mem_priv, vma);

这里发现传下来的是vm_area_struct,看着和上层调用的参数完全不一致。
这个结构是描述一段虚拟空间,所有vm_area_struct结构体组成一个AVL树。
mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。将应用传下来的参数赋值到vm_area_struct,申请虚拟地址空间。然后这个结构体传入驱动。

因为
.mmap = vb2_dc_mmap

static int vb2_dc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
……
		ret = dma_mmap_attrs(buf->dev, vma, buf->cookie,
		buf->dma_addr, buf->size, &buf->attrs);
……
	vma->vm_flags		|= VM_DONTEXPAND | VM_DONTDUMP;
	vma->vm_private_data	= &buf->handler;
	vma->vm_ops		= &vb2_common_vm_ops;
	//这个open只是增加refcount
	vma->vm_ops->open(vma);
static inline int
dma_mmap_attrs(struct device *dev, struct vm_area_struct *vma, void *cpu_addr,
	       dma_addr_t dma_addr, size_t size, struct dma_attrs *attrs)
{
	struct dma_map_ops *ops = get_dma_ops(dev);
	BUG_ON(!ops);
	if (ops->mmap)
		return ops->mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
	return dma_common_mmap(dev, vma, cpu_addr, dma_addr, size);
}

ops->mmap是不为空的,执行到iommu 的mmap函数

static int __iommu_mmap_attrs(struct device *dev, struct vm_area_struct *vma,
			      void *cpu_addr, dma_addr_t dma_addr, size_t size,
			      struct dma_attrs *attrs)
{
	struct vm_struct *area;
	int ret;

	vma->vm_page_prot = arch_get_dma_pgprot(attrs, vma->vm_page_prot,
					        is_device_dma_coherent(dev));

	if (dma_mmap_from_coherent(dev, vma, cpu_addr, size, &ret))
		return ret;

	area = find_vm_area(cpu_addr);
	if (WARN_ON(!area || !area->pages))
		return -ENXIO;
	//后面有空看下这个函数
	return iommu_dma_mmap(area->pages, size, vma);
}

调用完mmap之后,都是要将buf入队。执行VIDIOC_QBUF
vb2_core_qbuf会调用
if (q->start_streaming_called)
__enqueue_in_driver(vb);
__enqueue_in_driver内部调用
call_void_memop(vb, prepare, vb->planes[plane].mem_priv);
.prepare = vb2_dc_prepare,
会调用对应的这个prepare函数,里面调用
dma_sync_sg_for_device//可以让设备再次获得缓冲区
和rkisp1_buf_queue这里面会

static void rkisp1_buf_queue(struct vb2_buffer *vb)
{
	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
	struct rkisp1_buffer *ispbuf = to_rkisp1_buffer(vbuf);

ispbuf->buff_addr[i] = vb2_dma_contig_plane_dma_addr(vb, i);

所以物理地址赋值是在这里。

之后用这个地址去填充isp的寄存器,驱动dma数据就可以搬运到这里

 类似资料: