前言
本系列内容力求将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
选择做了一个追溯,试图理清楚串起所有函数的线,并理解驱动本质上是在做一件什么事情!