CNDRV PCI 驱动阅读过程
文章目录
看了一下目录结构,
core
目录下的
cndrv_core.c
文件主要就是提供一个逻辑层的封装接口,
cndrv_cdev.c
主要就是字符设备初始化打开关闭等相关的一些接口,
cdev_ctrl.c
是字符设备的读写控制等操作的封装接口;
plat
目录下就是平台相关的
PCI
驱动、
dma
操作驱动、
mbox
驱动、基于PCI驱动做的一些外设驱动、平台发送固件启动等一些内容。(由于手册中没有关于寄存器部分的描述,这里关于寄存器部分的内容就不做分析)
1. PCI驱动注册
此驱动是在RC
端使用的,注册成模块,然后调用core
层的驱动初始化。
core
核心部分的注册就调用了字符设备初始化的部分。
看一下字符设备驱动初始化的部分,也比较简单,创建了一个设备类,名字是cambricon_dev
。
这时候驱动就算是初始化完了。
注册部分总结:
注册部分是注册PCI驱动(RC端驱动)以及创建一个class类。
2. 驱动 probe 函数
接下来看一下cn_pci_driver
的内容,主要关注一下probe
函数,当设备接入并且匹配成功时,调用此probe
函数。一开始为此设备申请一些私有数据的内存,赋值初始化相关内容后,激活PCI EP
设备,预留IO和内存资源。接下来
接下来看一下各自初始化的内容。
2.1 BAR空间映射
循环从BAR0
到BAR5
进行映射,成功后记录映射了多少个BAR空间。
调用的映射单个BAR
空间的函数如下,获取BAR空间的起始地址和长度后进行pci_iomap()
映射,方便后续驱动可以使用。
BAR空间映射部分总结:
将EP设备的BAR空间都进行映射并记录。
2.2 设置 DMA 掩码
在这里都是设置的32位的掩码,即DMA的总线地址大小?
设置 DMA 掩码射部分总结:
设置DMA总线宽度为32位。
2.3 平台初始化
info->setup
实际是指向c20e_plat_init()
函数,在设备ID表中的最后一项指向c20e_pci_info
。
c20e_pci_info
里面就一个函数指针,指向函数c20e_plat_init()
函数。
2.3.1 内核预留内存初始化
c20e_plat_init()
函数里面的工作主要是对BAR2
空间进行一个划分,DESC_MEM_PHYS_BASE
是在内核里面预留的一段物理地址。
一开始的common_default_data()
是函数指针的赋值过程:
从设备树里面也可以看到,预留的64MB
物理内存里,有8MB
用作消息的内存,剩余的56MB
用作共享的内存,供自由分配。
平台初始化部分总结:
将寄存器虚拟地址指向
BAR0
的虚拟地址(BAR0
应该是在preboot
或者其他阶段里映射掉了),将BAR2
映射到内核预留的内存上,并进行区域划分。此平台上:
BAR0
->16MB
,BAR2
->64MB
,BAR4
->64MB
2.4 使能 MSI 中断
先关闭所有中断,再根据初始化的中断模式来使能对应的中断。
关闭中断的都是写寄存器的过程,此处不表。
如果是初始化的MSI-X
中断,需要先判断是否具有此能力。
这里就使能MSI
中断,注册中弄断处理函数cn_pci_msi_isr()
。
然后配置硬件寄存器:
实际走的是INT-X
中断,这里注册cn_pci_intx_isr()
中断处理函数。
2.4.1 PCI
中断处理函数
这里的读中断状态寄存器后根据中断状态去调用注册的中断处理函数,这里应该是dma_engine_isr()
函数,具体的注册过程在后面会有描述。看[2.5.1 注册DMA
引擎中断回调](# 2.5.1 注册DMA
引擎中断回调)。
读中断状态寄存器:
清除中断状态:
使能MSI中断部分总结:
根据是否有
MSI
能力,去注册MSI
中断,否则注册INT-X
中断,INT-X
中断里面处理的是DMA
引擎注册的中断任务。
2.5 DMA 引擎初始化
接下来就是DMA
引擎的初始化,除了硬件寄存器相关的参数写入,初始化了链表头avail_q
用来保存可用的DMA
通道信息,wait_q
用于DMA
通道都正在使用时进行一个等待。
硬件通道参数的检查与校验:
错误状态的清除:
接下来看一下DMA
引擎的初始化过程,在共享内存中划分一块自己的内存空间,初始化此引擎的相关锁和传输链表头,注册一个回调函数,由设备的pci_dev->irq
触发时进行回调,写硬件寄存器使能DMA
中断,然后将此通道加入设备私有数据的avail_q
链表,表明此通道后续可以使用。
申请资源的过程是在共享内存中用的,这个内存是在BAR
空间映射的时候对内核的保留内存进行映射的,看[2.3.1 内核预留内存初始化](# 2.3.1 内核预留内存初始化)章节。
使能DMA
引擎通道:
在[2.5.1 注册DMA
引擎中断](# 2.5.1 注册DMA
引擎中断回调)回调后,会将此引擎通道加入到avail_q
链表中,表明后续可以使用。
2.5.1 注册DMA
引擎中断回调
hw_dma_setup_irq()
直接调用cn_pci_register_interrupt()
进行初始化,PCIE_IRQ_DMA
这里是145
,4
个DMA
引擎通道应该就是用中断145-148
这几个。
cn_pci_register_interrupt()
这里就是直接对这个irq_desc
数据的数据进行赋值,然后PCI
中断里面会调用这个handler
进行处理。看[2.4.1 PCI
中断处理函数](# 2.4.1 PCI
中断处理函数)。
使能中断:
2.5.2 DMA
引擎中断处理函数
这里就是DMA
引擎的中断处理函数了,读本通道的中断状态,如果有中断,先关闭中断,处理中断;传输链表为空表明都已经处理过了,非空则调用engine_transfer_completion()
去处理传输,最后再使能本通道的DMA
中断。
读通道状态:
关闭通道中断:
engine_transfer_completion()
处理中,先将这个DMA
传输描述符从transfer_list
链表中删除,如果中断状态错误,清除错误状态,否则标志为正常完成,将通道置为空闲后唤醒等待队列。dma_xfer_submit()
函数中会等待这个传输完成的唤醒,看[2.8.4 发起DMA传输](# 2.8.4 发起DMA传输)。
清除错误状态寄存器:
DMA
引擎初始化部分总结:
初始化硬件
DMA
通道,注册到PCI-irq
的中断回调函数中,完成中断处理回调逻辑,将通道置为可用状态。
2.6 核心层的探测
核心层这里主要就是一个字符设备的初始化,包括一开始的内存资源申请,然后加入到全局的cn_core_list
链表中,执行平台初始化,再创建字符设备接口。
申请资源的直接使用kzalloc()
函数:
正确赋值ID后加入到链表尾部:
这里就调用之前注册的回调setup
函数来进行初始化,注册的过程是在c20e_plat_init()
中注册的,在[2.3.1 内核预留内存初始化](# 2.3.1 内核预留内存初始化)。
接下来是创建字符设备的过程以及sysfs
文件的创建。
字符设备创建时需要先申请设备号,配置kobject
之后初始化字符设备的操作集,并将设备增加到系统中,最后创建一个字符的sysfs
文件。
配置kobject
的名字按idx
进行编号:
这里是设置字符设备的操作集,看一下[2.6.1 字符操作集合](# 2.6.1 字符操作集合)。
最后的创建sysfs
文件,结束此过程的初始化。
2.6.1 字符操作集合
字符设备的操作都是调用的plat->fops
相关的操作函数,赋值的过程是在[2.3.1 内核预留内存初始化](# 2.3.1 内核预留内存初始化)时赋值的,需要看的回头看一下即可。
2.6.1.1 ioctl
这里的ioctl
指向cn_pci_ioctl()
函数:
这个ioctl
支持两个命令,用于DMA
传输。
用户态的DMA
传输需要先将用户态的内存映射到散列中,再发起实际的DMA
传输,实际DMA
传输的过程看[2.8.4 发起DMA传输](# 2.8.4 发起DMA传输)。
2.6.1.2 用户态内存的映射到散列
这里看一下用户态内存的映射到散列的过程,计算用户态数据占用的页大小,申请数组,找到用户态数据所在的页并保存起来,并赋值在散列表中。
获取当前进程的mm
内存结构,找到vm_area_struct
结构体,找到具体的页,保存在pages
数组中。
取消映射的过程:
2.6.1.2 read
这里的read
指向cn_pci_read()
函数,这里的pos
是指向的EP
端的地址:
cn_pci_read()
同样是调用cn_dma_transfer_user()
来进行用户态数据DMA
传输,看[2.6.1.1 ioctl](# 2.6.1.1 ioctl)关于这部分的分析。
2.6.1.3 write
这里的write
指向cn_pci_write()
函数,,这里的pos
是指向的EP
端的地址:
cn_pci_write()
同样是调用cn_dma_transfer_user()
来进行用户态数据DMA
传输,与read
方向相反,看[2.6.1.1 ioctl](# 2.6.1.1 ioctl)关于这部分的分析。
2.6.1.4 llseek
这里的llseek
指向cn_pci_llseek()
函数:
根据whence
操作偏移,返回新偏移。
2.6.1.5 mmap
这里的mmap
指向cn_pci_mmap()
函数:
这里根据vm_pgoff
来选择不同的BAR
空间进行映射,vm_pgoff
为0
时选择BAR0
,vm_pgoff
为1
是选择BAR2
,vm_pgoff
为2
时选择BAR4
进行映射,因为此芯片只有BAR0
、BAR2
和BAR4
。映射的页面属性是no cached
,应该是避免DMA
操作这些数据时的cache
一致性问题,最后将BAR
空间映射到用户态空间。这个代码里面没有对要映射的长度进行判断。
总结:
创建字符设备,字符操作时回调
PCI
驱动注册时注册的函数,提供读写、ioctl、mmap等操作函数。
2.7 外设注册
在这里,注册了一个ipc
通信的设备以及一个mbox
设备。
寄存器是BAR0
上映射的EP
的所有寄存器。
这个ipc
内存地址是从BAR2
上划分的一块内存。
2.7.1 ipc
从输入的起始地址开始预留8MB
的内存,用作各个通道的私有数据,增加通道信息后,打印通道信息,保存到共享内存中,最后创建平台设备。
接下来是将ipc_msg_channels
数组里面的通道信息增加到全局变量的chennel
数组中。
ipc_msg_channels
内容如下:
打印通道信息如下:
将全局通道信息chennel
拷贝到共享内存中:
创建平台设备:
指定的操作集如下:
总结:
设置一些不同的通道区域,保存到共享中,并生成平台设备。
2.7.2 mbox
mbox
的私有数据是EP
寄存器部分的数据,中断号是92
。
创建平台设备host_mbox
。
总结:
mbox
设备的私有数据设置为EP
部分寄存器,中断号为92
。
2.8 工作队列初始化
接下来是初始化一个工作队列,执行后续的任务。
接下来便是加载EP
端的固件,包括u-boot
、kernel
和rootfs
等资源,并启动EP
设备(PCIe
启动方式)。
2.8.1 加载固件
申请内存用来保存一个名为loader
私有数据,调用load_file_trans()
进行加载传输。
申请内存,保存加载固件中使用的blocks
数据,
从/lib/mlu_fw.conf
文件中获取相关环境配置信息,这里读MLU_HOME_PATH
这个环境变量的信息。
申请内存,将文件数据读入到内存中,然后读取一行数据,再从行数据中找是否存在对应的变量,获取变量的值。
返回文件的大小:
内核版本比较小时需要get_fs()
和set_fs()
操作。
从文件数据中获取一行数据:
从一行中获取环境变量的值:
获取变量的值:
传输单个文件,打开之后获取文件的大小,按照1MB
的大小计算要传输的次数,依次读取完成后,发起DMA
传输,传输结束后将文件关闭,资源释放。
统计块数据的方式如下,按照最大1MB
进行计算,并为每一块申请内存资源;在这之前,还需要找到对应的DMA
目的地址。
对应的目的地址如下所示:
结束时,对上面申请的资源进行释放。
在这里,遍历即将要发起DMA
进行传输数据,每次结束后都打印一下进度。
发起内核地址的DMA
传输之前,要进行一下页面的映射,用户态的地址空间同理也需要进行映射。内核的看[2.8.2 连续页面映射](# 2.8.2 连续页面映射)和[2.8.3 非连续页面映射](# 2.8.3 非连续页面映射),根据地址是内核线性地址还是通过vmalloc()
申请出来的非线性地址来调用不同的映射。
文件传输异常时,释放对应的资源。
2.8.2 连续页面映射
对传入的地址判断是否是内核的线性地址,申请一个散列结构,将地址所在的页赋值给散列数据,然后[2.8.4 发起DMA传输](# 2.8.4 发起DMA传输),结束后释放散列。
2.8.3 非连续页面映射
如果是vmalloc()
申请出来的内存,物理地址可能是不连续的,所以这里要映射到散列表中,通过vmalloc_to_page()
找到对应所在的物理页,设置到散列中,[2.8.4 发起DMA传输](# 2.8.4 发起DMA传输),结束后释放散列。
2.8.4 发起DMA传输
接下来就是DMA
传输的过程,先获取可用的DMA
通道,如果所有通道都被占用,那么就等待在这里,被信号打断的话就退出;然后提交DMA
传输,传输结束后将通道增加回avail_q
链表中,置通道可用,并唤醒在等待DMA
通道的等待队列。
avail_q
是DMA
可用引擎的链表,非空时从avail_q
中取出一个通道来使用。
提交DMA
传输的过程,先进行一个pci_map_sg()
对散列进行初始化,然后申请内部的DMA
请求并初始化请求的内容;循环处理每一个请求,初始化相关的传输描述符,发起硬件DMA
传输后,等待唤醒;处理结束状态,直到循环结束后,销毁传输描述符,释放内部DMA
请求资源。
遍历sg
去初始化DMA
描述符的过程。
传输请求的申请就是一个vmalloc
的调用:
传输相关的初始化,先初始化等待队列头,然后构造硬件DMA
相关的描述符。
硬件DMA
相关的描述符初始化和数据填充:
硬件DMA
相关的描述符初始化:
硬件DMA
相关的描述符数据填充:
将传输描述符加入到传输链表后,如果当前DMA
通道引擎未开启,则启动DMA
传输。
从传输链表中取出一个后,硬件发起DMA
引擎传输:
写寄存器发起DMA
引擎传输:
引擎传输结束时:
打印传输相关的数据:
打印硬件DMA
描述符的数据:
传输描述符端点释放,取消散列的映射:
释放申请的请求描述符资源:
PCI
私有数据、DMA
引擎、传输请求、传输描述符(不包含硬件DMA
描述符)的几个关系大概如下:
发起DMA
传输部分总结:
DMA
传输应该是本驱动的核心内容,传输过程中各种描述符的使用使代码看起来比较复杂,很多细节部分没有去细看。