谭钦红,张际生,李文杰,徐 沛
(重庆邮电大学信号与信息处理重点实验室,重庆 400065)
Linux作为开源操作系统,因其良好的可扩展性和网络应用的可靠性,受到众多领域开发者的青睐。目前在各种嵌入式设备,大型交换设备及PC机上均可看见它的身影。同时,Linux系统还具有支持多种总线标准的特点,其中包括外设组件互连标准(peripheral component interconnection,PCI)总线。PCI总线是目前计算机系统中应用最为广泛的一种总线标准,它不依赖于任何一种处理器架构,支持总线控制技术,配合存储器直接存取(direct memory acess,DMA)技术可使总线上的智能设备取得对总线的控制权,进而使智能设备具有与系统内存自主进行高速数据传输的能力。本文重点介绍Linux系统中基于PCI总线接口技术的DMA数据传输的实现机制。
在DMA传输方式出现之前,程序控制输入输出和中断控制输入输出是应用较多的两种传输控制方式。由于程序控制输入输出方式需要CPU反复测试外部设备的状态,CPU在大量时间处于查询和等待的状态下,使得整个计算机系统的效率十分低下,现已不再使用。在中断控制输入输出的方式中,CPU向输入输出设备发出读写一个数据字的命令后即可转入其他工作,而由外设独立完成输入输出操作。当输入输出操作完成后,设备再向CPU发出中断请求,这时CPU才暂停当前操作来响应中断,并再次向输入输出设备发出读写命令。虽然中断控制输入输出方式减少了CPU的等待时间,但是输入输出设备每传送完一个数据字都需要向CPU发出一次中断请求,CPU需要中断一次当前工作并进行保护现场操作,以便中断响应后可以继续执行之前的工作[1]。这样,在连续传送一个数据块的过程中,CPU反复多次被中断,并花费很多时间去处理中断,整个系统效率的提高依旧受到了限制。
DMA是一种允许外围设备直接从内存存取数据而无需CPU全程参与的硬件机制,常用在需要高速大批量数据传输的系统中。整个DMA数据传输操作在“DMA控制器”的控制下进行,CPU只需在数据传输开始和结束时做出相应处理,传输过程中则可处理其他进程[2]。这一机制使得CPU和输入输出设备处于并行操作的状态,大大提高了系统的效率。
实现DMA传送主要有以下4步基本操作。
1)外设发出DMA请求;
2)CPU响应DMA请求并将总线控制权交给DMA控制器,系统转到DMA工作方式;
3)执行DMA传输;
4)传输完成,将总线控制权交还CPU。
在进行以上4步操作前,还需要向系统申请一条DMA通道,通道的申请一般在外设打开时完成,在外设关闭时予以释放。当外设需传输数据时,系统应开辟一块数据缓存区用于数据的存放,同时该缓存区还需被映射成外设可直接访问的DMA缓存区。当传输完成时,外设通过一个中断函数通知CPU数据已传输完毕,并释放对总线的控制权。
PCI总线是目前应用极为广泛的一种计算机总线标准,通过PCI总线与系统相连的设备统称为PCI设备,具体的PCI设备可以是网络设备、字符设备或者USB主机控制器等。Linux系统采用pci_dev结构体来对每一个PCI设备做出描述,该结构体对应一个配置寄存器[3],其空间结构如图1所示。
尽管PCI配置寄存器包含众多内容,但有些配置寄存器是要求的,有些则是可选的。常常用到的有制造商标识(verdor id)和设备标识(device id),它们由制造商写入设备,在系统初始化时被读取,用来供驱动程序查找对应的设备。在Linux系统中,有一条指向所有PCI设备的链表pci_devices。PCI设备描述结构体pci_dev通过global_list成员将自身链接到这条全局的PCI设备链表上,这样驱动程序就能迅速地查找到对应的PCI设备了。
图1 PCI配置寄存器Fig.1 PCI configuration register
Linux系统用pci_driver结构体来统一定义PCI设备驱动,它是pci_dev设备描述结构体的成员函数,其部分代码如下所示。
尽管pci_driver结构体中成员函数较多,但驱动开发人员往往只需关心里面几个重要成员,其中就有 name,id_table,probe。name 代表适用于该 PCI设备的驱动程序的名字,该名称可由lspci命令查看。id_table用于表示驱动所支持的成员设备列表,该列表由probe成员调用[4]。当Linux内核启动并完成对所有PCI设备的扫描、登录和分配资源等初始化操作时,就会建立起系统中所有PCI设备的拓扑结构,probe函数将负责具体硬件的探测工作并保存配置信息,继而加载设备驱动程序。pci_driver结构体的实现形式如下。
注:xxx前缀由驱动开发人员自行命名以示区分。
在Linux系统中,PCI驱动只是为了辅助设备本身的驱动,是所有PCI设备自身驱动在系统中的接口,真正操作设备驱动程序入口是pci_driver结构体中的probe成员函数。probe函数中包括设备的打开、关闭、中断号获取、申请I/O端口、计时等函数,这些成员函数即是设备自身驱动的实现[5]。以下为probe函数部分示例。
在使用DMA方式传输数据前,需要向系统申请一个DMA通道,该通道的申请在外设的打开函数中实现。虽然现在大部分外设均支持DMA功能,但在申请DMA通道之前,最好先判定设备是否支持该功能。外设的打开函数如下。
打开函数中,dma_set_mask()用来判断设备是否具备DMA功能,内核默认设备能对任何32位地址进行DMA操作,该地址位宽可由驱动开发人员指定,例如此处用0xffffff表示设备只限定于对24位地址进行DMA操作;request_irq()成员函数用来向系统注册中断号。其中xxx_inerrupt为中断的实现函数。flag为中断模式,当选择中断为共享模式时,用dev_id来区分是哪个设备发出的中断申请;request_dma()成员函数用来申请DMA通道。其中channel为DMA通道编号,可选为0-7。name用来标识使用该通道的设备,可以在用户空间查看。一般来说,中断注册函数应该在DMA通道申请之前被执行,在DMA通道释放之前被注销[6]。在此将中断注册函数放在打开函数中对应的是PCI网络设备驱动模式,其他PCI设备中断注册函数亦可在初始化函数中实现。
当设备关闭时,需相应地释放中断资源和DMA通道,设备的关闭函数如下。
和打开函数相反,设备关闭函数是先注销中断资源而后释放DMA通道。中断注销函数为free_irq(),DMA通道释放函数为free_dma()。整个DMA通道的申请与释放流程如图2所示。
图2 DMA通道申请与释放Fig.2 DMA channel applications and release
3.2.1 缓存区映射
使用DMA方式传输数据时,需要开辟一块内存区域用于CPU与外设交互数据,这块内存区域被称为DMA缓存区。Linux2.6内核中能够用于分配DMA缓存区的函数有3个:kmalloc(),get_free_pages()和 pci_alloc_consistent()[7]。在这里采用 get_free_pages()函数作为缓存区分配手段,该函数有2个参数:flags,order。flag参数称为分配优先级,常常使用的有GFP_KERNEL或者GFP_ATOMIC;order参数是请求的缓冲页面大小,该参数是一个以2为底数的对数。以下为get_free_pages()函数的具体实现。
buffer=get_free_pages(unsigned int flags,unsigned int order);
如果分配成功,函数将返回缓存区第一页的起始地址,该地址是一个虚拟地址,无法被外设所访问。所以采用DMA方式使外设读写缓存区内时,都要将该缓存区地址映射成总线地址。对于PCI设备来说,缓存区的映射有两种方式:流DMA映射、一致DMA映射。驱动开发人员被建议使用流DMA映射而不是一致DMA映射。原因是一致DMA映射具有更长的存活周期,它会占用的一些相关的寄存器,但只在初期使用它们一次;此外,在一些硬件上,某些可以用于流式DMA映射上的优化手段不能被运用在一致性DMA映射上。流DMA映射实现函数如下。
dma_addr_t dma_map_single(struct device*dev,void*buffer,size_t size,enum dma_data_drection diretion);
该函数返回缓存区buffer的总线地址,同时将总线的控制权交给外设。函数中,size表示待发送数据的大小。diretion表示数据的传输方向,常用的选项有:DMA_TO_DEVICE表示数据发送到外设;DMA_FROM_DEVICE表示数据来自于外设。
3.2.2 数据传输实现
将总线控制权交给外设后,外设就可以从缓存区读写数据了。外设一般拥有2个及其以上独立DMA通道,每个通道包括1个DMA控制器和1个双向的FIFO。大多数DMA控制器具有一个相似的架构,如图3所示,它有1个DMA缓存区的开始地址和1个记录待传输数据位字节数的计数寄存器。随着每次的传输,DMA控制器增加其地址寄存器数值和减小计数寄存器数值。当计数寄存器减小到0时,DMA控制器产生一个中断,并准备下一次传输[8]。
为了更好地说明问题,以PCI9080芯片为例,说明外设发送数据到缓存区的过程。PCI9080是一款32位/33 MHz的通用PCI总线控制器专用芯片,它符合PCI总线规范2.2版本,突发传输速率可达132 MByte/s,支持主模式、从模式、DMA传输模式。芯片提供了2个独立的DMA数据通道,配备有支持外设与CPU存储器之间零等待状态突发传输的双向FIFO。PCI9080以其强大的功能为PCI总线接口的开发提供了一种简洁的方法,设计者只需设计好本地总线接口控制电路,即可实现PCI总线与系统内存间的高速数据传输。PCI9080与PCI总线、本地总线及DMA传输之间的关系如图4所示。
鉴于PCI9080的2个DMA数据通道传输原理一致,此处仅介绍如何利用DMA数据通道0实现PCI总线侧到本地总线侧的数据传输过程,实现传输的主要代码如下描述,代码中所涉及到的部分DMA配置寄存器说明如表1所示。
表1 相关DMA寄存器Tab.1 DMA registers
设计中采用PCI9080的DMA工作方式,在该工作方式下,PCI9080为PCI总线的主控设备,同时也是Local总线的控制者,2条总线间的数据传输通过设置其DMA控制器相关寄存器得以实现。以上述过程为例,可以分为如下6个步骤:1)设置方式寄存器(DMA_MODE)。PCI9080有两种传输方式:0表示块传输(适用于连续的源、目的存储空间),1表示聚/散传输;2)设置 Local地址寄存器(DMA_LADR)。设置Local总线侧地址空间;3)设置PCI地址寄存器(DMA_PADR)。设置PCI总线侧地址空间;4)设置传输计数寄存器(DMA_SIZE)。以字节为单位设置传输数据量;5)设置命令/状态寄存器(DMA_CSR)。设置DMA传输方向;6)设置命令/状态寄存器(DMA_CSR)。启动DMA传输操作,并读该寄存器返回传输状态。
启动DMA传输操作后,PCI9080输出LW/R#=1,表明本次DMA操作为写,即传输方向为PCI-to-Local。随后PCI9080将向CPU发送总线控制申请信号(LHOLD=1),CPU若发现总线空闲,则产生一个响应信号(LHOLDA=1),表示允许PCI9080控制总线,同时向CPU输出 LBE[3:0]#=0h,表明本次传输使用的总线宽度为32位。PCI9080检测到LHOLDA=1后,输出ADS#=0用于通知CPU本地总线上的地址即将有效,并从下一个时钟开始发送数据。CPU接收到ADS#=0后,获取PCI9080发送的地址,同时发出READY#=0信号,通知PCI9080开启数据传输。DMA传输(PCI-to-Local)方向信号时序图见图5所示。
图5 PCI-to-Local方向信号时序图Fig.5 PCI-to-Local the direction of signal timing diagram
在传输启动后的每一个时钟周期里,PCI9080每传输一个数据,其内部传输计数寄存器便自动减1,直至计数寄存器减为0,并输出BLAST#=0,表明这时传输的是最后一个数据。随后PCI9080释放对总线的控制权并向主机发出一个中断请求,从而触发中断服务例程执行。至此,一次完整的PCI设备DMA数据发送流程完毕,其过程如图6所示。
图6 数据发送流程Fig.6 Sending data process
为验证DMA方式在外设与系统内存间的数据传输性能,进行了多次传输测试。结果表明DMA存取技术以其高速大批量的传输特性,能很好地匹配PCI总线功能,大大提高了数据吞吐量,减少了CPU在数据传输操作中的参与程度,同时也减少了对CPU的占用时间,提高了系统的整体效率。
[1]刘秀萍,李艳芬.基于DMA方式的高速数据传输技术[J].导弹试验技术,2009,(02):61-63.LIU Xiu-ping,LI Yan-fen.High-speed data transfer technology based on DMA mode [J].Missile Test,2009,(02):61-63.
[2]朱红星,苗克坚.Linux下PCI设备流式DMA驱动开发[J].微处理机,2007,8(4):69-72.ZHU Hong-xing,MIAO Ke-jian.Linux PCI device under streaming DMA-driven development[J].Microprocessor,2007,8(4):69-72.
[3]BOVET Daniel P,CESATI Marco.深入了解 Linux 内核[M].中国电力出版社,2007:31-45.BOVET Daniel P,CESATI Marco.Depth understanding of the Linux kernel[M].China Electric Power Press,2007:31-45.
[4]CORBET Jonathan,RUBINI Alessandro,HARTMAN Greq Kroah.Linux.Device.Drivers[M].O'Reilly Media inc,2005.
[5]董春桥,李凯.Linux系统PCI设备驱动程序开发[J].计算机测量与控制,2005,13(11):1289-1291.DONG Chun-qiao,LI Kai.PCI device driver development Linux system [J].Computer Measurement and Control,2005,13(11):1289-1291.
[6]马萍,唐卫华,李绪志.基于PCIExpress总线高速数采卡的设计与实现[J].微计算机信息,2008,24(9-1):116-118.MA Ping,TANG Wei-hua,LI Xu-zhi.PCIExpress high speed data acquisition card based on can bus-based design and realization [J].Micro-computer Information,2008,24(9-1):116-118.
[7]林乃响.DMA编程在Linux系统下的应用[J].计算机与信息技术,2006,(3):38-39.LIN Nai-xiang.Application of DMA programming under Linux systems[J].Computer and Information Technology,2006,(3):38-39.
[8]曹宗凯,胡晨,姚国良.DMA在内存间数据拷贝中的应用及其性能分析[J].电子器件,2007,30(01):311-313.CAO Zong-kai,HU Chen,YAO Guo-liang.DMA memory copy of data in application and performance analysis of[J].Electronic Devices,2007,30(01):311-313.