NVDLA内核态驱动代码整理五


前言

本系列内容力求将nvdla的内核态驱动整理清楚,如果有分析不对的请指出。
前面已经分析了一大块代码了,链接分别如下:
系列文章1:NVDLA内核态驱动代码整理一
系列文章2:NVDLA内核态驱动代码整理二
系列文章3:NVDLA内核态驱动代码整理三
系列文章4:NVDLA内核态驱动代码整理四

欢迎阅读硬件信号和架构分析系列文章1:NVDLA硬件信号和架构设计整理一

本章是分析nvdla_core_callbacks.c代码第三部分。


可能仍然需要用到前面四篇文章下的结构体,因此此处贴出:

结构体功能
nvdla_gem_object包含重要的变量,首先是drm_gem_object,用于drm存储管理和分配的结构体;其次是*kvaddr:这是一个指针成员,通常用于存储内核虚拟地址。这个地址指向内核中的数据缓冲区,该缓冲区可能包含了与图形或DMA相关的数据。这个成员可能被用于快速访问数据,而无需进行物理内存地址转换;最后是和dma相关的地址和属性
nvdla_mem_handle作为媒介联通用户态空间任务结构体nvdla_ioctl_submit_task和内核态空间任务结构体nvdla_task
nvdla_ioctl_submit_task用户态空间任务结构体
nvdla_task内核态空间任务结构体
nvdla_device包含的信息是设备常用信息,比如中断、平台设备、drm设备等
nvdla_submit_args该结构体包含任务信息,用于用户态空间传入任务相关数据的参数,并通过该参数和nvdla_ioctl_submit_task交互,总体来说,任务粒度高于nvdla_ioctl_submit_task
drm_file包含针对该file的每个文件描述符操作后的状态变量
drm_gem_object描述drm的存储分配对象,包含了该对象归属的设备drm_device和对象的大小size
drm_device描述了drm设备结构体,包含了该总线设备的数据结构
sg_tableScatter-Gather表,用于描述分散在物理内存中的连续数据块的位置和大小
drm_ioctl_desc定义drmioctl操作,可以自行添加自定义ioctl操作,但需要注意ioctlflags
drm_ioctl_flagsioctlflags说明
drm_driver包含驱动的常见定义变量
nvdla_config实现NVDLA IP Core的内部配置,包括atom_sizebdma_enablerubik_enableweight_compress_support
dla_processordla_processor结构体是dla_processor_groupdla_engine的桥梁。
dla_processor_groupdla_processor_group结构体最重要的是作为乒乓寄存器组而存在,完成设备启动的初始配置,比如idactive,注意根据NVDLA硬件信号和架构设计整理一关于乒乓寄存器组的描述会帮助理解这个结构体的设计思路。另外该结构体也包含了dla_operation_containerdla_surface_containerunion,专门用于指向特定的硬件计算子模块比如bdmaconvsdp等的操作类型image surface
dla_enginedla_engine结构体的作用只有一个,那就是串东西,把用于设置乒乓寄存器组配置寄存器producerconsumer_ptrdla_processor,设置mac阵列大小、是否使能rubikbdmaweight_compressdla_configdla_taskdla_network_desc串起来,可以说是一家之主了。当然了,还有一个最重要的*driver_context,这个要把nvdla_device给映射起来,以便于访问nvdla设备的硬件资源抽象从而支持读取和写入寄存器获取专属锁来申请访问临界区
dla_network_descdla_network_desc囊括了运行网络的全部信息,我们可以很明显注意到几个信息,operation_desc_indexsurface_desc_indexdependency_graph_index,分别是操作、image surface和依赖图(也就是常见元操作)的索引
dla_taskdla_task结构体包含dla任务的common数据,用户态空间数据!!!
dla_bdma_transfer_descbdma的传输细节
dla_bdma_surface_descbdmasurface描述,需要确定source_typedestination_type,以及数据传输的num_transfers,还需要颇为详细的传输细节,相关变量在dla_bdma_transfer_desc结构体中定义。
dla_bdma_op_descbdmaop描述,dma的作用就是传输数据,因此num_transfers成为关键的指标。
dla_bdma_stat_descdla_bdma_stat_desc结构体——这个结构体是为了看bdma的状态,有三种状态:read_stallwrite_stallruntime
completion有两个成员变量,done代表信号量是否已满足,wait是一个链表的头
swait_queue_head链表swait_queue_head有一个spinlock,在操作链表前需要先获取该锁

补充nvdla_gem.c内函数:

函数原型功能
static int32_t nvdla_fill_task_desc(struct nvdla_ioctl_submit_task *local_task,struct nvdla_task *task)local_task的任务地址数量num_addresses和任务具体内容的指针handles,其中local_task->num_addresses * sizeof(struct nvdla_mem_handle)就是在申请所有具体任务相关数据的地址空间
static int32_t nvdla_submit(struct drm_device *drm, void *arg,struct drm_file *file)nvdla_submit函数传入参数arg(该参数的使之内容是nvdla_submit_args结构体类型的变量,包含内容为任务、任务的数量等),arg传入的任务转换为nvdla_ioctl_submit_task结构体类型的任务,随后调用nvdla_fill_task_desc完成用户态空间任务数据到内核态空间任务数据的下陷。与此同时,利用传入的drm_device结构体指针drm通过dev_get_drvdata来获取与其他子系统交互的过程中当前的driver data,从而引入完成nvdla_fill_task_desc功能的另一个关键变量task,并将drm_file结构体提交给task,其中drm_file结构体包含针对该file的每个文件描述符操作后的状态变量。最后使用nvdla_task_submit函数提交 NVDLA 任务并等待任务完成的函数。
static int32_t nvdla_gem_alloc(struct nvdla_gem_object *nobj)nvdla_gem_alloc函数,该函数传入的变量是nvdla用于存储管理的结构体nvdla_gem_object,根据前面介绍,该结构含有三个重要的变量,负责drm下存储分配和管理的drm_gem_object结构体、内核态虚拟地址kvaddrdma相关变量。整个函数实现的功能是dma地址分配。
static void nvdla_gem_free(struct nvdla_gem_object *nobj)释放nvdla_gem_alloc申请到的设备dma缓冲区
static struct nvdla_gem_object * nvdla_gem_create_object(struct drm_device *drm, uint32_t size)用于创建 NVDLA GEM对象的函数,随后分配和管理 DMA缓冲区的内核对象。前半部分的创建通过内核定义APIdrm_gem_private_object_init函数实现,后半部分调用nvdla_gem_alloc实现
static void nvdla_gem_free_object(struct drm_gem_object *dobj)用于释放 NVDLA GEM对象的函数,用于销毁和释放先前分配的 DMA缓冲区的内核对象
static struct nvdla_gem_object * nvdla_gem_create_with_handle(struct drm_file *file_priv,struct drm_device *drm, uint32_t size,uint32_t *handle)用于创建具有句柄(handle)的 NVDLA GEM对象的函数。它允许用户空间应用程序创建 GEM 对象,并返回一个句柄
static int32_t nvdla_gem_create(struct drm_device *drm, void *data, struct drm_file *file)nvdla_gem_create_with_handle(struct drm_file *file_priv,struct drm_device *drm, uint32_t size,uint32_t *handle)完全一样
static int32_t nvdla_drm_gem_object_mmap(struct drm_gem_object *dobj,struct vm_area_struct *vma)用于实现 NVDLA GEM对象的内存映射(mmap)操作的函数。内存映射允许用户空间应用程序将内核中的 GEM 对象映射到应用程序的地址空间中,以便应用程序可以直接访问该对象的数据。
static int32_t nvdla_drm_gem_mmap_buf(struct drm_gem_object *obj,struct vm_area_struct *vma)功能同nvdla_drm_gem_object_mmap
static int32_t nvdla_drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)功能同nvdla_drm_gem_object_mmap
static struct sg_table *nvdla_drm_gem_prime_get_sg_table(struct drm_gem_object *dobj)该函数实现了实现了在 GEM对象上获取 Scatter-Gather 表(SG 表)的操作。SG 表是一种数据结构,用于描述分散在物理内存中的连续数据块的位置和大小,通常在 DMA操作中使用,以便可以有效地传输分散的数据块。
static void *nvdla_drm_gem_prime_vmap(struct drm_gem_object *obj)用于返回虚拟地址
int32_t nvdla_gem_dma_addr(struct drm_device *dev, struct drm_file *file,uint32_t fd, dma_addr_t *addr)该函数的目的是获取给定文件描述符(fd)对应的GEM对象的DMA地址。首先,通过 drm_gem_prime_fd_to_handle 函数将文件描述符转换为GEM对象的句柄(handle)。然后,通过 drm_gem_object_lookup 函数查找具有给定句柄GEM对象。接着将找到的GEM对象转换为特定类型的GEM对象指针。最后,将GEM对象的DMA 地址(dma_addr)赋值给addr参数,并释放GEM对象的引用计数。总的来说,该函数目的是交给用户态空间数据handle来管理DRM device
static int32_t nvdla_gem_destroy(struct drm_device *drm, void *data, struct drm_file *file)销毁给定句柄对应的GEM对象

贴出前面提到的函数:

函数原型功能
dla_debugdla_infodla_warndla_error函数处理一般信息的函数,都采用了可变参数的方式来接受消息字符串和参数,通常是通过<stdarg.h>标准库中的宏来实现的。
dla_memsetdla_memcpy函数dla_memsetdla_memcpy函数分别用于将内存块的内容设置为指定的值、将一个内存块的内容复制到另一个内存块中
dla_get_time_us函数dla_get_time_us函数通过调用ktime_get_ns()函数来获取当前时间的纳秒级时间戳,然后将纳秒级时间戳除以NSEC_PER_USEC来将时间转换为微秒,并返回一个int64_t类型的整数表示微秒级的时间戳。
dla_reg_writedla_reg_read函数dla_reg_writedla_reg_read函数分别用于写和读寄存器
nvdla_engine_isr函数nvdla_engine_isr函数负责完成上自旋锁、完成硬件子单元乒乓寄存器组的初始化、执行计算任务、解除等待队列中的锁、释放自旋锁。
spin_lock_irqsavespin_unlock_irqrestore函数上自旋锁和释放自旋锁,前者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri指令配合,禁止中断后者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region临界区释放锁,与此同时,释放锁这件事情也就意味着临界区可以允许其余进程来使用相关资源,所以需要seti指令配合,恢复中断启用和优先级
dla_isr_handler函数dla_isr_handler函数,该函数用于处理与NVDLA引擎相关的中断事件。它接受nvdla_dev->engine_context作为参数,该参数通常包含了与引擎相关的上下文信息,以便进行特定的处理。
glb_reg_readglb_reg_write函数调用dla_reg_writedla_reg_read函数分别用于写和读寄存器,顺带挖出来一个三件套:dla_engine(实例化为engine) => driver_context(实例化为nvdla_device)nvdla_device(实例化为nvdla_dev) => engine_context(实例化为engine_data)engine_context => dla_engine(实例化为engine,见dla_isr_handler函数定义)三条链
completion函数complete函数用于唤醒等待中断事件完成的进程或线程。这通常用于实现异步通知机制,以便用户空间或其他内核组件可以等待某个事件的完成。依次完成:获取传入completiom等待队列锁,获取该锁的目的在于控制对等待队列增删的并发,并保存当前的中断状态;将x->done++;调用swake_up_locked()函数,将x->wait链表中的等待队列的任务唤醒;释放等待队列锁。

一、nvdla_core_callbacks.c代码解读三

1. dla_read_dma_address函数

继续读代码dla_read_dma_address函数,核心在于利用nvdla_gem_dma_addr函数来获取dma_addr,注意这个地址是总线地址,也就是从设备角度看到的地址。因此需要想方设法将传入的参数driver_contexttask_dataindexdstnvdla_gem_dma_addr的传入参数关联起来。

static int32_t dla_read_dma_address(void *driver_context, void *task_data,
						int16_t index, void *dst)
{
	int32_t ret = 0;
	struct nvdla_mem_handle *handles;
	dma_addr_t *phys_addr = (dma_addr_t *)(dst);
	struct nvdla_device *nvdla_dev =
			(struct nvdla_device *)driver_context; // 将传入的 driver_context 指针转换为指向 nvdla_device 结构体的指针 nvdla_dev,以便访问硬件设备的相关信息。
	struct nvdla_task *task = (struct nvdla_task *)task_data;
	// 将传入的 task_data 指针转换为指向 nvdla_task 结构体的指针 task,以便访问任务的相关信息。

	if (index == -1 || index > task->num_addresses)
		return -EINVAL;

	handles = (struct nvdla_mem_handle *)task->address_list;
	ret = nvdla_gem_dma_addr(nvdla_dev->drm, task->file,
					handles[index].handle,
					phys_addr);

	/* Add offset to IOVA address */
	*phys_addr = *phys_addr + handles[index].offset;

	return ret;
}

这就需要回到两组三件套,我们重新搬出来:

第一组三件套:
凡是出现了`drm_device *drm`,必然需要想到`drm_gem_object`结构体和`nvdla_gem_object`结构体,首先完成这三个之间的关系注册。
1struct nvdla_gem_object *nobj
2struct drm_gem_object *dobj = &nobj->object
3struct drm_device *drm = dobj->dev
第二组三件套:
1、`dla_engine(实例化为engine)` => `driver_context(实例化为nvdla_device)`
2、`nvdla_device(实例化为nvdla_dev)` => `engine_context(实例化为engine_data,见dla_isr_handler函数定义)`
3、`engine_context ` => `dla_engine(实例化为engine,见dla_isr_handler函数定义)`

等等,这两者之间似乎没法儿跨过去,但是注意到代码中

......
struct nvdla_device *nvdla_dev =
			(struct nvdla_device *)driver_context;
......
	ret = nvdla_gem_dma_addr(nvdla_dev->drm, task->file,
					handles[index].handle,
					phys_addr);
......
// 其中 nvdla_gem_dma_addr的原型是
// int32_t nvdla_gem_dma_addr(struct drm_device *dev, struct drm_file *file,uint32_t fd, dma_addr_t *addr)

所以可以合并两组三件套:

凡是出现了`drm_device *drm`,必然需要想到`drm_gem_object`结构体和`nvdla_gem_object`结构体,首先完成这三个之间的关系注册。
1struct nvdla_gem_object *nobj
2struct drm_gem_object *dobj = &nobj->object
3struct drm_device *drm = dobj->dev

4、`dla_engine(实例化为engine)` => `driver_context(实例化为nvdla_device)`
5、`nvdla_device(实例化为nvdla_dev)` => `engine_context(实例化为engine_data,见dla_isr_handler函数定义)`
6、`engine_context ` => `dla_engine(实例化为engine,见dla_isr_handler函数定义)`

7、`nvdla_device(实例化为nvdla_dev)` ==> `drm_device(实例化为drm_dev)`
8、从而借助7的关系可以回溯12

现在重新观察新的变量之间的关系:

......
	struct nvdla_task *task = (struct nvdla_task *)task_data;
......

	handles = (struct nvdla_mem_handle *)task->address_list;
......
	ret = nvdla_gem_dma_addr(nvdla_dev->drm, task->file,
					handles[index].handle,
					phys_addr);

这里我们再观察task_data结构体:

/**
 * @brief			Task information submitted from user space
 *
 * ref				Reference count for task
 * num_addresses		Number of addresses in address list
 * nvdla_dev			Pointer to NVDLA device
 * address_list			Address list
 * file				DRM file instance
 */
struct nvdla_task {
	struct kref ref;
	uint32_t num_addresses;
	struct nvdla_device *nvdla_dev;
	struct nvdla_mem_handle *address_list;
	struct drm_file *file;
};

所以nvdla_task除了前面表格中提到的作为内核态空间任务结构体之外,我们还需要更深入的认识,注意nvdla_device除了包含硬件抽象信息之外,还是driver_context,也就是驱动上下文,得益于nvdla_device的存在,其载体drm_device也是硬件抽象信息之一,因此关于drm_file也就有了存在意义,因为drm_file包含了每个文件描述符操作后的状态变量,除此之外,address_list包含了所有待处理文件的指针,可以认为就是fd(文件描述符)。总结下来一句话就是nvdla_task之所以区别于nvdla_device是因为nvdla_task的成员满足作为一个任务的必须要素,包括各个任务的fdfile状态硬件抽象nvdla_device。而nvlda_mem_handle依旧作为基本的地址描述要素十分丝滑地描述基地址偏移量

2. dla_read_dma_address函数

继续读代码dla_read_cpu_address函数,用于读取的地址是CPU视角的地址,注意和dla_read_dma_address函数的区别。(两者可以很好地对比,对于理解总线视角cpu视角的地址差异很有帮助)

static int32_t dla_read_cpu_address(void *driver_context, void *task_data,
						int16_t index, void *dst)
{
	uint64_t *temp = (uint64_t *)dst;
	struct nvdla_task *task = (struct nvdla_task *)task_data;

	if (index == -1 || index > task->num_addresses)
		return -EINVAL;

	*temp = (uint64_t)index;
	return 0;
}

3. dla_get_dma_address函数

继续读代码dla_get_dma_address函数将dla_get_dma_addressdla_read_dma_address合并,便于使用统一的destination变量来获取地址。

int32_t dla_get_dma_address(void *driver_context, void *task_data,
					int16_t index, void *dst_ptr,
					uint32_t destination)
{
	int32_t ret = 0;

	if (destination == DESTINATION_PROCESSOR) {
		ret = dla_read_cpu_address(driver_context, task_data,
						index, dst_ptr);
	} else if (destination == DESTINATION_DMA) {
		ret = dla_read_dma_address(driver_context, task_data,
						index, dst_ptr);
	} else {
		ret = -EINVAL;
	}

	return ret;
}

4. dla_data_write函数

继续读代码dla_data_write函数的定义如下:

int32_t dla_data_write(void *driver_context, void *task_data,
				void *src, uint64_t dst,
				uint32_t size, uint64_t offset)
{
	int32_t ret;
	void *ptr = NULL;
	struct dma_buf *buf;
	struct nvdla_mem_handle *handles;
	struct nvdla_task *task = (struct nvdla_task *)task_data;

	handles = task->address_list;
	buf = dma_buf_get(handles[dst].handle); // 根据所给的地址(句柄)返回dma_buf
	if (IS_ERR(buf)) {
		pr_err("%s: Failed get dma_buf for handle=%d\n", __func__,
						handles[dst].handle);
		return -EFAULT;
	}

	ret = dma_buf_begin_cpu_access(buf, DMA_BIDIRECTIONAL);
	if (ret)
		goto put_dma_buf;

	ptr = dma_buf_vmap(buf);
	if (!ptr) {
		pr_err("%s: Failed to vmap dma_buf for handle=%d\n", __func__,
						handles[dst].handle);
		ret = -ENOMEM;
		goto end_cpu_access;
	}


	memcpy((void *)((uint8_t *)ptr + offset), src, size);

	dma_buf_vunmap(buf, ptr);

end_cpu_access:
	dma_buf_end_cpu_access(buf, DMA_BIDIRECTIONAL);

put_dma_buf:
	dma_buf_put(buf);

	return ret;
}

这里重要的结构体和函数比较多,我们接下来一一介绍。

4.1 dma_buf结构体

dma_buf结构体如下:

/**
 * struct dma_buf - shared buffer object
 * @size: size of the buffer
 * @file: file pointer used for sharing buffers across, and for refcounting.
 * @attachments: list of dma_buf_attachment that denotes all devices attached.
 * @ops: dma_buf_ops associated with this buffer object.
 * @lock: used internally to serialize list manipulation, attach/detach and vmap/unmap
 * @vmapping_counter: used internally to refcnt the vmaps
 * @vmap_ptr: the current vmap ptr if vmapping_counter > 0
 * @exp_name: name of the exporter; useful for debugging.
 * @owner: pointer to exporter module; used for refcounting when exporter is a
 *         kernel module.
 * @list_node: node for dma_buf accounting and debugging.
 * @priv: exporter specific private data for this buffer object.
 * @resv: reservation object linked to this dma-buf
 * @poll: for userspace poll support
 * @cb_excl: for userspace poll support
 * @cb_shared: for userspace poll support
 *
 * This represents a shared buffer, created by calling dma_buf_export(). The
 * userspace representation is a normal file descriptor, which can be created by
 * calling dma_buf_fd().
 *
 * Shared dma buffers are reference counted using dma_buf_put() and
 * get_dma_buf().
 *
 * Device DMA access is handled by the separate &struct dma_buf_attachment.
 */
struct dma_buf {
	size_t size;
	struct file *file;
	struct list_head attachments;
	const struct dma_buf_ops *ops;
	struct mutex lock;
	unsigned vmapping_counter;
	void *vmap_ptr;
	const char *exp_name;
	struct module *owner;
	struct list_head list_node;
	void *priv;
	struct reservation_object *resv;

	/* poll support */
	wait_queue_head_t poll;

	struct dma_buf_poll_cb_t {
		struct dma_fence_cb cb;
		wait_queue_head_t *poll;

		__poll_t active;
	} cb_excl, cb_shared;
};

4.2 dma_buf_get函数

/**
 * dma_buf_get - returns the dma_buf structure related to an fd
 * @fd:	[in]	fd associated with the dma_buf to be returned
 *
 * On success, returns the dma_buf structure associated with an fd; uses
 * file's refcounting done by fget to increase refcount. returns ERR_PTR
 * otherwise.
 */
struct dma_buf *dma_buf_get(int fd)
{
	struct file *file;

	file = fget(fd);

	if (!file)
		return ERR_PTR(-EBADF);

	if (!is_dma_buf_file(file)) {
		fput(file);
		return ERR_PTR(-EINVAL);
	}

	return file->private_data;
}

该函数根据文件描述符fd返回dma_buf

4.3 dma_buf_begin_cpu_access和dma_buf_end_cpu_access函数

/**
 * dma_buf_begin_cpu_access - Must be called before accessing a dma_buf from the
 * cpu in the kernel context. Calls begin_cpu_access to allow exporter-specific
 * preparations. Coherency is only guaranteed in the specified range for the
 * specified access direction.
 * @dmabuf:	[in]	buffer to prepare cpu access for.
 * @direction:	[in]	length of range for cpu access.
 *
 * After the cpu access is complete the caller should call
 * dma_buf_end_cpu_access(). Only when cpu access is braketed by both calls is
 * it guaranteed to be coherent with other DMA access.
 *
 * Can return negative error values, returns 0 on success.
 */
int dma_buf_begin_cpu_access(struct dma_buf *dmabuf,
			     enum dma_data_direction direction)
{
	int ret = 0;

	if (WARN_ON(!dmabuf))
		return -EINVAL;

	if (dmabuf->ops->begin_cpu_access)
		ret = dmabuf->ops->begin_cpu_access(dmabuf, direction);

	/* Ensure that all fences are waited upon - but we first allow
	 * the native handler the chance to do so more efficiently if it
	 * chooses. A double invocation here will be reasonably cheap no-op.
	 */
	if (ret == 0)
		ret = __dma_buf_begin_cpu_access(dmabuf, direction);

	return ret;
}

dma_buf_begin_cpu_access函数的注释中可以看出该函数必须在内核上下文中从cpu访问dma_buf之前调用。调用begin_cpu_access以允许特定于导出程序的准备工作。只有在指定访问方向的指定范围内才能保证一致性。其中dmabuf为缓冲区,为其准备cpu访问;其中directioncpu访问范围的长度。cpu访问完成后,调用方调用dma_buf_end_cpu_access()。只有当cpu访问两个调用阻止时,它才能保证与其他DMA访问一致。
关于dma_buf_end_cpu_access略去不提,和dma_buf_begin_cpu_access函数构成可保证DMA访问一致的功能组。

4.4 dma_buf_vmap和dma_buf_vunmap函数

/**
 * dma_buf_vmap - Create virtual mapping for the buffer object into kernel
 * address space. Same restrictions as for vmap and friends apply.
 * @dmabuf:	[in]	buffer to vmap
 *
 * This call may fail due to lack of virtual mapping address space.
 * These calls are optional in drivers. The intended use for them
 * is for mapping objects linear in kernel space for high use objects.
 * Please attempt to use kmap/kunmap before thinking about these interfaces.
 *
 * Returns NULL on error.
 */
void *dma_buf_vmap(struct dma_buf *dmabuf)
{
	void *ptr;

	if (WARN_ON(!dmabuf))
		return NULL;

	if (!dmabuf->ops->vmap)
		return NULL;

	mutex_lock(&dmabuf->lock);
	if (dmabuf->vmapping_counter) {
		dmabuf->vmapping_counter++;
		BUG_ON(!dmabuf->vmap_ptr);
		ptr = dmabuf->vmap_ptr;
		goto out_unlock;
	}

	BUG_ON(dmabuf->vmap_ptr);

	ptr = dmabuf->ops->vmap(dmabuf);
	if (WARN_ON_ONCE(IS_ERR(ptr)))
		ptr = NULL;
	if (!ptr)
		goto out_unlock;

	dmabuf->vmap_ptr = ptr;
	dmabuf->vmapping_counter = 1;

out_unlock:
	mutex_unlock(&dmabuf->lock);
	return ptr;
}

dma_buf_vmap函数的功能是为缓冲区对象创建到内核地址空间虚拟映射。而dma_buf_vunmap则是解除前者发起的虚拟映射
综上所述,dla_data_write函数的功能就是CPU访问dma_buf(其中的访问流程和DMA一致性dma_buf_begin_cpu_accessdma_buf_end_cpu_access来完成和保证),并希望按照给定的数据源地址src和数据长度size写入由CPU申请好的dma_buf映射到内核态地址空间内的一段空间(这个功能由dma_buf_vmapdma_buf_vunmap来完成),注意还得按照dla_data_write给定的内核态地址空间的偏移量来写入。

5. dla_data_read函数

继续读代码dla_data_read函数的定义如下:

int32_t dla_data_read(void *driver_context, void *task_data,
				uint64_t src, void *dst,
				uint32_t size, uint64_t offset)
{
	int32_t ret;
	void *ptr = NULL;
	struct dma_buf *buf;
	struct nvdla_mem_handle *handles;
	struct nvdla_task *task = (struct nvdla_task *)task_data;

	handles = task->address_list;

	buf = dma_buf_get(handles[src].handle);
	if (IS_ERR(buf)) {
		pr_err("%s: Failed get dma_buf for handle=%d\n", __func__,
						handles[src].handle);
		return -EFAULT;
	}

	ret = dma_buf_begin_cpu_access(buf, DMA_BIDIRECTIONAL);
	if (ret)
		goto put_dma_buf;

	ptr = dma_buf_vmap(buf);
	if (!ptr) {
		pr_err("%s: Failed to vmap dma_buf for handle=%d\n", __func__,
						handles[src].handle);
		ret = -ENOMEM;
		goto end_cpu_access;
	}

	memcpy(dst, (void *)(((uint8_t *)ptr) + offset), size);

	dma_buf_vunmap(buf, ptr);

end_cpu_access:
	dma_buf_end_cpu_access(buf, DMA_BIDIRECTIONAL);

put_dma_buf:
	dma_buf_put(buf);

	return ret;
}

dla_data_read函数和dla_data_write函数类似,只是实现了读操作。

6. nvdla_task_submit函数

继续读代码nvdla_task_submit函数用于提交NVDLA任务并等待任务完成的函数,定义如下:

int32_t nvdla_task_submit(struct nvdla_device *nvdla_dev, struct nvdla_task *task)
{
	int32_t err = 0;
	uint32_t task_complete = 0;
	nvdla_dev->task = task;
	
	err = dla_execute_task(nvdla_dev->engine_context, (void *)task, nvdla_dev->config_data);
	if (err) {
		pr_err("Task execution failed\n");
		return err;
	}

	pr_debug("Wait for task complete\n");

	while (1) {
		unsigned long flags;

		wait_for_completion(&nvdla_dev->event_notifier);
		spin_lock_irqsave(&nvdla_dev->nvdla_lock, flags);
		err = dla_process_events(nvdla_dev->engine_context, &task_complete);
		spin_unlock_irqrestore(&nvdla_dev->nvdla_lock, flags);

		if (err || task_complete)
			break;
	}

	pr_debug("Task complete\n");

	dla_clear_task(nvdla_dev->engine_context);

	return err;
}

这个函数是重磅级函数,需要对内部若干个函数进行详细解释!

6.1 dla_execute_task函数

dla_execute_task函数的使用实例是:err = dla_execute_task(nvdla_dev->engine_context, (void *)task, nvdla_dev->config_data);,该函数接受任务的上下文、任务指针和配置数据作为参数,读取网络配置、初始化处理器和处理任务。如果任务执行失败,会打印错误信息并返回错误码。代码定义如下:

/**
 * Execute task selected by task scheduler
 *
 * 1. Read network configuration for the task
 * 2. Initiate processors with head of list for same op
 * 3. Start processing events received
 */

int
dla_execute_task(void *engine_context, void *task_data, void *config_data)
{
	int32_t ret;
	// 将传递给函数的 engine_context 指针转换为一个特定的结构体类型 struct dla_engine,并将其分配给名为 engine 的结构体指针。这个结构体应该包含关于深度学习引擎的信息。
	struct dla_engine *engine = (struct dla_engine *)engine_context;

	if (engine == NULL) {
		dla_error("engine is NULL\n"); // 如果 engine 为 NULL,说明传入的引擎上下文无效。
		ret = ERR(INVALID_INPUT);
		goto complete;
	}

	if (engine->task == NULL) {
		dla_error("task is NULL\n"); // 如果 engine->task 为 NULL,说明没有有效的任务数据。
		ret = ERR(INVALID_INPUT);
		goto complete;
	}

	if (engine->task->task_data != NULL) {
		// 如果 task_data 已经存在,说明已经有任务在执行。
		// 在这种情况下,它会打印一条警告消息 dla_warn("Already some task in progress");
		/* We have on the fly tasks running */
		dla_warn("Already some task in progress");
		ret = ERR(PROCESSOR_BUSY);
		goto complete;
	}

	engine->task->task_data = task_data; // 将传递给函数的 task_data 赋值给 engine->task->task_data,表示将任务数据存储到引擎中。
	engine->config_data = config_data;
	engine->network = &network; // 将一个名为 network 的结构体指针赋值给 engine->network,表示引擎将使用这个网络。
	engine->num_proc_hwl = 0;
	engine->stat_enable = 0;

	LOG_EVENT(0, 0, 0, LOG_TASK_START);

	ret = dla_read_network_config(engine); // 调用 dla_read_network_config 函数,该函数读取网络配置信息并返回一个整数值,将其存储在 ret 中。
	if (ret)
		goto complete;

	dla_debug_address_info(engine->task); // 这个函数似乎用于调试目的,它接受 engine->task 作为参数,并可能打印一些调试信息。

	/**
	 * If no operations in a task means nothing to do, NULL task
	 */
	if (engine->network->num_operations == 0)
		goto complete;

#if STAT_ENABLE
	if (network.stat_list_index != -1)
		engine->stat_enable = 1;
#endif /* STAT_ENABLE */

	ret = dla_initiate_processors(engine);
	engine->status = ret;

complete:
	LOG_EVENT(0, 0, 0, LOG_TASK_END); // 记录一个任务结束的事件,可能用于性能分析或日志记录。

	RETURN(ret);
}

比较直观,不赘述。

6.2 wait_for_completion函数

wait_for_completion函数的使用实例是:wait_for_completion(&nvdla_dev->event_notifier);,等待 NVDLA 设备的事件通知完成。在等待期间,线程会被阻塞,直到事件发生。代码定义如下:

/**
 * wait_for_completion: - waits for completion of a task
 * @x:  holds the state of this particular completion
 *
 * This waits to be signaled for completion of a specific task. It is NOT
 * interruptible and there is no timeout.
 *
 * See also similar routines (i.e. wait_for_completion_timeout()) with timeout
 * and interrupt capability. Also see complete().
 */
void __sched wait_for_completion(struct completion *x)
{
	wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
				||
				||
				\/
static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
	return __wait_for_common(x, schedule_timeout, timeout, state);
}	
				||
				||
				\/
static inline long __sched
__wait_for_common(struct completion *x,
		  long (*action)(long), long timeout, int state)
{
	might_sleep();

	complete_acquire(x);

	spin_lock_irq(&x->wait.lock);
	timeout = do_wait_for_common(x, action, timeout, state);
	spin_unlock_irq(&x->wait.lock);

	complete_release(x);

	return timeout;
}		
				||
				||
				\/
static inline long __sched
do_wait_for_common(struct completion *x,
		   long (*action)(long), long timeout, int state)
{
	if (!x->done) {
		DECLARE_WAITQUEUE(wait, current);

		__add_wait_queue_entry_tail_exclusive(&x->wait, &wait);
		do {
			if (signal_pending_state(state, current)) {
				timeout = -ERESTARTSYS;
				break;
			}
			__set_current_state(state);
			spin_unlock_irq(&x->wait.lock);
			timeout = action(timeout);
			spin_lock_irq(&x->wait.lock);
		} while (!x->done && timeout);
		__remove_wait_queue(&x->wait, &wait);
		if (!x->done)
			return timeout;
	}
	if (x->done != UINT_MAX)
		x->done--;
	return timeout ?: 1;
}				

有一个前面已经碰到过且极为相似的函数:completion函数。做个搬运工: complete函数用于唤醒等待中断事件完成的进程或线程。这通常用于实现异步通知机制,以便用户空间或其他内核组件可以等待某个事件的完成。依次完成:获取传入completiom等待队列锁,获取该锁的目的在于控制对等待队列增删的并发,并保存当前的中断状态;将x->done++;调用swake_up_locked()函数,将x->wait链表中的等待队列的任务唤醒;释放等待队列锁。

这两者之间的区别在于wait_for_completion函数循环等待done变为可用(正),completion函数为唤醒函数,当然是将done加一,唤醒待处理的函数。

关于完成量(对信号量的补充)和自旋锁引入背景介绍:内核编程中常见的一种模式是,在当前线程之外初始化某个活动,然后等待该活动的结束。这个活动可能是,创建一个新的内核线程或者新的用户空间进程、对一个已有进程的某个请求,或者某种类型的硬件动作,等等。在这种情况下,我们可以使用信号量来同步这两个任务。然而,内核中提供了另外一种机制——completion接口。Completion是一种轻量级的机制,他允许一个线程告诉另一个线程某个工作已经完成。

线程(进程)之间的同步大多使用completion,而互斥资源的保护大多使用信号量(互斥锁or自旋锁)。
参考来源

6.3 spin_lock_irqsave和spin_unlock_irqrestore函数

之前碰到过,做个搬运工回忆一下:上自旋锁和释放自旋锁。前者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri指令配合,禁止中断后者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region临界区释放锁,与此同时,释放锁这件事情也就意味着临界区可以允许其余进程来使用相关资源,所以需要seti指令配合,恢复中断启用和优先级

6.4 dla_process_events函数

dla_process_events函数的使用实例是:err = dla_process_events(nvdla_dev->engine_context, &task_complete);,该函数用于处理 NVDLA 设备的事件。代码定义如下:

int
dla_process_events(void *engine_context, uint32_t *task_complete)
{
	int32_t i;
	int32_t ret = 0;
	struct dla_engine *engine = (struct dla_engine *)engine_context;

	for (i = 0; i < DLA_OP_NUM; i++) { // DLA_OP_NUM 是一个常数,表示深度学习操作的数量。这个循环将用于遍历所有的处理器(processors)
		struct dla_processor *processor; // 声明一个名为 processor 的指向 struct dla_processor 结构体的指针,用于表示当前迭代中的处理器。

		processor = &engine->processors[i];
		// 调用 dla_handle_events 函数来处理当前处理器上的事件,函数返回一个整数值,将其存储在 ret 中。
		ret = dla_handle_events(processor);
		/**
		 * Incase engine status is non-zero, then don't
		 * update the engine status. We should keep its
		 * status for later cleaning of engine.
		 */
		if (!engine->status)
			engine->status = ret;
	}

	// 这是一个条件语句,检查引擎处理的硬件任务数量是否等于网络中的操作数量。
	if (engine->network->num_operations == engine->num_proc_hwl)
		// 如果条件成立,表示所有的硬件任务已经完成,因此将 task_complete 指针所指向的整数值设为 1。
		*task_complete = 1;

	RETURN(ret);
}

从这里可以看出最核心的是dla_handle_enevts函数,由于本章篇幅过长,因此单独再开一节介绍。

6.5 dla_clear_task函数

dla_clear_task函数的使用实例是:dla_clear_task(nvdla_dev->engine_context);,用于清除任务的上下文。代码定义如下:

void dla_clear_task(void *engine_context)
{
	int32_t i, j;
	// 将传递给函数的 engine_context 指针转换为一个特定的结构体类型 struct dla_engine,并将其分配给名为 engine 的结构体指针。这个结构体应该包含关于深度学习任务状态的信息。
	struct dla_engine *engine = (struct dla_engine *)engine_context;

	for (i = 0; i < DLA_OP_NUM; i++) { // DLA_OP_NUM 应该是一个常数,表示要处理的操作数量。
	// 获取 engine 结构体中的 processors 数组的第 i 个元素,这应该表示处理器。
		struct dla_processor *processor = &engine->processors[i];

		processor->roi_index = 0; // 将 processor 结构体中的 roi_index 成员设置为 0。roi_index 可能用于跟踪感兴趣区域的索引。
		processor->group_status = 0; // 将 processor 结构体中的 group_status 成员设置为 0,可能表示处理器组的状态。
		processor->rdma_status = 0; // 将 processor 结构体中的 rdma_status 成员设置为 0,可能表示 RDMA(Remote Direct Memory Access)的状态。

		processor->tail_op = NULL; // 将 processor 结构体中的 tail_op 成员设置为 NULL,这可能是一个指向操作的指针。

		for (j = 0; j < DLA_NUM_GROUPS; j++) {
			// 获取 groups 数组的第 j 个元素,这应该表示一个处理器组。
			struct dla_processor_group *group =
						&processor->groups[j];

			group->rdma_id = group->id; // 将 group 结构体中的 rdma_id 成员设置为与 id 相同,这可能是一个标识符。
			group->active = 0; // 将 group 结构体中的 active 成员设置为 0,可能表示处理器组的活跃状态。
			group->events = 0; // 将 group 结构体中的 events 成员设置为 0,可能用于跟踪事件数量。
			group->roi_index = 0;
			group->is_rdma_needed = 0;
			group->lut_index = -1; // 将 group 结构体中的 lut_index 成员设置为 -1,可能表示查找表的索引。
		}
	}

	engine->task->task_data = NULL;
	engine->network = NULL;
	engine->num_proc_hwl = 0;
	engine->status = 0;
	engine->stat_enable = 0;

	dla_info("reset engine done\n");
}

这个函数清除的内容可以看NVDLA内核态驱动代码整理四中的5.3.3 dla_engine结构体一节。

二、nvdla_core_callbacks.c代码内函数整理三

函数原型功能
dla_read_dma_address函数dla_read_dma_address函数的核心在于利用nvdla_gem_dma_addr函数来获取dma_addr,注意这个地址是总线地址,也就是从设备角度看到的地址。
dla_read_dma_address函数用于读取的地址是CPU视角的地址,注意和dla_read_dma_address函数的区别。(两者可以很好地对比,对于理解总线视角cpu视角的地址差异很有帮助)
dla_get_dma_address函数dla_get_dma_address函数将dla_get_dma_addressdla_read_dma_address合并,便于使用统一的destination变量来获取地址。
dla_data_write函数dla_data_write函数的功能就是CPU访问dma_buf(其中的访问流程和DMA一致性dma_buf_begin_cpu_accessdma_buf_end_cpu_access来完成和保证),并希望按照给定的数据源地址src和数据长度size写入由CPU申请好的dma_buf映射到内核态地址空间内的一段空间(这个功能由dma_buf_vmapdma_buf_vunmap来完成),注意还得按照dla_data_write给定的内核态地址空间的偏移量来写入。注意获取dma_buf是由文件描述符fd来完成的(dma_buf_get函数)
dma_buf_get函数根据文件描述符fd返回dma_buf
dma_buf_begin_cpu_accessdma_buf_end_cpu_access函数dma_buf_begin_cpu_access函数的注释中可以看出该函数必须在内核上下文中从cpu访问dma_buf之前调用。调用begin_cpu_access以允许特定于导出程序的准备工作。只有在指定访问方向的指定范围内才能保证一致性。其中dmabuf为缓冲区,为其准备cpu访问;其中directioncpu访问范围的长度。cpu访问完成后,调用方调用dma_buf_end_cpu_access()。只有当cpu访问两个调用阻止时,它才能保证与其他DMA访问一致。
dma_buf_vmapdma_buf_vunmap函数dma_buf_vmap函数的功能是为缓冲区对象创建到内核地址空间虚拟映射。而dma_buf_vunmap则是解除前者发起的虚拟映射
dla_data_read函数dla_data_read函数和dla_data_write函数类似,只是实现了读操作。
nvdla_task_submit函数nvdla_task_submit函数用于提交NVDLA任务并等待任务完成的函数,很核心的函数,由dla_execute_taskwait_for_completionspin_lock_irqsavespin_unlock_irqrestoredla_process_eventsdla_clear_task函数组成!!!
dla_execute_task函数该函数接受任务的上下文、任务指针和配置数据作为参数,读取网络配置、初始化处理器和处理任务。
wait_for_completion函数wait_for_completion函数等待 NVDLA 设备的事件通知完成。在等待期间,线程会被阻塞,直到事件发生。
dla_process_events函数dla_process_events函数用于处理 NVDLA 设备的事件。有个十分核心的函数!!!
dla_clear_task函数dla_clear_task函数用于清除任务的上下文。

三、nvdla_core_callbacks.c代码结构体整理三

结构体功能
nvdla_mem_handle(重新解释)作为媒介联通用户态空间任务结构体nvdla_ioctl_submit_task和内核态空间任务结构体nvdla_task,作为基本的地址描述要素十分丝滑地描述基地址偏移量
nvdla_task(重新解释)内核态空间任务结构体。nvdla_device除了包含硬件抽象信息之外,还是driver_context,也就是驱动上下文,得益于nvdla_device的存在,其载体drm_device也是硬件抽象信息之一,因此关于drm_file也就有了存在意义,因为drm_file包含了每个文件描述符操作后的状态变量,除此之外,address_list包含了所有待处理文件的指针,可以认为就是fd(文件描述符)。总结下来一句话就是nvdla_task之所以区别于nvdla_device是因为nvdla_task的成员满足作为一个任务的必须要素,包括各个任务的fdfile状态硬件抽象nvdla_device

四、反复出现的2组三件套(单列)

凡是出现了`drm_device *drm`,必然需要想到`drm_gem_object`结构体和`nvdla_gem_object`结构体,首先完成这三个之间的关系注册。
1struct nvdla_gem_object *nobj
2struct drm_gem_object *dobj = &nobj->object
3struct drm_device *drm = dobj->dev

4、`dla_engine(实例化为engine)` => `driver_context(实例化为nvdla_device)`
5、`nvdla_device(实例化为nvdla_dev)` => `engine_context(实例化为engine_data,见dla_isr_handler函数定义)`
6、`engine_context ` => `dla_engine(实例化为engine,见dla_isr_handler函数定义)`

7、`nvdla_device(实例化为nvdla_dev)` ==> `drm_device(实例化为drm_dev)`
8、从而借助7的关系可以回溯12

总结

本章对nvdla_core_callbacks.c代码做了简单整理,这边对上述提到的所有函数和结构体做一个合并处理:

nvdla_gem.c所有函数:

函数原型功能
static int32_t nvdla_fill_task_desc(struct nvdla_ioctl_submit_task *local_task,struct nvdla_task *task)local_task的任务地址数量num_addresses和任务具体内容的指针handles,其中local_task->num_addresses * sizeof(struct nvdla_mem_handle)就是在申请所有具体任务相关数据的地址空间
static int32_t nvdla_submit(struct drm_device *drm, void *arg,struct drm_file *file)nvdla_submit函数传入参数arg(该参数的使之内容是nvdla_submit_args结构体类型的变量,包含内容为任务、任务的数量等),arg传入的任务转换为nvdla_ioctl_submit_task结构体类型的任务,随后调用nvdla_fill_task_desc完成用户态空间任务数据到内核态空间任务数据的下陷。与此同时,利用传入的drm_device结构体指针drm通过dev_get_drvdata来获取与其他子系统交互的过程中当前的driver data,从而引入完成nvdla_fill_task_desc功能的另一个关键变量task,并将drm_file结构体提交给task,其中drm_file结构体包含针对该file的每个文件描述符操作后的状态变量。最后使用nvdla_task_submit函数提交 NVDLA 任务并等待任务完成的函数。
static int32_t nvdla_gem_alloc(struct nvdla_gem_object *nobj)nvdla_gem_alloc函数,该函数传入的变量是nvdla用于存储管理的结构体nvdla_gem_object,根据前面介绍,该结构含有三个重要的变量,负责drm下存储分配和管理的drm_gem_object结构体、内核态虚拟地址kvaddrdma相关变量。整个函数实现的功能是dma地址分配。
static void nvdla_gem_free(struct nvdla_gem_object *nobj)释放nvdla_gem_alloc申请到的设备dma缓冲区
static struct nvdla_gem_object * nvdla_gem_create_object(struct drm_device *drm, uint32_t size)用于创建 NVDLA GEM对象的函数,随后分配和管理 DMA缓冲区的内核对象。前半部分的创建通过内核定义APIdrm_gem_private_object_init函数实现,后半部分调用nvdla_gem_alloc实现
static void nvdla_gem_free_object(struct drm_gem_object *dobj)用于释放 NVDLA GEM对象的函数,用于销毁和释放先前分配的 DMA缓冲区的内核对象
static struct nvdla_gem_object * nvdla_gem_create_with_handle(struct drm_file *file_priv,struct drm_device *drm, uint32_t size,uint32_t *handle)用于创建具有句柄(handle)的 NVDLA GEM对象的函数。它允许用户空间应用程序创建 GEM 对象,并返回一个句柄
static int32_t nvdla_gem_create(struct drm_device *drm, void *data, struct drm_file *file)nvdla_gem_create_with_handle(struct drm_file *file_priv,struct drm_device *drm, uint32_t size,uint32_t *handle)完全一样
static int32_t nvdla_drm_gem_object_mmap(struct drm_gem_object *dobj,struct vm_area_struct *vma)用于实现 NVDLA GEM对象的内存映射(mmap)操作的函数。内存映射允许用户空间应用程序将内核中的 GEM 对象映射到应用程序的地址空间中,以便应用程序可以直接访问该对象的数据。
static int32_t nvdla_drm_gem_mmap_buf(struct drm_gem_object *obj,struct vm_area_struct *vma)功能同nvdla_drm_gem_object_mmap
static int32_t nvdla_drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)功能同nvdla_drm_gem_object_mmap
static struct sg_table *nvdla_drm_gem_prime_get_sg_table(struct drm_gem_object *dobj)该函数实现了实现了在 GEM对象上获取 Scatter-Gather 表(SG 表)的操作。SG 表是一种数据结构,用于描述分散在物理内存中的连续数据块的位置和大小,通常在 DMA操作中使用,以便可以有效地传输分散的数据块。
static void *nvdla_drm_gem_prime_vmap(struct drm_gem_object *obj)用于返回虚拟地址
int32_t nvdla_gem_dma_addr(struct drm_device *dev, struct drm_file *file,uint32_t fd, dma_addr_t *addr)该函数的目的是获取给定文件描述符(fd)对应的GEM对象的DMA地址。首先,通过 drm_gem_prime_fd_to_handle 函数将文件描述符转换为GEM对象的句柄(handle)。然后,通过 drm_gem_object_lookup 函数查找具有给定句柄GEM对象。接着将找到的GEM对象转换为特定类型的GEM对象指针。最后,将GEM对象的DMA 地址(dma_addr)赋值给addr参数,并释放GEM对象的引用计数。总的来说,该函数目的是交给用户态空间数据handle来管理DRM device
static int32_t nvdla_gem_destroy(struct drm_device *drm, void *data, struct drm_file *file)销毁给定句柄对应的GEM对象

nvdla_core_callbacks.c所有函数:

函数原型功能
dla_debugdla_infodla_warndla_error函数处理一般信息的函数,都采用了可变参数的方式来接受消息字符串和参数,通常是通过<stdarg.h>标准库中的宏来实现的。
dla_memsetdla_memcpy函数dla_memsetdla_memcpy函数分别用于将内存块的内容设置为指定的值、将一个内存块的内容复制到另一个内存块中
dla_get_time_us函数dla_get_time_us函数通过调用ktime_get_ns()函数来获取当前时间的纳秒级时间戳,然后将纳秒级时间戳除以NSEC_PER_USEC来将时间转换为微秒,并返回一个int64_t类型的整数表示微秒级的时间戳。
dla_reg_writedla_reg_read函数dla_reg_writedla_reg_read函数分别用于写和读寄存器
nvdla_engine_isr函数nvdla_engine_isr函数负责完成上自旋锁、完成硬件子单元乒乓寄存器组的初始化、执行计算任务、解除等待队列中的锁、释放自旋锁。
spin_lock_irqsavespin_unlock_irqrestore函数上自旋锁和释放自旋锁,前者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri指令配合,禁止中断后者:首先需要使用nvdla_device的专属锁&nvdla_dev->nvdla_lockcritical region临界区释放锁,与此同时,释放锁这件事情也就意味着临界区可以允许其余进程来使用相关资源,所以需要seti指令配合,恢复中断启用和优先级
dla_isr_handler函数dla_isr_handler函数,该函数用于处理与NVDLA引擎相关的中断事件。它接受nvdla_dev->engine_context作为参数,该参数通常包含了与引擎相关的上下文信息,以便进行特定的处理。
glb_reg_readglb_reg_write函数调用dla_reg_writedla_reg_read函数分别用于写和读寄存器,顺带挖出来一个三件套:dla_engine(实例化为engine) => driver_context(实例化为nvdla_device)nvdla_device(实例化为nvdla_dev) => engine_context(实例化为engine_data)engine_context => dla_engine(实例化为engine,见dla_isr_handler函数定义)三条链
completion函数complete函数用于唤醒等待中断事件完成的进程或线程。这通常用于实现异步通知机制,以便用户空间或其他内核组件可以等待某个事件的完成。依次完成:获取传入completiom等待队列锁,获取该锁的目的在于控制对等待队列增删的并发,并保存当前的中断状态;将x->done++;调用swake_up_locked()函数,将x->wait链表中的等待队列的任务唤醒;释放等待队列锁。
dla_read_dma_address函数dla_read_dma_address函数的核心在于利用nvdla_gem_dma_addr函数来获取dma_addr,注意这个地址是总线地址,也就是从设备角度看到的地址。
dla_read_dma_address函数用于读取的地址是CPU视角的地址,注意和dla_read_dma_address函数的区别。(两者可以很好地对比,对于理解总线视角cpu视角的地址差异很有帮助)
dla_get_dma_address函数dla_get_dma_address函数将dla_get_dma_addressdla_read_dma_address合并,便于使用统一的destination变量来获取地址。
dla_data_write函数dla_data_write函数的功能就是CPU访问dma_buf(其中的访问流程和DMA一致性dma_buf_begin_cpu_accessdma_buf_end_cpu_access来完成和保证),并希望按照给定的数据源地址src和数据长度size写入由CPU申请好的dma_buf映射到内核态地址空间内的一段空间(这个功能由dma_buf_vmapdma_buf_vunmap来完成),注意还得按照dla_data_write给定的内核态地址空间的偏移量来写入。注意获取dma_buf是由文件描述符fd来完成的(dma_buf_get函数)
dma_buf_get函数根据文件描述符fd返回dma_buf
dma_buf_begin_cpu_accessdma_buf_end_cpu_access函数dma_buf_begin_cpu_access函数的注释中可以看出该函数必须在内核上下文中从cpu访问dma_buf之前调用。调用begin_cpu_access以允许特定于导出程序的准备工作。只有在指定访问方向的指定范围内才能保证一致性。其中dmabuf为缓冲区,为其准备cpu访问;其中directioncpu访问范围的长度。cpu访问完成后,调用方调用dma_buf_end_cpu_access()。只有当cpu访问两个调用阻止时,它才能保证与其他DMA访问一致。
dma_buf_vmapdma_buf_vunmap函数dma_buf_vmap函数的功能是为缓冲区对象创建到内核地址空间虚拟映射。而dma_buf_vunmap则是解除前者发起的虚拟映射
dla_data_read函数dla_data_read函数和dla_data_write函数类似,只是实现了读操作。
nvdla_task_submit函数nvdla_task_submit函数用于提交NVDLA任务并等待任务完成的函数,很核心的函数,由dla_execute_taskwait_for_completionspin_lock_irqsavespin_unlock_irqrestoredla_process_eventsdla_clear_task函数组成!!!
dla_execute_task函数该函数接受任务的上下文、任务指针和配置数据作为参数,读取网络配置、初始化处理器和处理任务。
wait_for_completion函数wait_for_completion函数等待 NVDLA 设备的事件通知完成。在等待期间,线程会被阻塞,直到事件发生。
dla_process_events函数dla_process_events函数用于处理 NVDLA 设备的事件。有个十分核心的函数!!!
dla_clear_task函数dla_clear_task函数用于清除任务的上下文。

所有结构体:

结构体功能
nvdla_gem_object包含重要的变量,首先是drm_gem_object,用于drm存储管理和分配的结构体;其次是*kvaddr:这是一个指针成员,通常用于存储内核虚拟地址。这个地址指向内核中的数据缓冲区,该缓冲区可能包含了与图形或DMA相关的数据。这个成员可能被用于快速访问数据,而无需进行物理内存地址转换;最后是和dma相关的地址和属性
nvdla_mem_handle作为媒介联通用户态空间任务结构体nvdla_ioctl_submit_task和内核态空间任务结构体nvdla_task
nvdla_ioctl_submit_task用户态空间任务结构体
nvdla_task内核态空间任务结构体
nvdla_device包含的信息是设备常用信息,比如中断、平台设备、drm设备等
nvdla_submit_args该结构体包含任务信息,用于用户态空间传入任务相关数据的参数,并通过该参数和nvdla_ioctl_submit_task交互,总体来说,任务粒度高于nvdla_ioctl_submit_task
drm_file包含针对该file的每个文件描述符操作后的状态变量
drm_gem_object描述drm的存储分配对象,包含了该对象归属的设备drm_device和对象的大小size
drm_device描述了drm设备结构体,包含了该总线设备的数据结构
sg_tableScatter-Gather表,用于描述分散在物理内存中的连续数据块的位置和大小
drm_ioctl_desc定义drmioctl操作,可以自行添加自定义ioctl操作,但需要注意ioctlflags
drm_ioctl_flagsioctlflags说明
drm_driver包含驱动的常见定义变量
nvdla_config实现NVDLA IP Core的内部配置,包括atom_sizebdma_enablerubik_enableweight_compress_support
dla_processordla_processor结构体是dla_processor_groupdla_engine的桥梁。
dla_processor_groupdla_processor_group结构体最重要的是作为乒乓寄存器组而存在,完成设备启动的初始配置,比如idactive,注意根据NVDLA硬件信号和架构设计整理一关于乒乓寄存器组的描述会帮助理解这个结构体的设计思路。另外该结构体也包含了dla_operation_containerdla_surface_containerunion,专门用于指向特定的硬件计算子模块比如bdmaconvsdp等的操作类型image surface
dla_enginedla_engine结构体的作用只有一个,那就是串东西,把用于设置乒乓寄存器组配置寄存器producerconsumer_ptrdla_processor,设置mac阵列大小、是否使能rubikbdmaweight_compressdla_configdla_taskdla_network_desc串起来,可以说是一家之主了。当然了,还有一个最重要的*driver_context,这个要把nvdla_device给映射起来,以便于访问nvdla设备的硬件资源抽象从而支持读取和写入寄存器获取专属锁来申请访问临界区
dla_network_descdla_network_desc囊括了运行网络的全部信息,我们可以很明显注意到几个信息,operation_desc_indexsurface_desc_indexdependency_graph_index,分别是操作、image surface和依赖图(也就是常见元操作)的索引
dla_taskdla_task结构体包含dla任务的common数据,用户态空间数据!!!
dla_bdma_transfer_descbdma的传输细节
dla_bdma_surface_descbdmasurface描述,需要确定source_typedestination_type,以及数据传输的num_transfers,还需要颇为详细的传输细节,相关变量在dla_bdma_transfer_desc结构体中定义。
dla_bdma_op_descbdmaop描述,dma的作用就是传输数据,因此num_transfers成为关键的指标。
dla_bdma_stat_descdla_bdma_stat_desc结构体——这个结构体是为了看bdma的状态,有三种状态:read_stallwrite_stallruntime
completion有两个成员变量,done代表信号量是否已满足,wait是一个链表的头
swait_queue_head链表swait_queue_head有一个spinlock,在操作链表前需要先获取该锁
nvdla_mem_handle(重新解释)作为媒介联通用户态空间任务结构体nvdla_ioctl_submit_task和内核态空间任务结构体nvdla_task,作为基本的地址描述要素十分丝滑地描述基地址偏移量
nvdla_task(重新解释)内核态空间任务结构体。nvdla_device除了包含硬件抽象信息之外,还是driver_context,也就是驱动上下文,得益于nvdla_device的存在,其载体drm_device也是硬件抽象信息之一,因此关于drm_file也就有了存在意义,因为drm_file包含了每个文件描述符操作后的状态变量,除此之外,address_list包含了所有待处理文件的指针,可以认为就是fd(文件描述符)。总结下来一句话就是nvdla_task之所以区别于nvdla_device是因为nvdla_task的成员满足作为一个任务的必须要素,包括各个任务的fdfile状态硬件抽象nvdla_device
  • 9
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DentionY

谢谢您的支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值