文章目录
前言
本系列内容力求将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_table |
Scatter-Gather 表,用于描述分散在物理内存中的连续数据块的位置和大小 |
drm_ioctl_desc |
定义drm 的ioctl 操作,可以自行添加自定义ioctl 操作,但需要注意ioctl 的flags |
drm_ioctl_flags |
ioctl 的flags 说明 |
drm_driver |
包含驱动的常见定义变量 |
nvdla_config |
实现NVDLA IP Core 的内部配置,包括atom_size 、bdma_enable 、rubik_enable 、weight_compress_support |
dla_processor |
dla_processor 结构体是dla_processor_group 和dla_engine 的桥梁。 |
dla_processor_group |
dla_processor_group 结构体最重要的是作为乒乓寄存器组 而存在,完成设备启动的初始配置,比如id 和active ,注意根据NVDLA硬件信号和架构设计整理一关于乒乓寄存器组 的描述会帮助理解这个结构体的设计思路。另外该结构体也包含了dla_operation_container 和dla_surface_container 的union ,专门用于指向特定的硬件计算子模块 比如bdma 、conv 、sdp 等的操作类型 和image surface 。 |
dla_engine |
dla_engine 结构体的作用只有一个,那就是串东西 ,把用于设置乒乓寄存器组配置寄存器 、producer 和consumer_ptr 的dla_processor ,设置mac阵列大小 、是否使能rubik 、bdma 与weight_compress 的dla_config ,dla_task 和dla_network_desc 给串起来 ,可以说是一家之主了。当然了,还有一个最重要的*driver_context ,这个要把nvdla_device 给映射起来,以便于访问nvdla 设备的硬件资源抽象从而支持读取和写入寄存器 、获取专属锁来申请访问临界区 。 |
dla_network_desc |
dla_network_desc 囊括了运行网络的全部信息,我们可以很明显注意到几个信息,operation_desc_index 、surface_desc_index 和dependency_graph_index ,分别是操作、image surface 和依赖图(也就是常见元操作)的索引 |
dla_task |
dla_task 结构体包含dla任务的common数据,用户态空间数据!!! |
dla_bdma_transfer_desc |
bdma 的传输细节 |
dla_bdma_surface_desc |
bdma 的surface 描述,需要确定source_type 和destination_type ,以及数据传输的num_transfers ,还需要颇为详细的传输细节,相关变量在dla_bdma_transfer_desc 结构体中定义。 |
dla_bdma_op_desc |
bdma 的op 描述,dma 的作用就是传输数据,因此num_transfers 成为关键的指标。 |
dla_bdma_stat_desc |
dla_bdma_stat_desc 结构体——这个结构体是为了看bdma 的状态,有三种状态:read_stall 、write_stall 和runtime 。 |
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 结构体、内核态虚拟地址kvaddr 和dma 相关变量。整个函数实现的功能是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_debug 、dla_info 、dla_warn 和dla_error 函数 |
处理一般信息的函数,都采用了可变参数的方式来接受消息字符串和参数,通常是通过<stdarg.h> 标准库中的宏来实现的。 |
dla_memset 和dla_memcpy 函数 |
dla_memset 和dla_memcpy 函数分别用于将内存块的内容设置为指定的值、将一个内存块的内容复制到另一个内存块中 |
dla_get_time_us 函数 |
dla_get_time_us 函数通过调用ktime_get_ns() 函数来获取当前时间的纳秒级时间戳,然后将纳秒级时间戳除以NSEC_PER_USEC 来将时间转换为微秒,并返回一个int64_t 类型的整数表示微秒级的时间戳。 |
dla_reg_write 和dla_reg_read 函数 |
dla_reg_write 和dla_reg_read 函数分别用于写和读寄存器 |
nvdla_engine_isr 函数 |
nvdla_engine_isr 函数负责完成上自旋锁、完成硬件子单元乒乓寄存器组的初始化、执行计算任务、解除等待队列中的锁、释放自旋锁。 |
spin_lock_irqsave 和spin_unlock_irqrestore 函数 |
上自旋锁和释放自旋锁,前者 :首先需要使用nvdla_device 的专属锁&nvdla_dev->nvdla_lock 对critical region 上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri 指令配合,禁止中断 。 后者 :首先需要使用nvdla_device 的专属锁&nvdla_dev->nvdla_lock 对critical region 临界区释放锁,与此同时,释放锁这件事情也就意味着临界区可以允许其余进程来使用相关资源,所以需要seti 指令配合,恢复中断启用和优先级 。 |
dla_isr_handler 函数 |
dla_isr_handler 函数,该函数用于处理与NVDLA 引擎相关的中断事件。它接受nvdla_dev->engine_context 作为参数,该参数通常包含了与引擎相关的上下文信息,以便进行特定的处理。 |
glb_reg_read 和glb_reg_write 函数 |
调用dla_reg_write 和dla_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_context
、task_data
、index
和dst
与nvdla_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`结构体,首先完成这三个之间的关系注册。
1、struct nvdla_gem_object *nobj
2、struct drm_gem_object *dobj = &nobj->object
3、struct 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`结构体,首先完成这三个之间的关系注册。
1、struct nvdla_gem_object *nobj
2、struct drm_gem_object *dobj = &nobj->object
3、struct 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的关系可以回溯1和2
现在重新观察新的变量之间的关系:
......
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
的成员满足作为一个任务的必须要素,包括各个任务的fd
、file状态
、硬件抽象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_address
和dla_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
访问;其中direction
为cpu
访问范围的长度。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_access
和dma_buf_end_cpu_access
来完成和保证),并希望按照给定的数据源地址src
和数据长度size
写入由CPU
申请好的dma_buf
映射到内核态地址空间
内的一段空间(这个功能由dma_buf_vmap
和dma_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_lock
对critical region
上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri
指令配合,禁止中断
。 后者
:首先需要使用nvdla_device
的专属锁&nvdla_dev->nvdla_lock
对critical 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_address 和dla_read_dma_address 合并,便于使用统一的destination 变量来获取地址。 |
dla_data_write 函数 |
dla_data_write 函数的功能就是CPU 访问dma_buf (其中的访问流程和DMA一致性 由dma_buf_begin_cpu_access 和dma_buf_end_cpu_access 来完成和保证),并希望按照给定的数据源地址src 和数据长度size 写入由CPU 申请好的dma_buf 映射到内核态地址空间 内的一段空间(这个功能由dma_buf_vmap 和dma_buf_vunmap 来完成),注意还得按照dla_data_write 给定的内核态地址空间 的偏移量来写入。注意获取dma_buf 是由文件描述符fd 来完成的(dma_buf_get 函数) |
dma_buf_get 函数 |
根据文件描述符fd 返回dma_buf 。 |
dma_buf_begin_cpu_access 和dma_buf_end_cpu_access 函数 |
从dma_buf_begin_cpu_access 函数的注释中可以看出该函数必须在内核上下文 中从cpu 访问dma_buf 之前调用。调用begin_cpu_access 以允许特定于导出程序的准备工作。只有在指定访问方向的指定范围内才能保证一致性。其中dmabuf 为缓冲区,为其准备cpu 访问;其中direction 为cpu 访问范围的长度。cpu 访问完成后,调用方 应调用dma_buf_end_cpu_access() 。只有当cpu访问 被两个调用阻止 时,它才能保证与其他DMA访问 一致。 |
dma_buf_vmap 和dma_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_task 、wait_for_completion 、spin_lock_irqsave 和spin_unlock_irqrestore 、dla_process_events 和dla_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 的成员满足作为一个任务的必须要素,包括各个任务的fd 、file状态 、硬件抽象nvdla_device 。 |
四、反复出现的2组三件套(单列)
凡是出现了`drm_device *drm`,必然需要想到`drm_gem_object`结构体和`nvdla_gem_object`结构体,首先完成这三个之间的关系注册。
1、struct nvdla_gem_object *nobj
2、struct drm_gem_object *dobj = &nobj->object
3、struct 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的关系可以回溯1和2
总结
本章对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 结构体、内核态虚拟地址kvaddr 和dma 相关变量。整个函数实现的功能是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_debug 、dla_info 、dla_warn 和dla_error 函数 |
处理一般信息的函数,都采用了可变参数的方式来接受消息字符串和参数,通常是通过<stdarg.h> 标准库中的宏来实现的。 |
dla_memset 和dla_memcpy 函数 |
dla_memset 和dla_memcpy 函数分别用于将内存块的内容设置为指定的值、将一个内存块的内容复制到另一个内存块中 |
dla_get_time_us 函数 |
dla_get_time_us 函数通过调用ktime_get_ns() 函数来获取当前时间的纳秒级时间戳,然后将纳秒级时间戳除以NSEC_PER_USEC 来将时间转换为微秒,并返回一个int64_t 类型的整数表示微秒级的时间戳。 |
dla_reg_write 和dla_reg_read 函数 |
dla_reg_write 和dla_reg_read 函数分别用于写和读寄存器 |
nvdla_engine_isr 函数 |
nvdla_engine_isr 函数负责完成上自旋锁、完成硬件子单元乒乓寄存器组的初始化、执行计算任务、解除等待队列中的锁、释放自旋锁。 |
spin_lock_irqsave 和spin_unlock_irqrestore 函数 |
上自旋锁和释放自旋锁,前者 :首先需要使用nvdla_device 的专属锁&nvdla_dev->nvdla_lock 对critical region 上锁,与此同时,上锁这件事情得通告其余进程不要来打断,所以需要clri 指令配合,禁止中断 。 后者 :首先需要使用nvdla_device 的专属锁&nvdla_dev->nvdla_lock 对critical region 临界区释放锁,与此同时,释放锁这件事情也就意味着临界区可以允许其余进程来使用相关资源,所以需要seti 指令配合,恢复中断启用和优先级 。 |
dla_isr_handler 函数 |
dla_isr_handler 函数,该函数用于处理与NVDLA 引擎相关的中断事件。它接受nvdla_dev->engine_context 作为参数,该参数通常包含了与引擎相关的上下文信息,以便进行特定的处理。 |
glb_reg_read 和glb_reg_write 函数 |
调用dla_reg_write 和dla_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_address 和dla_read_dma_address 合并,便于使用统一的destination 变量来获取地址。 |
dla_data_write 函数 |
dla_data_write 函数的功能就是CPU 访问dma_buf (其中的访问流程和DMA一致性 由dma_buf_begin_cpu_access 和dma_buf_end_cpu_access 来完成和保证),并希望按照给定的数据源地址src 和数据长度size 写入由CPU 申请好的dma_buf 映射到内核态地址空间 内的一段空间(这个功能由dma_buf_vmap 和dma_buf_vunmap 来完成),注意还得按照dla_data_write 给定的内核态地址空间 的偏移量来写入。注意获取dma_buf 是由文件描述符fd 来完成的(dma_buf_get 函数) |
dma_buf_get 函数 |
根据文件描述符fd 返回dma_buf 。 |
dma_buf_begin_cpu_access 和dma_buf_end_cpu_access 函数 |
从dma_buf_begin_cpu_access 函数的注释中可以看出该函数必须在内核上下文 中从cpu 访问dma_buf 之前调用。调用begin_cpu_access 以允许特定于导出程序的准备工作。只有在指定访问方向的指定范围内才能保证一致性。其中dmabuf 为缓冲区,为其准备cpu 访问;其中direction 为cpu 访问范围的长度。cpu 访问完成后,调用方 应调用dma_buf_end_cpu_access() 。只有当cpu访问 被两个调用阻止 时,它才能保证与其他DMA访问 一致。 |
dma_buf_vmap 和dma_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_task 、wait_for_completion 、spin_lock_irqsave 和spin_unlock_irqrestore 、dla_process_events 和dla_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_table |
Scatter-Gather 表,用于描述分散在物理内存中的连续数据块的位置和大小 |
drm_ioctl_desc |
定义drm 的ioctl 操作,可以自行添加自定义ioctl 操作,但需要注意ioctl 的flags |
drm_ioctl_flags |
ioctl 的flags 说明 |
drm_driver |
包含驱动的常见定义变量 |
nvdla_config |
实现NVDLA IP Core 的内部配置,包括atom_size 、bdma_enable 、rubik_enable 、weight_compress_support |
dla_processor |
dla_processor 结构体是dla_processor_group 和dla_engine 的桥梁。 |
dla_processor_group |
dla_processor_group 结构体最重要的是作为乒乓寄存器组 而存在,完成设备启动的初始配置,比如id 和active ,注意根据NVDLA硬件信号和架构设计整理一关于乒乓寄存器组 的描述会帮助理解这个结构体的设计思路。另外该结构体也包含了dla_operation_container 和dla_surface_container 的union ,专门用于指向特定的硬件计算子模块 比如bdma 、conv 、sdp 等的操作类型 和image surface 。 |
dla_engine |
dla_engine 结构体的作用只有一个,那就是串东西 ,把用于设置乒乓寄存器组配置寄存器 、producer 和consumer_ptr 的dla_processor ,设置mac阵列大小 、是否使能rubik 、bdma 与weight_compress 的dla_config ,dla_task 和dla_network_desc 给串起来 ,可以说是一家之主了。当然了,还有一个最重要的*driver_context ,这个要把nvdla_device 给映射起来,以便于访问nvdla 设备的硬件资源抽象从而支持读取和写入寄存器 、获取专属锁来申请访问临界区 。 |
dla_network_desc |
dla_network_desc 囊括了运行网络的全部信息,我们可以很明显注意到几个信息,operation_desc_index 、surface_desc_index 和dependency_graph_index ,分别是操作、image surface 和依赖图(也就是常见元操作)的索引 |
dla_task |
dla_task 结构体包含dla任务的common数据,用户态空间数据!!! |
dla_bdma_transfer_desc |
bdma 的传输细节 |
dla_bdma_surface_desc |
bdma 的surface 描述,需要确定source_type 和destination_type ,以及数据传输的num_transfers ,还需要颇为详细的传输细节,相关变量在dla_bdma_transfer_desc 结构体中定义。 |
dla_bdma_op_desc |
bdma 的op 描述,dma 的作用就是传输数据,因此num_transfers 成为关键的指标。 |
dla_bdma_stat_desc |
dla_bdma_stat_desc 结构体——这个结构体是为了看bdma 的状态,有三种状态:read_stall 、write_stall 和runtime 。 |
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 的成员满足作为一个任务的必须要素,包括各个任务的fd 、file状态 、硬件抽象nvdla_device 。 |