基于RedHat 5.1 系统下的rapidio驱动开发技术方案

1.驱动开发环境

1.1驱动开发基础环境

桌面环境:RedHat 5.1,内核:Linux Kernel 2.6.29.6。

2驱动开发框架设计

2.1设备驱动程序模块简介

Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则是编译成可以动态加载的模块。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态地卸载,不利于调试,所以本驱动开发使用模块方式。

从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。

在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module( )和cleanup_module( ),而且至少要包含和两个头文件。在用gcc编译内核模块时,需要加上-DMODULE -D__KERNEL__ -DLINUX这几个参数,编译生成的模块(一般为.o文件)可以使用命令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用模块中的函数init_module( )。当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module( )。任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。

 

 

2.2 PCI总线

PCI是外围设备互连(Peripheral Component Interconnect)的简称,作为一种通用的总线接口标准,它在目前的计算机系统中得到了非常广泛的应用。PCI提供了一组完整的总线接口规范,其目的是描述如何将计算机系统中的外围设备以一种结构化和可控化的方式连接在一起,同时它还刻画了外围设备在连接时的电气特性和行为规约,并且详细定义了计算机系统中的各个不同部件之间应该如何正确地进行交互。

PCI将计算机系统中的总线子系统与存储子系统完全地分开,CPU通过一块称为PCI桥(PCI-Bridge)的设备来完成同总线子系统的交互,如图2-1所示。

图2-1 PCI子系统的体系结构

图2-1 PCI子系统的体系结构

2.3 PCI的地址空间与系统地址空间

PCI的地址空间:I/O空间,存储空间,配置空间。

PCI总线具有32位数据/地址复用总线,所以其存储地址空间为2的32次方=4GB。也就是PCI上的所有设备共同映射到这4GB上,每个PCI设备占用唯一的一段PCI地址,以便于PCI总线统一寻址。每个PCI设备通过PCI寄存器中的基地址寄存器来指定映射的首地址。PCI地址空间对应于计算机系统结构中的PCI总线。

假设在一个32位处理器中,其存储器域的0xF000-0000~0xF7FF-FFFF(共128MB)这段物理地址空间与PCI总线的地址空间存在映射关系,如下图2-2。

存储器域与PCI总线域

图2-2 存储器域与PCI总线域的映射关系

当处理器访问这段存储器地址空间时,HOST主桥将会认领这个存储器访问,并将这个存储器访问使用的物理地址空间转换为PCI总线地址空间,并与0x7000-0000~0x77FF-FFFF这段PCI总线地址空间对应。

为简化起见,我们假定在存储器域中只映射了PCI设备的存储器地址空间,而不映射PCI设备的I/O地址空间。而PCI设备的BAR空间使用0x7000-0000~0x77FF-FFFF这段PCI总线域的存储器地址空间。

PCI桥的Base、Limit寄存器保存“该桥所管理的PCI子树”的存储器或者I/O空间的基地址和长度。值得注意的是,PCI桥也是PCI总线上的一个设备,在其配置空间中也有BAR寄存器。在PCI设备的BAR寄存器中,包含该设备使用的PCI总线域的地址范围。在PCI设备的配置空间中共有6个BAR寄存器,因此一个PCI设备最多可以使用6组32位的PCI总线地址空间,或者3组64位的PCI总线地址空间。这些BAR空间可以保存PCI总线域的存储器地址空间或者I/O地址空间,目前多数PCI设备仅使用存储器地址空间。而在通常情况下,一个PCI设备使用2到3个BAR寄存器就足够了。

2.4 TSI 721芯片说明

Tsi721作为一个PCIe设备挂接在AMD780桥片上。具有6个BAR地址空间。BAR0用于访问Tsi721内部寄存器空间,BAR1作为Outbound doorbell地址空间,BAR2/BAR3组合为64位地址空间作为可预取的PCIe到S-RIO地址命中转换空间,BAR4/BAR5组合为64位地址空间作为不可预取的PCIe到S-RIO地址命中转换空间。

Tsi721包含PCIe接口、S-RIO接口、消息引擎、映射引擎以及块DMA引擎等几个主要功能块。

Tsi721通过PCIe接口经AMD780桥片与CPU联接。S-RIO接口经1848上RapidIO网络。

消息引擎实现PCIe接口与S-RIO接口之间的消息通信。包含8路Outbound Message DMA通道和8路Inbound Message DMA通道。Outbound Message DMA通道实现Tsi721主控发送消息,Mailbox可以为0、1、2、3,消息中传送的数据最大可达4KB。Inbound Message DMA通道实现消息接收,每个Mailbox分配两路DMA通道,一路用于ID不匹配的消息接收,一路用于ID匹配的消息接收,每路接收通道包含16个接收上下文,可以用于同时接收多个消息源。

映射引擎实现PCIe接口到S-RIO接口地址转换(PC2SR)和S-RIO接口到PCIe接口地址转换(SR2PC)。PC2SR包含8个本地地址映射窗,每个地址窗包含8个域,可以将地址窗空间转换为8个RapidIO地址段。CPU主控发起PCIe读写操作在BAR2/BAR3或BAR4/BAR5空间内,由该地址窗命中并转换为相应RapidIO地址的RapidIO数据包,实现CPU主控RapidIO数据包或维护包读写。SR2PC包含8个Inbound地址窗和8个Inbound doorbell接收队列。Inbound地址窗用于接收RapidIO读写访问,将命中的RapidIO地址转换为本地地址的PCIe访问。Inbound doorbell接收队列用于接收ID匹配的doorbell。

块DMA引擎实现DMA方式发起数据包或维护包的读写访问。包含8个DMA通道,每个DMA通道均以链式描述符的方式工作,用于主控发起RapidIO读写操作。

2.5 DMA传输说明

DMA传送操作分三个阶段:准备阶段、DMA传送阶段和传送结束阶段。

1)准备阶段:在这个阶段中,CPU通过指令向DMA控制器发送必要的传送参数。

①控制字送DMA控制器指出数据传送方向。

②预置MBAP,即数据块在主存缓冲区的首址。

③置DAR外设的地址,如外设为磁盘机,其地址包括:磁盘机号、盘面号、柱面号和扇区号。

④给WBC预置,指出数据传送字节/字数。

2)DMA传送阶段

DMA接口上传送的一批数据是一个个传送的,在周期挪用控制方式下DMA控制器主要完成以下五个操作。

①外设准备好一次数据传送后,接口向主机发DMA请求。

②CPU响应DMA请求,把总线使用权让给DMA控制器。DMA控制器控制源、目的端口准备传送数据。

③DMA周期挪用一次,交换一个数据信息。

④归还总线使用权,修改主存地址指针和传送计数值。

⑤判断这批数据是否传送完毕:是,结束该工作阶段;没有,又开始传送下一个数据。DMA中信息传送过程.

3)结束阶段

DMA在两种情况下都会进入结束阶段,一种情况是一批数据传送完毕,这是正常结束。另一种情况是DMA发生故障,也要进入结束阶段,这是非正常结束。不论是哪一种情况进入结束阶段,DMA都向主机发出中断请求,CPU执行服务程序查询DMA接口状态,根据状态进行不同的处理。

2.6驱动整体框架结构

结合上面的分析以及查看内核(Linux Kernel 2.6.29.6)的相关头文件,作出如下图的驱动框架结构,见图2-4 ,略。

 

3 驱动开发设计

进行pci设备驱动开发时必须了解如下的PCI数据结构。

pci_device_id

pci_driver

pci_dev

MODULE_DEVICE_TABLE宏,pci_device_id 结构需要被输出到用户空间, 来允许热插拔和模块加载系统知道什么模块使用什么硬件设备, 宏 MODULE_DEVICE_TABLE 完成这个。

3.1 TSI721驱动设计

根据PCI驱动设备要实现的功能,拟定TSI721设备驱动有如下基本模块:

  • 驱动程序的注册与注销
  • 设备的打开与释放
  • 设备的读写操作
  • 设备的控制操作
  • 设备的中断和轮询处理

3.1.1驱动程序的注册与注销

当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,调用如下的代码:

//__init 标记内核启动时所用的初始化代码,其所修饰的内容被放到.init.text section中

这里有个关键结构,tsi721_driver,需要自己定义,代码如下。其中宏DRV_NAME为驱动的名子且在内核中所有 PCI 驱动里面必须具有唯一性。当驱动在内核时,它显示在 sysfs 中在 /sys/bus/pci/drivers/ 下。tsi721_pci_tbl为MODULE_DEVICE_TABLE宏加载探测到的PCI驱动表。probe探测函数将负责完成对PCI设备硬件的检测工作。remove移除函数则负责卸载PCI设备及释放PCI设备占有的系统资源。

3.1.2 PCIe端的配置及地址映射

PCI/PCIe设备有自己的独立地址空间,这部分空间会映射到整个系统的地址空间。在命令行下执行如下命令可看到本驱动独立的地址空间,见图3-5。当然也可通过pci_resource_start,pci_resource_len及pci_resource_flags函数获取其独立地址空间。

tsi721设备地址空间

图3-5 tsi721设备地址空间

I/O 内存可分为可以或者不可以通过页表来存取。本驱动是通过页表存取,内核必须先重新编排物理地址,使其对驱动程序可见,这就意味着在进行任何I/O操作之前,必须调用映射函数;

本驱动IO内存的访问方法是:首先调用pci_enable_device ()激活设备,再通过pci_request_regions()函数通知内核当前PCI将使用这些内存地址,其他设备不能使用,接着将寄存器地址通过ioremap()或ioremap_nocache()映射到内核空间的虚拟地址,之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放pci_request_regions()申请的IO内存资源。

ioremap_nocache()函数说明:

void __iomem * ioremap_nocache (unsigned long phys_addr, unsigned long size);

phys_addr:要映射的物理地址

size:要映射资源的大小

映射结果可通过dmesg查看,如下图3-6:

地址

图3-6 tsi721实际分配的空间情况

3.1.3中断初始化

在执行中断初始化之前,要先行确定DMA的32位或64位寻址能力及其他相关设置,并查询及定义PCIe芯片中断寄存器的偏移量。

本驱动PCIe接口可以生成MSI、MSI – x和legacy INTx中断。INTx / MSI / MSI-x生成由一个自由运转中断定时器决定。 在MSI中断方式下,设备通过向OS预先分配的主存空间写入特定数据的方式请求CPU的中断服务,为PCIe系统首选的中断信号机制,对于PCIe到PCI/PCI-X的桥接设备和不能使用MSI机制的传统端点设备,采用INTx虚拟中断机制。

中断处理函数的功能是将有关中断接收的信息反馈给设备,并对数据进行相应读写。中断信号到来,系统调用相应的中断处理函数,函数判断中断号是否匹配,若是,则清除中断寄存器相应的位,即在驱动程序发起新的DMA之前设备不会产生其他中断,然后进行相应处理。因此在使用前要先清0,本驱动定义tsi721_disable_interrupt_ints()函数进行中断初始化工作。

3.1.4维护包

对于高速数据信号的采集处理,需要在驱动程序的初始化模块(probe)中申请大量的DMA循环缓冲区,申请的大小直接关系着能否实时对高速数据处理的成败。直接内存访问(DMA)是一种硬件机制,允许外围设备和主内存直接直接传输I/O数据,避免了大量的计算开销。

因此需定义维护包及块MDA描述符结构体来维护高速数据信号的读写:

 

3.1.5门铃设计及调试

根据TSI721芯片手册可知出站的门铃不需要任何设置。 Tsi721使用专用的PCI BAR1来生成门铃。该BAR1在探测程序中被映射。所以门铃初始化主要完成如下工作:

  • 初始化入站门铃处理DPC和队列
  • 为入站门铃队列分配缓冲区
  • 启用接收所有入站门铃

调试阶段门铃收到后直接打印到内核log中,实际调用时使用回调函数来把收到的门铃消息传给用户,其处理过程如下图,略,其中dbell->dinb()就是回调函数。

3.1.6入站内存映射

数据的接收需要预先配置好入站内存映射区域,用户调用驱动接口传递用户空间的虚拟地址,实际是对应TSI721设备的物理地址。再进行内存映射时,需要计算最小可接受的窗口大小和基本地址。然后扫描与inbound活动区域重叠并标记第一个可用区域IB窗口,扫描过程如下图3-10,略。

扫描到可用区域后,就可把相关信息写入TSI721设备对应的寄存器里,以此来配置入站内存映射区域。测试时,用户配置虚拟地址为0x2000000,大小为1MB(0x100000),执行后已实现入站内存映射,其打印结果如下图3-11,略。

3.1.7 DMA注册模块

由于TSI721设备发送数据有2种方式,即采用CPU发数据模式(需要配置outbound窗口)和DMA发数据模式。DMA发数据模式需要系统内核的支持,需要配置相关的宏定义。Linux Kernel 2.6.29.6并不支持DMA模式,因此再配置DMA注册模块时,需要先完成相关的宏定义,如宏CONFIG_DMA_ENGINE和CONFIG_RAPIDIO_DMA_ENGINE,同时完成dma引擎的驱动设计。dma引擎的驱动可放在后面考虑。

要实现DMA发送大数据,需要定义如下几个关键结构体及函数。

  • DMA描述符(struct tsi721_dma_desc)
  • DMA通道(struct tsi721_bdma_chan)
  • tsi721_bdma_handler(struct tsi721_bdma_chan *bdma_chan);
  • tsi721_register_dma(struct tsi721_device *priv);
  • tsi721_unregister_dma(struct tsi721_device *priv);
  • tsi721_dma_stop_all(struct tsi721_device *priv);

本驱动在处理数据时,DMA发送接收模块采用tasklet机制。该用于减少硬中断处理的时间,将本来是在硬中断服务程序中完成的任务转化成软中断完成,即是将一些非紧急的任务留到tasklet中完成,而紧急的任务则在硬中断服务程序中完成。tasaklet 有如下特征

  • 一个tasklet可被禁用或启用;只用启用的次数和禁用的次数相同时,tasklet才会被执行;
  • 和定时器类似,tasklet可以注册自己;
  • tasklet可被调度在一般优先级或者高优先级上执行,高优先级总会首先执行;
  • 如果系统负荷不重,则tasklet会立即得到执行,且始终不会晚于下一个定时器滴答;
  • 一个tasklet可以和其他tasklet并发,但对自身来讲必须严格串行处理,即一个tasklet不会在多个处理器上执行

其中关键初始化流程见下图3-12DMA通道初始化流程,略。

 

注册完tasklet及DMA通道后,用户如果发送或接收数据。应用接口驱动会通过rapidio驱动找到空闲的DMA通道,并通过device_alloc_chan_resoureses接口调用tsi721_alloc_chan_resources函数为该通道分配资源,其分配资源的过程和维护包资源分配大致相同,流程如下图3-13,略。如果该通道已经分配资源,则直接使用该通道执行DMA读或写操作。

3.1.8 msix中断

Tsi721设备在初始化时,会检测系统是否支持pci_msi中断机制,如果支持,则调用tsi721_enable_msix函数尝试启用MSI-X支持Tsi721。具体流程如下:

  • 初始化4个消息窗(inbound和outbound)
  • 如果配置DMA,则为DMA引擎初始化MSI-X条目
  • 调用pci_enable_msix函数,向PCI子系统请求分配n个msix中断
  • 将MSI-X矢量信息复制到tsi721私有结构中

确认MSI-X支持tsi721设备后,则需要调用相关函数为MSI-X模式注册中断服务。

3.1.9 TSI721与Rapidio驱动交互

Tsi721驱动负责tsi721设备的注册及相关资源的申请,同时提供各种跟硬件相关的底层接口,并通过Rapidio驱动初始化rio_mport结构体,并向rapidio驱动注册该设备。

Tsi721驱动为上层驱动提供如下接口,见图3-14,略:

3.2 dma驱动设计

Dma驱动主要是仿照linux内核4.1.14版本的dma驱动写的,主要提供DMA通道注册及DMA通道注销等功能,为tsi721、rapidio及rio_mport驱动提供接口支持。

主要接口如下:

dma_release_channel

dmaengine_get

dma_find_channel

dma_sync_wait

dma_issue_pending_all

dma_async_memcpy_buf_to_buf

dma_async_device_register

dma_channel_table_init

dma_wait_for_async_tx

dma_async_memcpy_pg_to_pg

dma_run_dependencies

dma_request_channel

dma_async_device_unregister

dma_async_tx_descriptor_init

dmaengine_put

dma_async_memcpy_buf_to_pg

3.3 rapidio驱动设计

Rapidio驱动主要是提供相关接口供其它驱动调用,大部分接口都依赖与tsi721向其注册的rio_mport设备。该驱动初始化如下图3-15,略:

其中对于tsi721底层设备的关键操作接口有rio_local_read_config_##size、rio_local_write_config_##size、rio_mport_read_config_##size、rio_mport_write_config_##size。这些接口会生成读写8/16/32位数的功能函数。其他涉及tsi721设备的相关配置都需要调用如上接口。

      3.1.1 rapidio驱动与tsi721驱动交互接口

Rapidio驱动主要提供rio_mport_initialize(struct rio_mport *mport)函数及rio_register_mport(struct rio_mport *port)函数供tsi721驱动调用。Rapidio驱动如果需要操作tsi721设备,则使用rio_mport提供的rio_ops接口。

rio_mport_initialize函数主要作用是初始化rio_mport设备的端口号等相关信息。rio_register_mport函数主要作用是向rio_mports链表里添加节点,实现如下图3-16,略:

3.1.2 rapidio驱动与路由驱动交互接口

Rapidio驱动主要提供rio_register_scan(int mport_id, struct rio_scan *scan_ops)函数及rio_init_mports(void)函数。rio_register_scan函数的功能是注册枚举/发现tsi721设备的端口。rio_init_mports(void)函数则执行枚举/发现tsi721设备,并创建工作队列。其他关键功能函数如下:

rio_route_clr_table

rio_route_get_entry

rio_route_add_entry

rio_lock_device

rio_mport_get_feature

rio_enable_rx_tx_por

3.1.3 rapidio驱动与应用驱动的交互接口

Rapidio驱动为应用层驱动提供相关接口来操作底层tsi721设备。如设置tsi721设备的端口号,申请inbound入站内存映射,申请dma通道,释放资源等。前文提到的读写8/16/32位数的功能函数则在应用驱动中多次使用。

  • 门铃消息服务(rio_request_inb_dbell)

应用层驱动调用该函数后,tsi721设备收门铃后通过回调的方式把门铃消息反馈给应用层驱动。关键函数rio_request_inb_dbell实现方式如下图3-17,释放门铃消息服务函数为rio_release_inb_dbell。

图3-17 rio_release_inb_dbell函数

  • inbound入站内存映射(rio_map_inb_region)

应用层驱动调用该函数后,tsi721设备会根据参数来配置内存映射。释放内存函数rio_unmap_inb_region。rio_map_inb_region实现方式如下图3-18:

图3-18 rio_map_inb_region函数

  • 快速DMA通道关联(rio_request_mport_dma)

应用层驱动调用该函数后,rapidio驱动会调用dma驱动来实现DMA通道关联。

3.4 路由驱动设计

Tsi721设备的数据收发都依赖于完备的路由网络,因此,配置好路由是实现各个Tsi721设备节点通信的关键所在。路由驱动分为2个部分:路由枚举(rio_enum_mport),路由发现(rio_disc_mport)。该驱动启动后会module_param(next_destid,int,0)获取命令行参数,保存下一个路由节点号。同时,向rapidio驱动注册路由枚举和路由发现这2个接口。Rapidio驱动会根据已经注册到rio_mports链表里的设备节点号来判断是执行枚举还是发现。

由于linux机器作为主节点,因此执行路由枚举。

路由枚举采用递归的形式枚举网络上所有tsi721设备,关键步骤如下图3-19,略

3.5 应用层驱动设计

在probe中需要注册字符设备,实现应用程序对PCIE设备的调用,例如打开,读取,控制等。这些功能都是通过file_operations操作接口实现。例如用户使用该设备完成读取操作,那么用户的过程为open(),read()。而用户调用的这些函数对于linux来说这些调用都会变成系统调用,并指向改设备对应的open(),read()函数,对于该设备来说,指向了xxx_open和xxx_read。

注册字符设备到系统里需要用到alloc_chrdev_region,class_create,device_create等函数。具体实现流程如下图3-20,略:

其中class_interface_register函数注册了处理本地rio_mport设备的接口。

 

mport_add_mport函数实现从LDM设备结构中添加rio_mport。然后调用mport_cdev_add函数创建mport_dev设备,并初始化相关参数。如下图3-21,略:

3.5.1 file_operations操作接口

结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对 设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。如下图3-22,略

其中ioctl函数较为重要,里面涉及各种对tsi721硬件的控制分支。ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制。

3.5.2 门铃操作接口

1)发送门铃接口

应用软件需要调用ioctl函数,选择SRIO_DOORBELL_RANGE分支,把需要发送的目的ID号及门铃数据传给驱动。驱动程序在通过rio_mport调用dsend接口,即可发送门铃。

2)接收门铃接口

由于接收门铃是被动操作,因此需要采用阻塞的方式,即有门铃数据来时,接收接口才继续执行下去。在接收门铃前,需要把tsi721驱动接收到的门铃数据通过回调的方式传给应用层驱动,因此,需要添加rio_mport_add_db_filter函数来调用rapidio驱动的回调函数rio_request_inb_dbell(md->mport, md, filter.low, filter.high,rio_mport_doorbell_handler);应用层的回调函数如下图3-23,略:

这样,tsi721驱动只要接收到门铃,就会触发rio_mport_add_event函数。该函数使用kfifo队列来存放门铃消息,并使用wake_up_interruptible(&priv->event_rx_wait);触发mport_read接口去从队列中去数据。

应用层需要通过系统调用read函数来读取门铃数据。mport_read函数关键操作如下图3-24,略

3.5.3 本地/目的id设置接口

由于用户对本地及目的id的设置,就要涉及到路由驱动的路由网络重建立,因此如何重新建立路由网络是本地及目的id设置接口的关键。通过查阅1848芯片手册的路由表流程及其相关特性,路由表流程如下图3-25:

路由表

图3-25 路由表流程

因此,我们决定采用如下方式:

  • 保持原有路由关系,获取路由交换的端口号。
  • 设置目的id号。
  • 重新添加新的路由关系。
  • 设置本地id号。

具体流程如下图3-26,略:

3.5.4 DMA接收/发送数据

应用层驱动的DMA接收/发送数据都是基于底层tsi721驱动及rapidio驱动建立好的DMA接口支持。然后根据工作模式,分配相应的内存空间,具体过程如下:

  • 分配入站内存映射
  • 分配DMA内存空间

DMA接收数据和发送数据有2种工作模式:

  • 基于连续的物理内存来接收/发送数据
  • 基于用户空间页表空间来接收/发送数据

模式1需要分配好连续的物理内存,因此用户层需要通过ioctl接口调用rio_mport_alloc_dma函数预先分配好空间,同时调用mmap函数来建立用户空间到内核空间的映射关系。模式2需要用户自己在用户空间分配好空间,应用层驱动获取用户分配的页表(页表的逻辑地址是连续的,但分配的空间越大理论上物理地址块越多)。

  • mmap函数映射

 

  • DMA接收/发送数据

 

备案号:苏ICP备15019812号-1