前言
本系列内容力求将nvdla的内核态驱动整理清楚,如果有分析不对的请指出。
前面已经分析了一大块代码了,链接分别如下:
系列文章1:NVDLA内核态驱动代码整理一
系列文章2:NVDLA内核态驱动代码整理二
系列文章3:NVDLA内核态驱动代码整理三
系列文章4:NVDLA内核态驱动代码整理四
系列文章5:NVDLA内核态驱动代码整理五
系列文章6:NVDLA内核态驱动代码整理六
系列文章7:NVDLA内核态驱动代码整理七
系列文章8:NVDLA内核态驱动代码整理八
欢迎阅读硬件信号和架构分析系列文章1:
架构开篇介绍文章:NVDLA内核态驱动代码整理三
系列文章1:NVDLA硬件信号和架构设计整理一
系列文章2:NVDLA硬件信号和架构设计整理二
系列文章3:NVDLA硬件信号和架构设计整理三
本篇是对前面NVDLA内核态驱动代码的汇总,如果需要详细细节,可以跳转前面列出的链接。
一、讲解函数汇总
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_cpu_address函数 | 用于读取的地址是CPU视角的地址,注意和dla_read_dma_address函数的区别。(两者可以很好地对比,对于理解总线视角和cpu视角的地址差异很有帮助) |
dla_get_dma_address函数 | dla_get_dma_address函数将dla_read_dma_address和dla_read_cpu_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函数用于清除任务的上下文。 |
dla_handle_events函数 | 是dla_process_events函数的核心函数。遍历Groups,依次传输权重数据、其他必要数据、执行任务,在处理完事件后,将当前组的事件标志清零,表示已经处理过这些事。 |
dla_update_consumer和dla_op_completion函数 | 分别用于传输必要数据、执行任务。 |
nvdla_probe和nvdla_remove函数 | 平台设备注册和注销函数 |
conv.c所有函数:
| 函数 | 功能 |
|---|---|
dla_conv_stat_data函数 | dla_conv_stat_data函数和打印相关 |
get_in_format函数 | get_in_format函数是为了获取输入格式是Feature还是Pixel |
dla_conv_set_producer函数 | dla_conv_set_producer函数是为了根据选择好的乒乓寄存器组编号来配置Convolution Core的四个子模块cacc、cmac、csc和cacc的S_POINTER寄存器,这里的S_POINTER是指向CSB Master和访问Groups的数据路径的指针Pointer |
dla_conv_enable函数 | dla_conv_enable函数是为了在确认CBUF向Convolution Core的数据流动已经结束了以后,启动CDMA的性能计数器,然后使能所有子模块,使能的方式就是把CACC_D_OP_ENABLE_0_OP_EN_ENABLE这个宏常量交给cacc、cmac、csc和cdma的D_OP_ENABLE寄存器,然后就可以启动了。 |
dla_conv_rdma_check函数 | dla_conv_rdma_check函数用于是否启动remote DMA。 |
dla_read_input_address函数 | dla_read_input_address函数内出现了俩函数:dla_get_dma_cube_address函数和dla_data_read函数。dla_get_dma_cube_address函数前面已经提到过,而后面的dla_data_read函数此前提到过,和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函数),而dla_data_read就是把写变成读。解释完2个函数的作用以后,我们看一下整体。对于输入层,如果是静态ROI,则从地址列表中读取该地址,索引在data cube中指定,也就是常规的dla_get_dma_cube_address函数。对于动态ROI,根据ROI信息和使用的Surface Address读取。所以输入数据的读取会采用两种方式,一种是无视地址,直接采用dla_get_dma_cube_address读取;另一种和感兴趣地址相关,需要给定感兴趣地址列表,然后使用dla_data_read函数读取。 |
processor_conv_program函数 | 主要完成convolution core运行所需要的各种配置,这些配置均是通过寄存器来完成,和硬件设计息息相关。 |
二、讲解结构体和联合体汇总
所有结构体:
| 结构体 | 功能 |
|---|---|
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。 |
of_device_id结构体 | 用于将不同type的nvdla与compatible属性结合在一起,在设备树文件中用到。 |
dla_engine结构体 | dla_engine结构体包含了dla_task、dla_config、dla_processor等重要的结构体。 |
dla_config结构体 | dla_config结构体包含了是否使能bdma、rubik,是否支持权重压缩和atom size。 |
dla_processor_group结构体 | dla_processor_group结构体包含了重要的dla_operation_container和dla_surface_container联合体,这两个联合体与6个子模块的操作与输入相关。 |
dla_operation_container联合体 | dla_operation_container联合体包含了bdma、conv、sdp、pdp、cdp、rubik等子模块的操作。 |
dla_surface_container联合体 | dla_surface_container联合体包含了bdma、conv、sdp、pdp、cdp、rubik等子模块的surface,每个surface下主要是该阶段的输入和输出数据,比如conv下的权重、WMB、WGS、输入数据和输出数据。 |
dla_conv_surface_desc结构体 | dla_conv_surface_desc结构体包含权重、WMB、WGS、输入数据和输出数据类型。 |
dla_conv_op_desc结构体 | dla_conv_op_desc结构体包含卷积操作的不同配置和拓扑参数等。 |
三、讲解寄存器汇总
| 寄存器 | 功能 |
|---|---|
S_STATUS | S_STATUS表示的含义是2个Register Groups的状态,请注意S_STATUS寄存器会出现在CDMA、CSC、CMAC_A、CMAC_B、CACC、SDP_RDMA、SDP、PDP_RDMA、PDP、CDP、RUBIK等。所以2个Register Groups内包含各个子模块的状态。 |
| 与之相关的寄存器实例1 | group下的id只有0或者1,那么当id为1时,此时mask掩码取值为CACC_S_STATUS_0_STATUS_1_FIELD,如果当id为0时,此时掩码为CACC_S_STATUS_0_STATUS_0_FIELD。 |
| 与之相关的寄存器实例2 | 同样道理,当id为1时,此时shift取值为CACC_S_STATUS_0_STATUS_1_SHIFT,如果当id为0时,此时为CACC_S_STATUS_0_STATUS_0_SHIFT。 |
D_MISC_CFG | 实例:CACC_D_MISC_CFG_0是指Register Group 0内关于CACC的卷积mode、数据精度、权重是否复用、输入数据是否复用做了一系列的配置。 |
D_DATAOUT_SIZE_0 | 实例:CACC_D_DATAOUT_SIZE_0指的是CACC子模块输出cube的宽和高 |
D_DATAOUT_SIZE_1 | 实例:CACC_D_DATAOUT_SIZE_1指的是CACC子模块输出cube的通道数。 |
D_DATAOUT_ADDR | 实例:CACC_D_DATAOUT_ADDR指的是CACC子模块输出cube的地址。 |
D_BATCH_NUMBER | 实例:CACC_D_BATCH_NUMBER指的是CACC子模块batch数目。 |
D_LINE_STRIDE | 实例:CACC_D_LINE_STRIDE指的是CACC子模块输出cube的line stride。 |
D_SURF_STRIDE | 实例:CACC_D_SURF_STRIDE指的是CACC子模块surface cube的line stride。 |
D_DATAOUT_MAP | 实例:CACC_D_DATAOUT_MAP指的是output cube是line pakced还是surface packed。 |
D_CLIP_CFG | 实例:CACC_D_CLIP_CFG指的是在发送到SDP之前数据截断的bit数。 |
D_MISC_CFG | 实例1:CMAC_A_D_MISC_CFG指的是卷积模式、数据精度等的配置。实例2:CMAC_B_D_MISC_CFG指的是卷积模式、数据精度等的配置。 |
D_MISC_CFG | 实例:CSC_D_MISC_CFG与IN_PRECISION、卷积mode、PROC_PRECISION、数据复用、权重复用、skip_data_rls和skip_weight_rls相关。 |
D_DATAIN_FORMAT | 实例:CSC_D_DATAIN_FORMAT指的是输入数据的format和pixel的format。 |
D_DATAIN_SIZE_EXT_0 | 实例:CSC_D_DATAIN_SIZE_EXT_0指的是在extension后的输入cube的宽和高。 |
D_DATAIN_SIZE_EXT_1 | 实例:CSC_D_DATAIN_SIZE_EXT_1指的是在extension后的输入cube的通道。 |
D_BATCH_NUMBER | 实例:CSC_D_BATCH_NUMBER指的是batch数。 |
D_POST_Y_EXTENSION | 实例:CSC_D_POST_Y_EXTENSION指的是针对image-in的后extension系数。 |
D_ENTRY_PER_SLICE | 实例:CSC_D_ENTRY_PER_SLICE指的是用于一个输入slice的CBUF entry数目。 |
D_WEIGHT_FORMAT | 实例:CSC_D_WEIGHT_FORMAT指的是权重是否压缩。 |
D_WEIGHT_SIZE_EXT_0 | 实例:CSC_D_WEIGHT_SIZE_EXT_0指的是extension后的weight的宽和高。 |
D_WEIGHT_SIZE_EXT_1 | 实例:CSC_D_WEIGHT_SIZE_EXT_1指的是extension后的weight的通道。 |
D_WEIGHT_BYTES和CSC_D_WMB_BYTES | 实例:CSC_D_WEIGHT_BYTES和CSC_D_WMB_BYTES指的是权重和WMB的总bytes数。 |
四、反复出现的三件套汇总
凡是出现了`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
五、从processor_conv_program函数到用户态任务提交函数
前面基本已经把所有重要的函数、结构体或者联合体与相关寄存器都讲解完了,然而还是缺了一根线将其串联起来。我尝试从conv.c的processor_conv_program函数作为起点,关联到用户态任务提交函数。
首先,conv.c下的函数:
dla_conv_program <= processor_conv_program
dla_conv_dump_config
dla_conv_is_ready
dla_conv_rdma_check
dla_conv_enable
dla_conv_set_producer
dla_conv_dump_stat
dla_conv_stat_data
这些函数都被注册提交给engine_data.c中的dla_engine结构体:
static struct dla_engine engine = {
......
.processors[DLA_OP_CONV] = {
.name = "Convolution",
.op_type = DLA_OP_CONV,
.program = dla_conv_program,
.enable = dla_conv_enable,
.set_producer = dla_conv_set_producer,
.is_ready = dla_conv_is_ready,
.dump_config = dla_conv_dump_config,
.rdma_check = dla_conv_rdma_check,
.get_stat_data = dla_conv_stat_data,
.dump_stat = dla_conv_dump_stat,
.consumer_ptr = 0,
.roi_index = 0,
.group_status = 0,
.rdma_status = 0,
.last_group = 1,
.groups[0] = {
.id = 0,
.rdma_id = 0,
.active = 0,
.events = 0,
.roi_index = 0,
.is_rdma_needed = 0,
.lut_index = -1,
.operation_desc = &operation_desc[DLA_OP_CONV][0],
.surface_desc = &surface_desc[DLA_OP_CONV][0],
},
.groups[1] = {
.id = 1,
.rdma_id = 0,
.active = 0,
.events = 0,
.roi_index = 0,
.is_rdma_needed = 0,
.lut_index = -1,
.operation_desc = &operation_desc[DLA_OP_CONV][1],
.surface_desc = &surface_desc[DLA_OP_CONV][1],
},
},
......
所以和乒乓寄存器组参数配置、使能哪一个寄存器组、子模块参数配置(与寄存器相关)、输入-输出-权重数据的指针、是否使能rubik等相关操作关系最大、最直接的结构体是dla_engine。当然以上只是dla_engine的冰山一角,因为上面一大块代码只是赋值,接着继续追溯dla_engine:
struct dla_engine {
struct dla_task *task;
struct dla_config *config_data;
struct dla_network_desc *network;
struct dla_processor processors[DLA_OP_NUM];
uint16_t num_proc_hwl;
int32_t status;
uint32_t stat_enable;
void *driver_context;
};
在将各个子模块的信息提交给dla_engine以后,下面的dla_register_driver向dla注册驱动。
int32_t dla_register_driver(void **engine_context, void *driver_context)
{
*engine_context = &engine;
engine.task = &global_task;
engine.driver_context = driver_context;
engine.task->task_data = NULL;
dla_init_op_cache(&engine);
dla_info("%d . %d . %d\n", FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_SUBMINOR);
RETURN(0);
}
随后接着追溯dla_register_driver:
static int32_t nvdla_probe(struct platform_device *pdev)
{
int32_t err = 0;
struct resource *res;
struct nvdla_device *nvdla_dev;
struct device *dev = &pdev->dev;
const struct of_device_id *match;
if (!pdev->dev.of_node)
return -EINVAL;
match = of_match_device(nvdla_of_match, &pdev->dev);
if (!match) {
pr_err("Missing DT entry!\n");
return -EINVAL;
}
nvdla_dev = devm_kzalloc(dev, sizeof(*nvdla_dev), GFP_KERNEL);
if (!nvdla_dev)
return -ENOMEM;
platform_set_drvdata(pdev, nvdla_dev);
nvdla_dev->pdev = pdev;
nvdla_dev->config_data = (struct nvdla_config *)match->data;
init_completion(&nvdla_dev->event_notifier);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 使用 devm_ioremap_resource 函数将设备的内存资源映射到内核地址空间,并将映射后的地址存储在 nvdla_dev->base 中。
nvdla_dev->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(nvdla_dev->base)) // 检查内存映射是否成功
return PTR_ERR(nvdla_dev->base);
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
// 获取设备的中断资源,这里获取了第一个中断资源。将结果存储在 res 变量中。
if (!res) {
dev_err(&pdev->dev, "no irq resource\n");
return -EINVAL;
}
// 将中断资源的起始地址存储在 nvdla_dev->irq 中,以便后续使用。
nvdla_dev->irq = res->start;
// 使用 devm_request_irq 函数请求设备的中断服务,并将中断处理函数设置为 nvdla_engine_isr。如果请求失败,将错误码存储在 err 中。
err = devm_request_irq(&pdev->dev, nvdla_dev->irq,
nvdla_engine_isr, 0,
dev_name(&pdev->dev), nvdla_dev);
if (err)
return err;
// 调用 dla_register_driver 函数注册驱动程序,并传递设备的引用和配置数据。
dla_register_driver(&nvdla_dev->engine_context, (void *)nvdla_dev);
// 调用 dla_clear_task 函数,用于清除任务相关的数据。此函数接受设备的引用作为参数,其中 nvdla_dev->engine_context 存储了引擎相关的上下文信息。
dla_clear_task(nvdla_dev->engine_context);
err = nvdla_drm_probe(nvdla_dev);
if (err)
dev_err(&pdev->dev, "failed to register drm device\n");
return err;
}
最后是通过nvdla_probe注册。那这里还有两个问题:1、如何确定是由dla_engine结构体哪一个processor来执行?2、用户态和内核态之间的任务接口交互?
先来回答第1个问题:
我们接着看nvdla_probe函数内的nvdla_engine_isr,并对它进行追溯:
static irqreturn_t nvdla_engine_isr(int32_t irq, void *data)
{
unsigned long flags;
struct nvdla_device *nvdla_dev = (struct nvdla_device *)data;
if (!nvdla_dev)
return IRQ_NONE;
spin_lock_irqsave(&nvdla_dev->nvdla_lock, flags);
dla_isr_handler(nvdla_dev->engine_context);
complete(&nvdla_dev->event_notifier);
spin_unlock_irqrestore(&nvdla_dev->nvdla_lock, flags);
return IRQ_HANDLED;
}
接着追溯dla_isr_handler函数:
int32_t dla_isr_handler(void *engine_data)
{
uint32_t mask;
uint32_t reg;
struct dla_processor *processor = NULL;
struct dla_processor_group *group;
struct dla_engine *engine = (struct dla_engine *)engine_data;
mask = glb_reg_read(S_INTR_MASK);
reg = glb_reg_read(S_INTR_STATUS);
dla_trace("Enter: dla_isr_handler, reg:%x, mask:%x\n", reg, mask);
if (reg & MASK(GLB_S_INTR_STATUS_0, CACC_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CACC_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, SDP_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_SDP];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, SDP_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_SDP];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDP_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_CDP];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDP_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_CDP];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, RUBIK_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_RUBIK];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, RUBIK_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_RUBIK];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, PDP_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_PDP];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, PDP_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_PDP];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, BDMA_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_BDMA];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, BDMA_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_BDMA];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_OP_COMPLETED);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDMA_DAT_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_CDMA_DT_DONE);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDMA_DAT_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_CDMA_DT_DONE);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDMA_WT_DONE_STATUS0)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[0];
group->events |= (1 << DLA_EVENT_CDMA_WT_DONE);
}
if (reg & MASK(GLB_S_INTR_STATUS_0, CDMA_WT_DONE_STATUS1)) {
processor = &engine->processors[DLA_OP_CONV];
group = &processor->groups[1];
group->events |= (1 << DLA_EVENT_CDMA_WT_DONE);
}
glb_reg_write(S_INTR_STATUS, reg);
mask = glb_reg_read(S_INTR_MASK);
reg = glb_reg_read(S_INTR_STATUS);
dla_trace("Exit: dla_isr_handler, reg:%x, mask:%x\n", reg, mask);
RETURN(0);
}
尽管目前无法精准解读出现的几个寄存器,但是对于解答第一个问题绰绰有余,每一个if的条件体内均做了三件事情:1、指定子模块;2、指定使用乒乓寄存器组中的哪一组;3、对events更新。
那接下来回答第二个问题:用户态和内核态之间的任务接口交互?
看看nvdla_submit函数:
static int32_t nvdla_submit(struct drm_device *drm, void *arg,
struct drm_file *file)
{
int32_t err = 0;
struct nvdla_task *task;
struct nvdla_ioctl_submit_task local_task;
struct nvdla_ioctl_submit_task __user *user_task;
struct nvdla_device *nvdla_dev = dev_get_drvdata(drm->dev);
struct nvdla_submit_args *args =
(struct nvdla_submit_args *)arg;
user_task = (struct nvdla_ioctl_submit_task __user *)
(uintptr_t)args->tasks;
if (!user_task)
return -EINVAL;
/* IOCTL copy descriptors */
if (copy_from_user(&local_task, (void __user *)user_task,
(sizeof(*user_task))))
return -EFAULT;
task = kzalloc(sizeof(*task), GFP_KERNEL);
if (task == NULL)
return -EFAULT;
nvdla_dev->task = task;
kref_init(&task->ref);
task->nvdla_dev = nvdla_dev;
task->file = file;
/* update task desc fields */
err = nvdla_fill_task_desc(&local_task, task);
if (err)
goto free_task_desc;
// 用于提交 NVDLA 任务并等待任务完成的函数
err = nvdla_task_submit(nvdla_dev, task); // 在nvdla_core_callbacks.c中声明
kfree(task->address_list);
free_task_desc:
kfree(task);
return err;
}
接着往后看nvdla_task_submit:
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;
}
这里面出现两个重要的函数:dla_execute_task和dla_process_events,我把之前给出的解释再搬出来!dla_execute_task函数接受任务的上下文、任务指针和配置数据作为参数,读取网络配置、初始化处理器和处理任务。dla_process_events函数用于处理 NVDLA 设备的事件,其中的关键函数是 dla_handle_events函数,其作用是遍历Groups,依次传输权重数据、其他必要数据、执行任务,在处理完事件后,将当前组的事件标志清零,表示已经处理过这些事。所以关联任务task、事件events和子模块、乒乓寄存器组id的终极函数是dla_handle_events函数。
static int
dla_handle_events(struct dla_processor *processor)
{
int32_t j;
int32_t ret = 0;
uint8_t group_id;
struct dla_processor_group *group;
dla_debug("Enter:%s, processor:%s\n", __func__, processor->name);
group_id = !processor->last_group;
for (j = 0; j < DLA_NUM_GROUPS; j++) {
group = &processor->groups[group_id];
if ((1 << DLA_EVENT_CDMA_WT_DONE) & group->events) {
dla_info("Handle cdma weight done event, processor %s "
"group %u\n", processor->name, group->id);
ret = dla_update_consumers(group,
group->op_desc,
DLA_EVENT_CDMA_WT_DONE);
if (ret)
goto exit;
}
if ((1 << DLA_EVENT_CDMA_DT_DONE) & group->events) {
dla_info("Handle cdma data done event, processor %s "
"group %u\n", processor->name, group->id);
ret = dla_update_consumers(group,
group->op_desc,
DLA_EVENT_CDMA_DT_DONE);
if (ret)
goto exit;
}
/**
* Handle complete after all other events
*/
if ((1 << DLA_EVENT_OP_COMPLETED) & group->events) {
dla_info("Handle op complete event, processor %s "
"group %u\n", processor->name, group->id);
ret = dla_op_completion(processor, group);
if (ret)
goto exit;
}
/**
* Clear all events
*/
group->events = 0; // 在处理完事件后,将当前组的事件标志清零,表示已经处理过这些事件。
group_id = !group_id;
}
exit:
dla_debug("Exit:%s, ret:%x\n", __func__, ret);
RETURN(ret);
}
六、分析汇总——三条线
1、设备注册:`processor_conv_program`函数指定conv子模块运行所需寄存器配置 => 将该函数提交给`dla_engine`结构体下的conv processor => 该结构体通过`dla_register_driver`函数向dla注册驱动 => 该函数作为`nvdla_probe`函数的一部分向nvdla注册驱动。
2、任务运行:`dla_handle_events`函数负责关联任务`task`、事件`events`和子模块、乒乓寄存器组`id` => `nvdla_task_submit`函数包含前述函数,为其创建初始化环境(通过`dla_execute_task`函数来实现)和使用自旋锁对多个processor访问共享资源的环境 => `nvdla_submit`函数用于用户态传入任务数据。
3、设备指定子模块:`dla_isr_handler`函数指定三件事情:1)、指定子模块;2)、指定使用乒乓寄存器组中的哪一组;3)、对`events`更新。 => 该函数是`nvdla_engine_isr`函数的一部分,后者为其创建使用自旋锁对多个processor访问共享资源的环境。
总结
本章除了做简单的汇总之外,分析了设备注册流程,还对任务task、事件events和子模块选择、乒乓寄存器组id选择做了一个追溯,试图理清楚串起所有函数的线,并理解驱动本质上是在做一件什么事情!
614

被折叠的 条评论
为什么被折叠?



